Skip to content

Commit

Permalink
Add manage_own_api_key cluster privilege (#45897) (#46023)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bizybot committed Aug 27, 2019
1 parent dd6c13f commit 7b6246e
Show file tree
Hide file tree
Showing 46 changed files with 1,768 additions and 741 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ static Request getApiKey(final GetApiKeyRequest getApiKeyRequest) throws IOExcep
if (Strings.hasText(getApiKeyRequest.getRealmName())) {
request.addParameter("realm_name", getApiKeyRequest.getRealmName());
}

request.addParameter("owner", Boolean.toString(getApiKeyRequest.ownedByAuthenticatedUser()));
return request;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,34 @@ public final class GetApiKeyRequest implements Validatable, ToXContentObject {
private final String userName;
private final String id;
private final String name;
private final boolean ownedByAuthenticatedUser;

// pkg scope for testing
GetApiKeyRequest(@Nullable String realmName, @Nullable String userName, @Nullable String apiKeyId,
@Nullable String apiKeyName) {
@Nullable String apiKeyName, boolean ownedByAuthenticatedUser) {
if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false
&& Strings.hasText(apiKeyName) == false) {
throwValidationError("One of [api key id, api key name, username, realm name] must be specified");
&& Strings.hasText(apiKeyName) == false && ownedByAuthenticatedUser == false) {
throwValidationError("One of [api key id, api key name, username, realm name] must be specified if [owner] flag is false");
}
if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) {
if (Strings.hasText(realmName) || Strings.hasText(userName)) {
throwValidationError(
"username or realm name must not be specified when the api key id or api key name is specified");
}
}
if (ownedByAuthenticatedUser) {
if (Strings.hasText(realmName) || Strings.hasText(userName)) {
throwValidationError("neither username nor realm-name may be specified when retrieving owned API keys");
}
}
if (Strings.hasText(apiKeyId) && Strings.hasText(apiKeyName)) {
throwValidationError("only one of [api key id, api key name] can be specified");
}
this.realmName = realmName;
this.userName = userName;
this.id = apiKeyId;
this.name = apiKeyName;
this.ownedByAuthenticatedUser = ownedByAuthenticatedUser;
}

