Prechádzať zdrojové kódy

[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.
Yogesh Gaikwad 7 rokov pred
rodič
commit
d810f1b094

+ 6 - 0
x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc

@@ -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

+ 24 - 24
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java

@@ -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);
             }

+ 6 - 1
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java

@@ -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)));

+ 14 - 3
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java

@@ -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());

+ 14 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java

@@ -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.
      *

+ 6 - 1
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java

@@ -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());