NIFI-5148 Refactoring Kerberos auth for Solr processors

- Created resuable KeytabUser and KeytabConfiguration in nifi-security-utils
- Refactored Solr processors to use a KeytabControllerService and no longer rely on JAAS system property
- Wrapped all calls in SolrProcessor onTrigger in a doAs when kerberos is enabled
- Added IT tests against MiniKDC
- This closes #2674
This commit is contained in:
Bryan Bende 2018-05-02 16:17:05 -04:00 committed by Matt Gilman
parent 9093d280f2
commit f69b720464
No known key found for this signature in database
GPG Key ID: DF61EC19432AEE37
19 changed files with 1149 additions and 123 deletions

9
NOTICE
View File

@ -50,3 +50,12 @@ This includes derived works from Apache Calcite available under Apache Software
Copyright 2012-2017 The Apache Software Foundation
The code can be found in nifi-nar-bundles/nifi-standard-nar/nifi-standard-processors/../FlowFileProjectTableScanRule
and nifi-nar-bundles/nifi-standard-nar/nifi-standard-processors/../FlowFileTableScan
This includes derived works from Apache Solr available under Apache Software License V2. Portions of the code found in
https://github.com/apache/lucene-solr/blob/branch_6x/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Krb5HttpClientConfigurer.java
Copyright 2006-2018 The Apache Software Foundation
The code can be found nifi-nar-bundles/nifi-solr-bundle/nifi-solr-processors/src/main/java/org/apache/nifi/processors/solr/kerberos/KerberosHttpClientConfigurer.java
This includes derived works from Apache Hadoop available under Apache Software License V2. Portions of the code found in
https://github.com/apache/hadoop/blob/release-2.7.3-RC2/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java
The code can be found nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/krb/KeytabUser.java

View File

@ -66,7 +66,16 @@
<artifactId>nifi-properties</artifactId>
<version>1.7.0-SNAPSHOT</version>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-minikdc</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>

View File

@ -0,0 +1,92 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.security.krb;
import org.apache.commons.lang3.Validate;
import org.apache.nifi.logging.ComponentLog;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.exception.ProcessException;
import javax.security.auth.login.LoginException;
import java.security.PrivilegedAction;
/**
* Helper class for processors to perform an action as a KeytabUser.
*/
public class KeytabAction {
private final KeytabUser keytabUser;
private final PrivilegedAction action;
private final ProcessContext context;
private final ComponentLog logger;
public KeytabAction(final KeytabUser keytabUser,
final PrivilegedAction action,
final ProcessContext context,
final ComponentLog logger) {
this.keytabUser = keytabUser;
this.action = action;
this.context = context;
this.logger = logger;
Validate.notNull(this.keytabUser);
Validate.notNull(this.action);
Validate.notNull(this.context);
Validate.notNull(this.logger);
}
public void execute() {
// lazily login the first time the processor executes
if (!keytabUser.isLoggedIn()) {
try {
keytabUser.login();
logger.info("Successful login for {}", new Object[]{keytabUser.getPrincipal()});
} catch (LoginException e) {
// make sure to yield so the processor doesn't keep retrying the rolled back flow files immediately
context.yield();
throw new ProcessException("Login failed due to: " + e.getMessage(), e);
}
}
// check if we need to re-login, will only happen if re-login window is reached (80% of TGT life)
try {
keytabUser.checkTGTAndRelogin();
} catch (LoginException e) {
// make sure to yield so the processor doesn't keep retrying the rolled back flow files immediately
context.yield();
throw new ProcessException("Relogin check failed due to: " + e.getMessage(), e);
}
// attempt to execute the action, if an exception is caught attempt to logout/login and retry
try {
keytabUser.doAs(action);
} catch (SecurityException se) {
logger.info("Privileged action failed, attempting relogin and retrying...");
logger.debug("", se);
try {
keytabUser.logout();
keytabUser.login();
keytabUser.doAs(action);
} catch (Exception e) {
// make sure to yield so the processor doesn't keep retrying the rolled back flow files immediately
context.yield();
throw new ProcessException("Retrying privileged action failed due to: " + e.getMessage(), e);
}
}
}
}

View File

@ -0,0 +1,86 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.security.krb;
import org.apache.commons.lang3.StringUtils;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* Custom JAAS Configuration object for a provided principal and keytab.
*/
public class KeytabConfiguration extends Configuration {
static final boolean IS_IBM = System.getProperty("java.vendor", "").contains("IBM");
static final String IBM_KRB5_LOGIN_MODULE = "com.ibm.security.auth.module.Krb5LoginModule";
static final String SUN_KRB5_LOGIN_MODULE = "com.sun.security.auth.module.Krb5LoginModule";
private final String principal;
private final String keytabFile;
private final AppConfigurationEntry kerberosKeytabConfigEntry;
public KeytabConfiguration(final String principal, final String keytabFile) {
if (StringUtils.isBlank(principal)) {
throw new IllegalArgumentException("Principal cannot be null");
}
if (StringUtils.isBlank(keytabFile)) {
throw new IllegalArgumentException("Keytab file cannot be null");
}
this.principal = principal;
this.keytabFile = keytabFile;
final Map<String, String> options = new HashMap<>();
options.put("principal", principal);
options.put("refreshKrb5Config", "true");
if (IS_IBM) {
options.put("useKeytab", keytabFile);
options.put("credsType", "both");
} else {
options.put("keyTab", keytabFile);
options.put("useKeyTab", "true");
options.put("isInitiator", "true");
options.put("doNotPrompt", "true");
options.put("storeKey", "true");
}
final String krbLoginModuleName = IS_IBM ? IBM_KRB5_LOGIN_MODULE : SUN_KRB5_LOGIN_MODULE;
this.kerberosKeytabConfigEntry = new AppConfigurationEntry(
krbLoginModuleName, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
}
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
return new AppConfigurationEntry[] {kerberosKeytabConfigEntry};
}
public String getPrincipal() {
return principal;
}
public String getKeytabFile() {
return keytabFile;
}
}

View File