private void throwValidationError(String message) {
Expand All @@ -79,13 +86,17 @@ public String getName() {
return name;
}

public boolean ownedByAuthenticatedUser() {
return ownedByAuthenticatedUser;
}

/**
* Creates get API key request for given realm name
* @param realmName realm name
* @return {@link GetApiKeyRequest}
*/
public static GetApiKeyRequest usingRealmName(String realmName) {
return new GetApiKeyRequest(realmName, null, null, null);
return new GetApiKeyRequest(realmName, null, null, null, false);
}

/**
Expand All @@ -94,7 +105,7 @@ public static GetApiKeyRequest usingRealmName(String realmName) {
* @return {@link GetApiKeyRequest}
*/
public static GetApiKeyRequest usingUserName(String userName) {
return new GetApiKeyRequest(null, userName, null, null);
return new GetApiKeyRequest(null, userName, null, null, false);
}

/**
Expand All @@ -104,25 +115,36 @@ public static GetApiKeyRequest usingUserName(String userName) {
* @return {@link GetApiKeyRequest}
*/
public static GetApiKeyRequest usingRealmAndUserName(String realmName, String userName) {
return new GetApiKeyRequest(realmName, userName, null, null);
return new GetApiKeyRequest(realmName, userName, null, null, false);
}

/**
* Creates get API key request for given api key id
* @param apiKeyId api key id
* @param ownedByAuthenticatedUser set {@code true} if the request is only for the API keys owned by current
* authenticated user else{@code false}
* @return {@link GetApiKeyRequest}
*/
public static GetApiKeyRequest usingApiKeyId(String apiKeyId) {
return new GetApiKeyRequest(null, null, apiKeyId, null);
public static GetApiKeyRequest usingApiKeyId(String apiKeyId, boolean ownedByAuthenticatedUser) {
return new GetApiKeyRequest(null, null, apiKeyId, null, ownedByAuthenticatedUser);
}

/**
* Creates get API key request for given api key name
* @param apiKeyName api key name
* @param ownedByAuthenticatedUser set {@code true} if the request is only for the API keys owned by current
* authenticated user else{@code false}
* @return {@link GetApiKeyRequest}
*/
public static GetApiKeyRequest usingApiKeyName(String apiKeyName) {
return new GetApiKeyRequest(null, null, null, apiKeyName);
public static GetApiKeyRequest usingApiKeyName(String apiKeyName, boolean ownedByAuthenticatedUser) {
return new GetApiKeyRequest(null, null, null, apiKeyName, ownedByAuthenticatedUser);
}

/**
* Creates get api key request to retrieve api key information for the api keys owned by the current authenticated user.
*/
public static GetApiKeyRequest forOwnedApiKeys() {
return new GetApiKeyRequest(null, null, null, null, true);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,34 @@ public final class InvalidateApiKeyRequest implements Validatable, ToXContentObj
private final String userName;
private final String id;
private final String name;
private final boolean ownedByAuthenticatedUser;

// pkg scope for testing
InvalidateApiKeyRequest(@Nullable String realmName, @Nullable String userName, @Nullable String apiKeyId,
@Nullable String apiKeyName) {
@Nullable String apiKeyName, boolean ownedByAuthenticatedUser) {
if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false
&& Strings.hasText(apiKeyName) == false) {
throwValidationError("One of [api key id, api key name, username, realm name] must be specified");
&& Strings.hasText(apiKeyName) == false && ownedByAuthenticatedUser == false) {
throwValidationError("One of [api key id, api key name, username, realm name] must be specified if [owner] flag is false");
}
if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) {
if (Strings.hasText(realmName) || Strings.hasText(userName)) {
throwValidationError(
"username or realm name must not be specified when the api key id or api key name is specified");
}
}
if (ownedByAuthenticatedUser) {
if (Strings.hasText(realmName) || Strings.hasText(userName)) {
throwValidationError("neither username nor realm-name may be specified when invalidating owned API keys");
}
}
if (Strings.hasText(apiKeyId) && Strings.hasText(apiKeyName)) {
throwValidationError("only one of [api key id, api key name] can be specified");
}
this.realmName = realmName;
this.userName = userName;
this.id = apiKeyId;
this.name = apiKeyName;
this.ownedByAuthenticatedUser = ownedByAuthenticatedUser;
}

private void throwValidationError(String message) {
Expand All @@ -79,13 +86,17 @@ public String getName() {
return name;
}

public boolean ownedByAuthenticatedUser() {
return ownedByAuthenticatedUser;
}

/**
* Creates invalidate API key request for given realm name
* @param realmName realm name
* @return {@link InvalidateApiKeyRequest}
*/
public static InvalidateApiKeyRequest usingRealmName(String realmName) {
return new InvalidateApiKeyRequest(realmName, null, null, null);
return new InvalidateApiKeyRequest(realmName, null, null, null, false);
}

/**
Expand All @@ -94,7 +105,7 @@ public static InvalidateApiKeyRequest usingRealmName(String realmName) {
* @return {@link InvalidateApiKeyRequest}
*/
public static InvalidateApiKeyRequest usingUserName(String userName) {
return new InvalidateApiKeyRequest(null, userName, null, null);
return new InvalidateApiKeyRequest(null, userName, null, null, false);
}

/**
Expand All @@ -104,25 +115,36 @@ public static InvalidateApiKeyRequest usingUserName(String userName) {
* @return {@link InvalidateApiKeyRequest}
*/
public static InvalidateApiKeyRequest usingRealmAndUserName(String realmName, String userName) {
return new InvalidateApiKeyRequest(realmName, userName, null, null);
return new InvalidateApiKeyRequest(realmName, userName, null, null, false);
}

/**
* Creates invalidate API key request for given api key id
* @param apiKeyId api key id
* @param ownedByAuthenticatedUser set {@code true} if the request is only for the API keys owned by current authenticated user else
* {@code false}
* @return {@link InvalidateApiKeyRequest}
*/
public static InvalidateApiKeyRequest usingApiKeyId(String apiKeyId) {
return new InvalidateApiKeyRequest(null, null, apiKeyId, null);
public static InvalidateApiKeyRequest usingApiKeyId(String apiKeyId, boolean ownedByAuthenticatedUser) {
return new InvalidateApiKeyRequest(null, null, apiKeyId, null, ownedByAuthenticatedUser);
}

/**
* Creates invalidate API key request for given api key name
* @param apiKeyName api key name
* @param ownedByAuthenticatedUser set {@code true} if the request is only for the API keys owned by current authenticated user else
* {@code false}
* @return {@link InvalidateApiKeyRequest}
*/
public static InvalidateApiKeyRequest usingApiKeyName(String apiKeyName) {
return new InvalidateApiKeyRequest(null, null, null, apiKeyName);
public static InvalidateApiKeyRequest usingApiKeyName(String apiKeyName, boolean ownedByAuthenticatedUser) {
return new InvalidateApiKeyRequest(null, null, null, apiKeyName, ownedByAuthenticatedUser);
}

/**
* Creates invalidate api key request to invalidate api keys owned by the current authenticated user.
*/
public static InvalidateApiKeyRequest forOwnedApiKeys() {
return new InvalidateApiKeyRequest(null, null, null, null, true);
}

@Override
Expand All @@ -140,6 +162,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
if (name != null) {
builder.field("name", name);
}
builder.field("owner", ownedByAuthenticatedUser);
return builder.endObject();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -462,10 +462,11 @@ public void testGetApiKey() throws IOException {
final Request request = SecurityRequestConverters.getApiKey(getApiKeyRequest);
assertEquals(HttpGet.METHOD_NAME, request.getMethod());
assertEquals("/_security/api_key", request.getEndpoint());
Map<String, String> mapOfParameters = new HashMap<>();
mapOfParameters.put("realm_name", realmName);
mapOfParameters.put("username", userName);
assertThat(request.getParameters(), equalTo(mapOfParameters));
Map<String, String> expectedMapOfParameters = new HashMap<>();
expectedMapOfParameters.put("realm_name", realmName);
expectedMapOfParameters.put("username", userName);
expectedMapOfParameters.put("owner", Boolean.FALSE.toString());
assertThat(request.getParameters(), equalTo(expectedMapOfParameters));
}

public void testInvalidateApiKey() throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1923,7 +1923,7 @@ public void testGetApiKey() throws Exception {
Instant.now().plusMillis(expiration.getMillis()), false, "test_user", "default_file");
{
// tag::get-api-key-id-request
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId());
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId(), false);
// end::get-api-key-id-request

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

{
// tag::get-api-key-name-request
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyName(createApiKeyResponse1.getName());
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyName(createApiKeyResponse1.getName(), false);
// end::get-api-key-name-request

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

{
// tag::get-api-keys-owned-by-authenticated-user-request
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.forOwnedApiKeys();
// end::get-api-keys-owned-by-authenticated-user-request

GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT);

assertThat(getApiKeyResponse.getApiKeyInfos(), is(notNullValue()));
assertThat(getApiKeyResponse.getApiKeyInfos().size(), is(1));
verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo);
}

{
// tag::get-user-realm-api-keys-request
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingRealmAndUserName("default_file", "test_user");
Expand All @@ -1986,7 +1998,7 @@ public void testGetApiKey() throws Exception {
}

{
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId());
GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId(), false);

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

{
// tag::invalidate-api-key-id-request
InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId());
InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId(), false);
// end::invalidate-api-key-id-request

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

// tag::invalidate-api-key-name-request
InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyName(createApiKeyResponse2.getName());
InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyName(createApiKeyResponse2.getName(),
false);
// end::invalidate-api-key-name-request

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

InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(createApiKeyResponse6.getId());
InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(createApiKeyResponse6.getId(), false);

ActionListener<InvalidateApiKeyResponse> listener;
// tag::invalidate-api-key-execute-listener
Expand Down Expand Up @@ -2201,6 +2214,30 @@ public void onFailure(Exception e) {
assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY)));
assertThat(response.getPreviouslyInvalidatedApiKeys().size(), equalTo(0));
}

