Skip to content

Commit

Permalink
Consider owner flag when retrieving/invalidating keys with API key …
Browse files Browse the repository at this point in the history
…service (#45421)

Actual invocation of API key service now takes `owner` flag from the
request into consideration by setting the values for `realm` and
`username` as per the current authentication before invoking API key service.
This allows for retrieving or invalidating API keys owned by the current
authenticated user.

Relates: #40031
  • Loading branch information
bizybot authored Aug 14, 2019
1 parent 9170c81 commit 1950e38
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ public static GetApiKeyRequest usingApiKeyName(String apiKeyName, boolean ownedB
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
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ public static InvalidateApiKeyRequest usingApiKeyName(String name, boolean owned
return new InvalidateApiKeyRequest(null, null, null, name, 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
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,47 @@
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.core.security.SecurityContext;
import org.elasticsearch.xpack.core.security.action.GetApiKeyAction;
import org.elasticsearch.xpack.core.security.action.GetApiKeyRequest;
import org.elasticsearch.xpack.core.security.action.GetApiKeyResponse;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.security.authc.ApiKeyService;

public final class TransportGetApiKeyAction extends HandledTransportAction<GetApiKeyRequest,GetApiKeyResponse> {

private final ApiKeyService apiKeyService;
private final SecurityContext securityContext;

@Inject
public TransportGetApiKeyAction(TransportService transportService, ActionFilters actionFilters, ApiKeyService apiKeyService) {
public TransportGetApiKeyAction(TransportService transportService, ActionFilters actionFilters, ApiKeyService apiKeyService,
SecurityContext context) {
super(GetApiKeyAction.NAME, transportService, actionFilters,
(Writeable.Reader<GetApiKeyRequest>) GetApiKeyRequest::new);
this.apiKeyService = apiKeyService;
this.securityContext = context;
}

@Override
protected void doExecute(Task task, GetApiKeyRequest request, ActionListener<GetApiKeyResponse> listener) {
apiKeyService.getApiKeys(request.getRealmName(), request.getUserName(), request.getApiKeyName(), request.getApiKeyId(), listener);
String apiKeyId = request.getApiKeyId();
String apiKeyName = request.getApiKeyName();
String username = request.getUserName();
String realm = request.getRealmName();

final Authentication authentication = securityContext.getAuthentication();
if (authentication == null) {
listener.onFailure(new IllegalStateException("authentication is required"));
}
if (request.ownedByAuthenticatedUser()) {
assert username == null;
assert realm == null;
// restrict username and realm to current authenticated user.
username = authentication.getUser().principal();
realm = authentication.getAuthenticatedBy().getName();
}

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,47 @@
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.core.security.SecurityContext;
import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction;
import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest;
import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyResponse;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.security.authc.ApiKeyService;

public final class TransportInvalidateApiKeyAction extends HandledTransportAction<InvalidateApiKeyRequest, InvalidateApiKeyResponse> {

private final ApiKeyService apiKeyService;
private final SecurityContext securityContext;

@Inject
public TransportInvalidateApiKeyAction(TransportService transportService, ActionFilters actionFilters, ApiKeyService apiKeyService) {
public TransportInvalidateApiKeyAction(TransportService transportService, ActionFilters actionFilters, ApiKeyService apiKeyService,
SecurityContext context) {
super(InvalidateApiKeyAction.NAME, transportService, actionFilters,
(Writeable.Reader<InvalidateApiKeyRequest>) InvalidateApiKeyRequest::new);
(Writeable.Reader<InvalidateApiKeyRequest>) InvalidateApiKeyRequest::new);
this.apiKeyService = apiKeyService;
this.securityContext = context;
}

@Override
protected void doExecute(Task task, InvalidateApiKeyRequest request, ActionListener<InvalidateApiKeyResponse> listener) {
apiKeyService.invalidateApiKeys(request.getRealmName(), request.getUserName(), request.getName(), request.getId(), listener);
String apiKeyId = request.getId();
String apiKeyName = request.getName();
String username = request.getUserName();
String realm = request.getRealmName();

final Authentication authentication = securityContext.getAuthentication();
if (authentication == null) {
listener.onFailure(new IllegalStateException("authentication is required"));
}
if (request.ownedByAuthenticatedUser()) {
assert username == null;
assert realm == null;
// restrict username and realm to current authenticated user.
username = authentication.getUser().principal();
realm = authentication.getAuthenticatedBy().getName();
}

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,27 @@ public void wipeSecurityIndex() throws InterruptedException {
deleteSecurityIndex();
}

@Override
public String configRoles() {
return super.configRoles() + "\n" +
"manage_api_key_role:\n" +
" cluster: [\"manage_api_key\"]\n";
}

@Override
public String configUsers() {
final String usersPasswdHashed = new String(
getFastStoredHashAlgoForTests().hash(SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING));
return super.configUsers() +
"user_with_manage_api_key_role:" + usersPasswdHashed + "\n";
}

@Override
public String configUsersRoles() {
return super.configUsersRoles() +
"manage_api_key_role:user_with_manage_api_key_role\n";
}

private void awaitApiKeysRemoverCompletion() throws InterruptedException {
for (ApiKeyService apiKeyService : internalCluster().getInstances(ApiKeyService.class)) {
final boolean done = awaitBusy(() -> apiKeyService.isExpirationInProgress() == false);
Expand Down Expand Up @@ -479,10 +500,48 @@ public void testGetApiKeysForApiKeyName() throws InterruptedException, Execution
verifyGetResponse(1, responses, response, Collections.singleton(responses.get(0).getId()), null);
}

private void verifyGetResponse(int noOfApiKeys, List<CreateApiKeyResponse> responses, GetApiKeyResponse response,
public void testGetApiKeysOwnedByCurrentAuthenticatedUser() throws InterruptedException, ExecutionException {
int noOfSuperuserApiKeys = randomIntBetween(3, 5);
int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5);
List<CreateApiKeyResponse> defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null);
List<CreateApiKeyResponse> userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role",
noOfApiKeysForUserWithManageApiKeyRole, null);
final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken
.basicAuthHeaderValue("user_with_manage_api_key_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)));

PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.forOwnedApiKeys(), listener);
GetApiKeyResponse response = listener.get();
verifyGetResponse("user_with_manage_api_key_role", noOfApiKeysForUserWithManageApiKeyRole, userWithManageApiKeyRoleApiKeys,
response, userWithManageApiKeyRoleApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()), null);
}

public void testInvalidateApiKeysOwnedByCurrentAuthenticatedUser() throws InterruptedException, ExecutionException {
int noOfSuperuserApiKeys = randomIntBetween(3, 5);
int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5);
List<CreateApiKeyResponse> defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null);
List<CreateApiKeyResponse> userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role",
noOfApiKeysForUserWithManageApiKeyRole, null);
final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken
.basicAuthHeaderValue("user_with_manage_api_key_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)));

PlainActionFuture<InvalidateApiKeyResponse> listener = new PlainActionFuture<>();
client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.forOwnedApiKeys(), listener);
InvalidateApiKeyResponse invalidateResponse = listener.get();

verifyInvalidateResponse(noOfApiKeysForUserWithManageApiKeyRole, userWithManageApiKeyRoleApiKeys, invalidateResponse);
}

private void verifyGetResponse(int expectedNumberOfApiKeys, List<CreateApiKeyResponse> responses, GetApiKeyResponse response,
Set<String> validApiKeyIds,
List<String> invalidatedApiKeyIds) {
assertThat(response.getApiKeyInfos().length, equalTo(noOfApiKeys));
verifyGetResponse(SecuritySettingsSource.TEST_SUPERUSER, expectedNumberOfApiKeys, responses, response, validApiKeyIds,
invalidatedApiKeyIds);
}

private void verifyGetResponse(String user, int expectedNumberOfApiKeys, List<CreateApiKeyResponse> responses,
GetApiKeyResponse response, Set<String> validApiKeyIds, List<String> invalidatedApiKeyIds) {
assertThat(response.getApiKeyInfos().length, equalTo(expectedNumberOfApiKeys));
List<String> expectedIds = responses.stream().filter(o -> validApiKeyIds.contains(o.getId())).map(o -> o.getId())
.collect(Collectors.toList());
List<String> actualIds = Arrays.stream(response.getApiKeyInfos()).filter(o -> o.isInvalidated() == false).map(o -> o.getId())
Expand All @@ -494,7 +553,7 @@ private void verifyGetResponse(int noOfApiKeys, List<CreateApiKeyResponse> respo
.collect(Collectors.toList());
assertThat(actualNames, containsInAnyOrder(expectedNames.toArray(Strings.EMPTY_ARRAY)));
Set<String> expectedUsernames = (validApiKeyIds.isEmpty()) ? Collections.emptySet()
: Collections.singleton(SecuritySettingsSource.TEST_SUPERUSER);
: Set.of(user);
Set<String> actualUsernames = Arrays.stream(response.getApiKeyInfos()).filter(o -> o.isInvalidated() == false)
.map(o -> o.getUsername()).collect(Collectors.toSet());
assertThat(actualUsernames, containsInAnyOrder(expectedUsernames.toArray(Strings.EMPTY_ARRAY)));
Expand All @@ -503,15 +562,18 @@ private void verifyGetResponse(int noOfApiKeys, List<CreateApiKeyResponse> respo
.map(o -> o.getId()).collect(Collectors.toList());
assertThat(invalidatedApiKeyIds, containsInAnyOrder(actualInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY)));
}

}

private List<CreateApiKeyResponse> createApiKeys(int noOfApiKeys, TimeValue expiration) {
return createApiKeys(SecuritySettingsSource.TEST_SUPERUSER, noOfApiKeys, expiration);
}

private List<CreateApiKeyResponse> createApiKeys(String user, int noOfApiKeys, TimeValue expiration) {
List<CreateApiKeyResponse> responses = new ArrayList<>();
for (int i = 0; i < noOfApiKeys; i++) {
final RoleDescriptor descriptor = new RoleDescriptor("role", new String[] { "monitor" }, null, null);
Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken
.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)));
.basicAuthHeaderValue(user, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)));
final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client)
.setName("test-key-" + randomAlphaOfLengthBetween(5, 9) + i).setExpiration(expiration)
.setRoleDescriptors(Collections.singletonList(descriptor)).get();
Expand Down

0 comments on commit 1950e38

Please sign in to comment.