@ -0,0 +1,88 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.security.krb;
import javax.security.auth.login.LoginException;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
/**
* A keytab-based user that can login/logout and perform actions as the given user.
*/
public interface KeytabUser {
/**
* Performs a login for the given user.
*
* @throws LoginException if the login fails
*/
void login() throws LoginException;
/**
* Performs a logout for the given user.
*
* @throws LoginException if the logout fails
*/
void logout() throws LoginException;
/**
* Executes the given action as the given user.
*
* @param action the action to execute
* @param <T> the type of response
* @return the result of the action
* @throws IllegalStateException if attempting to execute an action before performing a login
*/
<T> T doAs(PrivilegedAction<T> action) throws IllegalStateException;
/**
* Executes the given action as the given user.
*
* @param action the action to execute
* @param <T> the type of response
* @return the result of the action
* @throws IllegalStateException if attempting to execute an action before performing a login
* @throws PrivilegedActionException if the action itself threw an exception
*/
<T> T doAs(PrivilegedExceptionAction<T> action)
throws IllegalStateException, PrivilegedActionException;
/**
* Performs a re-login if the TGT is close to expiration.
*
* @return true if a relogin was performed, false otherwise
* @throws LoginException if the relogin fails
*/
boolean checkTGTAndRelogin() throws LoginException;
/**
* @return true if this user is currently logged in, false otherwise
*/
boolean isLoggedIn();
/**
* @return the principal for this user
*/
String getPrincipal();
/**
* @return the keytab file for this user
*/
String getKeytabFile();
}

View File

@ -0,0 +1,260 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.security.krb;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.kerberos.KerberosTicket;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Used to authenticate and execute actions when Kerberos is enabled and a keytab is being used.
*
* Some of the functionality in this class is adapted from Hadoop's UserGroupInformation.
*/
public class StandardKeytabUser implements KeytabUser {
private static final Logger LOGGER = LoggerFactory.getLogger(StandardKeytabUser.class);
static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
/**
* Percentage of the ticket window to use before we renew the TGT.
*/
static final float TICKET_RENEW_WINDOW = 0.80f;
private final String principal;
private final String keytabFile;
private final AtomicBoolean loggedIn = new AtomicBoolean(false);
private Subject subject;
private LoginContext loginContext;
public StandardKeytabUser(final String principal, final String keytabFile) {
this.principal = principal;
this.keytabFile = keytabFile;
Validate.notBlank(principal);
Validate.notBlank(keytabFile);
}
/**
* Performs a login using the specified principal and keytab.
*
* @throws LoginException if the login fails
*/
@Override
public synchronized void login() throws LoginException {
if (isLoggedIn()) {
return;
}
try {
// If it's the first time ever calling login then we need to initialize a new context
if (loginContext == null) {
LOGGER.debug("Initializing new login context...");
this.subject = new Subject();
final Configuration config = new KeytabConfiguration(principal, keytabFile);
this.loginContext = new LoginContext("KeytabConf", subject, null, config);
}
loginContext.login();
loggedIn.set(true);
LOGGER.debug("Successful login for {}", new Object[]{principal});
} catch (LoginException le) {
throw new LoginException("Unable to login with " + principal + " and " + keytabFile + " due to: " + le.getMessage());
}
}
/**
* Performs a logout of the current user.
*
* @throws LoginException if the logout fails
*/
@Override
public synchronized void logout() throws LoginException {
if (!isLoggedIn()) {
return;
}
try {
loginContext.logout();
loggedIn.set(false);
LOGGER.debug("Successful logout for {}", new Object[]{principal});
subject = null;
loginContext = null;
} catch (LoginException e) {
throw new LoginException("Logout failed due to: " + e.getMessage());
}
}
/**
* Executes the PrivilegedAction as this user.
*
* @param action the action to execute
* @param <T> the type of result
* @return the result of the action
* @throws IllegalStateException if this method is called while not logged in
*/
@Override
public <T> T doAs(final PrivilegedAction<T> action) throws IllegalStateException {
if (!isLoggedIn()) {
throw new IllegalStateException("Must login before executing actions");
}
return Subject.doAs(subject, action);
}
/**
* Executes the PrivilegedAction as this user.
*
* @param action the action to execute
* @param <T> the type of result
* @return the result of the action
* @throws IllegalStateException if this method is called while not logged in
* @throws PrivilegedActionException if an exception is thrown from the action
*/
@Override
public <T> T doAs(final PrivilegedExceptionAction<T> action)
throws IllegalStateException, PrivilegedActionException {
if (!isLoggedIn()) {
throw new IllegalStateException("Must login before executing actions");
}
return Subject.doAs(subject, action);
}
/**
* Re-login a user from keytab if TGT is expired or is close to expiry.
*
* @throws LoginException if an error happens performing the re-login
*/
@Override
public synchronized boolean checkTGTAndRelogin() throws LoginException {
final KerberosTicket tgt = getTGT();
if (tgt == null) {
LOGGER.debug("TGT was not found");
}
if (tgt != null && System.currentTimeMillis() < getRefreshTime(tgt)) {
LOGGER.debug("TGT was found, but has not reached expiration window");
return false;
}
LOGGER.debug("Performing relogin for {}", new Object[]{principal});
logout();
login();
return true;
}
/**
* Get the Kerberos TGT.
*
* @return the user's TGT or null if none was found
*/
private synchronized KerberosTicket getTGT() {
final Set<KerberosTicket> tickets = subject.getPrivateCredentials(KerberosTicket.class);
for (KerberosTicket ticket : tickets) {
if (isTGSPrincipal(ticket.getServer())) {
return ticket;
}
}
return null;
}
/**
* TGS must have the server principal of the form "krbtgt/FOO@FOO".
*
* @param principal the principal to check
* @return true if the principal is the TGS, false otherwise
*/
private boolean isTGSPrincipal(final KerberosPrincipal principal) {
if (principal == null) {
return false;
}
if (principal.getName().equals("krbtgt/" + principal.getRealm() + "@" + principal.getRealm())) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Found TGT principal: " + principal.getName());
}
return true;
}
return false;
}
private long getRefreshTime(final KerberosTicket tgt) {
long start = tgt.getStartTime().getTime();
long end = tgt.getEndTime().getTime();
if (LOGGER.isTraceEnabled()) {
final SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
final String startDate = dateFormat.format(new Date(start));
final String endDate = dateFormat.format(new Date(end));
LOGGER.trace("TGT valid starting at: " + startDate);
LOGGER.trace("TGT expires at: " + endDate);
}
return start + (long) ((end - start) * TICKET_RENEW_WINDOW);
}
/**
* @return true if this user is currently logged in, false otherwise
*/
@Override
public boolean isLoggedIn() {
return loggedIn.get();
}
/**
* @return the principal for this user
*/
@Override
public String getPrincipal() {
return principal;
}
/**
* @return the keytab file for this user
*/
@Override
public String getKeytabFile() {
return keytabFile;
}
// Visible for testing
Subject getSubject() {
return this.subject;
}
}