{
createApiKeyRequest = new CreateApiKeyRequest("k7", roles, expiration, refreshPolicy);
CreateApiKeyResponse createApiKeyResponse7 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT);
assertThat(createApiKeyResponse7.getName(), equalTo("k7"));
assertNotNull(createApiKeyResponse7.getKey());

// tag::invalidate-api-keys-owned-by-authenticated-user-request
InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.forOwnedApiKeys();
// end::invalidate-api-keys-owned-by-authenticated-user-request

InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateApiKey(invalidateApiKeyRequest,
RequestOptions.DEFAULT);

final List<ElasticsearchException> errors = invalidateApiKeyResponse.getErrors();
final List<String> invalidatedApiKeyIds = invalidateApiKeyResponse.getInvalidatedApiKeys();
final List<String> previouslyInvalidatedApiKeyIds = invalidateApiKeyResponse.getPreviouslyInvalidatedApiKeys();

assertTrue(errors.isEmpty());
List<String> expectedInvalidatedApiKeyIds = Arrays.asList(createApiKeyResponse7.getId());
assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY)));
assertThat(previouslyInvalidatedApiKeyIds.size(), equalTo(0));
}

}

public void testDelegatePkiAuthentication() throws Exception {
Expand Down
Loading

0 comments on commit 7b6246e

Please sign in to comment.