|
@@ -19,7 +19,6 @@ import org.elasticsearch.action.admin.indices.refresh.RefreshRequestBuilder;
|
|
|
import org.elasticsearch.action.admin.indices.refresh.RefreshResponse;
|
|
|
import org.elasticsearch.action.get.GetAction;
|
|
|
import org.elasticsearch.action.get.GetRequest;
|
|
|
-import org.elasticsearch.action.get.GetResponse;
|
|
|
import org.elasticsearch.action.support.PlainActionFuture;
|
|
|
import org.elasticsearch.action.support.WriteRequest;
|
|
|
import org.elasticsearch.action.update.UpdateResponse;
|
|
@@ -59,8 +58,13 @@ import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyResponse;
|
|
|
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyAction;
|
|
|
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyRequest;
|
|
|
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyResponse;
|
|
|
+import org.elasticsearch.xpack.core.security.action.apikey.UpdateApiKeyAction;
|
|
|
import org.elasticsearch.xpack.core.security.action.apikey.UpdateApiKeyRequest;
|
|
|
import org.elasticsearch.xpack.core.security.action.apikey.UpdateApiKeyResponse;
|
|
|
+import org.elasticsearch.xpack.core.security.action.role.PutRoleAction;
|
|
|
+import org.elasticsearch.xpack.core.security.action.role.PutRoleRequest;
|
|
|
+import org.elasticsearch.xpack.core.security.action.role.PutRoleResponse;
|
|
|
+import org.elasticsearch.xpack.core.security.action.role.RoleDescriptorRequestValidator;
|
|
|
import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction;
|
|
|
import org.elasticsearch.xpack.core.security.action.token.CreateTokenRequestBuilder;
|
|
|
import org.elasticsearch.xpack.core.security.action.token.CreateTokenResponse;
|
|
@@ -71,9 +75,9 @@ import org.elasticsearch.xpack.core.security.authc.Authentication;
|
|
|
import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper;
|
|
|
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
|
|
|
import org.elasticsearch.xpack.core.security.authc.RealmDomain;
|
|
|
-import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
|
|
|
import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
|
|
|
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
|
|
|
+import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver;
|
|
|
import org.elasticsearch.xpack.core.security.user.User;
|
|
|
import org.elasticsearch.xpack.security.authz.RoleDescriptorTests;
|
|
|
import org.elasticsearch.xpack.security.transport.filter.IPFilter;
|
|
@@ -105,7 +109,9 @@ import java.util.stream.IntStream;
|
|
|
import java.util.stream.Stream;
|
|
|
|
|
|
import static org.elasticsearch.test.SecuritySettingsSource.ES_TEST_ROOT_USER;
|
|
|
-import static org.elasticsearch.test.SecuritySettingsSourceField.ES_TEST_ROOT_ROLE;
|
|
|
+import static org.elasticsearch.test.SecuritySettingsSource.HASHER;
|
|
|
+import static org.elasticsearch.test.SecuritySettingsSource.TEST_ROLE;
|
|
|
+import static org.elasticsearch.test.SecuritySettingsSource.TEST_USER_NAME;
|
|
|
import static org.elasticsearch.test.SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING;
|
|
|
import static org.elasticsearch.test.TestMatchers.throwableWithMessage;
|
|
|
import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
|
|
@@ -1427,56 +1433,52 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
}
|
|
|
|
|
|
public void testUpdateApiKey() throws ExecutionException, InterruptedException, IOException {
|
|
|
- final Tuple<CreateApiKeyResponse, Map<String, Object>> createdApiKey = createApiKey(ES_TEST_ROOT_USER, null);
|
|
|
+ final Tuple<CreateApiKeyResponse, Map<String, Object>> createdApiKey = createApiKey(TEST_USER_NAME, null);
|
|
|
final var apiKeyId = createdApiKey.v1().getId();
|
|
|
-
|
|
|
final var newRoleDescriptors = randomRoleDescriptors();
|
|
|
final boolean nullRoleDescriptors = newRoleDescriptors == null;
|
|
|
+ // Role descriptor corresponding to SecuritySettingsSource.TEST_ROLE_YML
|
|
|
final var expectedLimitedByRoleDescriptors = Set.of(
|
|
|
- new RoleDescriptor(randomAlphaOfLength(10), new String[] { "all" }, null, null)
|
|
|
+ new RoleDescriptor(
|
|
|
+ TEST_ROLE,
|
|
|
+ new String[] { "ALL" },
|
|
|
+ new RoleDescriptor.IndicesPrivileges[] {
|
|
|
+ RoleDescriptor.IndicesPrivileges.builder().indices("*").allowRestrictedIndices(true).privileges("ALL").build() },
|
|
|
+ null
|
|
|
+ )
|
|
|
);
|
|
|
final var request = new UpdateApiKeyRequest(apiKeyId, newRoleDescriptors, ApiKeyTests.randomMetadata());
|
|
|
|
|
|
- final var serviceWithNodeName = getServiceWithNodeName();
|
|
|
final PlainActionFuture<UpdateApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
- serviceWithNodeName.service()
|
|
|
- .updateApiKey(
|
|
|
- fileRealmAuth(serviceWithNodeName.nodeName(), ES_TEST_ROOT_USER, ES_TEST_ROOT_ROLE),
|
|
|
- request,
|
|
|
- expectedLimitedByRoleDescriptors,
|
|
|
- listener
|
|
|
- );
|
|
|
- final var response = listener.get();
|
|
|
+ final UpdateApiKeyResponse response = executeUpdateApiKey(TEST_USER_NAME, request, listener);
|
|
|
|
|
|
assertNotNull(response);
|
|
|
assertTrue(response.isUpdated());
|
|
|
|
|
|
- // Correct data returned from GET API
|
|
|
- Client client = client().filterWithHeader(
|
|
|
- Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING))
|
|
|
- );
|
|
|
final PlainActionFuture<GetApiKeyResponse> getListener = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyId(apiKeyId, false), getListener);
|
|
|
- GetApiKeyResponse getResponse = getListener.get();
|
|
|
+ client().filterWithHeader(
|
|
|
+ Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_USER_NAME, TEST_PASSWORD_SECURE_STRING))
|
|
|
+ ).execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyId(apiKeyId, false), getListener);
|
|
|
+ final GetApiKeyResponse getResponse = getListener.get();
|
|
|
assertEquals(1, getResponse.getApiKeyInfos().length);
|
|
|
// When metadata for the update request is null (i.e., absent), we don't overwrite old metadata with it
|
|
|
final var expectedMetadata = request.getMetadata() != null ? request.getMetadata() : createdApiKey.v2();
|
|
|
assertEquals(expectedMetadata == null ? Map.of() : expectedMetadata, getResponse.getApiKeyInfos()[0].getMetadata());
|
|
|
- assertEquals(ES_TEST_ROOT_USER, getResponse.getApiKeyInfos()[0].getUsername());
|
|
|
+ assertEquals(TEST_USER_NAME, getResponse.getApiKeyInfos()[0].getUsername());
|
|
|
assertEquals("file", getResponse.getApiKeyInfos()[0].getRealm());
|
|
|
|
|
|
// Test authenticate works with updated API key
|
|
|
final var authResponse = authenticateWithApiKey(apiKeyId, createdApiKey.v1().getKey());
|
|
|
- assertThat(authResponse.get(User.Fields.USERNAME.getPreferredName()), equalTo(ES_TEST_ROOT_USER));
|
|
|
+ assertThat(authResponse.get(User.Fields.USERNAME.getPreferredName()), equalTo(TEST_USER_NAME));
|
|
|
|
|
|
// Document updated as expected
|
|
|
final var updatedApiKeyDoc = getApiKeyDocument(apiKeyId);
|
|
|
expectMetadataForApiKey(expectedMetadata, updatedApiKeyDoc);
|
|
|
- expectRoleDescriptorForApiKey("limited_by_role_descriptors", expectedLimitedByRoleDescriptors, updatedApiKeyDoc);
|
|
|
+ expectRoleDescriptorsForApiKey("limited_by_role_descriptors", expectedLimitedByRoleDescriptors, updatedApiKeyDoc);
|
|
|
if (nullRoleDescriptors) {
|
|
|
// Default role descriptor assigned to api key in `createApiKey`
|
|
|
final var expectedRoleDescriptor = new RoleDescriptor("role", new String[] { "monitor" }, null, null);
|
|
|
- expectRoleDescriptorForApiKey("role_descriptors", List.of(expectedRoleDescriptor), updatedApiKeyDoc);
|
|
|
+ expectRoleDescriptorsForApiKey("role_descriptors", List.of(expectedRoleDescriptor), updatedApiKeyDoc);
|
|
|
|
|
|
// Create user action unauthorized because we did not update key role; it only has `monitor` cluster priv
|
|
|
final Map<String, String> authorizationHeaders = Collections.singletonMap(
|
|
@@ -1487,7 +1489,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
assertThat(e.getMessage(), containsString("unauthorized"));
|
|
|
assertThat(e.getCause(), instanceOf(ElasticsearchSecurityException.class));
|
|
|
} else {
|
|
|
- expectRoleDescriptorForApiKey("role_descriptors", newRoleDescriptors, updatedApiKeyDoc);
|
|
|
+ expectRoleDescriptorsForApiKey("role_descriptors", newRoleDescriptors, updatedApiKeyDoc);
|
|
|
// Create user action authorized because we updated key role to `all` cluster priv
|
|
|
final var authorizationHeaders = Collections.singletonMap(
|
|
|
"Authorization",
|
|
@@ -1497,71 +1499,119 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private List<RoleDescriptor> randomRoleDescriptors() {
|
|
|
- int caseNo = randomIntBetween(0, 2);
|
|
|
- return switch (caseNo) {
|
|
|
- case 0 -> List.of(new RoleDescriptor(randomAlphaOfLength(10), new String[] { "all" }, null, null));
|
|
|
- case 1 -> List.of(
|
|
|
- new RoleDescriptor(randomAlphaOfLength(10), new String[] { "all" }, null, null),
|
|
|
- RoleDescriptorTests.randomRoleDescriptor()
|
|
|
- );
|
|
|
- case 2 -> null;
|
|
|
- default -> throw new IllegalStateException("unexpected case no");
|
|
|
- };
|
|
|
+ public void testUpdateApiKeyAutoUpdatesUserRoles() throws IOException, ExecutionException, InterruptedException {
|
|
|
+ // Create separate native realm user and role for user role change test
|
|
|
+ final var nativeRealmUser = randomAlphaOfLengthBetween(5, 10);
|
|
|
+ final var nativeRealmRole = randomAlphaOfLengthBetween(5, 10);
|
|
|
+ createNativeRealmUser(
|
|
|
+ nativeRealmUser,
|
|
|
+ nativeRealmRole,
|
|
|
+ new String(HASHER.hash(TEST_PASSWORD_SECURE_STRING)),
|
|
|
+ Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_USER_NAME, TEST_PASSWORD_SECURE_STRING))
|
|
|
+ );
|
|
|
+ final List<String> clusterPrivileges = new ArrayList<>(randomSubsetOf(ClusterPrivilegeResolver.names()));
|
|
|
+ // At a minimum include privilege to manage own API key to ensure no 403
|
|
|
+ clusterPrivileges.add(randomFrom("manage_api_key", "manage_own_api_key"));
|
|
|
+ final RoleDescriptor roleDescriptorBeforeUpdate = putRoleWithClusterPrivileges(
|
|
|
+ nativeRealmRole,
|
|
|
+ clusterPrivileges.toArray(new String[0])
|
|
|
+ );
|
|
|
+
|
|
|
+ // Create api key
|
|
|
+ final CreateApiKeyResponse createdApiKey = createApiKeys(
|
|
|
+ Collections.singletonMap("Authorization", basicAuthHeaderValue(nativeRealmUser, TEST_PASSWORD_SECURE_STRING)),
|
|
|
+ 1,
|
|
|
+ null,
|
|
|
+ "all"
|
|
|
+ ).v1().get(0);
|
|
|
+ final String apiKeyId = createdApiKey.getId();
|
|
|
+ expectRoleDescriptorsForApiKey("limited_by_role_descriptors", Set.of(roleDescriptorBeforeUpdate), getApiKeyDocument(apiKeyId));
|
|
|
+
|
|
|
+ final List<String> newClusterPrivileges = new ArrayList<>(randomSubsetOf(ClusterPrivilegeResolver.names()));
|
|
|
+ // At a minimum include privilege to manage own API key to ensure no 403
|
|
|
+ newClusterPrivileges.add(randomFrom("manage_api_key", "manage_own_api_key"));
|
|
|
+ // Update user role
|
|
|
+ final RoleDescriptor roleDescriptorAfterUpdate = putRoleWithClusterPrivileges(
|
|
|
+ nativeRealmRole,
|
|
|
+ newClusterPrivileges.toArray(new String[0])
|
|
|
+ );
|
|
|
+
|
|
|
+ // Update API key
|
|
|
+ final PlainActionFuture<UpdateApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
+ final UpdateApiKeyResponse response = executeUpdateApiKey(nativeRealmUser, UpdateApiKeyRequest.usingApiKeyId(apiKeyId), listener);
|
|
|
+
|
|
|
+ assertNotNull(response);
|
|
|
+ assertTrue(response.isUpdated());
|
|
|
+ expectRoleDescriptorsForApiKey("limited_by_role_descriptors", Set.of(roleDescriptorAfterUpdate), getApiKeyDocument(apiKeyId));
|
|
|
}
|
|
|
|
|
|
public void testUpdateApiKeyNotFoundScenarios() throws ExecutionException, InterruptedException {
|
|
|
- final Tuple<CreateApiKeyResponse, Map<String, Object>> createdApiKey = createApiKey(ES_TEST_ROOT_USER, null);
|
|
|
+ final Tuple<CreateApiKeyResponse, Map<String, Object>> createdApiKey = createApiKey(TEST_USER_NAME, null);
|
|
|
final var apiKeyId = createdApiKey.v1().getId();
|
|
|
final var expectedRoleDescriptor = new RoleDescriptor(randomAlphaOfLength(10), new String[] { "all" }, null, null);
|
|
|
final var request = new UpdateApiKeyRequest(apiKeyId, List.of(expectedRoleDescriptor), ApiKeyTests.randomMetadata());
|
|
|
|
|
|
// Validate can update own API key
|
|
|
- final var serviceWithNodeName = getServiceWithNodeName();
|
|
|
final PlainActionFuture<UpdateApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
- serviceWithNodeName.service()
|
|
|
- .updateApiKey(
|
|
|
- fileRealmAuth(serviceWithNodeName.nodeName(), ES_TEST_ROOT_USER, ES_TEST_ROOT_ROLE),
|
|
|
- request,
|
|
|
- Set.of(expectedRoleDescriptor),
|
|
|
- listener
|
|
|
- );
|
|
|
- final var response = listener.get();
|
|
|
-
|
|
|
+ final UpdateApiKeyResponse response = executeUpdateApiKey(TEST_USER_NAME, request, listener);
|
|
|
assertNotNull(response);
|
|
|
assertTrue(response.isUpdated());
|
|
|
|
|
|
// Test not found exception on non-existent API key
|
|
|
final var otherApiKeyId = randomValueOtherThan(apiKeyId, () -> randomAlphaOfLength(20));
|
|
|
- doTestUpdateApiKeyNotFound(
|
|
|
- serviceWithNodeName,
|
|
|
- fileRealmAuth(serviceWithNodeName.nodeName(), ES_TEST_ROOT_USER, ES_TEST_ROOT_ROLE),
|
|
|
- new UpdateApiKeyRequest(otherApiKeyId, request.getRoleDescriptors(), request.getMetadata())
|
|
|
- );
|
|
|
+ doTestUpdateApiKeyNotFound(new UpdateApiKeyRequest(otherApiKeyId, request.getRoleDescriptors(), request.getMetadata()));
|
|
|
|
|
|
// Test not found exception on other user's API key
|
|
|
final Tuple<CreateApiKeyResponse, Map<String, Object>> otherUsersApiKey = createApiKey("user_with_manage_api_key_role", null);
|
|
|
doTestUpdateApiKeyNotFound(
|
|
|
- serviceWithNodeName,
|
|
|
- fileRealmAuth(serviceWithNodeName.nodeName(), ES_TEST_ROOT_USER, ES_TEST_ROOT_ROLE),
|
|
|
new UpdateApiKeyRequest(otherUsersApiKey.v1().getId(), request.getRoleDescriptors(), request.getMetadata())
|
|
|
);
|
|
|
|
|
|
// Test not found exception on API key of user with the same username but from a different realm
|
|
|
+ // Create native realm user with same username but different password to allow us to create an API key for _that_ user
|
|
|
+ // instead of file realm one
|
|
|
+ final var passwordSecureString = new SecureString("x-pack-test-other-password".toCharArray());
|
|
|
+ createNativeRealmUser(
|
|
|
+ TEST_USER_NAME,
|
|
|
+ TEST_ROLE,
|
|
|
+ new String(HASHER.hash(passwordSecureString)),
|
|
|
+ Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_USER_NAME, TEST_PASSWORD_SECURE_STRING))
|
|
|
+ );
|
|
|
+ final CreateApiKeyResponse apiKeyForNativeRealmUser = createApiKeys(
|
|
|
+ Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_USER_NAME, passwordSecureString)),
|
|
|
+ 1,
|
|
|
+ null,
|
|
|
+ "all"
|
|
|
+ ).v1().get(0);
|
|
|
doTestUpdateApiKeyNotFound(
|
|
|
- serviceWithNodeName,
|
|
|
- Authentication.newRealmAuthentication(
|
|
|
- new User(ES_TEST_ROOT_USER, ES_TEST_ROOT_ROLE),
|
|
|
- // Use native realm; no need to actually create user since we are injecting the authentication object directly
|
|
|
- new Authentication.RealmRef(NativeRealmSettings.DEFAULT_NAME, NativeRealmSettings.TYPE, serviceWithNodeName.nodeName())
|
|
|
- ),
|
|
|
- new UpdateApiKeyRequest(apiKeyId, request.getRoleDescriptors(), request.getMetadata())
|
|
|
+ new UpdateApiKeyRequest(apiKeyForNativeRealmUser.getId(), request.getRoleDescriptors(), request.getMetadata())
|
|
|
);
|
|
|
}
|
|
|
|
|
|
public void testInvalidUpdateApiKeyScenarios() throws ExecutionException, InterruptedException {
|
|
|
- final Tuple<CreateApiKeyResponse, Map<String, Object>> createdApiKey = createApiKey(ES_TEST_ROOT_USER, null);
|
|
|
- final var apiKeyId = createdApiKey.v1().getId();
|
|
|
+ final List<String> apiKeyPrivileges = new ArrayList<>(randomSubsetOf(ClusterPrivilegeResolver.names()));
|
|
|
+ // At a minimum include privilege to manage own API key to ensure no 403
|
|
|
+ apiKeyPrivileges.add(randomFrom("manage_api_key", "manage_own_api_key"));
|
|
|
+ final CreateApiKeyResponse createdApiKey = createApiKeys(TEST_USER_NAME, 1, null, apiKeyPrivileges.toArray(new String[0])).v1()
|
|
|
+ .get(0);
|
|
|
+ final var apiKeyId = createdApiKey.getId();
|
|
|
+
|
|
|
+ final var roleDescriptor = new RoleDescriptor(randomAlphaOfLength(10), new String[] { "manage_own_api_key" }, null, null);
|
|
|
+ final var request = new UpdateApiKeyRequest(apiKeyId, List.of(roleDescriptor), ApiKeyTests.randomMetadata());
|
|
|
+ PlainActionFuture<UpdateApiKeyResponse> updateListener = new PlainActionFuture<>();
|
|
|
+ client().filterWithHeader(
|
|
|
+ Collections.singletonMap(
|
|
|
+ "Authorization",
|
|
|
+ "ApiKey " + getBase64EncodedApiKeyValue(createdApiKey.getId(), createdApiKey.getKey())
|
|
|
+ )
|
|
|
+ ).execute(UpdateApiKeyAction.INSTANCE, request, updateListener);
|
|
|
+
|
|
|
+ final var apiKeysNotAllowedEx = expectThrows(ExecutionException.class, updateListener::get);
|
|
|
+ assertThat(apiKeysNotAllowedEx.getCause(), instanceOf(IllegalArgumentException.class));
|
|
|
+ assertThat(
|
|
|
+ apiKeysNotAllowedEx.getMessage(),
|
|
|
+ containsString("authentication via API key not supported: only the owner user can update an API key")
|
|
|
+ );
|
|
|
|
|
|
final boolean invalidated = randomBoolean();
|
|
|
if (invalidated) {
|
|
@@ -1581,18 +1631,11 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
assertThat(expirationDateUpdatedResponse.getResult(), is(DocWriteResponse.Result.UPDATED));
|
|
|
}
|
|
|
|
|
|
- final var roleDescriptor = new RoleDescriptor(randomAlphaOfLength(10), new String[] { "all" }, null, null);
|
|
|
- final var request = new UpdateApiKeyRequest(apiKeyId, List.of(roleDescriptor), ApiKeyTests.randomMetadata());
|
|
|
-
|
|
|
- final var serviceWithNodeName = getServiceWithNodeName();
|
|
|
- PlainActionFuture<UpdateApiKeyResponse> updateListener = new PlainActionFuture<>();
|
|
|
- serviceWithNodeName.service()
|
|
|
- .updateApiKey(
|
|
|
- fileRealmAuth(serviceWithNodeName.nodeName(), ES_TEST_ROOT_USER, ES_TEST_ROOT_ROLE),
|
|
|
- request,
|
|
|
- Set.of(roleDescriptor),
|
|
|
- updateListener
|
|
|
- );
|
|
|
+ updateListener = new PlainActionFuture<>();
|
|
|
+ final Client client = client().filterWithHeader(
|
|
|
+ Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_USER_NAME, TEST_PASSWORD_SECURE_STRING))
|
|
|
+ );
|
|
|
+ client.execute(UpdateApiKeyAction.INSTANCE, request, updateListener);
|
|
|
final var ex = expectThrows(ExecutionException.class, updateListener::get);
|
|
|
|
|
|
assertThat(ex.getCause(), instanceOf(IllegalArgumentException.class));
|
|
@@ -1601,17 +1644,41 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
} else {
|
|
|
assertThat(ex.getMessage(), containsString("cannot update expired API key [" + apiKeyId + "]"));
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- updateListener = new PlainActionFuture<>();
|
|
|
- serviceWithNodeName.service()
|
|
|
- .updateApiKey(AuthenticationTestHelper.builder().apiKey().build(false), request, Set.of(roleDescriptor), updateListener);
|
|
|
- final var apiKeysNotAllowedEx = expectThrows(ExecutionException.class, updateListener::get);
|
|
|
+ public void testUpdateApiKeyAccountsForSecurityDomains() throws ExecutionException, InterruptedException {
|
|
|
+ final Tuple<CreateApiKeyResponse, Map<String, Object>> createdApiKey = createApiKey(TEST_USER_NAME, null);
|
|
|
+ final var apiKeyId = createdApiKey.v1().getId();
|
|
|
|
|
|
- assertThat(apiKeysNotAllowedEx.getCause(), instanceOf(IllegalArgumentException.class));
|
|
|
- assertThat(
|
|
|
- apiKeysNotAllowedEx.getMessage(),
|
|
|
- containsString("authentication via an API key is not supported for updating API keys")
|
|
|
+ final ServiceWithNodeName serviceWithNodeName = getServiceWithNodeName();
|
|
|
+ final PlainActionFuture<UpdateApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
+ final RealmConfig.RealmIdentifier creatorRealmOnCreatedApiKey = new RealmConfig.RealmIdentifier(FileRealmSettings.TYPE, "file");
|
|
|
+ final RealmConfig.RealmIdentifier otherRealmInDomain = AuthenticationTestHelper.randomRealmIdentifier(true);
|
|
|
+ final var realmDomain = new RealmDomain(
|
|
|
+ ESTestCase.randomAlphaOfLengthBetween(3, 8),
|
|
|
+ Set.of(creatorRealmOnCreatedApiKey, otherRealmInDomain)
|
|
|
+ );
|
|
|
+ // Update should work for any of the realms within the domain
|
|
|
+ final var authenticatingRealm = randomFrom(creatorRealmOnCreatedApiKey, otherRealmInDomain);
|
|
|
+ final var authentication = randomValueOtherThanMany(
|
|
|
+ Authentication::isApiKey,
|
|
|
+ () -> AuthenticationTestHelper.builder()
|
|
|
+ .user(new User(TEST_USER_NAME, TEST_ROLE))
|
|
|
+ .realmRef(
|
|
|
+ new Authentication.RealmRef(
|
|
|
+ authenticatingRealm.getName(),
|
|
|
+ authenticatingRealm.getType(),
|
|
|
+ serviceWithNodeName.nodeName(),
|
|
|
+ realmDomain
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .build()
|
|
|
);
|
|
|
+ serviceWithNodeName.service().updateApiKey(authentication, UpdateApiKeyRequest.usingApiKeyId(apiKeyId), Set.of(), listener);
|
|
|
+ final UpdateApiKeyResponse response = listener.get();
|
|
|
+
|
|
|
+ assertNotNull(response);
|
|
|
+ assertTrue(response.isUpdated());
|
|
|
}
|
|
|
|
|
|
public void testUpdateApiKeyClearsApiKeyDocCache() throws IOException, ExecutionException, InterruptedException {
|
|
@@ -1650,17 +1717,15 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
|
|
|
// Update the first key
|
|
|
final PlainActionFuture<UpdateApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
- serviceForDoc1.updateApiKey(
|
|
|
- fileRealmAuth(serviceWithNameForDoc1.nodeName(), ES_TEST_ROOT_USER, ES_TEST_ROOT_ROLE),
|
|
|
- new UpdateApiKeyRequest(apiKey1.v1(), List.of(), null),
|
|
|
- Set.of(),
|
|
|
- listener
|
|
|
+ final Client client = client().filterWithHeader(
|
|
|
+ Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING))
|
|
|
);
|
|
|
+ client.execute(UpdateApiKeyAction.INSTANCE, new UpdateApiKeyRequest(apiKey1.v1(), List.of(), null), listener);
|
|
|
final var response = listener.get();
|
|
|
assertNotNull(response);
|
|
|
assertTrue(response.isUpdated());
|
|
|
|
|
|
- // The cache entry should be gone for the first key
|
|
|
+ // The doc cache entry should be gone for the first key
|
|
|
if (sameServiceNode) {
|
|
|
assertEquals(1, serviceForDoc1.getDocCache().count());
|
|
|
assertNull(serviceForDoc1.getDocCache().get(apiKey1.v1()));
|
|
@@ -1675,44 +1740,34 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
assertEquals(serviceForDoc2AuthCacheCount, serviceForDoc2.getApiKeyAuthCache().count());
|
|
|
}
|
|
|
|
|
|
- private void doTestUpdateApiKeyNotFound(
|
|
|
- ServiceWithNodeName serviceWithNodeName,
|
|
|
- Authentication authentication,
|
|
|
- UpdateApiKeyRequest request
|
|
|
- ) {
|
|
|
+ private List<RoleDescriptor> randomRoleDescriptors() {
|
|
|
+ int caseNo = randomIntBetween(0, 2);
|
|
|
+ return switch (caseNo) {
|
|
|
+ case 0 -> List.of(new RoleDescriptor(randomAlphaOfLength(10), new String[] { "all" }, null, null));
|
|
|
+ case 1 -> List.of(
|
|
|
+ new RoleDescriptor(randomAlphaOfLength(10), new String[] { "all" }, null, null),
|
|
|
+ randomValueOtherThanMany(
|
|
|
+ rd -> RoleDescriptorRequestValidator.validate(rd) != null,
|
|
|
+ () -> RoleDescriptorTests.randomRoleDescriptor(false)
|
|
|
+ )
|
|
|
+ );
|
|
|
+ case 2 -> null;
|
|
|
+ default -> throw new IllegalStateException("unexpected case no");
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private void doTestUpdateApiKeyNotFound(UpdateApiKeyRequest request) {
|
|
|
final PlainActionFuture<UpdateApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
- serviceWithNodeName.service().updateApiKey(authentication, request, Set.of(), listener);
|
|
|
+ final Client client = client().filterWithHeader(
|
|
|
+ Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_USER_NAME, TEST_PASSWORD_SECURE_STRING))
|
|
|
+ );
|
|
|
+ client.execute(UpdateApiKeyAction.INSTANCE, request, listener);
|
|
|
final var ex = expectThrows(ExecutionException.class, listener::get);
|
|
|
assertThat(ex.getCause(), instanceOf(ResourceNotFoundException.class));
|
|
|
assertThat(ex.getMessage(), containsString("no API key owned by requesting user found for ID [" + request.getId() + "]"));
|
|
|
}
|
|
|
|
|
|
- private static Authentication fileRealmAuth(String nodeName, String userName, String roleName) {
|
|
|
- boolean includeDomain = randomBoolean();
|
|
|
- final var realmName = "file";
|
|
|
- final String realmType = FileRealmSettings.TYPE;
|
|
|
- return randomValueOtherThanMany(
|
|
|
- Authentication::isApiKey,
|
|
|
- () -> AuthenticationTestHelper.builder()
|
|
|
- .user(new User(userName, roleName))
|
|
|
- .realmRef(
|
|
|
- new Authentication.RealmRef(
|
|
|
- realmName,
|
|
|
- realmType,
|
|
|
- nodeName,
|
|
|
- includeDomain
|
|
|
- ? new RealmDomain(
|
|
|
- ESTestCase.randomAlphaOfLengthBetween(3, 8),
|
|
|
- Set.of(new RealmConfig.RealmIdentifier(realmType, realmName))
|
|
|
- )
|
|
|
- : null
|
|
|
- )
|
|
|
- )
|
|
|
- .build()
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- private void expectMetadataForApiKey(Map<String, Object> expectedMetadata, Map<String, Object> actualRawApiKeyDoc) {
|
|
|
+ private void expectMetadataForApiKey(final Map<String, Object> expectedMetadata, final Map<String, Object> actualRawApiKeyDoc) {
|
|
|
assertNotNull(actualRawApiKeyDoc);
|
|
|
@SuppressWarnings("unchecked")
|
|
|
final var actualMetadata = (Map<String, Object>) actualRawApiKeyDoc.get("metadata_flattened");
|
|
@@ -1720,10 +1775,10 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
}
|
|
|
|
|
|
@SuppressWarnings("unchecked")
|
|
|
- private void expectRoleDescriptorForApiKey(
|
|
|
- String roleDescriptorType,
|
|
|
- Collection<RoleDescriptor> expectedRoleDescriptors,
|
|
|
- Map<String, Object> actualRawApiKeyDoc
|
|
|
+ private void expectRoleDescriptorsForApiKey(
|
|
|
+ final String roleDescriptorType,
|
|
|
+ final Collection<RoleDescriptor> expectedRoleDescriptors,
|
|
|
+ final Map<String, Object> actualRawApiKeyDoc
|
|
|
) throws IOException {
|
|
|
assertNotNull(actualRawApiKeyDoc);
|
|
|
assertThat(roleDescriptorType, in(new String[] { "role_descriptors", "limited_by_role_descriptors" }));
|
|
@@ -1743,12 +1798,11 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
}
|
|
|
|
|
|
private Map<String, Object> getApiKeyDocument(String apiKeyId) {
|
|
|
- final GetResponse getResponse = client().execute(GetAction.INSTANCE, new GetRequest(SECURITY_MAIN_ALIAS, apiKeyId)).actionGet();
|
|
|
- return getResponse.getSource();
|
|
|
+ return client().execute(GetAction.INSTANCE, new GetRequest(SECURITY_MAIN_ALIAS, apiKeyId)).actionGet().getSource();
|
|
|
}
|
|
|
|
|
|
private ServiceWithNodeName getServiceWithNodeName() {
|
|
|
- final var nodeName = internalCluster().getNodeNames()[0];
|
|
|
+ final var nodeName = randomFrom(internalCluster().getNodeNames());
|
|
|
final var service = internalCluster().getInstance(ApiKeyService.class, nodeName);
|
|
|
return new ServiceWithNodeName(service, nodeName);
|
|
|
}
|
|
@@ -1967,10 +2021,19 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
}
|
|
|
|
|
|
private void createUserWithRunAsRole(Map<String, String> authHeaders) throws ExecutionException, InterruptedException {
|
|
|
+ createNativeRealmUser("user_with_run_as_role", "run_as_role", SecuritySettingsSource.TEST_PASSWORD_HASHED, authHeaders);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void createNativeRealmUser(
|
|
|
+ final String username,
|
|
|
+ final String role,
|
|
|
+ final String passwordHashed,
|
|
|
+ final Map<String, String> authHeaders
|
|
|
+ ) throws ExecutionException, InterruptedException {
|
|
|
final PutUserRequest putUserRequest = new PutUserRequest();
|
|
|
- putUserRequest.username("user_with_run_as_role");
|
|
|
- putUserRequest.roles("run_as_role");
|
|
|
- putUserRequest.passwordHash(SecuritySettingsSource.TEST_PASSWORD_HASHED.toCharArray());
|
|
|
+ putUserRequest.username(username);
|
|
|
+ putUserRequest.roles(role);
|
|
|
+ putUserRequest.passwordHash(passwordHashed.toCharArray());
|
|
|
PlainActionFuture<PutUserResponse> listener = new PlainActionFuture<>();
|
|
|
final Client client = client().filterWithHeader(authHeaders);
|
|
|
client.execute(PutUserAction.INSTANCE, putUserRequest, listener);
|
|
@@ -1978,6 +2041,20 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
assertTrue(putUserResponse.created());
|
|
|
}
|
|
|
|
|
|
+ private RoleDescriptor putRoleWithClusterPrivileges(final String nativeRealmRoleName, String... clusterPrivileges)
|
|
|
+ throws InterruptedException, ExecutionException {
|
|
|
+ final PutRoleRequest putRoleRequest = new PutRoleRequest();
|
|
|
+ putRoleRequest.name(nativeRealmRoleName);
|
|
|
+ for (final String clusterPrivilege : clusterPrivileges) {
|
|
|
+ putRoleRequest.cluster(clusterPrivilege);
|
|
|
+ }
|
|
|
+ final PlainActionFuture<PutRoleResponse> roleListener = new PlainActionFuture<>();
|
|
|
+ client().filterWithHeader(Map.of("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)))
|
|
|
+ .execute(PutRoleAction.INSTANCE, putRoleRequest, roleListener);
|
|
|
+ assertNotNull(roleListener.get());
|
|
|
+ return putRoleRequest.roleDescriptor();
|
|
|
+ }
|
|
|
+
|
|
|
private Client getClientForRunAsUser() {
|
|
|
return client().filterWithHeader(
|
|
|
Map.of(
|
|
@@ -1989,6 +2066,18 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ private UpdateApiKeyResponse executeUpdateApiKey(
|
|
|
+ final String username,
|
|
|
+ final UpdateApiKeyRequest request,
|
|
|
+ final PlainActionFuture<UpdateApiKeyResponse> listener
|
|
|
+ ) throws InterruptedException, ExecutionException {
|
|
|
+ final Client client = client().filterWithHeader(
|
|
|
+ Collections.singletonMap("Authorization", basicAuthHeaderValue(username, TEST_PASSWORD_SECURE_STRING))
|
|
|
+ );
|
|
|
+ client.execute(UpdateApiKeyAction.INSTANCE, request, listener);
|
|
|
+ return listener.get();
|
|
|
+ }
|
|
|
+
|
|
|
private void assertErrorMessage(final ElasticsearchSecurityException ese, String action, String userName, String apiKeyId) {
|
|
|
assertThat(
|
|
|
ese,
|