View File

@ -0,0 +1,74 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.security.krb;
import org.apache.hadoop.minikdc.MiniKdc;
import java.io.File;
import java.util.Properties;
/**
* Wrapper around MiniKdc.
*/
public class KDCServer {
private final File baseDir;
private final Properties kdcProperties;
private MiniKdc kdc;
public KDCServer(final File baseDir) {
this.baseDir = baseDir;
this.kdcProperties = MiniKdc.createConf();
this.kdcProperties.setProperty(MiniKdc.INSTANCE, "DefaultKrbServer");
this.kdcProperties.setProperty(MiniKdc.ORG_NAME, "NIFI");
this.kdcProperties.setProperty(MiniKdc.ORG_DOMAIN, "COM");
}
public void setMaxTicketLifetime(final String lifetimeSeconds) {
this.kdcProperties.setProperty(MiniKdc.MAX_TICKET_LIFETIME, lifetimeSeconds);
}
public void setProperty(final String key, final String value) {
this.kdcProperties.setProperty(key, value);
}
public synchronized void start() throws Exception {
if (kdc == null) {
kdc = new MiniKdc(kdcProperties, baseDir);
}
kdc.start();
System.setProperty("java.security.krb5.conf", kdc.getKrb5conf().getAbsolutePath());
}
public synchronized void stop() {
if (kdc != null) {
kdc.stop();
}
}
public String getRealm() {
return kdc.getRealm();
}
public void createKeytabFile(final File keytabFile, final String... names) throws Exception {
kdc.createPrincipal(keytabFile, names);
}
}

View File

@ -0,0 +1,158 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.security.krb;
import org.apache.nifi.logging.ComponentLog;
import org.apache.nifi.processor.ProcessContext;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.login.LoginException;
import java.io.File;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
public class KeytabUserIT {
@ClassRule
public static TemporaryFolder tmpDir = new TemporaryFolder();
private static KDCServer kdc;
private static KerberosPrincipal principal1;
private static File principal1KeytabFile;
private static KerberosPrincipal principal2;
private static File principal2KeytabFile;
@BeforeClass
public static void setupClass() throws Exception {
kdc = new KDCServer(tmpDir.newFolder("mini-kdc_"));
kdc.setMaxTicketLifetime("15"); // set ticket lifetime to 15 seconds so we can test relogin
kdc.start();
principal1 = new KerberosPrincipal("user1@" + kdc.getRealm());
principal1KeytabFile = tmpDir.newFile("user1.keytab");
kdc.createKeytabFile(principal1KeytabFile, "user1");
principal2 = new KerberosPrincipal("user2@" + kdc.getRealm());
principal2KeytabFile = tmpDir.newFile("user2.keytab");
kdc.createKeytabFile(principal2KeytabFile, "user2");
}
@Test
public void testSuccessfulLoginAndLogout() throws LoginException {
// perform login for user1
final KeytabUser user1 = new StandardKeytabUser(principal1.getName(), principal1KeytabFile.getAbsolutePath());
user1.login();
// perform login for user2
final KeytabUser user2 = new StandardKeytabUser(principal2.getName(), principal2KeytabFile.getAbsolutePath());
user2.login();
// verify user1 Subject only has user1 principal
final Subject user1Subject = ((StandardKeytabUser) user1).getSubject();
final Set<Principal> user1SubjectPrincipals = user1Subject.getPrincipals();
assertEquals(1, user1SubjectPrincipals.size());
assertEquals(principal1.getName(), user1SubjectPrincipals.iterator().next().getName());
// verify user2 Subject only has user2 principal
final Subject user2Subject = ((StandardKeytabUser) user2).getSubject();
final Set<Principal> user2SubjectPrincipals = user2Subject.getPrincipals();
assertEquals(1, user2SubjectPrincipals.size());
assertEquals(principal2.getName(), user2SubjectPrincipals.iterator().next().getName());
// call check/relogin and verify neither user performed a relogin
assertFalse(user1.checkTGTAndRelogin());
assertFalse(user2.checkTGTAndRelogin());
// perform logout for both users
user1.logout();
user2.logout();
// verify subjects have no more principals
assertEquals(0, user1Subject.getPrincipals().size());
assertEquals(0, user2Subject.getPrincipals().size());
}
@Test
public void testLoginWithUnknownPrincipal() throws LoginException {
final String unknownPrincipal = "doesnotexist@" + kdc.getRealm();
final KeytabUser user1 = new StandardKeytabUser(unknownPrincipal, principal1KeytabFile.getAbsolutePath());
try {
user1.login();
fail("Login should have failed");
} catch (Exception e) {
// exception is expected here
//e.printStackTrace();
}
}
@Test
public void testCheckTGTAndRelogin() throws LoginException, InterruptedException {
final KeytabUser user1 = new StandardKeytabUser(principal1.getName(), principal1KeytabFile.getAbsolutePath());
user1.login();
// Since we set the lifetime to 15 seconds we should hit a relogin before 15 attempts
boolean performedRelogin = false;
for (int i=0; i < 30; i++) {
Thread.sleep(1000);
System.out.println("checkTGTAndRelogin #" + i);
performedRelogin = user1.checkTGTAndRelogin();
if (performedRelogin) {
System.out.println("Performed relogin!");
break;
}
}
assertEquals(true, performedRelogin);
}
@Test
public void testKeytabAction() {
final KeytabUser user1 = new StandardKeytabUser(principal1.getName(), principal1KeytabFile.getAbsolutePath());
final AtomicReference<String> resultHolder = new AtomicReference<>(null);
final PrivilegedAction privilegedAction = () -> {
resultHolder.set("SUCCESS");
return null;
};
final ProcessContext context = Mockito.mock(ProcessContext.class);
final ComponentLog logger = Mockito.mock(ComponentLog.class);
// create the action to test and execute it
final KeytabAction keytabAction = new KeytabAction(user1, privilegedAction, context, logger);
keytabAction.execute();
// if the result holder has the string success then we know the action executed
assertEquals("SUCCESS", resultHolder.get());
}
}

