Skip to content

Commit 7b6246e

Browse files
authored
Add manage_own_api_key cluster privilege (#45897) (#46023)
The existing privilege model for API keys with privileges like `manage_api_key`, `manage_security` etc. are too permissive and we would want finer-grained control over the cluster privileges for API keys. Previously APIs created would also need these privileges to get its own information. This commit adds support for `manage_own_api_key` cluster privilege which only allows api key cluster actions on API keys owned by the currently authenticated user. Also adds support for retrieval of the API key self-information when authenticating via API key without the need for the additional API key privileges. To support this privilege, we are introducing additional authentication context along with the request context such that it can be used to authorize cluster actions based on the current user authentication. The API key get and invalidate APIs introduce an `owner` flag that can be set to true if the API key request (Get or Invalidate) is for the API keys owned by the currently authenticated user only. In that case, `realm` and `username` cannot be set as they are assumed to be the currently authenticated ones. The changes cover HLRC changes, documentation for the API changes. Closes #40031
1 parent dd6c13f commit 7b6246e

File tree

46 files changed

+1768
-741
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1768
-741
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ static Request getApiKey(final GetApiKeyRequest getApiKeyRequest) throws IOExcep
301301
if (Strings.hasText(getApiKeyRequest.getRealmName())) {
302302
request.addParameter("realm_name", getApiKeyRequest.getRealmName());
303303
}
304-
304+
request.addParameter("owner", Boolean.toString(getApiKeyRequest.ownedByAuthenticatedUser()));
305305
return request;
306306
}
307307

client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyRequest.java

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,34 @@ public final class GetApiKeyRequest implements Validatable, ToXContentObject {
3636
private final String userName;
3737
private final String id;
3838
private final String name;
39+
private final boolean ownedByAuthenticatedUser;
3940

4041
// pkg scope for testing
4142
GetApiKeyRequest(@Nullable String realmName, @Nullable String userName, @Nullable String apiKeyId,
42-
@Nullable String apiKeyName) {
43+
@Nullable String apiKeyName, boolean ownedByAuthenticatedUser) {
4344
if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false
44-
&& Strings.hasText(apiKeyName) == false) {
45-
throwValidationError("One of [api key id, api key name, username, realm name] must be specified");
45+
&& Strings.hasText(apiKeyName) == false && ownedByAuthenticatedUser == false) {
46+
throwValidationError("One of [api key id, api key name, username, realm name] must be specified if [owner] flag is false");
4647
}
4748
if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) {
4849
if (Strings.hasText(realmName) || Strings.hasText(userName)) {
4950
throwValidationError(
5051
"username or realm name must not be specified when the api key id or api key name is specified");
5152
}
5253
}
54+
if (ownedByAuthenticatedUser) {
55+
if (Strings.hasText(realmName) || Strings.hasText(userName)) {
56+
throwValidationError("neither username nor realm-name may be specified when retrieving owned API keys");
57+
}
58+
}
5359
if (Strings.hasText(apiKeyId) && Strings.hasText(apiKeyName)) {
5460
throwValidationError("only one of [api key id, api key name] can be specified");
5561
}
5662
this.realmName = realmName;
5763
this.userName = userName;
5864
this.id = apiKeyId;
5965
this.name = apiKeyName;
66+
this.ownedByAuthenticatedUser = ownedByAuthenticatedUser;
6067
}
6168

