KAFKA-2711; SaslClientAuthenticator no longer needs KerberosNameParser in constructor

Also refactor `KerberosNameParser` and `KerberosName` to make the code
clearer and easier to use when `shortName` is not needed.

Author: Ismael Juma <ismael@juma.me.uk>

Reviewers: Jun Rao <junrao@gmail.com>

Closes #390 from ijuma/kafka-2711
This commit is contained in:
Ismael Juma 2015-10-30 08:53:16 -07:00 committed by Jun Rao
parent 622730905d
commit d9ae33d4c0
9 changed files with 114 additions and 95 deletions

View File

@ -21,7 +21,7 @@ import java.util.Map;
import org.apache.kafka.common.config.SaslConfigs;
import org.apache.kafka.common.security.JaasUtils;
import org.apache.kafka.common.security.auth.PrincipalBuilder;
import org.apache.kafka.common.security.kerberos.KerberosNameParser;
import org.apache.kafka.common.security.kerberos.KerberosShortNamer;
import org.apache.kafka.common.security.kerberos.LoginManager;
import org.apache.kafka.common.security.authenticator.SaslClientAuthenticator;
import org.apache.kafka.common.security.authenticator.SaslServerAuthenticator;
@ -45,7 +45,7 @@ public class SaslChannelBuilder implements ChannelBuilder {
private PrincipalBuilder principalBuilder;
private SslFactory sslFactory;
private Map<String, ?> configs;
private KerberosNameParser kerberosNameParser;
private KerberosShortNamer kerberosShortNamer;
public SaslChannelBuilder(Mode mode, LoginType loginType, SecurityProtocol securityProtocol) {
this.mode = mode;
@ -66,7 +66,10 @@ public class SaslChannelBuilder implements ChannelBuilder {
} catch (Exception ke) {
defaultRealm = "";
}
kerberosNameParser = new KerberosNameParser(defaultRealm, (List<String>) configs.get(SaslConfigs.SASL_KERBEROS_PRINCIPAL_TO_LOCAL_RULES));
List<String> principalToLocalRules = (List<String>) configs.get(SaslConfigs.SASL_KERBEROS_PRINCIPAL_TO_LOCAL_RULES);
if (principalToLocalRules != null)
kerberosShortNamer = KerberosShortNamer.fromUnparsedRules(defaultRealm, principalToLocalRules);
if (this.securityProtocol == SecurityProtocol.SASL_SSL) {
this.sslFactory = new SslFactory(mode);
@ -83,10 +86,10 @@ public class SaslChannelBuilder implements ChannelBuilder {
TransportLayer transportLayer = buildTransportLayer(id, key, socketChannel);
Authenticator authenticator;
if (mode == Mode.SERVER)
authenticator = new SaslServerAuthenticator(id, loginManager.subject(), kerberosNameParser);
authenticator = new SaslServerAuthenticator(id, loginManager.subject(), kerberosShortNamer);
else
authenticator = new SaslClientAuthenticator(id, loginManager.subject(), loginManager.serviceName(),
socketChannel.socket().getInetAddress().getHostName(), kerberosNameParser);
socketChannel.socket().getInetAddress().getHostName());
authenticator.configure(transportLayer, this.principalBuilder, this.configs);
return new KafkaChannel(id, transportLayer, authenticator, maxReceiveSize);
} catch (Exception e) {

View File

@ -48,7 +48,6 @@ import org.apache.kafka.common.security.auth.KafkaPrincipal;
import org.apache.kafka.common.security.auth.PrincipalBuilder;
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.security.kerberos.KerberosNameParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -64,7 +63,6 @@ public class SaslClientAuthenticator implements Authenticator {
private final String servicePrincipal;
private final String host;
private final String node;
private final KerberosNameParser kerberosNameParser;
// assigned in `configure`
private SaslClient saslClient;
@ -77,12 +75,11 @@ public class SaslClientAuthenticator implements Authenticator {
private SaslState saslState = SaslState.INITIAL;
public SaslClientAuthenticator(String node, Subject subject, String servicePrincipal, String host, KerberosNameParser kerberosNameParser) throws IOException {
public SaslClientAuthenticator(String node, Subject subject, String servicePrincipal, String host) throws IOException {
this.node = node;
this.subject = subject;
this.host = host;
this.servicePrincipal = servicePrincipal;
this.kerberosNameParser = kerberosNameParser;
}
public void configure(TransportLayer transportLayer, PrincipalBuilder principalBuilder, Map<String, ?> configs) throws KafkaException {
@ -91,7 +88,7 @@ public class SaslClientAuthenticator implements Authenticator {
// determine client principal from subject.
Principal clientPrincipal = subject.getPrincipals().iterator().next();
this.clientPrincipalName = kerberosNameParser.parse(clientPrincipal.getName()).toString();
this.clientPrincipalName = clientPrincipal.getName();
this.saslClient = createSaslClient();
} catch (Exception e) {
throw new KafkaException("Failed to configure SaslClientAuthenticator", e);

View File

@ -35,7 +35,7 @@ import javax.security.sasl.SaslException;
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.security.kerberos.KerberosName;
import org.apache.kafka.common.security.kerberos.KerberosNameParser;
import org.apache.kafka.common.security.kerberos.KerberosShortNamer;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
@ -59,7 +59,7 @@ public class SaslServerAuthenticator implements Authenticator {
private final SaslServer saslServer;
private final Subject subject;
private final String node;
private final KerberosNameParser kerberosNameParser;
private final KerberosShortNamer kerberosNamer;
// assigned in `configure`
private TransportLayer transportLayer;
@ -68,14 +68,14 @@ public class SaslServerAuthenticator implements Authenticator {
private NetworkReceive netInBuffer;
private NetworkSend netOutBuffer;
public SaslServerAuthenticator(String node, final Subject subject, KerberosNameParser kerberosNameParser) throws IOException {
public SaslServerAuthenticator(String node, final Subject subject, KerberosShortNamer kerberosNameParser) throws IOException {
if (subject == null)
throw new IllegalArgumentException("subject cannot be null");
if (subject.getPrincipals().isEmpty())
throw new IllegalArgumentException("subject must have at least one principal");
this.node = node;
this.subject = subject;
this.kerberosNameParser = kerberosNameParser;
this.kerberosNamer = kerberosNameParser;
saslServer = createSaslServer();
}
@ -86,11 +86,11 @@ public class SaslServerAuthenticator implements Authenticator {
private SaslServer createSaslServer() throws IOException {
// server is using a JAAS-authenticated subject: determine service principal name and hostname from kafka server's subject.
final SaslServerCallbackHandler saslServerCallbackHandler = new SaslServerCallbackHandler(
Configuration.getConfiguration(), kerberosNameParser);
Configuration.getConfiguration(), kerberosNamer);
final Principal servicePrincipal = subject.getPrincipals().iterator().next();
KerberosName kerberosName;
try {
kerberosName = kerberosNameParser.parse(servicePrincipal.getName());
kerberosName = KerberosName.parse(servicePrincipal.getName());
} catch (IllegalArgumentException e) {
throw new KafkaException("Principal has name with unexpected format " + servicePrincipal);
}

View File

@ -20,7 +20,7 @@ package org.apache.kafka.common.security.authenticator;
import java.io.IOException;
import org.apache.kafka.common.security.kerberos.KerberosNameParser;
import org.apache.kafka.common.security.kerberos.KerberosShortNamer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.auth.callback.Callback;
@ -36,13 +36,13 @@ import org.apache.kafka.common.security.JaasUtils;
public class SaslServerCallbackHandler implements CallbackHandler {
private static final Logger LOG = LoggerFactory.getLogger(SaslServerCallbackHandler.class);
private final KerberosNameParser kerberosNameParser;
private final KerberosShortNamer kerberosShortNamer;
public SaslServerCallbackHandler(Configuration configuration, KerberosNameParser kerberosNameParser) throws IOException {
public SaslServerCallbackHandler(Configuration configuration, KerberosShortNamer kerberosNameParser) throws IOException {
AppConfigurationEntry[] configurationEntries = configuration.getAppConfigurationEntry(JaasUtils.LOGIN_CONTEXT_SERVER);
if (configurationEntries == null)
throw new IOException("Could not find a 'KafkaServer' entry in this configuration: Kafka Server cannot start.");
this.kerberosNameParser = kerberosNameParser;
this.kerberosShortNamer = kerberosNameParser;
}
public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
@ -68,9 +68,9 @@ public class SaslServerCallbackHandler implements CallbackHandler {
authorizationID);
ac.setAuthorized(true);
KerberosName kerberosName = kerberosNameParser.parse(authenticationID);
KerberosName kerberosName = KerberosName.parse(authenticationID);
try {
String userName = kerberosName.shortName();
String userName = kerberosShortNamer.shortName(kerberosName);
LOG.info("Setting authorizedID: {}", userName);
ac.setAuthorizedID(userName);
} catch (IOException e) {

View File

@ -0,0 +1,30 @@
/**
* 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.kafka.common.security.kerberos;
import java.io.IOException;
public class BadFormatString extends IOException {
BadFormatString(String msg) {
super(msg);
}
BadFormatString(String msg, Throwable err) {
super(msg, err);
}
}

View File

@ -18,11 +18,16 @@
package org.apache.kafka.common.security.kerberos;
import java.io.IOException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class KerberosName {
/**
* A pattern that matches a Kerberos name with at most 3 components.
*/
private static final Pattern NAME_PARSER = Pattern.compile("([^/@]*)(/([^/@]*))?@([^/@]*)");
/** The first component of the name */
private final String serviceName;
/** The second component of the name. It may be null. */
@ -30,19 +35,31 @@ public class KerberosName {
/** The realm of the name. */
private final String realm;
/* Rules for the translation of the principal name into an operating system name */
private final List<KerberosRule> principalToLocalRules;
/**
* Creates an instance of `KerberosName` with the provided parameters.
*/
public KerberosName(String serviceName, String hostName, String realm, List<KerberosRule> principalToLocalRules) {
public KerberosName(String serviceName, String hostName, String realm) {
if (serviceName == null)
throw new IllegalArgumentException("serviceName must not be null");
this.serviceName = serviceName;
this.hostName = hostName;
this.realm = realm;
this.principalToLocalRules = principalToLocalRules;
}
/**
* Create a name from the full Kerberos principal name.
*/
public static KerberosName parse(String principalName) {
Matcher match = NAME_PARSER.matcher(principalName);
if (!match.matches()) {
if (principalName.contains("@")) {
throw new IllegalArgumentException("Malformed Kerberos name: " + principalName);
} else {
return new KerberosName(principalName, null, null);
}
} else {
return new KerberosName(match.group(1), match.group(3), match.group(4));
}
}
/**
@ -87,28 +104,4 @@ public class KerberosName {
return realm;
}
/**
* Get the translation of the principal name into an operating system
* user name.
* @return the short name
* @throws IOException
*/
public String shortName() throws IOException {
String[] params;
if (hostName == null) {
// if it is already simple, just return it
if (realm == null)
return serviceName;
params = new String[]{realm, serviceName};
} else {
params = new String[]{realm, serviceName, hostName};
}
for (KerberosRule r : principalToLocalRules) {
String result = r.apply(params);
if (result != null)
return result;
}
throw new NoMatchingRule("No rules applied to " + toString());
}
}

View File

@ -108,10 +108,10 @@ class KerberosRule {
* @param format the string to replace parameters again
* @param params the list of parameters
* @return the generated string with the parameter references replaced.
* @throws KerberosNameParser.BadFormatString
* @throws BadFormatString
*/
static String replaceParameters(String format,
String[] params) throws KerberosNameParser.BadFormatString {
String[] params) throws BadFormatString {
Matcher match = PARAMETER_PATTERN.matcher(format);
int start = 0;
StringBuilder result = new StringBuilder();
@ -122,13 +122,13 @@ class KerberosRule {
try {
int num = Integer.parseInt(paramNum);
if (num < 0 || num > params.length) {
throw new KerberosNameParser.BadFormatString("index " + num + " from " + format +
throw new BadFormatString("index " + num + " from " + format +
" is outside of the valid range 0 to " +
(params.length - 1));
}
result.append(params[num]);
} catch (NumberFormatException nfe) {
throw new KerberosNameParser.BadFormatString("bad format in username mapping in " +
throw new BadFormatString("bad format in username mapping in " +
paramNum, nfe);
}

View File

@ -30,42 +30,23 @@ import java.util.regex.Pattern;
* particular, it splits them apart and translates them down into local
* operating system names.
*/
public class KerberosNameParser {
/**
* A pattern that matches a Kerberos name with at most 3 components.
*/
private static final Pattern NAME_PARSER = Pattern.compile("([^/@]*)(/([^/@]*))?@([^/@]*)");
public class KerberosShortNamer {
/**
* A pattern for parsing a auth_to_local rule.
*/
private static final Pattern RULE_PARSER = Pattern.compile("((DEFAULT)|(RULE:\\[(\\d*):([^\\]]*)](\\(([^)]*)\\))?(s/([^/]*)/([^/]*)/(g)?)?))");
/**
* The list of translation rules.
*/
/* Rules for the translation of the principal name into an operating system name */
private final List<KerberosRule> principalToLocalRules;
public KerberosNameParser(String defaultRealm, List<String> principalToLocalRules) {
List<String> rules = principalToLocalRules == null ? Collections.singletonList("DEFAULT") : principalToLocalRules;
this.principalToLocalRules = parseRules(defaultRealm, rules);
public KerberosShortNamer(List<KerberosRule> principalToLocalRules) {
this.principalToLocalRules = principalToLocalRules;
}
/**
* Create a name from the full Kerberos principal name.
*/
public KerberosName parse(String principalName) {
Matcher match = NAME_PARSER.matcher(principalName);
if (!match.matches()) {
if (principalName.contains("@")) {
throw new IllegalArgumentException("Malformed Kerberos name: " + principalName);
} else {
return new KerberosName(principalName, null, null, principalToLocalRules);
}
} else {
return new KerberosName(match.group(1), match.group(3), match.group(4), principalToLocalRules);
}
public static KerberosShortNamer fromUnparsedRules(String defaultRealm, List<String> principalToLocalRules) {
List<String> rules = principalToLocalRules == null ? Collections.singletonList("DEFAULT") : principalToLocalRules;
return new KerberosShortNamer(parseRules(defaultRealm, rules));
}
private static List<KerberosRule> parseRules(String defaultRealm, List<String> rules) {
@ -93,13 +74,28 @@ public class KerberosNameParser {
return result;
}
public static class BadFormatString extends IOException {
BadFormatString(String msg) {
super(msg);
/**
* Get the translation of the principal name into an operating system
* user name.
* @return the short name
* @throws IOException
*/
public String shortName(KerberosName kerberosName) throws IOException {
String[] params;
if (kerberosName.hostName() == null) {
// if it is already simple, just return it
if (kerberosName.realm() == null)
return kerberosName.serviceName();
params = new String[]{kerberosName.realm(), kerberosName.serviceName()};
} else {
params = new String[]{kerberosName.realm(), kerberosName.serviceName(), kerberosName.hostName()};
}
BadFormatString(String msg, Throwable err) {
super(msg, err);
for (KerberosRule r : principalToLocalRules) {
String result = r.apply(params);
if (result != null)
return result;
}
throw new NoMatchingRule("No rules applied to " + toString());
}
}

View File

@ -36,24 +36,24 @@ public class KerberosNameTest {
"RULE:[2:$1](App\\..*)s/App\\.(.*)/$1/g",
"DEFAULT"
));
KerberosNameParser parser = new KerberosNameParser("REALM.COM", rules);
KerberosShortNamer shortNamer = KerberosShortNamer.fromUnparsedRules("REALM.COM", rules);
KerberosName name = parser.parse("App.service-name/example.com@REALM.COM");
KerberosName name = KerberosName.parse("App.service-name/example.com@REALM.COM");
assertEquals("App.service-name", name.serviceName());
assertEquals("example.com", name.hostName());
assertEquals("REALM.COM", name.realm());
assertEquals("service-name", name.shortName());
assertEquals("service-name", shortNamer.shortName(name));
name = parser.parse("App.service-name@REALM.COM");
name = KerberosName.parse("App.service-name@REALM.COM");
assertEquals("App.service-name", name.serviceName());
assertNull(name.hostName());
assertEquals("REALM.COM", name.realm());
assertEquals("service-name", name.shortName());
assertEquals("service-name", shortNamer.shortName(name));
name = parser.parse("user/host@REALM.COM");
name = KerberosName.parse("user/host@REALM.COM");
assertEquals("user", name.serviceName());
assertEquals("host", name.hostName());
assertEquals("REALM.COM", name.realm());
assertEquals("user", name.shortName());
assertEquals("user", shortNamer.shortName(name));
}
}