mirror of
https://github.com/apache/nifi.git
synced 2025-02-28 14:39:10 +00:00
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:
parent
9093d280f2
commit
f69b720464
9
NOTICE
9
NOTICE
@ -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
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user