6269
private void throwValidationError(String message) {
@@ -79,13 +86,17 @@ public String getName() {
7986
return name;
8087
}
8188

89+
public boolean ownedByAuthenticatedUser() {
90+
return ownedByAuthenticatedUser;
91+
}
92+
8293
/**
8394
* Creates get API key request for given realm name
8495
* @param realmName realm name
8596
* @return {@link GetApiKeyRequest}
8697
*/
8798
public static GetApiKeyRequest usingRealmName(String realmName) {
88-
return new GetApiKeyRequest(realmName, null, null, null);
99+
return new GetApiKeyRequest(realmName, null, null, null, false);
89100
}
90101

91102
/**
@@ -94,7 +105,7 @@ public static GetApiKeyRequest usingRealmName(String realmName) {
94105
* @return {@link GetApiKeyRequest}
95106
*/
96107
public static GetApiKeyRequest usingUserName(String userName) {
97-
return new GetApiKeyRequest(null, userName, null, null);
108+
return new GetApiKeyRequest(null, userName, null, null, false);
98109
}
99110

100111
/**
@@ -104,25 +115,36 @@ public static GetApiKeyRequest usingUserName(String userName) {
104115
* @return {@link GetApiKeyRequest}
105116
*/
106117
public static GetApiKeyRequest usingRealmAndUserName(String realmName, String userName) {
107-
return new GetApiKeyRequest(realmName, userName, null, null);
118+
return new GetApiKeyRequest(realmName, userName, null, null, false);
108119
}
109120

110121
/**
111122
* Creates get API key request for given api key id
112123
* @param apiKeyId api key id
124+
* @param ownedByAuthenticatedUser set {@code true} if the request is only for the API keys owned by current
125+
* authenticated user else{@code false}
113126
* @return {@link GetApiKeyRequest}
114127
*/
115-
public static GetApiKeyRequest usingApiKeyId(String apiKeyId) {
116-
return new GetApiKeyRequest(null, null, apiKeyId, null);
128+
public static GetApiKeyRequest usingApiKeyId(String apiKeyId, boolean ownedByAuthenticatedUser) {
129+
return new GetApiKeyRequest(null, null, apiKeyId, null, ownedByAuthenticatedUser);
117130
}
118131

119132
/**
120133
* Creates get API key request for given api key name
121134
* @param apiKeyName api key name
135+
* @param ownedByAuthenticatedUser set {@code true} if the request is only for the API keys owned by current
136+
* authenticated user else{@code false}
122137
* @return {@link GetApiKeyRequest}
123138
*/
124-
public static GetApiKeyRequest usingApiKeyName(String apiKeyName) {
125-
return new GetApiKeyRequest(null, null, null, apiKeyName);
139+
public static GetApiKeyRequest usingApiKeyName(String apiKeyName, boolean ownedByAuthenticatedUser) {
140+
return new GetApiKeyRequest(null, null, null, apiKeyName, ownedByAuthenticatedUser);
141+
}
142+
143+
/**
144+
* Creates get api key request to retrieve api key information for the api keys owned by the current authenticated user.
145+
*/
146+
public static GetApiKeyRequest forOwnedApiKeys() {
147+
return new GetApiKeyRequest(null, null, null, null, true);
126148
}
127149

128150
@Override

client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateApiKeyRequest.java

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,34 @@ public final class InvalidateApiKeyRequest implements Validatable, ToXContentObj
3636
private final String userName;
3737
private final String id;
3838
private final String name;
39+
private final boolean ownedByAuthenticatedUser;
3940

4041
// pkg scope for testing
4142
InvalidateApiKeyRequest(@Nullable String realmName, @Nullable String userName, @Nullable String apiKeyId,
42-
@Nullable String apiKeyName) {
43+
@Nullable String apiKeyName, boolean ownedByAuthenticatedUser) {
4344
if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false
44-
&& Strings.hasText(apiKeyName) == false) {
45-
throwValidationError("One of [api key id, api key name, username, realm name] must be specified");
45+
&& Strings.hasText(apiKeyName) == false && ownedByAuthenticatedUser == false) {
46+
throwValidationError("One of [api key id, api key name, username, realm name] must be specified if [owner] flag is false");
4647
}
4748
if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) {
4849
if (Strings.hasText(realmName) || Strings.hasText(userName)) {
4950
throwValidationError(
5051
"username or realm name must not be specified when the api key id or api key name is specified");
5152
}
5253
}
54+
if (ownedByAuthenticatedUser) {
55+
if (Strings.hasText(realmName) || Strings.hasText(userName)) {
56+
throwValidationError("neither username nor realm-name may be specified when invalidating owned API keys");
57+
}
58+
}
5359
if (Strings.hasText(apiKeyId) && Strings.hasText(apiKeyName)) {
5460
throwValidationError("only one of [api key id, api key name] can be specified");
5561
}
5662
this.realmName = realmName;
5763
this.userName = userName;
5864
this.id = apiKeyId;
5965
this.name = apiKeyName;
66+
this.ownedByAuthenticatedUser = ownedByAuthenticatedUser;
6067
}
6168

6269
private void throwValidationError(String message) {
@@ -79,13 +86,17 @@ public String getName() {
7986
return name;
8087
}
8188

89+
public boolean ownedByAuthenticatedUser() {
90+
return ownedByAuthenticatedUser;
91+
}
92+
8293
/**
8394
* Creates invalidate API key request for given realm name
8495
* @param realmName realm name
8596
* @return {@link InvalidateApiKeyRequest}
8697
*/
8798
public static InvalidateApiKeyRequest usingRealmName(String realmName) {
88-
return new InvalidateApiKeyRequest(realmName, null, null, null);
99+
return new InvalidateApiKeyRequest(realmName, null, null, null, false);
89100
}
90101

91102
/**
@@ -94,7 +105,7 @@ public static InvalidateApiKeyRequest usingRealmName(String realmName) {
94105
* @return {@link InvalidateApiKeyRequest}
95106
*/
96107
public static InvalidateApiKeyRequest usingUserName(String userName) {
97-
return new InvalidateApiKeyRequest(null, userName, null, null);
108+
return new InvalidateApiKeyRequest(null, userName, null, null, false);
98109
}
99110

100111
/**
@@ -104,25 +115,36 @@ public static InvalidateApiKeyRequest usingUserName(String userName) {
104115
* @return {@link InvalidateApiKeyRequest}
105116
*/
106117
public static InvalidateApiKeyRequest usingRealmAndUserName(String realmName, String userName) {
107-
return new InvalidateApiKeyRequest(realmName, userName, null, null);
118+
return new InvalidateApiKeyRequest(realmName, userName, null, null, false);
108119
}
109120

110121
/**
111122
* Creates invalidate API key request for given api key id
112123
* @param apiKeyId api key id
124+
* @param ownedByAuthenticatedUser set {@code true} if the request is only for the API keys owned by current authenticated user else
125+
* {@code false}
113126
* @return {@link InvalidateApiKeyRequest}
114127
*/
115-
public static InvalidateApiKeyRequest usingApiKeyId(String apiKeyId) {
116-
return new InvalidateApiKeyRequest(null, null, apiKeyId, null);
128+
public static InvalidateApiKeyRequest usingApiKeyId(String apiKeyId, boolean ownedByAuthenticatedUser) {
129+
return new InvalidateApiKeyRequest(null, null, apiKeyId, null, ownedByAuthenticatedUser);
117130
}
118131

119132
/**
120133
* Creates invalidate API key request for given api key name
121134
* @param apiKeyName api key name
135+
* @param ownedByAuthenticatedUser set {@code true} if the request is only for the API keys owned by current authenticated user else
136+
* {@code false}
122137
* @return {@link InvalidateApiKeyRequest}
123138
*/
124-
public static InvalidateApiKeyRequest usingApiKeyName(String apiKeyName) {
125-
return new InvalidateApiKeyRequest(null, null, null, apiKeyName);
139+
public static InvalidateApiKeyRequest usingApiKeyName(String apiKeyName, boolean ownedByAuthenticatedUser) {
140+
return new InvalidateApiKeyRequest(null, null, null, apiKeyName, ownedByAuthenticatedUser);
141+
}
142+
143+
/**
144+
* Creates invalidate api key request to invalidate api keys owned by the current authenticated user.
145+
*/
146+
public static InvalidateApiKeyRequest forOwnedApiKeys() {
147+
return new InvalidateApiKeyRequest(null, null, null, null, true);
126148
}
127149

128150
@Override
@@ -140,6 +162,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
140162
if (name != null) {
141163
builder.field("name", name);
142164
}
165+
builder.field("owner", ownedByAuthenticatedUser);
143166
return builder.endObject();
144167
}
145168
}

client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -462,10 +462,11 @@ public void testGetApiKey() throws IOException {
462462
final Request request = SecurityRequestConverters.getApiKey(getApiKeyRequest);
463463
assertEquals(HttpGet.METHOD_NAME, request.getMethod());
464464
assertEquals("/_security/api_key", request.getEndpoint());
465-
Map<String, String> mapOfParameters = new HashMap<>();
466-
mapOfParameters.put("realm_name", realmName);
467-
mapOfParameters.put("username", userName);
468-
assertThat(request.getParameters(), equalTo(mapOfParameters));
465+
Map<String, String> expectedMapOfParameters = new HashMap<>();
466+
expectedMapOfParameters.put("realm_name", realmName);
467+
expectedMapOfParameters.put("username", userName);
468+
expectedMapOfParameters.put("owner", Boolean.FALSE.toString());
469+
assertThat(request.getParameters(), equalTo(expectedMapOfParameters));
469470
}
470471

471472
public void testInvalidateApiKey() throws IOException {

client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1923,7 +1923,7 @@ public void testGetApiKey() throws Exception {
19231923
Instant.now().plusMillis(expiration.getMillis()), false, "test_user", "default_file");
19241924
{
19251925
// tag::get-api-key-id-request
1926-
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId());
1926+
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId(), false);
19271927
// end::get-api-key-id-request
19281928

19291929
// tag::get-api-key-execute
@@ -1937,7 +1937,7 @@ public void testGetApiKey() throws Exception {
19371937

19381938
{
19391939
// tag::get-api-key-name-request
1940-
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyName(createApiKeyResponse1.getName());
1940+
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyName(createApiKeyResponse1.getName(), false);
19411941
// end::get-api-key-name-request
19421942

19431943
GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT);
@@ -1971,6 +1971,18 @@ public void testGetApiKey() throws Exception {
19711971
verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo);
19721972
}
19731973

1974+
{
1975+
// tag::get-api-keys-owned-by-authenticated-user-request
1976+
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.forOwnedApiKeys();
1977+
// end::get-api-keys-owned-by-authenticated-user-request
1978+
1979+
GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT);
1980+
1981+
assertThat(getApiKeyResponse.getApiKeyInfos(), is(notNullValue()));
1982+
assertThat(getApiKeyResponse.getApiKeyInfos().size(), is(1));
1983+
verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo);
1984+
}
1985+
19741986
{
19751987
// tag::get-user-realm-api-keys-request
19761988
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingRealmAndUserName("default_file", "test_user");
@@ -1986,7 +1998,7 @@ public void testGetApiKey() throws Exception {
19861998
}
19871999

19882000
{
1989-
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId());
2001+
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId(), false);
19902002

