[Kerberos] Add realm name & UPN to user metadata (#33338)
We have a Kerberos setting to remove realm part from the user principal name (remove_realm_name). If this is true then the realm name is removed to form username but in the process, the realm name is lost. For scenarios like Kerberos cross-realm authentication, one could make use of the realm name to determine role mapping for users coming from different realms. This commit adds user metadata for kerberos_realm and kerberos_user_principal_name.
This commit is contained in:
parent
8ae1eeb303
commit
d810f1b094
|
@ -165,6 +165,12 @@ POST _xpack/security/role_mapping/kerbrolemapping
|
|||
--------------------------------------------------
|
||||
// CONSOLE
|
||||
|
||||
In case you want to support Kerberos cross realm authentication you may
|
||||
need to map roles based on the Kerberos realm name. For such scenarios
|
||||
following are the additional user metadata available for role mapping:
|
||||
- `kerberos_realm` will be set to Kerberos realm name.
|
||||
- `kerberos_user_principal_name` will be set to user principal name from the Kerberos ticket.
|
||||
|
||||
For more information, see {stack-ov}/mapping-roles.html[Mapping users and groups to roles].
|
||||
|
||||
NOTE: The Kerberos realm supports
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.ietf.jgss.GSSException;
|
|||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -58,6 +59,9 @@ import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthentica
|
|||
*/
|
||||
public final class KerberosRealm extends Realm implements CachingRealm {
|
||||
|
||||
public static final String KRB_METADATA_REALM_NAME_KEY = "kerberos_realm";
|
||||
public static final String KRB_METADATA_UPN_KEY = "kerberos_user_principal_name";
|
||||
|
||||
private final Cache<String, User> userPrincipalNameToUserCache;
|
||||
private final NativeRoleMappingStore userRoleMapper;
|
||||
private final KerberosTicketValidator kerberosTicketValidator;
|
||||
|
@ -151,8 +155,7 @@ public final class KerberosRealm extends Realm implements CachingRealm {
|
|||
kerberosTicketValidator.validateTicket((byte[]) kerbAuthnToken.credentials(), keytabPath, enableKerberosDebug,
|
||||
ActionListener.wrap(userPrincipalNameOutToken -> {
|
||||
if (userPrincipalNameOutToken.v1() != null) {
|
||||
final String username = maybeRemoveRealmName(userPrincipalNameOutToken.v1());
|
||||
resolveUser(username, userPrincipalNameOutToken.v2(), listener);
|
||||
resolveUser(userPrincipalNameOutToken.v1(), userPrincipalNameOutToken.v2(), listener);
|
||||
} else {
|
||||
/**
|
||||
* This is when security context could not be established may be due to ongoing
|
||||
|
@ -171,23 +174,8 @@ public final class KerberosRealm extends Realm implements CachingRealm {
|
|||
}, e -> handleException(e, listener)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Usually principal names are in the form 'user/instance@REALM'. This method
|
||||
* removes '@REALM' part from the principal name if
|
||||
* {@link KerberosRealmSettings#SETTING_REMOVE_REALM_NAME} is {@code true} else
|
||||
* will return the input string.
|
||||
*
|
||||
* @param principalName user principal name
|
||||
* @return username after removal of realm
|
||||
*/
|
||||
protected String maybeRemoveRealmName(final String principalName) {
|
||||
if (this.removeRealmName) {
|
||||
int foundAtIndex = principalName.indexOf('@');
|
||||
if (foundAtIndex > 0) {
|
||||
return principalName.substring(0, foundAtIndex);
|
||||
}
|
||||
}
|
||||
return principalName;
|
||||
private String[] splitUserPrincipalName(final String userPrincipalName) {
|
||||
return userPrincipalName.split("@");
|
||||
}
|
||||
|
||||
private void handleException(Exception e, final ActionListener<AuthenticationResult> listener) {
|
||||
|
@ -205,13 +193,21 @@ public final class KerberosRealm extends Realm implements CachingRealm {
|
|||
}
|
||||
}
|
||||
|
||||
private void resolveUser(final String username, final String outToken, final ActionListener<AuthenticationResult> listener) {
|
||||
private void resolveUser(final String userPrincipalName, final String outToken, final ActionListener<AuthenticationResult> listener) {
|
||||
// if outToken is present then it needs to be communicated with peer, add it to
|
||||
// response header in thread context.
|
||||
if (Strings.hasText(outToken)) {
|
||||
threadPool.getThreadContext().addResponseHeader(WWW_AUTHENTICATE, NEGOTIATE_AUTH_HEADER_PREFIX + outToken);
|
||||
}
|
||||
|
||||
final String[] userAndRealmName = splitUserPrincipalName(userPrincipalName);
|
||||
/*
|
||||
* Usually principal names are in the form 'user/instance@REALM'. If
|
||||
* KerberosRealmSettings#SETTING_REMOVE_REALM_NAME is true then remove
|
||||
* '@REALM' part from the user principal name to get username.
|
||||
*/
|
||||
final String username = (this.removeRealmName) ? userAndRealmName[0] : userPrincipalName;
|
||||
|
||||
if (delegatedRealms.hasDelegation()) {
|
||||
delegatedRealms.resolve(username, listener);
|
||||
} else {
|
||||
|
@ -219,15 +215,19 @@ public final class KerberosRealm extends Realm implements CachingRealm {
|
|||
if (user != null) {
|
||||
listener.onResponse(AuthenticationResult.success(user));
|
||||
} else {
|
||||
buildUser(username, listener);
|
||||
final String realmName = (userAndRealmName.length > 1) ? userAndRealmName[1] : null;
|
||||
final Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put(KRB_METADATA_REALM_NAME_KEY, realmName);
|
||||
metadata.put(KRB_METADATA_UPN_KEY, userPrincipalName);
|
||||
buildUser(username, metadata, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void buildUser(final String username, final ActionListener<AuthenticationResult> listener) {
|
||||
final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Collections.emptySet(), null, this.config);
|
||||
private void buildUser(final String username, final Map<String, Object> metadata, final ActionListener<AuthenticationResult> listener) {
|
||||
final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Collections.emptySet(), metadata, this.config);
|
||||
userRoleMapper.resolveRoles(userData, ActionListener.wrap(roles -> {
|
||||
final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true);
|
||||
final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, userData.getMetadata(), true);
|
||||
if (userPrincipalNameToUserCache != null) {
|
||||
userPrincipalNameToUserCache.put(username, computedUser);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,9 @@ import org.ietf.jgss.GSSException;
|
|||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
|
@ -86,7 +88,10 @@ public class KerberosRealmAuthenticateFailedTests extends KerberosRealmTestCase
|
|||
assertThat(result, is(notNullValue()));
|
||||
if (validTicket) {
|
||||
final String expectedUsername = maybeRemoveRealmName(username);
|
||||
final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true);
|
||||
final Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(username));
|
||||
metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, username);
|
||||
final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true);
|
||||
assertSuccessAuthenticationResult(expectedUser, outToken, result);
|
||||
} else {
|
||||
assertThat(result.getStatus(), is(equalTo(AuthenticationResult.Status.TERMINATE)));
|
||||
|
|
|
@ -18,7 +18,9 @@ import org.ietf.jgss.GSSException;
|
|||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
|
@ -40,7 +42,10 @@ public class KerberosRealmCacheTests extends KerberosRealmTestCase {
|
|||
final KerberosRealm kerberosRealm = createKerberosRealm(username);
|
||||
|
||||
final String expectedUsername = maybeRemoveRealmName(username);
|
||||
final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true);
|
||||
final Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(username));
|
||||
metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, username);
|
||||
final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true);
|
||||
final byte[] decodedTicket = randomByteArrayOfLength(10);
|
||||
final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings()));
|
||||
final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings());
|
||||
|
@ -72,7 +77,10 @@ public class KerberosRealmCacheTests extends KerberosRealmTestCase {
|
|||
final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings());
|
||||
mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(authNUsername, outToken), null);
|
||||
final String expectedUsername = maybeRemoveRealmName(authNUsername);
|
||||
final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true);
|
||||
final Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(authNUsername));
|
||||
metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, authNUsername);
|
||||
final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true);
|
||||
|
||||
final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket);
|
||||
final User user1 = authenticateAndAssertResult(kerberosRealm, expectedUser, kerberosAuthenticationToken, outToken);
|
||||
|
@ -110,7 +118,10 @@ public class KerberosRealmCacheTests extends KerberosRealmTestCase {
|
|||
final KerberosRealm kerberosRealm = createKerberosRealm(username);
|
||||
|
||||
final String expectedUsername = maybeRemoveRealmName(username);
|
||||
final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true);
|
||||
final Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(username));
|
||||
metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, username);
|
||||
final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true);
|
||||
final byte[] decodedTicket = randomByteArrayOfLength(10);
|
||||
final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings()));
|
||||
final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings());
|
||||
|
|
|
@ -160,6 +160,7 @@ public abstract class KerberosRealmTestCase extends ESTestCase {
|
|||
if (withInstance) {
|
||||
principalName.append("/").append(randomAlphaOfLength(5));
|
||||
}
|
||||
principalName.append("@");
|
||||
principalName.append(randomAlphaOfLength(5).toUpperCase(Locale.ROOT));
|
||||
return principalName.toString();
|
||||
}
|
||||
|
@ -183,6 +184,19 @@ public abstract class KerberosRealmTestCase extends ESTestCase {
|
|||
return principalName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and returns realm part from the principal name.
|
||||
* @param principalName user principal name
|
||||
* @return realm name if found else returns {@code null}
|
||||
*/
|
||||
protected String realmName(final String principalName) {
|
||||
String[] values = principalName.split("@");
|
||||
if (values.length > 1) {
|
||||
return values[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write content to provided keytab file.
|
||||
*
|
||||
|
|
|
@ -38,7 +38,9 @@ import java.nio.file.attribute.PosixFilePermissions;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
@ -71,7 +73,10 @@ public class KerberosRealmTests extends KerberosRealmTestCase {
|
|||
final String username = randomPrincipalName();
|
||||
final KerberosRealm kerberosRealm = createKerberosRealm(username);
|
||||
final String expectedUsername = maybeRemoveRealmName(username);
|
||||
final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true);
|
||||
final Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(username));
|
||||
metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, username);
|
||||
final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true);
|
||||
final byte[] decodedTicket = "base64encodedticket".getBytes(StandardCharsets.UTF_8);
|
||||
final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings()));
|
||||
final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings());
|
||||
|
|
Loading…
Reference in New Issue