View File

@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.security.krb;
import org.junit.Test;
import javax.security.auth.login.AppConfigurationEntry;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class TestKeytabConfiguration {
@Test
public void testCreatingKeytabConfiguration() {
final String principal = "foo@NIFI.COM";
final String keytab = "src/test/resources/foo.keytab";
final KeytabConfiguration configuration = new KeytabConfiguration(principal, keytab);
assertEquals(principal, configuration.getPrincipal());
assertEquals(keytab, configuration.getKeytabFile());
final AppConfigurationEntry[] entries = configuration.getAppConfigurationEntry("KeytabConfig");
assertNotNull(entries);
assertEquals(1, entries.length);
final AppConfigurationEntry entry = entries[0];
assertEquals(KeytabConfiguration.SUN_KRB5_LOGIN_MODULE, entry.getLoginModuleName());
assertEquals(principal, entry.getOptions().get("principal"));
assertEquals(keytab, entry.getOptions().get("keyTab"));
}
}

View File

@ -75,6 +75,11 @@
<artifactId>nifi-ssl-context-service-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-kerberos-credentials-service-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>

View File

@ -73,9 +73,9 @@ import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.params.CursorMarkParams;
import static org.apache.nifi.processors.solr.SolrUtils.KERBEROS_CREDENTIALS_SERVICE;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_TYPE;
import static org.apache.nifi.processors.solr.SolrUtils.COLLECTION;
import static org.apache.nifi.processors.solr.SolrUtils.JAAS_CLIENT_APP_NAME;
import static org.apache.nifi.processors.solr.SolrUtils.SSL_CONTEXT_SERVICE;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_SOCKET_TIMEOUT;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_CONNECTION_TIMEOUT;
@ -181,7 +181,7 @@ public class GetSolr extends SolrProcessor {
descriptors.add(DATE_FILTER);
descriptors.add(RETURN_FIELDS);
descriptors.add(BATCH_SIZE);
descriptors.add(JAAS_CLIENT_APP_NAME);
descriptors.add(KERBEROS_CREDENTIALS_SERVICE);
descriptors.add(BASIC_USERNAME);
descriptors.add(BASIC_PASSWORD);
descriptors.add(SSL_CONTEXT_SERVICE);
@ -286,7 +286,7 @@ public class GetSolr extends SolrProcessor {
}
@Override
public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
public void doOnTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
final ComponentLog logger = getLogger();
final AtomicBoolean continuePaging = new AtomicBoolean(true);

View File

@ -0,0 +1,92 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.processors.solr;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.auth.AuthSchemeRegistry;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.entity.BufferedHttpEntity;
import org.apache.http.impl.auth.SPNegoSchemeFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.protocol.HttpContext;
import org.apache.solr.client.solrj.impl.HttpClientConfigurer;
import org.apache.solr.client.solrj.impl.SolrPortAwareCookieSpecFactory;
import org.apache.solr.common.params.SolrParams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.security.Principal;
/**
* This class is a modified version of Krb5HttpClientConfigurer that is part of SolrJ.
*
* In our case we don't want to warn about the useSubjectCreds property since we know we are going to do a
* login and will have subject creds, and we also don't want to mess the static JAAS configuration of the JVM.
*/
public class KerberosHttpClientConfigurer extends HttpClientConfigurer {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public void configure(DefaultHttpClient httpClient, SolrParams config) {
super.configure(httpClient, config);
logger.info("Setting up SPNego auth...");
//Enable only SPNEGO authentication scheme.
final AuthSchemeRegistry registry = new AuthSchemeRegistry();
registry.register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, false));
httpClient.setAuthSchemes(registry);
// Get the credentials from the JAAS configuration rather than here
final Credentials useJaasCreds = new Credentials() {
public String getPassword() {
return null;
}
public Principal getUserPrincipal() {
return null;
}
};
final SolrPortAwareCookieSpecFactory cookieFactory = new SolrPortAwareCookieSpecFactory();
httpClient.getCookieSpecs().register(cookieFactory.POLICY_NAME, cookieFactory);
httpClient.getParams().setParameter(ClientPNames.COOKIE_POLICY, cookieFactory.POLICY_NAME);
httpClient.getCredentialsProvider().setCredentials(AuthScope.ANY, useJaasCreds);
httpClient.addRequestInterceptor(bufferedEntityInterceptor);
}
// Set a buffered entity based request interceptor
private HttpRequestInterceptor bufferedEntityInterceptor = new HttpRequestInterceptor() {
@Override
public void process(HttpRequest request, HttpContext context) throws HttpException,
IOException {
if(request instanceof HttpEntityEnclosingRequest) {
HttpEntityEnclosingRequest enclosingRequest = ((HttpEntityEnclosingRequest) request);
HttpEntity requestEntity = enclosingRequest.getEntity();
enclosingRequest.setEntity(new BufferedHttpEntity(requestEntity));
}
}
};
}

View File