19912003
ActionListener<GetApiKeyResponse> listener;
19922004
// tag::get-api-key-execute-listener
@@ -2047,7 +2059,7 @@ public void testInvalidateApiKey() throws Exception {
20472059

20482060
{
20492061
// tag::invalidate-api-key-id-request
2050-
InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId());
2062+
InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId(), false);
20512063
// end::invalidate-api-key-id-request
20522064

20532065
// tag::invalidate-api-key-execute
@@ -2072,7 +2084,8 @@ public void testInvalidateApiKey() throws Exception {
20722084
assertNotNull(createApiKeyResponse2.getKey());
20732085

20742086
// tag::invalidate-api-key-name-request
2075-
InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyName(createApiKeyResponse2.getName());
2087+
InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyName(createApiKeyResponse2.getName(),
2088+
false);
20762089
// end::invalidate-api-key-name-request
20772090

20782091
InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateApiKey(invalidateApiKeyRequest,
@@ -2165,7 +2178,7 @@ public void testInvalidateApiKey() throws Exception {
21652178
assertThat(createApiKeyResponse6.getName(), equalTo("k6"));
21662179
assertNotNull(createApiKeyResponse6.getKey());
21672180

2168-
InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(createApiKeyResponse6.getId());
2181+
InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(createApiKeyResponse6.getId(), false);
21692182

21702183
ActionListener<InvalidateApiKeyResponse> listener;
21712184
// tag::invalidate-api-key-execute-listener
@@ -2201,6 +2214,30 @@ public void onFailure(Exception e) {
22012214
assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY)));
22022215
assertThat(response.getPreviouslyInvalidatedApiKeys().size(), equalTo(0));
22032216
}
2217+
2218+
{
2219+
createApiKeyRequest = new CreateApiKeyRequest("k7", roles, expiration, refreshPolicy);
2220+
CreateApiKeyResponse createApiKeyResponse7 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT);
2221+
assertThat(createApiKeyResponse7.getName(), equalTo("k7"));
2222+
assertNotNull(createApiKeyResponse7.getKey());
2223+
2224+
// tag::invalidate-api-keys-owned-by-authenticated-user-request
2225+
InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.forOwnedApiKeys();
2226+
// end::invalidate-api-keys-owned-by-authenticated-user-request
2227+
2228+
InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateApiKey(invalidateApiKeyRequest,
2229+
RequestOptions.DEFAULT);
2230+
2231+
final List<ElasticsearchException> errors = invalidateApiKeyResponse.getErrors();
2232+
final List<String> invalidatedApiKeyIds = invalidateApiKeyResponse.getInvalidatedApiKeys();
2233+
final List<String> previouslyInvalidatedApiKeyIds = invalidateApiKeyResponse.getPreviouslyInvalidatedApiKeys();
2234+
2235+
assertTrue(errors.isEmpty());
2236+
List<String> expectedInvalidatedApiKeyIds = Arrays.asList(createApiKeyResponse7.getId());
2237+
assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY)));
2238+
assertThat(previouslyInvalidatedApiKeyIds.size(), equalTo(0));
2239+
}
2240+
22042241
}
22052242

22062243
public void testDelegatePkiAuthentication() throws Exception {

0 commit comments

Comments
 (0)