Skip to content

Commit ed2062f

Browse files
authored
Add manage_own_api_key cluster privilege (#45696)
This commit adds `manage_own_api_key` cluster privilege which only allows api key cluster actions on API keys owned by the currently authenticated user. Relates: #40031
1 parent 4c23349 commit ed2062f

File tree

13 files changed

+367
-43
lines changed

13 files changed

+367
-43
lines changed

x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ A successful call returns an object with "cluster" and "index" fields.
7575
"manage_ingest_pipelines",
7676
"manage_ml",
7777
"manage_oidc",
78+
"manage_own_api_key",
7879
"manage_pipeline",
7980
"manage_rollup",
8081
"manage_saml",

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -158,32 +158,60 @@ public interface PermissionCheck {
158158
boolean implies(PermissionCheck otherPermissionCheck);
159159
}
160160

161-
// Automaton based permission check
162-
private static class AutomatonPermissionCheck implements PermissionCheck {
161+
/**
162+
* Base for implementing cluster action based {@link PermissionCheck}.
163+
* It enforces the checks at cluster action level and then hands it off to the implementations
164+
* to enforce checks based on {@link TransportRequest} and/or {@link Authentication}.
165+
*/
166+
public abstract static class ActionBasedPermissionCheck implements PermissionCheck {
163167
private final Automaton automaton;
164168
private final Predicate<String> actionPredicate;
165169

166-
AutomatonPermissionCheck(final Automaton automaton) {
170+
public ActionBasedPermissionCheck(final Automaton automaton) {
167171
this.automaton = automaton;
168172
this.actionPredicate = Automatons.predicate(automaton);
169173
}
170174

171175
@Override
172-
public boolean check(final String action, final TransportRequest request, final Authentication authentication) {
173-
return actionPredicate.test(action);
176+
public final boolean check(final String action, final TransportRequest request, final Authentication authentication) {
177+
return actionPredicate.test(action) && extendedCheck(action, request, authentication);
174178
}
175179

180+
protected abstract boolean extendedCheck(String action, TransportRequest request, Authentication authentication);
181+
176182
@Override
177-
public boolean implies(final PermissionCheck permissionCheck) {
178-
if (permissionCheck instanceof AutomatonPermissionCheck) {
179-
return Operations.subsetOf(((AutomatonPermissionCheck) permissionCheck).automaton, this.automaton);
183+
public final boolean implies(final PermissionCheck permissionCheck) {
184+
if (permissionCheck instanceof ActionBasedPermissionCheck) {
185+
return Operations.subsetOf(((ActionBasedPermissionCheck) permissionCheck).automaton, this.automaton) &&
186+
doImplies((ActionBasedPermissionCheck) permissionCheck);
180187
}
181188
return false;
182189
}
190+
191+
protected abstract boolean doImplies(ActionBasedPermissionCheck permissionCheck);
192+
}
193+
194+
// Automaton based permission check
195+
private static class AutomatonPermissionCheck extends ActionBasedPermissionCheck {
196+
197+
AutomatonPermissionCheck(final Automaton automaton) {
198+
super(automaton);
199+
}
200+
201+
@Override
202+
protected boolean extendedCheck(String action, TransportRequest request, Authentication authentication) {
203+
return true;
204+
}
205+
206+
@Override
207+
protected boolean doImplies(ActionBasedPermissionCheck permissionCheck) {
208+
return permissionCheck instanceof AutomatonPermissionCheck;
209+
}
210+
183211
}
184212

185213
// action, request based permission check
186-
private static class ActionRequestBasedPermissionCheck extends AutomatonPermissionCheck {
214+
private static class ActionRequestBasedPermissionCheck extends ActionBasedPermissionCheck {
187215
private final ClusterPrivilege clusterPrivilege;
188216
private final Predicate<TransportRequest> requestPredicate;
189217

@@ -195,18 +223,16 @@ private static class ActionRequestBasedPermissionCheck extends AutomatonPermissi
195223
}
196224

197225
@Override
198-
public boolean check(final String action, final TransportRequest request, final Authentication authentication) {
199-
return super.check(action, request, authentication) && requestPredicate.test(request);
226+
protected boolean extendedCheck(String action, TransportRequest request, Authentication authentication) {
227+
return requestPredicate.test(request);
200228
}
201229

202230
@Override
203-
public boolean implies(final PermissionCheck permissionCheck) {
204-
if (super.implies(permissionCheck)) {
205-
if (permissionCheck instanceof ActionRequestBasedPermissionCheck) {
206-
final ActionRequestBasedPermissionCheck otherCheck =
207-
(ActionRequestBasedPermissionCheck) permissionCheck;
208-
return this.clusterPrivilege.equals(otherCheck.clusterPrivilege);
209-
}
231+
protected boolean doImplies(final ActionBasedPermissionCheck permissionCheck) {
232+
if (permissionCheck instanceof ActionRequestBasedPermissionCheck) {
233+
final ActionRequestBasedPermissionCheck otherCheck =
234+
(ActionRequestBasedPermissionCheck) permissionCheck;
235+
return this.clusterPrivilege.equals(otherCheck.clusterPrivilege);
210236
}
211237
return false;
212238
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ public class ClusterPrivilegeResolver {
103103
public static final NamedClusterPrivilege MANAGE_SLM = new ActionClusterPrivilege("manage_slm", MANAGE_SLM_PATTERN);
104104
public static final NamedClusterPrivilege READ_SLM = new ActionClusterPrivilege("read_slm", READ_SLM_PATTERN);
105105

106+
public static final NamedClusterPrivilege MANAGE_OWN_API_KEY = ManageOwnApiKeyClusterPrivilege.INSTANCE;
107+
106108
private static final Map<String, NamedClusterPrivilege> VALUES = Stream.of(
107109
NONE,
108110
ALL,
@@ -131,7 +133,8 @@ public class ClusterPrivilegeResolver {
131133
MANAGE_ILM,
132134
READ_ILM,
133135
MANAGE_SLM,
134-
READ_SLM).collect(Collectors.toUnmodifiableMap(NamedClusterPrivilege::name, Function.identity()));
136+
READ_SLM,
137+
MANAGE_OWN_API_KEY).collect(Collectors.toUnmodifiableMap(NamedClusterPrivilege::name, Function.identity()));
135138

136139
/**
137140
* Resolves a {@link NamedClusterPrivilege} from a given name if it exists.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
*
3+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
4+
* or more contributor license agreements. Licensed under the Elastic License;
5+
* you may not use this file except in compliance with the Elastic License.
6+
*
7+
*/
8+
9+
package org.elasticsearch.xpack.core.security.authz.privilege;
10+
11+
import org.elasticsearch.common.Strings;
12+
import org.elasticsearch.transport.TransportRequest;
13+
import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest;
14+
import org.elasticsearch.xpack.core.security.action.GetApiKeyRequest;
15+
import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest;
16+
import org.elasticsearch.xpack.core.security.authc.Authentication;
17+
import org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission;
18+
import org.elasticsearch.xpack.core.security.support.Automatons;
19+
20+
/**
21+
* Named cluster privilege for managing API keys owned by the current authenticated user.
22+
*/
23+
public class ManageOwnApiKeyClusterPrivilege implements NamedClusterPrivilege {
24+
public static final ManageOwnApiKeyClusterPrivilege INSTANCE = new ManageOwnApiKeyClusterPrivilege();
25+
private static final String PRIVILEGE_NAME = "manage_own_api_key";
26+
private static final String API_KEY_REALM_TYPE = "_es_api_key";
27+
private static final String API_KEY_ID_KEY = "_security_api_key_id";
28+
29+
private ManageOwnApiKeyClusterPrivilege() {
30+
}
31+
32+
@Override
33+
public String name() {
34+
return PRIVILEGE_NAME;
35+
}
36+
37+
@Override
38+
public ClusterPermission.Builder buildPermission(ClusterPermission.Builder builder) {
39+
return builder.add(this, ManageOwnClusterPermissionCheck.INSTANCE);
40+
}
41+
42+
private static final class ManageOwnClusterPermissionCheck extends ClusterPermission.ActionBasedPermissionCheck {
43+
public static final ManageOwnClusterPermissionCheck INSTANCE = new ManageOwnClusterPermissionCheck();
44+
45+
private ManageOwnClusterPermissionCheck() {
46+
super(Automatons.patterns("cluster:admin/xpack/security/api_key/*"));
47+
}
48+
49+
@Override
50+
protected boolean extendedCheck(String action, TransportRequest request, Authentication authentication) {
51+
if (request instanceof CreateApiKeyRequest) {
52+
return true;
53+
} else if (request instanceof GetApiKeyRequest) {
54+
final GetApiKeyRequest getApiKeyRequest = (GetApiKeyRequest) request;
55+
return checkIfUserIsOwnerOfApiKeys(authentication, getApiKeyRequest.getApiKeyId(), getApiKeyRequest.getUserName(),
56+
getApiKeyRequest.getRealmName(), getApiKeyRequest.ownedByAuthenticatedUser());
57+
} else if (request instanceof InvalidateApiKeyRequest) {
58+
final InvalidateApiKeyRequest invalidateApiKeyRequest = (InvalidateApiKeyRequest) request;
59+
return checkIfUserIsOwnerOfApiKeys(authentication, invalidateApiKeyRequest.getId(),
60+
invalidateApiKeyRequest.getUserName(), invalidateApiKeyRequest.getRealmName(),
61+
invalidateApiKeyRequest.ownedByAuthenticatedUser());
62+
}
63+
throw new IllegalArgumentException(
64+
"manage own api key privilege only supports API key requests (not " + request.getClass().getName() + ")");
65+
}
66+
67+
@Override
68+
protected boolean doImplies(ClusterPermission.ActionBasedPermissionCheck permissionCheck) {
69+
return permissionCheck instanceof ManageOwnClusterPermissionCheck;
70+
}
71+
72+
private boolean checkIfUserIsOwnerOfApiKeys(Authentication authentication, String apiKeyId, String username, String realmName,
73+
boolean ownedByAuthenticatedUser) {
74+
if (isCurrentAuthenticationUsingSameApiKeyIdFromRequest(authentication, apiKeyId)) {
75+
return true;
76+
} else {
77+
/*
78+
* TODO bizybot we need to think on how we can propagate appropriate error message to the end user when username, realm name
79+
* is missing. This is similar to the problem of propagating right error messages in case of access denied.
80+
*/
81+
if (authentication.getAuthenticatedBy().getType().equals(API_KEY_REALM_TYPE)) {
82+
// API key cannot own any other API key so deny access
83+
return false;
84+
} else if (ownedByAuthenticatedUser) {
85+
return true;
86+
} else if (Strings.hasText(username) && Strings.hasText(realmName)) {
87+
final String authenticatedUserPrincipal = authentication.getUser().principal();
88+
final String authenticatedUserRealm = authentication.getAuthenticatedBy().getName();
89+
return username.equals(authenticatedUserPrincipal) && realmName.equals(authenticatedUserRealm);
90+
}
91+
}
92+
return false;
93+
}
94+
95+
private boolean isCurrentAuthenticationUsingSameApiKeyIdFromRequest(Authentication authentication, String apiKeyId) {
96+
if (authentication.getAuthenticatedBy().getType().equals(API_KEY_REALM_TYPE)) {
97+
// API key id from authentication must match the id from request
98+
final String authenticatedApiKeyId = (String) authentication.getMetadata().get(API_KEY_ID_KEY);
99+
if (Strings.hasText(apiKeyId)) {
100+
return apiKeyId.equals(authenticatedApiKeyId);
101+
}
102+
}
103+
return false;
104+
}
105+
}
106+
}

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermissionTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ public int hashCode() {
276276
@Override
277277
public String toString() {
278278
return "MockConfigurableClusterPrivilege{" +
279-
"requestAuthnPredicate=" + requestPredicate +
279+
"requestPredicate=" + requestPredicate +
280280
'}';
281281
}
282282

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
*
3+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
4+
* or more contributor license agreements. Licensed under the Elastic License;
5+
* you may not use this file except in compliance with the Elastic License.
6+
*
7+
*/
8+
9+
package org.elasticsearch.xpack.core.security.authz.privilege;
10+
11+
import org.elasticsearch.test.ESTestCase;
12+
import org.elasticsearch.transport.TransportRequest;
13+
import org.elasticsearch.xpack.core.security.action.GetApiKeyRequest;
14+
import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest;
15+
import org.elasticsearch.xpack.core.security.authc.Authentication;
16+
import org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission;
17+
import org.elasticsearch.xpack.core.security.user.User;
18+
19+
import java.util.Map;
20+
21+
import static org.mockito.Mockito.mock;
22+
import static org.mockito.Mockito.when;
23+
24+
public class ManageOwnApiKeyClusterPrivilegeTests extends ESTestCase {
25+
26+
public void testAuthenticationWithApiKeyAllowsAccessToApiKeyActionsWhenItIsOwner() {
27+
final ClusterPermission clusterPermission =
28+
ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder()).build();
29+
30+
final String apiKeyId = randomAlphaOfLengthBetween(4, 7);
31+
final Authentication authentication = createMockAuthentication("joe","_es_api_key", "_es_api_key",
32+
Map.of("_security_api_key_id", apiKeyId));
33+
final TransportRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean());
34+
final TransportRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean());
35+
36+
assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication));
37+
assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication));
38+
assertFalse(clusterPermission.check("cluster:admin/something", mock(TransportRequest.class), authentication));
39+
}
40+
41+
public void testAuthenticationWithApiKeyDeniesAccessToApiKeyActionsWhenItIsNotOwner() {
42+
final ClusterPermission clusterPermission =
43+
ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder()).build();
44+
45+
final String apiKeyId = randomAlphaOfLengthBetween(4, 7);
46+
final Authentication authentication = createMockAuthentication("joe","_es_api_key", "_es_api_key",
47+
Map.of("_security_api_key_id", randomAlphaOfLength(7)));
48+
final TransportRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean());
49+
final TransportRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean());
50+
51+
assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication));
52+
assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication));
53+
}
54+
55+
public void testAuthenticationWithUserAllowsAccessToApiKeyActionsWhenItIsOwner() {
56+
final ClusterPermission clusterPermission =
57+
ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder()).build();
58+
59+
final Authentication authentication = createMockAuthentication("joe","realm1", "native", Map.of());
60+
final TransportRequest getApiKeyRequest = GetApiKeyRequest.usingRealmAndUserName("realm1", "joe");
61+
final TransportRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingRealmAndUserName("realm1", "joe");
62+
63+
assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication));
64+
assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication));
65+
assertFalse(clusterPermission.check("cluster:admin/something", mock(TransportRequest.class), authentication));
66+
}
67+
68+
public void testAuthenticationWithUserAllowsAccessToApiKeyActionsWhenItIsOwner_WithOwnerFlagOnly() {
69+
final ClusterPermission clusterPermission =
70+
ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder()).build();
71+
72+
final Authentication authentication = createMockAuthentication("joe","realm1", "native", Map.of());
73+
final TransportRequest getApiKeyRequest = GetApiKeyRequest.forOwnedApiKeys();
74+
final TransportRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.forOwnedApiKeys();
75+
76+
assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication));
77+
assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication));
78+
assertFalse(clusterPermission.check("cluster:admin/something", mock(TransportRequest.class), authentication));
79+
}
80+
81+
public void testAuthenticationWithUserDeniesAccessToApiKeyActionsWhenItIsNotOwner() {
82+
final ClusterPermission clusterPermission =
83+
ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder()).build();
84+
85+
final Authentication authentication = createMockAuthentication("joe", "realm1", "native", Map.of());
86+
final TransportRequest getApiKeyRequest = randomFrom(
87+
GetApiKeyRequest.usingRealmAndUserName("realm1", randomAlphaOfLength(7)),
88+
GetApiKeyRequest.usingRealmAndUserName(randomAlphaOfLength(5), "joe"),
89+
new GetApiKeyRequest(randomAlphaOfLength(5), randomAlphaOfLength(7), null, null, false));
90+
final TransportRequest invalidateApiKeyRequest = randomFrom(
91+
InvalidateApiKeyRequest.usingRealmAndUserName("realm1", randomAlphaOfLength(7)),
92+
InvalidateApiKeyRequest.usingRealmAndUserName(randomAlphaOfLength(5), "joe"),
93+
new InvalidateApiKeyRequest(randomAlphaOfLength(5), randomAlphaOfLength(7), null, null, false));
94+
95+
assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/get", getApiKeyRequest, authentication));
96+
assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", invalidateApiKeyRequest, authentication));
97+
}
98+
99+
private Authentication createMockAuthentication(String username, String realmName, String realmType, Map<String, Object> metadata) {
100+
final User user = new User(username);
101+
final Authentication authentication = mock(Authentication.class);
102+
final Authentication.RealmRef authenticatedBy = mock(Authentication.RealmRef.class);
103+
when(authentication.getUser()).thenReturn(user);
104+
when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy);
105+
when(authenticatedBy.getName()).thenReturn(realmName);
106+
when(authenticatedBy.getType()).thenReturn(realmType);
107+
when(authentication.getMetadata()).thenReturn(metadata);
108+
return authentication;
109+
}
110+
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetApiKeyAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ protected void doExecute(Task task, GetApiKeyRequest request, ActionListener<Get
5050
assert realm == null;
5151
// restrict username and realm to current authenticated user.
5252
username = authentication.getUser().principal();
53-
realm = authentication.getAuthenticatedBy().getName();
53+
realm = ApiKeyService.getCreatorRealmName(authentication);
5454
}
5555

5656
apiKeyService.getApiKeys(realm, username, apiKeyName, apiKeyId, listener);

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportInvalidateApiKeyAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ protected void doExecute(Task task, InvalidateApiKeyRequest request, ActionListe
5050
assert realm == null;
5151
// restrict username and realm to current authenticated user.
5252
username = authentication.getUser().principal();
53-
realm = authentication.getAuthenticatedBy().getName();
53+
realm = ApiKeyService.getCreatorRealmName(authentication);
5454
}
5555

5656
apiKeyService.invalidateApiKeys(realm, username, apiKeyName, apiKeyId, listener);

0 commit comments

Comments
 (0)