Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider owner flag when retrieving/invalidating keys with API key service #45421

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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