Skip to content

Commit b81d646

Browse files
authored
Kerberos auth to local rules support (#2043)
1 parent ed88249 commit b81d646

20 files changed

+1035
-189
lines changed

cruise-control/src/main/java/com/linkedin/kafka/cruisecontrol/config/constants/WebServerConfig.java

+17
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,18 @@ public final class WebServerConfig {
321321
+ "to authenticate clients. This principal is stored in spnego.keytab.file. This must be a fully qualified principal "
322322
+ "in the service/host@REALM format (service is usually HTTP).";
323323

324+
/**
325+
* <code>spnego.principal.to.local.rules</code>
326+
*/
327+
public static final String SPNEGO_PRINCIPAL_TO_LOCAL_RULES_CONFIG =
328+
"spnego.principal.to.local.rules";
329+
public static final String DEFAULT_SPNEGO_PRINCIPAL_TO_LOCAL_RULES = null;
330+
public static final String SPNEGO_PRINCIPAL_TO_LOCAL_RULES_DOC = "A list of rules for mapping from principal "
331+
+ "names to short names (typically operating system usernames). The rules are evaluated in order and the "
332+
+ "first rule that matches a principal name is used to map it to a short name. Any later rules in the list are "
333+
+ "ignored. By default, principal names of the form <code>{username}/{hostname}@{REALM}</code> are mapped "
334+
+ "to <code>{username}</code>. When not specified, the short name will be used.";
335+
324336
/**
325337
* <code>trusted.proxy.services</code>
326338
*/
@@ -573,6 +585,11 @@ public static ConfigDef define(ConfigDef configDef) {
573585
DEFAULT_SPNEGO_PRINCIPAL,
574586
ConfigDef.Importance.MEDIUM,
575587
SPNEGO_PRINCIPAL_DOC)
588+
.define(SPNEGO_PRINCIPAL_TO_LOCAL_RULES_CONFIG,
589+
ConfigDef.Type.LIST,
590+
DEFAULT_SPNEGO_PRINCIPAL_TO_LOCAL_RULES,
591+
ConfigDef.Importance.MEDIUM,
592+
SPNEGO_PRINCIPAL_TO_LOCAL_RULES_DOC)
576593
.define(TRUSTED_PROXY_SERVICES_CONFIG,
577594
ConfigDef.Type.LIST,
578595
DEFAULT_TRUSTED_PROXY_SERVICES,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2023 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information.
3+
*/
4+
5+
package com.linkedin.kafka.cruisecontrol.servlet.security;
6+
7+
import org.eclipse.jetty.security.SpnegoUserIdentity;
8+
import org.eclipse.jetty.security.SpnegoUserPrincipal;
9+
import org.eclipse.jetty.security.authentication.AuthorizationService;
10+
import org.eclipse.jetty.server.UserIdentity;
11+
import org.eclipse.jetty.util.security.Credential;
12+
import javax.security.auth.Subject;
13+
import javax.servlet.http.HttpServletRequest;
14+
import java.security.Principal;
15+
16+
public class DummyAuthorizationService implements AuthorizationService {
17+
18+
private static final Credential NO_CREDENTIAL = new Credential() {
19+
@Override
20+
public boolean check(Object credentials) {
21+
return false;
22+
}
23+
};
24+
25+
@Override
26+
public UserIdentity getUserIdentity(HttpServletRequest request, String name) {
27+
return createUserIdentity(name);
28+
}
29+
30+
private UserIdentity createUserIdentity(String username) {
31+
Principal userPrincipal = new SpnegoUserPrincipal(username, "");
32+
Subject subject = new Subject();
33+
subject.getPrincipals().add(userPrincipal);
34+
subject.getPrivateCredentials().add(NO_CREDENTIAL);
35+
subject.setReadOnly();
36+
37+
return new SpnegoUserIdentity(subject, userPrincipal, null);
38+
}
39+
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2023 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information.
3+
*/
4+
5+
package com.linkedin.kafka.cruisecontrol.servlet.security.spnego;
6+
7+
import java.util.Objects;
8+
9+
public class PrincipalName {
10+
private final String _primary;
11+
private final String _instance;
12+
private final String _realm;
13+
14+
public PrincipalName(String primary, String instance, String realm) {
15+
_primary = Objects.requireNonNull(primary, "primary must not be null");
16+
_instance = instance;
17+
_realm = realm;
18+
}
19+
20+
public PrincipalName(String primary) {
21+
_primary = Objects.requireNonNull(primary, "primary must not be null");
22+
_instance = null;
23+
_realm = null;
24+
}
25+
26+
public String getPrimary() {
27+
return _primary;
28+
}
29+
30+
public String getInstance() {
31+
return _instance;
32+
}
33+
34+
public String getRealm() {
35+
return _realm;
36+
}
37+
38+
@Override
39+
public boolean equals(Object o) {
40+
if (this == o) {
41+
return true;
42+
}
43+
if (o == null || !Objects.equals(getClass(), o.getClass())) {
44+
return false;
45+
}
46+
PrincipalName principalName = (PrincipalName) o;
47+
return _primary.equals(principalName._primary) && Objects.equals(_instance, principalName._instance)
48+
&& Objects.equals(_realm, principalName._realm);
49+
}
50+
51+
@Override
52+
public int hashCode() {
53+
return Objects.hash(_primary, _instance, _realm);
54+
}
55+
56+
@Override
57+
public String toString() {
58+
return "PrincipalName{"
59+
+ "primary='" + _primary + '\''
60+
+ ", instance='" + _instance + '\''
61+
+ ", realm='" + _realm + '\''
62+
+ '}';
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2023 LinkedIn Corp. Licensed under the BSD 2-Clause License (the "License"). See License in the project root for license information.
3+
*/
4+
5+
package com.linkedin.kafka.cruisecontrol.servlet.security.spnego;
6+
7+
import org.apache.kafka.common.config.ConfigDef.Validator;
8+
import org.apache.kafka.common.config.ConfigException;
9+
import java.util.regex.Matcher;
10+
import java.util.regex.Pattern;
11+
12+
public class PrincipalValidator implements Validator {
13+
private static final Pattern PRINCIPAL_REGEX =
14+
Pattern.compile("(?<primary>[^/\\s@]+)(/(?<instance>[\\w.-]+))?(@(?<realm>(\\S+)))?");
15+
16+
private final boolean _instanceRequired;
17+
private final boolean _realmRequired;
18+
19+
public PrincipalValidator(boolean instanceRequired, boolean realmRequired) {
20+
_instanceRequired = instanceRequired;
21+
_realmRequired = realmRequired;
22+
}
23+
24+
/**
25+
* Creates a PrincipalName object.
26+
* @param configName The name of the configuration
27+
* @param principal The principal which will be the base of the PrincipalName object
28+
* @return PrincipalName object
29+
*/
30+
public static PrincipalName parsePrincipal(String configName, String principal) {
31+
Matcher matcher = PRINCIPAL_REGEX.matcher(principal);
32+
if (!matcher.matches()) {
33+
throw new ConfigException(configName, principal, "Invalid principal");
34+
}
35+
String primary = matcher.group("primary");
36+
String instance = matcher.group("instance");
37+
String realm = matcher.group("realm");
38+
return new PrincipalName(primary, instance, realm);
39+
}
40+
41+
@Override
42+
public void ensureValid(String name, Object value) {
43+
if (value == null) {
44+
return;
45+
}
46+
47+
if (!(value instanceof String)) {
48+
throw new ConfigException(name, value, "Value must be string");
49+
}
50+
51+
String strVal = (String) value;
52+
PrincipalName principalName = parsePrincipal(name, strVal);
53+
if (_instanceRequired && principalName.getInstance() == null) {
54+
throw new ConfigException(name, strVal, "Principal must contain the instance section");
55+
}
56+
if (_realmRequired && principalName.getRealm() == null) {
57+
throw new ConfigException(name, strVal, "Principal must contain the realm section");
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)