-
Notifications
You must be signed in to change notification settings - Fork 25.6k
Add manage_own_api_key cluster privilege
#45696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 31 commits
80fa13f
60f436b
1af1308
8896dcd
4976d78
54490a7
9033996
541cfad
aa623c3
efc2c2b
7db504c
adc6d69
2497c6f
6bd259a
66fc5b3
508a718
29cefd2
0eba55f
de88e11
69b56c6
a94fa92
d5a295c
73498ea
2f7933c
5c9c422
186e599
45b0192
dc512c2
d740034
7a9cfb7
569cae6
1a2a371
43cf4e3
1e59c70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| /* | ||
| * | ||
| * 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()); | ||
| } | ||
| return false; | ||
bizybot marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| @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. | ||
| */ | ||
| String authenticatedUserPrincipal = authentication.getUser().principal(); | ||
| String authenticatedUserRealm = authentication.getAuthenticatedBy().getName(); | ||
bizybot marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
bizybot marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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)) { | ||
| return username.equals(authenticatedUserPrincipal) && realmName.equals(authenticatedUserRealm); | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private boolean isCurrentAuthenticationUsingSameApiKeyIdFromRequest(Authentication authentication, String apiKeyId) { | ||
bizybot marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (authentication.getAuthenticatedBy().getType().equals(API_KEY_REALM_TYPE)) { | ||
| // API key id from authentication must match the id from request | ||
| String authenticatedApiKeyId = (String) authentication.getMetadata().get(API_KEY_ID_KEY); | ||
| if (Strings.hasText(apiKeyId)) { | ||
| return apiKeyId.equals(authenticatedApiKeyId); | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| /* | ||
| * | ||
| * 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 = randomFrom(GetApiKeyRequest.usingRealmAndUserName("realm1", "joe"), | ||
| GetApiKeyRequest.forOwnedApiKeys()); | ||
bizybot marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| final TransportRequest invalidateApiKeyRequest = randomFrom(InvalidateApiKeyRequest.usingRealmAndUserName("realm1", "joe"), | ||
| GetApiKeyRequest.forOwnedApiKeys()); | ||
bizybot marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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<String, Object> 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,7 +50,11 @@ protected void doExecute(Task task, GetApiKeyRequest request, ActionListener<Get | |
| assert realm == null; | ||
| // restrict username and realm to current authenticated user. | ||
| username = authentication.getUser().principal(); | ||
| realm = authentication.getAuthenticatedBy().getName(); | ||
| if (authentication.getAuthenticatedBy().getType().equals(ApiKeyService.API_KEY_REALM_TYPE)) { | ||
| realm = (String) authentication.getMetadata().get(ApiKeyService.API_KEY_CREATOR_REALM); | ||
| } else { | ||
| realm = authentication.getAuthenticatedBy().getName(); | ||
| } | ||
|
||
| } | ||
|
|
||
| apiKeyService.getApiKeys(realm, username, apiKeyName, apiKeyId, listener); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.