-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-4292: Configurable SASL callback handlers #2022
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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, | ||
|
|
@@ -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(); | ||
|
|
@@ -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()); | ||
| } | ||
|
|
@@ -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); | ||
| } | ||
|
|
@@ -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) { | ||
|
|
@@ -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 { | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
|
|
@@ -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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
|---|---|---|
|
|
@@ -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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| */ | ||
| void configure(Map<String, ?> configs, Mode mode, Subject subject, String saslMechanism); | ||
| void configure(Map<String, ?> configs, String saslMechanism, List<AppConfigurationEntry> jaasConfigEntries); | ||
|
|
||
| /** | ||
| * Closes this instance. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
||
|
|
@@ -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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
@@ -54,4 +54,3 @@ public interface Login { | |
| */ | ||
| void close(); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.