@ -54,20 +54,20 @@ import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_TYPE_CLOUD;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_TYPE;
import static org.apache.nifi.processors.solr.SolrUtils.BASIC_PASSWORD;
import static org.apache.nifi.processors.solr.SolrUtils.BASIC_USERNAME;
import static org.apache.nifi.processors.solr.SolrUtils.COLLECTION;
import static org.apache.nifi.processors.solr.SolrUtils.JAAS_CLIENT_APP_NAME;
import static org.apache.nifi.processors.solr.SolrUtils.SSL_CONTEXT_SERVICE;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_SOCKET_TIMEOUT;
import static org.apache.nifi.processors.solr.SolrUtils.KERBEROS_CREDENTIALS_SERVICE;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_CONNECTION_TIMEOUT;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_LOCATION;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_MAX_CONNECTIONS;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_MAX_CONNECTIONS_PER_HOST;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_SOCKET_TIMEOUT;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_TYPE;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_TYPE_CLOUD;
import static org.apache.nifi.processors.solr.SolrUtils.SSL_CONTEXT_SERVICE;
import static org.apache.nifi.processors.solr.SolrUtils.ZK_CLIENT_TIMEOUT;
import static org.apache.nifi.processors.solr.SolrUtils.ZK_CONNECTION_TIMEOUT;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_LOCATION;
import static org.apache.nifi.processors.solr.SolrUtils.BASIC_USERNAME;
import static org.apache.nifi.processors.solr.SolrUtils.BASIC_PASSWORD;
@Tags({"Apache", "Solr", "Put", "Send"})
@InputRequirement(Requirement.INPUT_REQUIRED)
@ -135,7 +135,7 @@ public class PutSolrContentStream extends SolrProcessor {
descriptors.add(CONTENT_STREAM_PATH);
descriptors.add(CONTENT_TYPE);
descriptors.add(COMMIT_WITHIN);
descriptors.add(JAAS_CLIENT_APP_NAME);
descriptors.add(KERBEROS_CREDENTIALS_SERVICE);
descriptors.add(BASIC_USERNAME);
descriptors.add(BASIC_PASSWORD);
descriptors.add(SSL_CONTEXT_SERVICE);
@ -176,7 +176,7 @@ public class PutSolrContentStream extends SolrProcessor {
}
@Override
public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
protected void doOnTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
final FlowFile flowFile = session.get();
if ( flowFile == null ) {
return;

View File

@ -63,7 +63,7 @@ import java.util.concurrent.atomic.AtomicReference;
import static org.apache.nifi.processors.solr.SolrUtils.BASIC_PASSWORD;
import static org.apache.nifi.processors.solr.SolrUtils.BASIC_USERNAME;
import static org.apache.nifi.processors.solr.SolrUtils.COLLECTION;
import static org.apache.nifi.processors.solr.SolrUtils.JAAS_CLIENT_APP_NAME;
import static org.apache.nifi.processors.solr.SolrUtils.KERBEROS_CREDENTIALS_SERVICE;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_CONNECTION_TIMEOUT;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_LOCATION;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_MAX_CONNECTIONS;
@ -163,7 +163,7 @@ public class PutSolrRecord extends SolrProcessor {
descriptors.add(RECORD_READER);
descriptors.add(FIELDS_TO_INDEX);
descriptors.add(COMMIT_WITHIN);
descriptors.add(JAAS_CLIENT_APP_NAME);
descriptors.add(KERBEROS_CREDENTIALS_SERVICE);
descriptors.add(BASIC_USERNAME);
descriptors.add(BASIC_PASSWORD);
descriptors.add(SSL_CONTEXT_SERVICE);
@ -205,7 +205,7 @@ public class PutSolrRecord extends SolrProcessor {
}
@Override
public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
public void doOnTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
FlowFile flowFile = session.get();
if ( flowFile == null ) {
return;

View File

@ -72,9 +72,9 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.apache.nifi.processors.solr.SolrUtils.KERBEROS_CREDENTIALS_SERVICE;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_TYPE;
import static org.apache.nifi.processors.solr.SolrUtils.COLLECTION;
import static org.apache.nifi.processors.solr.SolrUtils.JAAS_CLIENT_APP_NAME;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_TYPE_CLOUD;
import static org.apache.nifi.processors.solr.SolrUtils.SSL_CONTEXT_SERVICE;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_SOCKET_TIMEOUT;
@ -259,7 +259,7 @@ public class QuerySolr extends SolrProcessor {
descriptors.add(SOLR_PARAM_START);
descriptors.add(SOLR_PARAM_ROWS);
descriptors.add(AMOUNT_DOCUMENTS_TO_RETURN);
descriptors.add(JAAS_CLIENT_APP_NAME);
descriptors.add(KERBEROS_CREDENTIALS_SERVICE);
descriptors.add(BASIC_USERNAME);
descriptors.add(BASIC_PASSWORD);
descriptors.add(SSL_CONTEXT_SERVICE);
@ -306,7 +306,7 @@ public class QuerySolr extends SolrProcessor {
}
@Override
public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
public void doOnTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
final ComponentLog logger = getLogger();
FlowFile flowFileOriginal = session.get();

View File

@ -23,27 +23,33 @@ import org.apache.nifi.annotation.lifecycle.OnScheduled;
import org.apache.nifi.annotation.lifecycle.OnStopped;
import org.apache.nifi.components.ValidationContext;
import org.apache.nifi.components.ValidationResult;
import org.apache.nifi.kerberos.KerberosCredentialsService;
import org.apache.nifi.processor.AbstractProcessor;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.ProcessSession;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.security.krb.KeytabAction;
import org.apache.nifi.security.krb.KeytabUser;
import org.apache.nifi.security.krb.StandardKeytabUser;
import org.apache.nifi.ssl.SSLContextService;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.Krb5HttpClientConfigurer;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginException;
import java.io.IOException;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_TYPE_CLOUD;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_TYPE;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_TYPE_STANDARD;
import static org.apache.nifi.processors.solr.SolrUtils.COLLECTION;
import static org.apache.nifi.processors.solr.SolrUtils.JAAS_CLIENT_APP_NAME;
import static org.apache.nifi.processors.solr.SolrUtils.SSL_CONTEXT_SERVICE;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_LOCATION;
import static org.apache.nifi.processors.solr.SolrUtils.BASIC_USERNAME;
import static org.apache.nifi.processors.solr.SolrUtils.BASIC_PASSWORD;
import static org.apache.nifi.processors.solr.SolrUtils.BASIC_USERNAME;
import static org.apache.nifi.processors.solr.SolrUtils.COLLECTION;
import static org.apache.nifi.processors.solr.SolrUtils.KERBEROS_CREDENTIALS_SERVICE;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_LOCATION;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_TYPE;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_TYPE_CLOUD;
import static org.apache.nifi.processors.solr.SolrUtils.SOLR_TYPE_STANDARD;
import static org.apache.nifi.processors.solr.SolrUtils.SSL_CONTEXT_SERVICE;
/**
* A base class for processors that interact with Apache Solr.
@ -57,6 +63,8 @@ public abstract class SolrProcessor extends AbstractProcessor {
private volatile String basicPassword;
private volatile boolean basicAuthEnabled = false;
private volatile KeytabUser keytabUser;
@OnScheduled
public final void onScheduled(final ProcessContext context) throws IOException {
this.solrLocation = context.getProperty(SOLR_LOCATION).evaluateAttributeExpressions().getValue();
@ -65,7 +73,17 @@ public abstract class SolrProcessor extends AbstractProcessor {
if (!StringUtils.isBlank(basicUsername) && !StringUtils.isBlank(basicPassword)) {
basicAuthEnabled = true;
}
this.solrClient = createSolrClient(context, solrLocation);
final KerberosCredentialsService kerberosCredentialsService = context.getProperty(KERBEROS_CREDENTIALS_SERVICE).asControllerService(KerberosCredentialsService.class);
if (kerberosCredentialsService != null) {
this.keytabUser = createKeytabUser(kerberosCredentialsService);
}
}
protected KeytabUser createKeytabUser(final KerberosCredentialsService kerberosCredentialsService) {
return new StandardKeytabUser(kerberosCredentialsService.getPrincipal(), kerberosCredentialsService.getKeytab());
}
@OnStopped
@ -77,8 +95,42 @@ public abstract class SolrProcessor extends AbstractProcessor {
getLogger().debug("Error closing SolrClient", e);
}
}
if (keytabUser != null) {
try {
keytabUser.logout();
keytabUser = null;
} catch (LoginException e) {
getLogger().debug("Error logging out keytab user", e);
}
}
}
@Override
public final void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
final KeytabUser keytabUser = getKerberosKeytabUser();
if (keytabUser == null) {
doOnTrigger(context, session);
} else {
// wrap doOnTrigger in a privileged action
final PrivilegedAction action = () -> {
doOnTrigger(context, session);
return null;
};
// execute the privileged action as the given keytab user
final KeytabAction keytabAction = new KeytabAction(keytabUser, action, context, getLogger());
keytabAction.execute();
}
}
/**
* This should be implemented just like the normal onTrigger method. When a KerberosCredentialsService is configured,
* this method will be wrapped in a PrivilegedAction and executed with the credentials of the service, otherwise this
* will be executed like a a normal call to onTrigger.
*/
protected abstract void doOnTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException;
/**
* Create a SolrClient based on the type of Solr specified.
*
@ -116,6 +168,10 @@ public abstract class SolrProcessor extends AbstractProcessor {
return basicAuthEnabled;
}
protected final KeytabUser getKerberosKeytabUser() {
return keytabUser;
}
@Override
final protected Collection<ValidationResult> customValidate(ValidationContext context) {
final List<ValidationResult> problems = new ArrayList<>();
@ -131,29 +187,6 @@ public abstract class SolrProcessor extends AbstractProcessor {
}
}
// If a JAAS Client App Name is provided then the system property for the JAAS config file must be set,
// and that config file must contain an entry for the name provided by the processor
final String jaasAppName = context.getProperty(JAAS_CLIENT_APP_NAME).getValue();
if (!StringUtils.isEmpty(jaasAppName)) {
final String loginConf = System.getProperty(Krb5HttpClientConfigurer.LOGIN_CONFIG_PROP);
if (StringUtils.isEmpty(loginConf)) {
problems.add(new ValidationResult.Builder()
.subject(JAAS_CLIENT_APP_NAME.getDisplayName())
.valid(false)
.explanation("the system property " + Krb5HttpClientConfigurer.LOGIN_CONFIG_PROP + " must be set when providing a JAAS Client App Name")
.build());
} else {
final Configuration config = javax.security.auth.login.Configuration.getConfiguration();
if (config.getAppConfigurationEntry(jaasAppName) == null) {
problems.add(new ValidationResult.Builder()
.subject(JAAS_CLIENT_APP_NAME.getDisplayName())
.valid(false)
.explanation("'" + jaasAppName + "' does not exist in " + loginConf)
.build());
}
}
}
// For solr cloud the location will be the ZooKeeper host:port so we can't validate the SSLContext, but for standard solr
// we can validate if the url starts with https we need an SSLContextService, if it starts with http we can't have an SSLContextService
if (SOLR_TYPE_STANDARD.equals(context.getProperty(SOLR_TYPE).getValue())) {
@ -180,7 +213,10 @@ public abstract class SolrProcessor extends AbstractProcessor {
final String username = context.getProperty(BASIC_USERNAME).evaluateAttributeExpressions().getValue();
final String password = context.getProperty(BASIC_PASSWORD).evaluateAttributeExpressions().getValue();
if (!StringUtils.isBlank(username) && StringUtils.isBlank(password)) {
final boolean basicUsernameProvided = !StringUtils.isBlank(username);
final boolean basicPasswordProvided = !StringUtils.isBlank(password);
if (basicUsernameProvided && !basicPasswordProvided) {
problems.add(new ValidationResult.Builder()
.subject(BASIC_PASSWORD.getDisplayName())
.valid(false)
@ -188,7 +224,7 @@ public abstract class SolrProcessor extends AbstractProcessor {
.build());
}
if (!StringUtils.isBlank(password) && StringUtils.isBlank(username)) {
if (basicPasswordProvided && !basicUsernameProvided) {
problems.add(new ValidationResult.Builder()
.subject(BASIC_USERNAME.getDisplayName())
.valid(false)
@ -196,6 +232,16 @@ public abstract class SolrProcessor extends AbstractProcessor {
.build());
}
// Validate that only kerberos or basic auth can be set, but not both
final KerberosCredentialsService kerberosCredentialsService = context.getProperty(KERBEROS_CREDENTIALS_SERVICE).asControllerService(KerberosCredentialsService.class);
if (kerberosCredentialsService != null && basicUsernameProvided && basicPasswordProvided) {
problems.add(new ValidationResult.Builder()
.subject(KERBEROS_CREDENTIALS_SERVICE.getDisplayName())
.valid(false)
.explanation("basic auth and kerberos cannot be configured at the same time")
.build());
}
Collection<ValidationResult> otherProblems = this.additionalCustomValidation(context);
if (otherProblems != null) {
problems.addAll(otherProblems);

View File

@ -29,6 +29,7 @@ import org.apache.nifi.context.PropertyContext;
import org.apache.nifi.expression.AttributeExpression;
import org.apache.nifi.expression.ExpressionLanguageScope;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.kerberos.KerberosCredentialsService;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.io.OutputStreamCallback;
import org.apache.nifi.processor.util.StandardValidators;
@ -48,13 +49,14 @@ import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.impl.HttpClientUtil;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.impl.Krb5HttpClientConfigurer;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.MultiMapSolrParams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
import java.io.IOException;
@ -77,6 +79,8 @@ import java.util.concurrent.TimeUnit;
public class SolrUtils {
static final Logger LOGGER = LoggerFactory.getLogger(SolrUtils.class);
public static final AllowableValue SOLR_TYPE_CLOUD = new AllowableValue(
"Cloud", "Cloud", "A SolrCloud instance.");
@ -137,13 +141,12 @@ public class SolrUtils {
.sensitive(true)
.build();
public static final PropertyDescriptor JAAS_CLIENT_APP_NAME = new PropertyDescriptor
.Builder().name("JAAS Client App Name")
.description("The name of the JAAS configuration entry to use when performing Kerberos authentication to Solr. If this property is " +
"not provided, Kerberos authentication will not be attempted. The value must match an entry in the file specified by the " +
"system property java.security.auth.login.config.")
static final PropertyDescriptor KERBEROS_CREDENTIALS_SERVICE = new PropertyDescriptor.Builder()
.name("kerberos-credentials-service")
.displayName("Kerberos Credentials Service")
.description("Specifies the Kerberos Credentials Controller Service that should be used for authenticating with Kerberos")
.identifiesControllerService(KerberosCredentialsService.class)
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
public static final PropertyDescriptor SSL_CONTEXT_SERVICE = new PropertyDescriptor.Builder()
@ -209,7 +212,7 @@ public class SolrUtils {
final Integer maxConnections = context.getProperty(SOLR_MAX_CONNECTIONS).asInteger();
final Integer maxConnectionsPerHost = context.getProperty(SOLR_MAX_CONNECTIONS_PER_HOST).asInteger();
final SSLContextService sslContextService = context.getProperty(SSL_CONTEXT_SERVICE).asControllerService(SSLContextService.class);
final String jaasClientAppName = context.getProperty(JAAS_CLIENT_APP_NAME).getValue();
final KerberosCredentialsService kerberosCredentialsService = context.getProperty(KERBEROS_CREDENTIALS_SERVICE).asControllerService(KerberosCredentialsService.class);
final ModifiableSolrParams params = new ModifiableSolrParams();
params.set(HttpClientUtil.PROP_SO_TIMEOUT, socketTimeout);
@ -217,10 +220,9 @@ public class SolrUtils {
params.set(HttpClientUtil.PROP_MAX_CONNECTIONS, maxConnections);
params.set(HttpClientUtil.PROP_MAX_CONNECTIONS_PER_HOST, maxConnectionsPerHost);
// has to happen before the client is created below so that correct configurer would be set if neeeded
if (!StringUtils.isEmpty(jaasClientAppName)) {
System.setProperty("solr.kerberos.jaas.appname", jaasClientAppName);
HttpClientUtil.setConfigurer(new Krb5HttpClientConfigurer());
// has to happen before the client is created below so that correct configurer would be set if needed
if (kerberosCredentialsService != null) {
HttpClientUtil.setConfigurer(new KerberosHttpClientConfigurer());
}
final HttpClient httpClient = HttpClientUtil.createClient(params);

View File

@ -17,9 +17,11 @@
package org.apache.nifi.processors.solr;
import org.apache.nifi.controller.AbstractControllerService;
import org.apache.nifi.kerberos.KerberosCredentialsService;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.reporting.InitializationException;
import org.apache.nifi.security.krb.KeytabUser;
import org.apache.nifi.ssl.SSLContextService;
import org.apache.nifi.util.TestRunner;
import org.apache.nifi.util.TestRunners;
@ -28,21 +30,20 @@ import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.impl.Krb5HttpClientConfigurer;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.StringUtils;
import org.apache.solr.common.util.NamedList;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import javax.net.ssl.SSLContext;
import java.io.File;
import javax.security.auth.login.LoginException;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
@ -426,30 +427,103 @@ public class TestPutSolrContentStream {
}
@Test
public void testJAASClientAppNameValidation() {
final TestRunner runner = TestRunners.newTestRunner(PutSolrContentStream.class);
runner.setProperty(SolrUtils.SOLR_TYPE, SolrUtils.SOLR_TYPE_STANDARD.getValue());
runner.setProperty(SolrUtils.SOLR_LOCATION, "http://localhost:8443/solr");
public void testBasicAuthAndKerberosNotAllowedTogether() throws IOException, InitializationException {
final SolrClient solrClient = createEmbeddedSolrClient(DEFAULT_SOLR_CORE);
final TestableProcessor proc = new TestableProcessor(solrClient);
final TestRunner runner = createDefaultTestRunner(proc);
runner.assertValid();
// clear the jaas config system property if it was set
final String jaasConfig = System.getProperty(Krb5HttpClientConfigurer.LOGIN_CONFIG_PROP);
if (!StringUtils.isEmpty(jaasConfig)) {
System.clearProperty(Krb5HttpClientConfigurer.LOGIN_CONFIG_PROP);
runner.setProperty(SolrUtils.BASIC_USERNAME, "user1");
runner.setProperty(SolrUtils.BASIC_PASSWORD, "password");
runner.assertValid();
final String principal = "nifi@FOO.COM";
final String keytab = "src/test/resources/foo.keytab";
final KerberosCredentialsService kerberosCredentialsService = new MockKerberosCredentialsService(principal, keytab);
runner.addControllerService("kerb-credentials", kerberosCredentialsService);
runner.enableControllerService(kerberosCredentialsService);
runner.setProperty(SolrUtils.KERBEROS_CREDENTIALS_SERVICE, "kerb-credentials");
runner.assertNotValid();
runner.removeProperty(SolrUtils.BASIC_USERNAME);
runner.removeProperty(SolrUtils.BASIC_PASSWORD);
runner.assertValid();
proc.onScheduled(runner.getProcessContext());
final KeytabUser keytabUser = proc.getMockKerberosKeytabUser();
Assert.assertNotNull(keytabUser);
Assert.assertEquals(principal, keytabUser.getPrincipal());
Assert.assertEquals(keytab, keytabUser.getKeytabFile());
}
@Test
public void testUpdateWithKerberosAuth() throws IOException, InitializationException, LoginException {
final String principal = "nifi@FOO.COM";
final String keytab = "src/test/resources/foo.keytab";
// Setup a mock KeytabUser that will still execute the privileged action
final KeytabUser keytabUser = Mockito.mock(KeytabUser.class);
when(keytabUser.getPrincipal()).thenReturn(principal);
when(keytabUser.getKeytabFile()).thenReturn(keytab);
when(keytabUser.doAs(any(PrivilegedAction.class))).thenAnswer((invocation -> {
final PrivilegedAction action = (PrivilegedAction) invocation.getArguments()[0];
action.run();
return null;
})
);
// Configure the processor with the mock KeytabUser and with a credentials service
final SolrClient solrClient = createEmbeddedSolrClient(DEFAULT_SOLR_CORE);
final TestableProcessor proc = new TestableProcessor(solrClient, keytabUser);
final TestRunner runner = createDefaultTestRunner(proc);
final KerberosCredentialsService kerberosCredentialsService = new MockKerberosCredentialsService(principal, keytab);
runner.addControllerService("kerb-credentials", kerberosCredentialsService);
runner.enableControllerService(kerberosCredentialsService);
runner.setProperty(SolrUtils.KERBEROS_CREDENTIALS_SERVICE, "kerb-credentials");
// Run an update and verify the update worked based on a flow file going to success
try (FileInputStream fileIn = new FileInputStream(SOLR_JSON_MULTIPLE_DOCS_FILE)) {
runner.enqueue(fileIn);
runner.run(1, false);
runner.assertTransferCount(PutSolrContentStream.REL_FAILURE, 0);
runner.assertTransferCount(PutSolrContentStream.REL_CONNECTION_FAILURE, 0);
runner.assertTransferCount(PutSolrContentStream.REL_SUCCESS, 1);
} finally {
try {
proc.getSolrClient().close();
} catch (Exception e) {
}
}
// should be invalid if we have a client name but not config file
runner.setProperty(SolrUtils.JAAS_CLIENT_APP_NAME, "Client");
runner.assertNotValid();
// Verify that during the update the user was logged in, TGT was checked, and the action was executed
verify(keytabUser, times(1)).login();
verify(keytabUser, times(1)).checkTGTAndRelogin();
verify(keytabUser, times(1)).doAs(any(PrivilegedAction.class));
}
// should be invalid if we have a client name that is not in the config file
final File jaasConfigFile = new File("src/test/resources/jaas-client.conf");
System.setProperty(Krb5HttpClientConfigurer.LOGIN_CONFIG_PROP, jaasConfigFile.getAbsolutePath());
runner.assertNotValid();
// should be valid now that the name matches up with the config file
runner.setProperty(SolrUtils.JAAS_CLIENT_APP_NAME, "SolrJClient");
runner.assertValid();
private class MockKerberosCredentialsService extends AbstractControllerService implements KerberosCredentialsService {
private String principal;
private String keytab;
public MockKerberosCredentialsService(String principal, String keytab) {
this.principal = principal;
this.keytab = keytab;
}
@Override
public String getKeytab() {
return keytab;
}
@Override
public String getPrincipal() {
return principal;
}
}
/**
@ -573,14 +647,34 @@ public class TestPutSolrContentStream {
// Override createSolrClient and return the passed in SolrClient
private class TestableProcessor extends PutSolrContentStream {
private SolrClient solrClient;
private KeytabUser keytabUser;
public TestableProcessor(SolrClient solrClient) {
this.solrClient = solrClient;
}
public TestableProcessor(SolrClient solrClient, KeytabUser keytabUser) {
this.solrClient = solrClient;
this.keytabUser = keytabUser;
}
@Override
protected SolrClient createSolrClient(ProcessContext context, String solrLocation) {
return solrClient;
}
@Override
protected KeytabUser createKeytabUser(KerberosCredentialsService kerberosCredentialsService) {
if (keytabUser != null) {
return keytabUser;
} else {
return super.createKeytabUser(kerberosCredentialsService);
}
}
public KeytabUser getMockKerberosKeytabUser() {
return super.getKerberosKeytabUser();
}
}
// Create an EmbeddedSolrClient with the given core name.

View File

@ -37,18 +37,15 @@ import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.impl.Krb5HttpClientConfigurer;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.StringUtils;
import org.apache.solr.common.util.NamedList;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@ -612,39 +609,6 @@ public class TestPutSolrRecord {
runner.assertValid();
}
@Test
public void testJAASClientAppNameValidation() throws InitializationException {
final TestRunner runner = TestRunners.newTestRunner(PutSolrRecord.class);
MockRecordParser recordParser = new MockRecordParser();
recordParser.addRecord(1, "Abhinav","R",8,"Chemistry","term1", 98);
runner.addControllerService("parser", recordParser);
runner.enableControllerService(recordParser);
runner.setProperty(PutSolrRecord.RECORD_READER, "parser");
runner.setProperty(SolrUtils.SOLR_TYPE, SolrUtils.SOLR_TYPE_STANDARD.getValue());
runner.setProperty(SolrUtils.SOLR_LOCATION, "http://localhost:8443/solr");
runner.assertValid();
// clear the jaas config system property if it was set
final String jaasConfig = System.getProperty(Krb5HttpClientConfigurer.LOGIN_CONFIG_PROP);
if (!StringUtils.isEmpty(jaasConfig)) {
System.clearProperty(Krb5HttpClientConfigurer.LOGIN_CONFIG_PROP);
}
// should be invalid if we have a client name but not config file
runner.setProperty(SolrUtils.JAAS_CLIENT_APP_NAME, "Client");
runner.assertNotValid();
// should be invalid if we have a client name that is not in the config file
final File jaasConfigFile = new File("src/test/resources/jaas-client.conf");
System.setProperty(Krb5HttpClientConfigurer.LOGIN_CONFIG_PROP, jaasConfigFile.getAbsolutePath());
runner.assertNotValid();
// should be valid now that the name matches up with the config file
runner.setProperty(SolrUtils.JAAS_CLIENT_APP_NAME, "SolrJClient");
runner.assertValid();
}
/**
* Creates a base TestRunner with Solr Type of standard.
*/