Skip to content

Commit aa623c3

Browse files
author
Yogesh Gaikwad
committed
Add manage_own_api_key cluster privilege
This commit adds `manage_own_api_key` cluster privilege which only allows api key cluster actions on API keys owned by the current authenticated user.
1 parent 541cfad commit aa623c3

File tree

3 files changed

+180
-1
lines changed

3 files changed

+180
-1
lines changed

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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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+
import java.util.function.BiPredicate;
21+
import java.util.function.Predicate;
22+
23+
/**
24+
* Named cluster privilege for managing API keys owned by the current authenticated user.
25+
*/
26+
public class ManageOwnApiKeyClusterPrivilege implements NamedClusterPrivilege {
27+
public static final ManageOwnApiKeyClusterPrivilege INSTANCE = new ManageOwnApiKeyClusterPrivilege();
28+
private static final Predicate<String> ACTION_PREDICATE = Automatons.predicate("cluster:admin/xpack/security/api_key/*");
29+
private final String name;
30+
private final BiPredicate<TransportRequest, Authentication> requestAuthnPredicate;
31+
32+
private ManageOwnApiKeyClusterPrivilege() {
33+
this.name = "manage_own_api_key";
34+
this.requestAuthnPredicate = (request, authentication) -> {
35+
if (request instanceof CreateApiKeyRequest) {
36+
return true;
37+
} else if (request instanceof GetApiKeyRequest) {
38+
final GetApiKeyRequest getApiKeyRequest = (GetApiKeyRequest) request;
39+
return checkIfUserIsOwnerOfApiKeys(authentication, getApiKeyRequest.getApiKeyId(), getApiKeyRequest.getUserName(),
40+
getApiKeyRequest.getRealmName());
41+
} else if (request instanceof InvalidateApiKeyRequest) {
42+
final InvalidateApiKeyRequest invalidateApiKeyRequest = (InvalidateApiKeyRequest) request;
43+
return checkIfUserIsOwnerOfApiKeys(authentication, invalidateApiKeyRequest.getId(), invalidateApiKeyRequest.getUserName(),
44+
invalidateApiKeyRequest.getRealmName());
45+
}
46+
return false;
47+
};
48+
}
49+
50+
private boolean checkIfUserIsOwnerOfApiKeys(Authentication authentication, String apiKeyId, String username, String realmName) {
51+
if (isCurrentAuthenticationUsingSameApiKeyIdFromRequest(authentication, apiKeyId)) {
52+
return true;
53+
} else {
54+
/*
55+
* TODO ygaikwad we need to think on how we can propagate appropriate error message to the end user when username, realm name
56+
* is missing. This is similar to the problem of propagating proper error messages in case of access denied.
57+
*/
58+
String authenticatedUserPrincipal = authentication.getUser().principal();
59+
String authenticatedUserRealm = authentication.getAuthenticatedBy().getName();
60+
if (Strings.hasText(username) && Strings.hasText(realmName)) {
61+
return username.equals(authenticatedUserPrincipal) && realmName.equals(authenticatedUserRealm);
62+
}
63+
}
64+
return false;
65+
}
66+
67+
private boolean isCurrentAuthenticationUsingSameApiKeyIdFromRequest(Authentication authentication, String apiKeyId) {
68+
// TODO ygaikwad replace with constants after merging other change
69+
if (authentication.getAuthenticatedBy().getType().equals("_es_api_key")) {
70+
// API key id from authentication must match the id from request
71+
String authenticatedApiKeyId = (String) authentication.getMetadata().get("_security_api_key_id");
72+
if (Strings.hasText(apiKeyId)) {
73+
return apiKeyId.equals(authenticatedApiKeyId);
74+
}
75+
}
76+
return false;
77+
}
78+
79+
@Override
80+
public String name() {
81+
return name;
82+
}
83+
84+
@Override
85+
public ClusterPermission.Builder buildPermission(ClusterPermission.Builder builder) {
86+
return builder.add(this, ACTION_PREDICATE, requestAuthnPredicate);
87+
}
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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 testActionRequestAuthenticationBasedPredicateWhenAuthenticatingWithApiKey() {
27+
final ClusterPermission clusterPermission =
28+
ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder()).build();
29+
{
30+
final String apiKeyId = randomAlphaOfLengthBetween(4, 7);
31+
final Authentication authentication = createMockAuthentication("_es_api_key", "_es_api_key",
32+
Map.of("_security_api_key_id", apiKeyId));
33+
final TransportRequest request = randomFrom(GetApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean()),
34+
InvalidateApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean()));
35+
36+
assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/get", request, authentication));
37+
assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", request, authentication));
38+
assertFalse(clusterPermission.check("cluster:admin/something", request, authentication));
39+
}
40+
{
41+
final String apiKeyId = randomAlphaOfLengthBetween(4, 7);
42+
final Authentication authentication = createMockAuthentication("_es_api_key", "_es_api_key",
43+
Map.of("_security_api_key_id", randomAlphaOfLength(7)));
44+
final TransportRequest request = randomFrom(GetApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean()),
45+
InvalidateApiKeyRequest.usingApiKeyId(apiKeyId, randomBoolean()));
46+
47+
assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/get", request, authentication));
48+
assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", request, authentication));
49+
}
50+
}
51+
52+
public void testActionRequestAuthenticationBasedPredicateWhenRequestContainsUsernameAndRealmName() {
53+
final ClusterPermission clusterPermission =
54+
ManageOwnApiKeyClusterPrivilege.INSTANCE.buildPermission(ClusterPermission.builder()).build();
55+
{
56+
final Authentication authentication = createMockAuthentication("realm1", "native", Map.of());
57+
final TransportRequest request = randomFrom(GetApiKeyRequest.usingRealmAndUserName("realm1", "joe"),
58+
InvalidateApiKeyRequest.usingRealmAndUserName("realm1", "joe"));
59+
60+
assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/get", request, authentication));
61+
assertTrue(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", request, authentication));
62+
assertFalse(clusterPermission.check("cluster:admin/something", request, authentication));
63+
}
64+
{
65+
final Authentication authentication = createMockAuthentication("realm1", "native", Map.of());
66+
final TransportRequest request = randomFrom(
67+
GetApiKeyRequest.usingRealmAndUserName("realm1", randomAlphaOfLength(7)),
68+
GetApiKeyRequest.usingRealmAndUserName(randomAlphaOfLength(5), "joe"),
69+
InvalidateApiKeyRequest.usingRealmAndUserName("realm1", randomAlphaOfLength(7)),
70+
InvalidateApiKeyRequest.usingRealmAndUserName(randomAlphaOfLength(5), "joe"));
71+
72+
assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/get", request, authentication));
73+
assertFalse(clusterPermission.check("cluster:admin/xpack/security/api_key/invalidate", request, authentication));
74+
}
75+
}
76+
77+
private Authentication createMockAuthentication(String realmName, String realmType, Map<String, Object> metadata) {
78+
final User user = new User("joe");
79+
final Authentication authentication = mock(Authentication.class);
80+
final Authentication.RealmRef authenticatedBy = mock(Authentication.RealmRef.class);
81+
when(authentication.getUser()).thenReturn(user);
82+
when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy);
83+
when(authenticatedBy.getName()).thenReturn(realmName);
84+
when(authenticatedBy.getType()).thenReturn(realmType);
85+
when(authentication.getMetadata()).thenReturn(metadata);
86+
return authentication;
87+
}
88+
}

0 commit comments

Comments
 (0)