Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ public class SaslConfigs {
+ "JAAS configuration file format is described <a href=\"http://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/tutorials/LoginConfigFile.html\">here</a>. "
+ "The format for the value is: '<loginModuleClass> <controlFlag> (<optionName>=<optionValue>)*;'";

public static final String SASL_CLIENT_CALLBACK_HANDLER_CLASS = "sasl.client.callback.handler.class";
public static final String SASL_CLIENT_CALLBACK_HANDLER_CLASS_DOC = "The fully qualified name of a SASL client callback handler class "
+ "that implements the AuthenticateCallbackHandler interface.";

public static final String SASL_LOGIN_CALLBACK_HANDLER_CLASS = "sasl.login.callback.handler.class";
public static final String SASL_LOGIN_CALLBACK_HANDLER_CLASS_DOC = "The fully qualified name of a SASL login callback handler class "

@rondagostino rondagostino Mar 30, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this documentation should state that "For brokers, login callback handler config must be prefixed with listener prefix and SASL mechanism name in lower-case" because this is valid to use on both the client-side and the broker side.

+ "that implements the AuthenticateCallbackHandler interface.";

public static final String SASL_LOGIN_CLASS = "sasl.login.class";
public static final String SASL_LOGIN_CLASS_DOC = "The fully qualified name of a class that implements the Login interface. "
+ "For brokers, login config must be prefixed with listener prefix and SASL mechanism name in lower-case. For example, "
+ "listener.name.sasl_ssl.scram-sha-256.sasl.login.class=com.example.CustomScramLogin";

public static final String SASL_KERBEROS_SERVICE_NAME = "sasl.kerberos.service.name";
public static final String SASL_KERBEROS_SERVICE_NAME_DOC = "The Kerberos principal name that Kafka runs as. "
+ "This can be defined either in Kafka's JAAS config or in Kafka's config.";
Expand Down Expand Up @@ -95,6 +108,9 @@ public static void addClientSaslSupport(ConfigDef config) {
.define(SaslConfigs.SASL_KERBEROS_TICKET_RENEW_JITTER, ConfigDef.Type.DOUBLE, SaslConfigs.DEFAULT_KERBEROS_TICKET_RENEW_JITTER, ConfigDef.Importance.LOW, SaslConfigs.SASL_KERBEROS_TICKET_RENEW_JITTER_DOC)
.define(SaslConfigs.SASL_KERBEROS_MIN_TIME_BEFORE_RELOGIN, ConfigDef.Type.LONG, SaslConfigs.DEFAULT_KERBEROS_MIN_TIME_BEFORE_RELOGIN, ConfigDef.Importance.LOW, SaslConfigs.SASL_KERBEROS_MIN_TIME_BEFORE_RELOGIN_DOC)
.define(SaslConfigs.SASL_MECHANISM, ConfigDef.Type.STRING, SaslConfigs.DEFAULT_SASL_MECHANISM, ConfigDef.Importance.MEDIUM, SaslConfigs.SASL_MECHANISM_DOC)
.define(SaslConfigs.SASL_JAAS_CONFIG, ConfigDef.Type.PASSWORD, null, ConfigDef.Importance.MEDIUM, SaslConfigs.SASL_JAAS_CONFIG_DOC);
.define(SaslConfigs.SASL_JAAS_CONFIG, ConfigDef.Type.PASSWORD, null, ConfigDef.Importance.MEDIUM, SaslConfigs.SASL_JAAS_CONFIG_DOC)
.define(SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS, ConfigDef.Type.CLASS, null, ConfigDef.Importance.MEDIUM, SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS_DOC)
.define(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS, ConfigDef.Type.CLASS, null, ConfigDef.Importance.MEDIUM, SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS_DOC)
.define(SaslConfigs.SASL_LOGIN_CLASS, ConfigDef.Type.CLASS, null, ConfigDef.Importance.MEDIUM, SaslConfigs.SASL_LOGIN_CLASS_DOC);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class BrokerSecurityConfigs {
public static final String SASL_KERBEROS_PRINCIPAL_TO_LOCAL_RULES_CONFIG = "sasl.kerberos.principal.to.local.rules";
public static final String SSL_CLIENT_AUTH_CONFIG = "ssl.client.auth";
public static final String SASL_ENABLED_MECHANISMS_CONFIG = "sasl.enabled.mechanisms";
public static final String SASL_SERVER_CALLBACK_HANDLER_CLASS = "sasl.server.callback.handler.class";

public static final String PRINCIPAL_BUILDER_CLASS_DOC = "The fully qualified name of a class that implements the " +
"KafkaPrincipalBuilder interface, which is used to build the KafkaPrincipal object used during " +
Expand Down Expand Up @@ -67,4 +68,9 @@ public class BrokerSecurityConfigs {
+ "Only GSSAPI is enabled by default.";
public static final List<String> DEFAULT_SASL_ENABLED_MECHANISMS = Collections.singletonList(SaslConfigs.GSSAPI_MECHANISM);

public static final String SASL_SERVER_CALLBACK_HANDLER_CLASS_DOC = "The fully qualified name of a SASL server callback handler "
+ "class that implements the AuthenticateCallbackHandler interface. Server callback handlers must be prefixed with "
+ "listener prefix and SASL mechanism name in lower-case. For example, "
+ "listener.name.sasl_ssl.plain.sasl.server.callback.handler.class=com.example.CustomPlainCallbackHandler.";

}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ public String configPrefix() {
}

public String saslMechanismConfigPrefix(String saslMechanism) {
return configPrefix() + saslMechanism.toLowerCase(Locale.ROOT) + ".";
return configPrefix() + saslMechanismPrefix(saslMechanism);
}

public static String saslMechanismPrefix(String saslMechanism) {
return saslMechanism.toLowerCase(Locale.ROOT) + ".";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,35 @@
import org.apache.kafka.common.config.SslConfigs;
import org.apache.kafka.common.config.internals.BrokerSecurityConfigs;
import org.apache.kafka.common.memory.MemoryPool;
import org.apache.kafka.common.security.auth.SecurityProtocol;
import org.apache.kafka.common.security.JaasContext;
import org.apache.kafka.common.security.token.delegation.DelegationTokenCache;
import org.apache.kafka.common.security.kerberos.KerberosShortNamer;
import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
import org.apache.kafka.common.security.auth.Login;
import org.apache.kafka.common.security.auth.SecurityProtocol;
import org.apache.kafka.common.security.authenticator.CredentialCache;
import org.apache.kafka.common.security.authenticator.DefaultLogin;
import org.apache.kafka.common.security.authenticator.LoginManager;
import org.apache.kafka.common.security.authenticator.SaslClientAuthenticator;
import org.apache.kafka.common.security.authenticator.SaslClientCallbackHandler;
import org.apache.kafka.common.security.authenticator.SaslServerAuthenticator;
import org.apache.kafka.common.security.authenticator.SaslServerCallbackHandler;
import org.apache.kafka.common.security.kerberos.KerberosClientCallbackHandler;
import org.apache.kafka.common.security.kerberos.KerberosLogin;
import org.apache.kafka.common.security.kerberos.KerberosShortNamer;
import org.apache.kafka.common.security.plain.internal.PlainSaslServer;
import org.apache.kafka.common.security.plain.internal.PlainServerCallbackHandler;
import org.apache.kafka.common.security.scram.ScramCredential;
import org.apache.kafka.common.security.scram.internal.ScramMechanism;
import org.apache.kafka.common.security.scram.internal.ScramServerCallbackHandler;
import org.apache.kafka.common.security.ssl.SslFactory;
import org.apache.kafka.common.security.token.delegation.DelegationTokenCache;
import org.apache.kafka.common.utils.Java;
import org.apache.kafka.common.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.io.IOException;
import java.net.Socket;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
Expand Down Expand Up @@ -66,6 +79,7 @@ public class SaslChannelBuilder implements ChannelBuilder, ListenerReconfigurabl
private SslFactory sslFactory;
private Map<String, ?> configs;
private KerberosShortNamer kerberosShortNamer;
private Map<String, AuthenticateCallbackHandler> saslCallbackHandlers;

public SaslChannelBuilder(Mode mode,
Map<String, JaasContext> jaasContexts,
Expand All @@ -87,15 +101,25 @@ public SaslChannelBuilder(Mode mode,
this.clientSaslMechanism = clientSaslMechanism;
this.credentialCache = credentialCache;
this.tokenCache = tokenCache;
this.saslCallbackHandlers = new HashMap<>();
}

@SuppressWarnings("unchecked")
@Override
public void configure(Map<String, ?> configs) throws KafkaException {
try {
this.configs = configs;
boolean hasKerberos = jaasContexts.containsKey(SaslConfigs.GSSAPI_MECHANISM);
if (mode == Mode.SERVER)
createServerCallbackHandlers(configs);
else
createClientCallbackHandler(configs);
for (Map.Entry<String, AuthenticateCallbackHandler> entry : saslCallbackHandlers.entrySet()) {
String mechanism = entry.getKey();
entry.getValue().configure(configs, mechanism, jaasContexts.get(mechanism).configurationEntries());
}

if (hasKerberos) {
Class<? extends Login> defaultLoginClass = DefaultLogin.class;
if (jaasContexts.containsKey(SaslConfigs.GSSAPI_MECHANISM)) {
String defaultRealm;
try {
defaultRealm = defaultKerberosRealm();
Expand All @@ -106,12 +130,13 @@ public void configure(Map<String, ?> configs) throws KafkaException {
List<String> principalToLocalRules = (List<String>) configs.get(BrokerSecurityConfigs.SASL_KERBEROS_PRINCIPAL_TO_LOCAL_RULES_CONFIG);
if (principalToLocalRules != null)
kerberosShortNamer = KerberosShortNamer.fromUnparsedRules(defaultRealm, principalToLocalRules);
defaultLoginClass = KerberosLogin.class;
}
for (Map.Entry<String, JaasContext> entry : jaasContexts.entrySet()) {
String mechanism = entry.getKey();
// With static JAAS configuration, use KerberosLogin if Kerberos is enabled. With dynamic JAAS configuration,
// use KerberosLogin only for the LoginContext corresponding to GSSAPI
LoginManager loginManager = LoginManager.acquireLoginManager(entry.getValue(), mechanism, hasKerberos, configs);
LoginManager loginManager = LoginManager.acquireLoginManager(entry.getValue(), mechanism, defaultLoginClass, configs);
loginManagers.put(mechanism, loginManager);
subjects.put(mechanism, loginManager.subject());
}
Expand All @@ -120,7 +145,7 @@ public void configure(Map<String, ?> configs) throws KafkaException {
this.sslFactory = new SslFactory(mode, "none", isInterBrokerListener);
this.sslFactory.configure(configs);
}
} catch (Exception e) {
} catch (Throwable e) {
close();
throw new KafkaException(e);
}
Expand Down Expand Up @@ -156,11 +181,20 @@ public KafkaChannel buildChannel(String id, SelectionKey key, int maxReceiveSize
TransportLayer transportLayer = buildTransportLayer(id, key, socketChannel);
Authenticator authenticator;
if (mode == Mode.SERVER) {
authenticator = buildServerAuthenticator(configs, id, transportLayer, subjects);
authenticator = buildServerAuthenticator(configs,
saslCallbackHandlers,
id,
transportLayer,
subjects);
} else {
LoginManager loginManager = loginManagers.get(clientSaslMechanism);
authenticator = buildClientAuthenticator(configs, id, socket.getInetAddress().getHostName(),
loginManager.serviceName(), transportLayer, loginManager.subject());
authenticator = buildClientAuthenticator(configs,
saslCallbackHandlers.get(clientSaslMechanism),
id,
socket.getInetAddress().getHostName(),
loginManager.serviceName(),
transportLayer,
subjects.get(clientSaslMechanism));
}
return new KafkaChannel(id, transportLayer, authenticator, maxReceiveSize, memoryPool != null ? memoryPool : MemoryPool.NONE);
} catch (Exception e) {
Expand All @@ -174,6 +208,8 @@ public void close() {
for (LoginManager loginManager : loginManagers.values())
loginManager.release();
loginManagers.clear();
for (AuthenticateCallbackHandler handler : saslCallbackHandlers.values())
handler.close();
}

private TransportLayer buildTransportLayer(String id, SelectionKey key, SocketChannel socketChannel) throws IOException {
Expand All @@ -186,16 +222,24 @@ private TransportLayer buildTransportLayer(String id, SelectionKey key, SocketCh
}

// Visible to override for testing
protected SaslServerAuthenticator buildServerAuthenticator(Map<String, ?> configs, String id,
TransportLayer transportLayer, Map<String, Subject> subjects) throws IOException {
return new SaslServerAuthenticator(configs, id, jaasContexts, subjects,
protected SaslServerAuthenticator buildServerAuthenticator(Map<String, ?> configs,
Map<String, AuthenticateCallbackHandler> callbackHandlers,
String id,
TransportLayer transportLayer,
Map<String, Subject> subjects) throws IOException {
return new SaslServerAuthenticator(configs, callbackHandlers, id, jaasContexts, subjects,
kerberosShortNamer, credentialCache, listenerName, securityProtocol, transportLayer, tokenCache);
}

// Visible to override for testing
protected SaslClientAuthenticator buildClientAuthenticator(Map<String, ?> configs, String id,
String serverHost, String servicePrincipal, TransportLayer transportLayer, Subject subject) throws IOException {
return new SaslClientAuthenticator(configs, id, subject, servicePrincipal,
protected SaslClientAuthenticator buildClientAuthenticator(Map<String, ?> configs,
AuthenticateCallbackHandler callbackHandler,
String id,
String serverHost,
String servicePrincipal,
TransportLayer transportLayer, Subject subject) throws IOException {
return new SaslClientAuthenticator(configs, callbackHandler,
id, jaasContexts.get(clientSaslMechanism), subject, servicePrincipal,
serverHost, clientSaslMechanism, handshakeRequestEnable, transportLayer);
}

Expand Down Expand Up @@ -224,4 +268,29 @@ private static String defaultKerberosRealm() throws ClassNotFoundException, NoSu
getDefaultRealmMethod = classRef.getDeclaredMethod("getDefaultRealm", new Class[0]);
return (String) getDefaultRealmMethod.invoke(kerbConf, new Object[0]);
}

private void createClientCallbackHandler(Map<String, ?> configs) {
Class<? extends AuthenticateCallbackHandler> clazz = (Class<? extends AuthenticateCallbackHandler>) configs.get(SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment below about the order of built-in mechanisms vs. non-built-in ones.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as before, callback handlers can be overridden for built-in mechanisms too.

if (clazz == null)
clazz = clientSaslMechanism.equals(SaslConfigs.GSSAPI_MECHANISM) ? KerberosClientCallbackHandler.class : SaslClientCallbackHandler.class;
AuthenticateCallbackHandler callbackHandler = Utils.newInstance(clazz);
saslCallbackHandlers.put(clientSaslMechanism, callbackHandler);
}

private void createServerCallbackHandlers(Map<String, ?> configs) throws ClassNotFoundException {
for (String mechanism : jaasContexts.keySet()) {
AuthenticateCallbackHandler callbackHandler;
String prefix = ListenerName.saslMechanismPrefix(mechanism);
String className = (String) configs.get(prefix + BrokerSecurityConfigs.SASL_SERVER_CALLBACK_HANDLER_CLASS);
if (className != null)
callbackHandler = Utils.newInstance(className, AuthenticateCallbackHandler.class);
else if (mechanism.equals(PlainSaslServer.PLAIN_MECHANISM))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it ever be acceptable to use the config map to override the callback handlers for the built-in mechanism implementations (GSSAPI, PLAIN, and the two SCRAM-related ones)? If so then the code is fine, but if not then the built-in mechanism names should be explicitly checked for first. Same goes for the client side checks above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rondagostino Thanks for the review. Yes, it is acceptable to override callback handlers for built-in mechanisms. One of the motivations for the KIP is to enable customization of built-in mechanisms to integrate with existing authentication servers (e.g. use an existing server or database to verify passwords for PLAIN).

callbackHandler = new PlainServerCallbackHandler();
else if (ScramMechanism.isScram(mechanism))
callbackHandler = new ScramServerCallbackHandler(credentialCache.cache(mechanism, ScramCredential.class), tokenCache);
else
callbackHandler = new SaslServerCallbackHandler();
saslCallbackHandlers.put(mechanism, callbackHandler);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,14 @@ public Password dynamicJaasConfig() {
* If login module name is specified, return option value only from that module.
*/
public String configEntryOption(String key, String loginModuleName) {
return configEntryOption(configurationEntries, key, loginModuleName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems configEntryOption() is never used?

}

/**
* Returns the configuration option for <code>key</code> from this context.
* If login module name is specified, return option value only from that module.
*/
public static String configEntryOption(List<AppConfigurationEntry> configurationEntries, String key, String loginModuleName) {
for (AppConfigurationEntry entry : configurationEntries) {
if (loginModuleName != null && !loginModuleName.equals(entry.getLoginModuleName()))
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.kafka.common.security.authenticator;

import java.util.Map;
package org.apache.kafka.common.security.auth;

import org.apache.kafka.common.network.Mode;
import java.util.List;
import java.util.Map;

import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.AppConfigurationEntry;

/*
* Callback handler for SASL-based authentication
*/
public interface AuthCallbackHandler extends CallbackHandler {
public interface AuthenticateCallbackHandler extends CallbackHandler {

/**
* Configures this callback handler.
*
* @param configs Configuration
* @param mode The mode that indicates if this is a client or server connection
* @param subject Subject from login context
* Configures this callback handler for the specified SASL mechanism.
* @param configs Configuration options
* @param saslMechanism Negotiated SASL mechanism
* @param jaasConfigEntries JAAS configuration entries from the JAAS login context.
* This list contains a single entry for clients and may contain more than
* one entry for servers if multiple mechanisms are enabled on a listener.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, in the ChannelBuilder, it seems that we always pass in the entry for one mechanism.

Also, could we add some comments to clarify the difference between configs and jaasConfigEntries? For example, if a key config is present in both, which one takes precedence.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jaasConfigEntries can contain multiple entries if using static JAAS config with multiple entries in a single KafkaServer login context when multiple mechanisms are enabled. Updated the doc to reflect that and also added clarification for configs and jaasConfigEntries.

*/
void configure(Map<String, ?> configs, Mode mode, Subject subject, String saslMechanism);
void configure(Map<String, ?> configs, String saslMechanism, List<AppConfigurationEntry> jaasConfigEntries);

/**
* Closes this instance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.kafka.common.security.authenticator;

import org.apache.kafka.common.security.JaasContext;
package org.apache.kafka.common.security.auth;

import java.util.Map;

import javax.security.auth.Subject;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

Expand All @@ -32,7 +31,8 @@ public interface Login {
/**
* Configures this login instance.
*/
void configure(Map<String, ?> configs, JaasContext jaasContext);
void configure(Map<String, ?> configs, String contextName, Configuration configuration,
AuthenticateCallbackHandler loginCallbackHandler);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be useful to add a comment to describe the difference between configs and Configuration.


/**
* Performs login for each login module specified for the login context of this instance.
Expand All @@ -54,4 +54,3 @@ public interface Login {
*/
void close();
}

Loading