diff --git a/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc b/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc index b6afc70715a55..685f7731371ab 100644 --- a/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc +++ b/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc @@ -75,6 +75,7 @@ A successful call returns an object with "cluster" and "index" fields. "manage_ingest_pipelines", "manage_ml", "manage_oidc", + "manage_own_api_key", "manage_pipeline", "manage_rollup", "manage_saml", diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java index 9d32ee9909c75..964cc1275b029 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java @@ -158,32 +158,60 @@ public interface PermissionCheck { boolean implies(PermissionCheck otherPermissionCheck); } - // Automaton based permission check - private static class AutomatonPermissionCheck implements PermissionCheck { + /** + * Base for implementing cluster action based {@link PermissionCheck}. + * It enforces the checks at cluster action level and then hands it off to the implementations + * to enforce checks based on {@link TransportRequest} and/or {@link Authentication}. + */ + public abstract static class ActionBasedPermissionCheck implements PermissionCheck { private final Automaton automaton; private final Predicate actionPredicate; - AutomatonPermissionCheck(final Automaton automaton) { + public ActionBasedPermissionCheck(final Automaton automaton) { this.automaton = automaton; this.actionPredicate = Automatons.predicate(automaton); } @Override - public boolean check(final String action, final TransportRequest request, final Authentication authentication) { - return actionPredicate.test(action); + public final boolean check(final String action, final TransportRequest request, final Authentication authentication) { + return actionPredicate.test(action) && extendedCheck(action, request, authentication); } + protected abstract boolean extendedCheck(String action, TransportRequest request, Authentication authentication); + @Override - public boolean implies(final PermissionCheck permissionCheck) { - if (permissionCheck instanceof AutomatonPermissionCheck) { - return Operations.subsetOf(((AutomatonPermissionCheck) permissionCheck).automaton, this.automaton); + public final boolean implies(final PermissionCheck permissionCheck) { + if (permissionCheck instanceof ActionBasedPermissionCheck) { + return Operations.subsetOf(((ActionBasedPermissionCheck) permissionCheck).automaton, this.automaton) && + doImplies((ActionBasedPermissionCheck) permissionCheck); } return false; } + + protected abstract boolean doImplies(ActionBasedPermissionCheck permissionCheck); + } + + // Automaton based permission check + private static class AutomatonPermissionCheck extends ActionBasedPermissionCheck { + + AutomatonPermissionCheck(final Automaton automaton) { + super(automaton); + } + + @Override + protected boolean extendedCheck(String action, TransportRequest request, Authentication authentication) { + return true; + } + + @Override + protected boolean doImplies(ActionBasedPermissionCheck permissionCheck) { + return permissionCheck instanceof AutomatonPermissionCheck; + } + } // action, request based permission check - private static class ActionRequestBasedPermissionCheck extends AutomatonPermissionCheck { + private static class ActionRequestBasedPermissionCheck extends ActionBasedPermissionCheck { private final ClusterPrivilege clusterPrivilege; private final Predicate requestPredicate; @@ -195,18 +223,16 @@ private static class ActionRequestBasedPermissionCheck extends AutomatonPermissi } @Override - public boolean check(final String action, final TransportRequest request, final Authentication authentication) { - return super.check(action, request, authentication) && requestPredicate.test(request); + protected boolean extendedCheck(String action, TransportRequest request, Authentication authentication) { + return requestPredicate.test(request); } @Override - public boolean implies(final PermissionCheck permissionCheck) { - if (super.implies(permissionCheck)) { - if (permissionCheck instanceof ActionRequestBasedPermissionCheck) { - final ActionRequestBasedPermissionCheck otherCheck = - (ActionRequestBasedPermissionCheck) permissionCheck; - return this.clusterPrivilege.equals(otherCheck.clusterPrivilege); - } + protected boolean doImplies(final ActionBasedPermissionCheck permissionCheck) { + if (permissionCheck instanceof ActionRequestBasedPermissionCheck) { + final ActionRequestBasedPermissionCheck otherCheck = + (ActionRequestBasedPermissionCheck) permissionCheck; + return this.clusterPrivilege.equals(otherCheck.clusterPrivilege); } return false; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java index 755d76e76aa03..4f6e2afd9ecd1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java @@ -103,6 +103,8 @@ public class ClusterPrivilegeResolver { public static final NamedClusterPrivilege MANAGE_SLM = new ActionClusterPrivilege("manage_slm", MANAGE_SLM_PATTERN); public static final NamedClusterPrivilege READ_SLM = new ActionClusterPrivilege("read_slm", READ_SLM_PATTERN); + public static final NamedClusterPrivilege MANAGE_OWN_API_KEY = ManageOwnApiKeyClusterPrivilege.INSTANCE; + private static final Map VALUES = Stream.of( NONE, ALL, @@ -131,7 +133,8 @@ public class ClusterPrivilegeResolver { MANAGE_ILM, READ_ILM, MANAGE_SLM, - READ_SLM).collect(Collectors.toUnmodifiableMap(NamedClusterPrivilege::name, Function.identity())); + READ_SLM, + MANAGE_OWN_API_KEY).collect(Collectors.toUnmodifiableMap(NamedClusterPrivilege::name, Function.identity())); /** * Resolves a {@link NamedClusterPrivilege} from a given name if it exists. diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilege.java new file mode 100644 index 0000000000000..bea9b16ebfc1d --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilege.java @@ -0,0 +1,106 @@ +/* + * + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + * + */ + +package org.elasticsearch.xpack.core.security.authz.privilege; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.GetApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission; +import org.elasticsearch.xpack.core.security.support.Automatons; + +/** + * Named cluster privilege for managing API keys owned by the current authenticated user. + */ +public class ManageOwnApiKeyClusterPrivilege implements NamedClusterPrivilege { + public static final ManageOwnApiKeyClusterPrivilege INSTANCE = new ManageOwnApiKeyClusterPrivilege(); + private static final String PRIVILEGE_NAME = "manage_own_api_key"; + private static final String API_KEY_REALM_TYPE = "_es_api_key"; + private static final String API_KEY_ID_KEY = "_security_api_key_id"; + + private ManageOwnApiKeyClusterPrivilege() { + } + + @Override + public String name() { + return PRIVILEGE_NAME; + } + + @Override + public ClusterPermission.Builder buildPermission(ClusterPermission.Builder builder) { + return builder.add(this, ManageOwnClusterPermissionCheck.INSTANCE); + } + + private static final class ManageOwnClusterPermissionCheck extends ClusterPermission.ActionBasedPermissionCheck { + public static final ManageOwnClusterPermissionCheck INSTANCE = new ManageOwnClusterPermissionCheck(); + + private ManageOwnClusterPermissionCheck() { + super(Automatons.patterns("cluster:admin/xpack/security/api_key/*")); + } + + @Override + protected boolean extendedCheck(String action, TransportRequest request, Authentication authentication) { + if (request instanceof CreateApiKeyRequest) { + return true; + } else if (request instanceof GetApiKeyRequest) { + final GetApiKeyRequest getApiKeyRequest = (GetApiKeyRequest) request; + return checkIfUserIsOwnerOfApiKeys(authentication, getApiKeyRequest.getApiKeyId(), getApiKeyRequest.getUserName(), + getApiKeyRequest.getRealmName(), getApiKeyRequest.ownedByAuthenticatedUser()); + } else if (request instanceof InvalidateApiKeyRequest) { + final InvalidateApiKeyRequest invalidateApiKeyRequest = (InvalidateApiKeyRequest) request; + return checkIfUserIsOwnerOfApiKeys(authentication, invalidateApiKeyRequest.getId(), + invalidateApiKeyRequest.getUserName(), invalidateApiKeyRequest.getRealmName(), + invalidateApiKeyRequest.ownedByAuthenticatedUser()); + } + throw new IllegalArgumentException( + "manage own api key privilege only supports API key requests (not " + request.getClass().getName() + ")"); + } + + @Override + protected boolean doImplies(ClusterPermission.ActionBasedPermissionCheck permissionCheck) { + return permissionCheck instanceof ManageOwnClusterPermissionCheck; + } + + private boolean checkIfUserIsOwnerOfApiKeys(Authentication authentication, String apiKeyId, String username, String realmName, + boolean ownedByAuthenticatedUser) { + if (isCurrentAuthenticationUsingSameApiKeyIdFromRequest(authentication, apiKeyId)) { + return true; + } else { + /* + * TODO bizybot we need to think on how we can propagate appropriate error message to the end user when username, realm name + * is missing. This is similar to the problem of propagating right error messages in case of access denied. + */ + if (authentication.getAuthenticatedBy().getType().equals(API_KEY_REALM_TYPE)) { + // API key cannot own any other API key so deny access + return false; + } else if (ownedByAuthenticatedUser) { + return true; + } else if (Strings.hasText(username) && Strings.hasText(realmName)) { + final String authenticatedUserPrincipal = authentication.getUser().principal(); + final String authenticatedUserRealm = authentication.getAuthenticatedBy().getName(); + return username.equals(authenticatedUserPrincipal) && realmName.equals(authenticatedUserRealm); + } + } + return false; + } + + private boolean isCurrentAuthenticationUsingSameApiKeyIdFromRequest(Authentication authentication, String apiKeyId) { + if (authentication.getAuthenticatedBy().getType().equals(API_KEY_REALM_TYPE)) { + // API key id from authentication must match the id from request + final String authenticatedApiKeyId = (String) authentication.getMetadata().get(API_KEY_ID_KEY); + if (Strings.hasText(apiKeyId)) { + return apiKeyId.equals(authenticatedApiKeyId); + } + } + return false; + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermissionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermissionTests.java index fe08db0d2e2e0..5a52519c7b72e 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermissionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermissionTests.java @@ -276,7 +276,7 @@ public int hashCode() { @Override public String toString() { return "MockConfigurableClusterPrivilege{" + - "requestAuthnPredicate=" + requestPredicate + + "requestPredicate=" + requestPredicate + '}'; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilegeTests.java new file mode 100644 index 0000000000000..c6d67b9e00b58 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilegeTests.java @@ -0,0 +1,110 @@ +/* + * + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + * + */ + +package org.elasticsearch.xpack.core.security.authz.privilege; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.action.GetApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission; +import org.elasticsearch.xpack.core.security.user.User; + +import java.util.Map; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ManageOwnApiKeyClusterPrivilegeTests extends ESTestCase { + + public void testAuthenticationWithApiKeyAllowsAccessToApiKeyActionsWhenItIsOwner() { + final ClusterPermission clusterPermission = + ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder()).build(); + + final String apiKeyId = randomAlphaOfLengthBetween(4, 7); + final Authentication authentication = createMockAuthentication("joe","_es_api_key", "_es_api_key", + Map.of("_security_api_key_id", apiKeyId)); + final TransportRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean()); + final TransportRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean()); + + assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication)); + assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication)); + assertFalse(clusterPermission.check("cluster:admin/something", mock(TransportRequest.class), authentication)); + } + + public void testAuthenticationWithApiKeyDeniesAccessToApiKeyActionsWhenItIsNotOwner() { + final ClusterPermission clusterPermission = + ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder()).build(); + + final String apiKeyId = randomAlphaOfLengthBetween(4, 7); + final Authentication authentication = createMockAuthentication("joe","_es_api_key", "_es_api_key", + Map.of("_security_api_key_id", randomAlphaOfLength(7))); + final TransportRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean()); + final TransportRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean()); + + assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication)); + assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication)); + } + + public void testAuthenticationWithUserAllowsAccessToApiKeyActionsWhenItIsOwner() { + final ClusterPermission clusterPermission = + ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder()).build(); + + final Authentication authentication = createMockAuthentication("joe","realm1", "native", Map.of()); + final TransportRequest getApiKeyRequest = GetApiKeyRequest.usingRealmAndUserName("realm1", "joe"); + final TransportRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingRealmAndUserName("realm1", "joe"); + + assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication)); + assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication)); + assertFalse(clusterPermission.check("cluster:admin/something", mock(TransportRequest.class), authentication)); + } + + public void testAuthenticationWithUserAllowsAccessToApiKeyActionsWhenItIsOwner_WithOwnerFlagOnly() { + final ClusterPermission clusterPermission = + ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder()).build(); + + final Authentication authentication = createMockAuthentication("joe","realm1", "native", Map.of()); + final TransportRequest getApiKeyRequest = GetApiKeyRequest.forOwnedApiKeys(); + final TransportRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.forOwnedApiKeys(); + + assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication)); + assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication)); + assertFalse(clusterPermission.check("cluster:admin/something", mock(TransportRequest.class), authentication)); + } + + public void testAuthenticationWithUserDeniesAccessToApiKeyActionsWhenItIsNotOwner() { + final ClusterPermission clusterPermission = + ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder()).build(); + + final Authentication authentication = createMockAuthentication("joe", "realm1", "native", Map.of()); + final TransportRequest getApiKeyRequest = randomFrom( + GetApiKeyRequest.usingRealmAndUserName("realm1", randomAlphaOfLength(7)), + GetApiKeyRequest.usingRealmAndUserName(randomAlphaOfLength(5), "joe"), + new GetApiKeyRequest(randomAlphaOfLength(5), randomAlphaOfLength(7), null, null, false)); + final TransportRequest invalidateApiKeyRequest = randomFrom( + InvalidateApiKeyRequest.usingRealmAndUserName("realm1", randomAlphaOfLength(7)), + InvalidateApiKeyRequest.usingRealmAndUserName(randomAlphaOfLength(5), "joe"), + new InvalidateApiKeyRequest(randomAlphaOfLength(5), randomAlphaOfLength(7), null, null, false)); + + assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication)); + assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication)); + } + + private Authentication createMockAuthentication(String username, String realmName, String realmType, Map metadata) { + final User user = new User(username); + final Authentication authentication = mock(Authentication.class); + final Authentication.RealmRef authenticatedBy = mock(Authentication.RealmRef.class); + when(authentication.getUser()).thenReturn(user); + when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); + when(authenticatedBy.getName()).thenReturn(realmName); + when(authenticatedBy.getType()).thenReturn(realmType); + when(authentication.getMetadata()).thenReturn(metadata); + return authentication; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetApiKeyAction.java index 9a3c0ed8326c9..994cb90b5f2b6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetApiKeyAction.java @@ -50,7 +50,7 @@ protected void doExecute(Task task, GetApiKeyRequest request, ActionListener PASSWORD_HASHING_ALGORITHM = new Setting<>( "xpack.security.authc.api_key.hashing.algorithm", "pbkdf2", Function.identity(), v -> { if (Hasher.getAvailableAlgoStoredHash().contains(v.toLowerCase(Locale.ROOT)) == false) { @@ -520,6 +522,7 @@ private void validateApiKeyExpiration(Map source, ApiKeyCredenti : limitedByRoleDescriptors.keySet().toArray(Strings.EMPTY_ARRAY); final User apiKeyUser = new User(principal, roleNames, null, null, metadata, true); final Map authResultMetadata = new HashMap<>(); + authResultMetadata.put(API_KEY_CREATOR_REALM, creator.get("realm")); authResultMetadata.put(API_KEY_ROLE_DESCRIPTORS_KEY, roleDescriptors); authResultMetadata.put(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, limitedByRoleDescriptors); authResultMetadata.put(API_KEY_ID_KEY, credentials.getId()); @@ -891,6 +894,21 @@ public void getApiKeys(String realmName, String username, String apiKeyName, Str } } + /** + * Returns realm name for the authenticated user. + * If the user is authenticated by realm type {@value API_KEY_REALM_TYPE} + * then it will return the realm name of user who created this API key. + * @param authentication {@link Authentication} + * @return realm name + */ + public static String getCreatorRealmName(final Authentication authentication) { + if (authentication.getAuthenticatedBy().getType().equals(API_KEY_REALM_TYPE)) { + return (String) authentication.getMetadata().get(API_KEY_CREATOR_REALM); + } else { + return authentication.getAuthenticatedBy().getName(); + } + } + final class CachedApiKeyHashResult { final boolean success; final char[] hash; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index 69153379f3b15..bd81d6db4743a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -64,6 +64,7 @@ import org.elasticsearch.xpack.security.audit.AuditLevel; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.audit.AuditUtil; +import org.elasticsearch.xpack.security.authc.ApiKeyService; import org.elasticsearch.xpack.security.authz.interceptor.RequestInterceptor; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; @@ -572,6 +573,14 @@ private ElasticsearchSecurityException denialException(Authentication authentica return authorizationError("action [{}] is unauthorized for user [{}] run as [{}]", cause, action, authUser.principal(), authentication.getUser().principal()); } + // check for authentication by API key + if (authentication.getAuthenticatedBy().getType().equals(ApiKeyService.API_KEY_REALM_TYPE)) { + final String apiKeyId = (String) authentication.getMetadata().get(ApiKeyService.API_KEY_ID_KEY); + assert apiKeyId != null : "api key id must be present in the metadata"; + logger.debug("action [{}] is unauthorized for API key id [{}] of user [{}]", action, apiKeyId, authUser.principal()); + return authorizationError("action [{}] is unauthorized for API key id [{}] of user [{}]", cause, action, apiKeyId, + authUser.principal()); + } logger.debug("action [{}] is unauthorized for user [{}]", action, authUser.principal()); return authorizationError("action [{}] is unauthorized for user [{}]", cause, action, authUser.principal()); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java index bec82b17c1495..4c9e944c14f0f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java @@ -93,7 +93,9 @@ public void wipeSecurityIndex() throws InterruptedException { public String configRoles() { return super.configRoles() + "\n" + "manage_api_key_role:\n" + - " cluster: [\"manage_api_key\"]\n"; + " cluster: [\"manage_api_key\"]\n" + + "manage_own_api_key_role:\n" + + " cluster: [\"manage_own_api_key\"]\n"; } @Override @@ -101,13 +103,15 @@ public String configUsers() { final String usersPasswdHashed = new String( getFastStoredHashAlgoForTests().hash(SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)); return super.configUsers() + - "user_with_manage_api_key_role:" + usersPasswdHashed + "\n"; + "user_with_manage_api_key_role:" + usersPasswdHashed + "\n" + + "user_with_manage_own_api_key_role:" + usersPasswdHashed + "\n"; } @Override public String configUsersRoles() { return super.configUsersRoles() + - "manage_api_key_role:user_with_manage_api_key_role\n"; + "manage_api_key_role:user_with_manage_api_key_role\n" + + "manage_own_api_key_role:user_with_manage_own_api_key_role\n"; } private void awaitApiKeysRemoverCompletion() throws InterruptedException { @@ -505,15 +509,16 @@ public void testGetApiKeysOwnedByCurrentAuthenticatedUser() throws InterruptedEx int noOfSuperuserApiKeys = randomIntBetween(3, 5); int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); List defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null); - List userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role", - noOfApiKeysForUserWithManageApiKeyRole, null); + String userWithManageApiKeyRole = randomFrom("user_with_manage_api_key_role", "user_with_manage_own_api_key_role"); + List userWithManageApiKeyRoleApiKeys = createApiKeys(userWithManageApiKeyRole, + noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue("user_with_manage_api_key_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + .basicAuthHeaderValue(userWithManageApiKeyRole, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.forOwnedApiKeys(), listener); GetApiKeyResponse response = listener.get(); - verifyGetResponse("user_with_manage_api_key_role", noOfApiKeysForUserWithManageApiKeyRole, userWithManageApiKeyRoleApiKeys, + verifyGetResponse(userWithManageApiKeyRole, noOfApiKeysForUserWithManageApiKeyRole, userWithManageApiKeyRoleApiKeys, response, userWithManageApiKeyRoleApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()), null); } @@ -521,10 +526,11 @@ public void testInvalidateApiKeysOwnedByCurrentAuthenticatedUser() throws Interr int noOfSuperuserApiKeys = randomIntBetween(3, 5); int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); List defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null); - List userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role", - noOfApiKeysForUserWithManageApiKeyRole, null); + String userWithManageApiKeyRole = randomFrom("user_with_manage_api_key_role", "user_with_manage_own_api_key_role"); + List userWithManageApiKeyRoleApiKeys = createApiKeys(userWithManageApiKeyRole, + noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue("user_with_manage_api_key_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + .basicAuthHeaderValue(userWithManageApiKeyRole, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.forOwnedApiKeys(), listener); @@ -533,21 +539,62 @@ public void testInvalidateApiKeysOwnedByCurrentAuthenticatedUser() throws Interr verifyInvalidateResponse(noOfApiKeysForUserWithManageApiKeyRole, userWithManageApiKeyRoleApiKeys, invalidateResponse); } - public void testApiKeyAuthorizationApiKeyMustBeAbleToRetrieveItsOwnInformation() throws InterruptedException, ExecutionException { - List responses = createApiKeys(2, null); + public void testApiKeyAuthorizationApiKeyMustBeAbleToRetrieveItsOwnInformationButNotAnyOtherKeysCreatedBySameOwner() + throws InterruptedException, ExecutionException { + List responses = createApiKeys(SecuritySettingsSource.TEST_SUPERUSER,2, null, (String[]) null); final String base64ApiKeyKeyValue = Base64.getEncoder().encodeToString( (responses.get(0).getId() + ":" + responses.get(0).getKey().toString()).getBytes(StandardCharsets.UTF_8)); Client client = client().filterWithHeader(Map.of("Authorization", "ApiKey " + base64ApiKeyKeyValue)); PlainActionFuture listener = new PlainActionFuture<>(); - client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyId(responses.get(0).getId(), false), listener); + client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyId(responses.get(0).getId(), randomBoolean()), listener); GetApiKeyResponse response = listener.get(); verifyGetResponse(1, responses, response, Collections.singleton(responses.get(0).getId()), null); final PlainActionFuture failureListener = new PlainActionFuture<>(); // for any other API key id, it must deny access - client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyId(responses.get(1).getId(), false), failureListener); + client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyId(responses.get(1).getId(), randomBoolean()), + failureListener); + ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, () -> failureListener.actionGet()); + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", SecuritySettingsSource.TEST_SUPERUSER, + responses.get(0).getId()); + + final PlainActionFuture failureListener1 = new PlainActionFuture<>(); + client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.forOwnedApiKeys(), failureListener1); + ese = expectThrows(ElasticsearchSecurityException.class, () -> failureListener1.actionGet()); + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", SecuritySettingsSource.TEST_SUPERUSER, + responses.get(0).getId()); + } + + public void testApiKeyWithManageOwnPrivilegeIsAbleToInvalidateItselfButNotAnyOtherKeysCreatedBySameOwner() + throws InterruptedException, ExecutionException { + List responses = createApiKeys(SecuritySettingsSource.TEST_SUPERUSER, 2, null, "manage_own_api_key"); + final String base64ApiKeyKeyValue = Base64.getEncoder().encodeToString( + (responses.get(0).getId() + ":" + responses.get(0).getKey().toString()).getBytes(StandardCharsets.UTF_8)); + Client client = client().filterWithHeader(Map.of("Authorization", "ApiKey " + base64ApiKeyKeyValue)); + + final PlainActionFuture failureListener = new PlainActionFuture<>(); + // for any other API key id, it must deny access + client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyId(responses.get(1).getId(), randomBoolean()), + failureListener); ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, () -> failureListener.actionGet()); - assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", SecuritySettingsSource.TEST_SUPERUSER); + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/invalidate", SecuritySettingsSource.TEST_SUPERUSER, + responses.get(0).getId()); + + final PlainActionFuture failureListener1 = new PlainActionFuture<>(); + client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.forOwnedApiKeys(), failureListener1); + ese = expectThrows(ElasticsearchSecurityException.class, () -> failureListener1.actionGet()); + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/invalidate", SecuritySettingsSource.TEST_SUPERUSER, + responses.get(0).getId()); + + PlainActionFuture listener = new PlainActionFuture<>(); + client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyId(responses.get(0).getId(), randomBoolean()), + listener); + InvalidateApiKeyResponse invalidateResponse = listener.get(); + + assertThat(invalidateResponse.getInvalidatedApiKeys().size(), equalTo(1)); + assertThat(invalidateResponse.getInvalidatedApiKeys(), containsInAnyOrder(responses.get(0).getId())); + assertThat(invalidateResponse.getPreviouslyInvalidatedApiKeys().size(), equalTo(0)); + assertThat(invalidateResponse.getErrors().size(), equalTo(0)); } private void verifyGetResponse(int expectedNumberOfApiKeys, List responses, @@ -582,13 +629,13 @@ private void verifyGetResponse(String user, int expectedNumberOfApiKeys, List createApiKeys(int noOfApiKeys, TimeValue expiration) { - return createApiKeys(SecuritySettingsSource.TEST_SUPERUSER, noOfApiKeys, expiration); + return createApiKeys(SecuritySettingsSource.TEST_SUPERUSER, noOfApiKeys, expiration, "monitor"); } - private List createApiKeys(String user, int noOfApiKeys, TimeValue expiration) { + private List createApiKeys(String user, int noOfApiKeys, TimeValue expiration, String... clusterPrivileges) { List responses = new ArrayList<>(); for (int i = 0; i < noOfApiKeys; i++) { - final RoleDescriptor descriptor = new RoleDescriptor("role", new String[] { "monitor" }, null, null); + final RoleDescriptor descriptor = new RoleDescriptor("role", clusterPrivileges, null, null); Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken .basicAuthHeaderValue(user, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client) @@ -602,7 +649,8 @@ private List createApiKeys(String user, int noOfApiKeys, T return responses; } - private void assertErrorMessage(final ElasticsearchSecurityException ese, String action, String userName) { - assertThat(ese.getMessage(), is("action [" + action + "] is unauthorized for user [" + userName + "]")); + private void assertErrorMessage(final ElasticsearchSecurityException ese, String action, String userName, String apiKeyId) { + assertThat(ese.getMessage(), + is("action [" + action + "] is unauthorized for API key id [" + apiKeyId + "] of user [" + userName + "]")); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index 0491d20d74c8a..031f5ccec0696 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -191,6 +191,7 @@ public void testValidateApiKey() throws Exception { sourceMap.put("limited_by_role_descriptors", Collections.singletonMap("limited role", Collections.singletonMap("cluster", "all"))); Map creatorMap = new HashMap<>(); creatorMap.put("principal", "test_user"); + creatorMap.put("realm", "realm1"); creatorMap.put("metadata", Collections.emptyMap()); sourceMap.put("creator", creatorMap); sourceMap.put("api_key_invalidated", false); @@ -209,6 +210,7 @@ public void testValidateApiKey() throws Exception { assertThat(result.getMetadata().get(ApiKeyService.API_KEY_ROLE_DESCRIPTORS_KEY), equalTo(sourceMap.get("role_descriptors"))); assertThat(result.getMetadata().get(ApiKeyService.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY), equalTo(sourceMap.get("limited_by_role_descriptors"))); + assertThat(result.getMetadata().get(ApiKeyService.API_KEY_CREATOR_REALM), is("realm1")); sourceMap.put("expiration_time", Clock.systemUTC().instant().plus(1L, ChronoUnit.HOURS).toEpochMilli()); future = new PlainActionFuture<>(); @@ -222,6 +224,7 @@ public void testValidateApiKey() throws Exception { assertThat(result.getMetadata().get(ApiKeyService.API_KEY_ROLE_DESCRIPTORS_KEY), equalTo(sourceMap.get("role_descriptors"))); assertThat(result.getMetadata().get(ApiKeyService.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY), equalTo(sourceMap.get("limited_by_role_descriptors"))); + assertThat(result.getMetadata().get(ApiKeyService.API_KEY_CREATOR_REALM), is("realm1")); sourceMap.put("expiration_time", Clock.systemUTC().instant().minus(1L, ChronoUnit.HOURS).toEpochMilli()); future = new PlainActionFuture<>(); diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml index 2e23a85b7e737..df1978f443fc1 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml @@ -15,5 +15,5 @@ setup: # This is fragile - it needs to be updated every time we add a new cluster/index privilege # I would much prefer we could just check that specific entries are in the array, but we don't have # an assertion for that - - length: { "cluster" : 28 } + - length: { "cluster" : 29 } - length: { "index" : 16 }