|
@@ -37,6 +37,7 @@ import org.elasticsearch.common.util.set.Sets;
|
|
|
import org.elasticsearch.common.xcontent.XContentHelper;
|
|
|
import org.elasticsearch.core.TimeValue;
|
|
|
import org.elasticsearch.core.Tuple;
|
|
|
+import org.elasticsearch.index.query.QueryBuilders;
|
|
|
import org.elasticsearch.rest.RestStatus;
|
|
|
import org.elasticsearch.test.ESTestCase;
|
|
|
import org.elasticsearch.test.SecurityIntegTestCase;
|
|
@@ -65,6 +66,9 @@ 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.QueryApiKeyAction;
|
|
|
+import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyRequest;
|
|
|
+import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse;
|
|
|
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;
|
|
@@ -85,6 +89,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmDomain;
|
|
|
import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
|
|
|
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
|
|
|
import org.elasticsearch.xpack.core.security.authz.RoleDescriptorTests;
|
|
|
+import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection;
|
|
|
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver;
|
|
|
import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore;
|
|
|
import org.elasticsearch.xpack.core.security.user.User;
|
|
@@ -112,6 +117,7 @@ import java.util.concurrent.ExecutionException;
|
|
|
import java.util.concurrent.ExecutorService;
|
|
|
import java.util.concurrent.Future;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.function.Function;
|
|
|
import java.util.function.Supplier;
|
|
|
import java.util.stream.Collectors;
|
|
|
import java.util.stream.IntStream;
|
|
@@ -121,6 +127,7 @@ import static org.elasticsearch.test.SecuritySettingsSource.ES_TEST_ROOT_USER;
|
|
|
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.ES_TEST_ROOT_ROLE_DESCRIPTOR;
|
|
|
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;
|
|
@@ -134,6 +141,7 @@ import static org.hamcrest.Matchers.contains;
|
|
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
|
|
import static org.hamcrest.Matchers.containsString;
|
|
|
import static org.hamcrest.Matchers.empty;
|
|
|
+import static org.hamcrest.Matchers.emptyIterable;
|
|
|
import static org.hamcrest.Matchers.equalTo;
|
|
|
import static org.hamcrest.Matchers.greaterThan;
|
|
|
import static org.hamcrest.Matchers.hasKey;
|
|
@@ -643,15 +651,21 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
assertThat(invalidateResponse.getPreviouslyInvalidatedApiKeys().size(), equalTo(0));
|
|
|
assertThat(invalidateResponse.getErrors().size(), equalTo(0));
|
|
|
|
|
|
+ final boolean withLimitedBy = randomBoolean();
|
|
|
PlainActionFuture<GetApiKeyResponse> getApiKeyResponseListener = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingRealmName("file"), getApiKeyResponseListener);
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().realmName("file").withLimitedBy(withLimitedBy).build(),
|
|
|
+ getApiKeyResponseListener
|
|
|
+ );
|
|
|
GetApiKeyResponse response = getApiKeyResponseListener.get();
|
|
|
- verifyGetResponse(
|
|
|
+ verifyApiKeyInfos(
|
|
|
2,
|
|
|
responses,
|
|
|
tuple.v2(),
|
|
|
List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
|
|
|
- response,
|
|
|
+ withLimitedBy ? List.of(ES_TEST_ROOT_ROLE_DESCRIPTOR) : null,
|
|
|
+ response.getApiKeyInfos(),
|
|
|
Collections.singleton(responses.get(0).getId()),
|
|
|
Collections.singletonList(responses.get(1).getId())
|
|
|
);
|
|
@@ -685,15 +699,21 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
expectedValidKeyIds = responses.stream().map(o -> o.getId()).collect(Collectors.toSet());
|
|
|
}
|
|
|
|
|
|
+ final boolean withLimitedBy = randomBoolean();
|
|
|
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingRealmName("file"), listener);
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().realmName("file").withLimitedBy(withLimitedBy).build(),
|
|
|
+ listener
|
|
|
+ );
|
|
|
GetApiKeyResponse response = listener.get();
|
|
|
- verifyGetResponse(
|
|
|
+ verifyApiKeyInfos(
|
|
|
noOfApiKeys,
|
|
|
responses,
|
|
|
tuple.v2(),
|
|
|
List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
|
|
|
- response,
|
|
|
+ withLimitedBy ? List.of(ES_TEST_ROOT_ROLE_DESCRIPTOR) : null,
|
|
|
+ response.getApiKeyInfos(),
|
|
|
expectedValidKeyIds,
|
|
|
invalidatedApiKeyIds
|
|
|
);
|
|
@@ -706,15 +726,21 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
Client client = client().filterWithHeader(
|
|
|
Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING))
|
|
|
);
|
|
|
+ final boolean withLimitedBy = randomBoolean();
|
|
|
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingUserName(ES_TEST_ROOT_USER), listener);
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().userName(ES_TEST_ROOT_USER).withLimitedBy(withLimitedBy).build(),
|
|
|
+ listener
|
|
|
+ );
|
|
|
GetApiKeyResponse response = listener.get();
|
|
|
- verifyGetResponse(
|
|
|
+ verifyApiKeyInfos(
|
|
|
noOfApiKeys,
|
|
|
responses,
|
|
|
tuple.v2(),
|
|
|
List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
|
|
|
- response,
|
|
|
+ withLimitedBy ? List.of(ES_TEST_ROOT_ROLE_DESCRIPTOR) : null,
|
|
|
+ response.getApiKeyInfos(),
|
|
|
responses.stream().map(o -> o.getId()).collect(Collectors.toSet()),
|
|
|
null
|
|
|
);
|
|
@@ -726,15 +752,21 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
Client client = client().filterWithHeader(
|
|
|
Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING))
|
|
|
);
|
|
|
+ final boolean withLimitedBy = randomBoolean();
|
|
|
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingRealmAndUserName("file", ES_TEST_ROOT_USER), listener);
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().realmName("file").userName(ES_TEST_ROOT_USER).withLimitedBy(withLimitedBy).build(),
|
|
|
+ listener
|
|
|
+ );
|
|
|
GetApiKeyResponse response = listener.get();
|
|
|
- verifyGetResponse(
|
|
|
+ verifyApiKeyInfos(
|
|
|
1,
|
|
|
responses,
|
|
|
tuple.v2(),
|
|
|
List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
|
|
|
- response,
|
|
|
+ withLimitedBy ? List.of(ES_TEST_ROOT_ROLE_DESCRIPTOR) : null,
|
|
|
+ response.getApiKeyInfos(),
|
|
|
Collections.singleton(responses.get(0).getId()),
|
|
|
null
|
|
|
);
|
|
@@ -746,15 +778,21 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
Client client = client().filterWithHeader(
|
|
|
Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING))
|
|
|
);
|
|
|
+ final boolean withLimitedBy = randomBoolean();
|
|
|
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyId(responses.get(0).getId(), false), listener);
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().apiKeyId(responses.get(0).getId()).withLimitedBy(withLimitedBy).build(),
|
|
|
+ listener
|
|
|
+ );
|
|
|
GetApiKeyResponse response = listener.get();
|
|
|
- verifyGetResponse(
|
|
|
+ verifyApiKeyInfos(
|
|
|
1,
|
|
|
responses,
|
|
|
tuple.v2(),
|
|
|
List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
|
|
|
- response,
|
|
|
+ withLimitedBy ? List.of(ES_TEST_ROOT_ROLE_DESCRIPTOR) : null,
|
|
|
+ response.getApiKeyInfos(),
|
|
|
Collections.singleton(responses.get(0).getId()),
|
|
|
null
|
|
|
);
|
|
@@ -779,30 +817,41 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
final List<CreateApiKeyResponse> createApiKeyResponses2 = tuple2.v1();
|
|
|
|
|
|
Client client = client().filterWithHeader(headers);
|
|
|
+ final boolean withLimitedBy = randomBoolean();
|
|
|
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
@SuppressWarnings("unchecked")
|
|
|
List<CreateApiKeyResponse> responses = randomFrom(createApiKeyResponses1, createApiKeyResponses2);
|
|
|
List<Map<String, Object>> metadatas = responses == createApiKeyResponses1 ? tuple1.v2() : tuple2.v2();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyName(responses.get(0).getName(), false), listener);
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().apiKeyName(responses.get(0).getName()).withLimitedBy(withLimitedBy).build(),
|
|
|
+ listener
|
|
|
+ );
|
|
|
// role descriptors are the same between randomization
|
|
|
- verifyGetResponse(
|
|
|
+ verifyApiKeyInfos(
|
|
|
1,
|
|
|
responses,
|
|
|
metadatas,
|
|
|
List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
|
|
|
- listener.get(),
|
|
|
+ withLimitedBy ? List.of(ES_TEST_ROOT_ROLE_DESCRIPTOR) : null,
|
|
|
+ listener.get().getApiKeyInfos(),
|
|
|
Collections.singleton(responses.get(0).getId()),
|
|
|
null
|
|
|
);
|
|
|
|
|
|
PlainActionFuture<GetApiKeyResponse> listener2 = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyName("test-key*", false), listener2);
|
|
|
- verifyGetResponse(
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().apiKeyName("test-key*").withLimitedBy(withLimitedBy).build(),
|
|
|
+ listener2
|
|
|
+ );
|
|
|
+ verifyApiKeyInfos(
|
|
|
noOfApiKeys,
|
|
|
createApiKeyResponses1,
|
|
|
tuple1.v2(),
|
|
|
List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
|
|
|
- listener2.get(),
|
|
|
+ withLimitedBy ? List.of(ES_TEST_ROOT_ROLE_DESCRIPTOR) : null,
|
|
|
+ listener2.get().getApiKeyInfos(),
|
|
|
createApiKeyResponses1.stream().map(CreateApiKeyResponse::getId).collect(Collectors.toSet()),
|
|
|
null
|
|
|
);
|
|
@@ -812,31 +861,54 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
);
|
|
|
|
|
|
PlainActionFuture<GetApiKeyResponse> listener3 = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyName("*", false), listener3);
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().apiKeyName("*").withLimitedBy(withLimitedBy).build(),
|
|
|
+ listener3
|
|
|
+ );
|
|
|
responses = Stream.concat(createApiKeyResponses1.stream(), createApiKeyResponses2.stream()).collect(Collectors.toList());
|
|
|
metadatas = Stream.concat(tuple1.v2().stream(), tuple2.v2().stream()).collect(Collectors.toList());
|
|
|
- verifyGetResponse(
|
|
|
+ verifyApiKeyInfos(
|
|
|
2 * noOfApiKeys,
|
|
|
responses,
|
|
|
metadatas,
|
|
|
List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
|
|
|
- listener3.get(),
|
|
|
+ withLimitedBy ? List.of(ES_TEST_ROOT_ROLE_DESCRIPTOR) : null,
|
|
|
+ listener3.get().getApiKeyInfos(),
|
|
|
responses.stream().map(CreateApiKeyResponse::getId).collect(Collectors.toSet()),
|
|
|
null
|
|
|
);
|
|
|
|
|
|
PlainActionFuture<GetApiKeyResponse> listener4 = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyName("does-not-exist*", false), listener4);
|
|
|
- verifyGetResponse(0, Collections.emptyList(), null, List.of(), listener4.get(), Collections.emptySet(), null);
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().apiKeyName("does-not-exist*").withLimitedBy(withLimitedBy).build(),
|
|
|
+ listener4
|
|
|
+ );
|
|
|
+ verifyApiKeyInfos(
|
|
|
+ 0,
|
|
|
+ Collections.emptyList(),
|
|
|
+ null,
|
|
|
+ List.of(),
|
|
|
+ List.of(),
|
|
|
+ listener4.get().getApiKeyInfos(),
|
|
|
+ Collections.emptySet(),
|
|
|
+ null
|
|
|
+ );
|
|
|
|
|
|
PlainActionFuture<GetApiKeyResponse> listener5 = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyName("another-test-key*", false), listener5);
|
|
|
- verifyGetResponse(
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().apiKeyName("another-test-key*").withLimitedBy(withLimitedBy).build(),
|
|
|
+ listener5
|
|
|
+ );
|
|
|
+ verifyApiKeyInfos(
|
|
|
noOfApiKeys,
|
|
|
createApiKeyResponses2,
|
|
|
tuple2.v2(),
|
|
|
List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
|
|
|
- listener5.get(),
|
|
|
+ withLimitedBy ? List.of(ES_TEST_ROOT_ROLE_DESCRIPTOR) : null,
|
|
|
+ listener5.get().getApiKeyInfos(),
|
|
|
createApiKeyResponses2.stream().map(CreateApiKeyResponse::getId).collect(Collectors.toSet()),
|
|
|
null
|
|
|
);
|
|
@@ -858,16 +930,37 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
Collections.singletonMap("Authorization", basicAuthHeaderValue(userWithManageApiKeyRole, TEST_PASSWORD_SECURE_STRING))
|
|
|
);
|
|
|
|
|
|
+ final boolean withLimitedBy = randomBoolean();
|
|
|
+ final List<RoleDescriptor> expectedLimitedByRoleDescriptors;
|
|
|
+ if (withLimitedBy) {
|
|
|
+ if (userWithManageApiKeyRole.equals("user_with_manage_api_key_role")) {
|
|
|
+ expectedLimitedByRoleDescriptors = List.of(
|
|
|
+ new RoleDescriptor("manage_api_key_role", new String[] { "manage_api_key" }, null, null)
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ expectedLimitedByRoleDescriptors = List.of(
|
|
|
+ new RoleDescriptor("manage_own_api_key_role", new String[] { "manage_own_api_key" }, null, null)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ expectedLimitedByRoleDescriptors = null;
|
|
|
+ }
|
|
|
+
|
|
|
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.forOwnedApiKeys(), listener);
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().ownedByAuthenticatedUser().withLimitedBy(withLimitedBy).build(),
|
|
|
+ listener
|
|
|
+ );
|
|
|
GetApiKeyResponse response = listener.get();
|
|
|
- verifyGetResponse(
|
|
|
+ verifyApiKeyInfos(
|
|
|
userWithManageApiKeyRole,
|
|
|
noOfApiKeysForUserWithManageApiKeyRole,
|
|
|
userWithManageApiKeyRoleApiKeys,
|
|
|
tuple.v2(),
|
|
|
List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
|
|
|
- response,
|
|
|
+ expectedLimitedByRoleDescriptors,
|
|
|
+ response.getApiKeyInfos(),
|
|
|
userWithManageApiKeyRoleApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()),
|
|
|
null
|
|
|
);
|
|
@@ -886,16 +979,24 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
"monitor"
|
|
|
);
|
|
|
List<CreateApiKeyResponse> userWithManageOwnApiKeyRoleApiKeys = tuple.v1();
|
|
|
+ final boolean withLimitedBy = randomBoolean();
|
|
|
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
- getClientForRunAsUser().execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.forOwnedApiKeys(), listener);
|
|
|
+ getClientForRunAsUser().execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().ownedByAuthenticatedUser().withLimitedBy(withLimitedBy).build(),
|
|
|
+ listener
|
|
|
+ );
|
|
|
GetApiKeyResponse response = listener.get();
|
|
|
- verifyGetResponse(
|
|
|
+ verifyApiKeyInfos(
|
|
|
"user_with_manage_own_api_key_role",
|
|
|
noOfApiKeysForUserWithManageApiKeyRole,
|
|
|
userWithManageOwnApiKeyRoleApiKeys,
|
|
|
tuple.v2(),
|
|
|
List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
|
|
|
- response,
|
|
|
+ withLimitedBy
|
|
|
+ ? List.of(new RoleDescriptor("manage_own_api_key_role", new String[] { "manage_own_api_key" }, null, null))
|
|
|
+ : null,
|
|
|
+ response.getApiKeyInfos(),
|
|
|
userWithManageOwnApiKeyRoleApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()),
|
|
|
null
|
|
|
);
|
|
@@ -914,20 +1015,24 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
"monitor"
|
|
|
);
|
|
|
List<CreateApiKeyResponse> userWithManageOwnApiKeyRoleApiKeys = tuple.v1();
|
|
|
+ final boolean withLimitedBy = randomBoolean();
|
|
|
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
getClientForRunAsUser().execute(
|
|
|
GetApiKeyAction.INSTANCE,
|
|
|
- GetApiKeyRequest.usingRealmAndUserName("file", "user_with_manage_own_api_key_role"),
|
|
|
+ GetApiKeyRequest.builder().realmName("file").userName("user_with_manage_own_api_key_role").withLimitedBy(withLimitedBy).build(),
|
|
|
listener
|
|
|
);
|
|
|
GetApiKeyResponse response = listener.get();
|
|
|
- verifyGetResponse(
|
|
|
+ verifyApiKeyInfos(
|
|
|
"user_with_manage_own_api_key_role",
|
|
|
noOfApiKeysForUserWithManageApiKeyRole,
|
|
|
userWithManageOwnApiKeyRoleApiKeys,
|
|
|
tuple.v2(),
|
|
|
List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
|
|
|
- response,
|
|
|
+ withLimitedBy
|
|
|
+ ? List.of(new RoleDescriptor("manage_own_api_key_role", new String[] { "manage_own_api_key" }, null, null))
|
|
|
+ : null,
|
|
|
+ response.getApiKeyInfos(),
|
|
|
userWithManageOwnApiKeyRoleApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()),
|
|
|
null
|
|
|
);
|
|
@@ -988,8 +1093,9 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
final Client client = client().filterWithHeader(
|
|
|
Collections.singletonMap("Authorization", basicAuthHeaderValue("user_with_manage_api_key_role", TEST_PASSWORD_SECURE_STRING))
|
|
|
);
|
|
|
+ final boolean withLimitedBy = randomBoolean();
|
|
|
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, new GetApiKeyRequest(), listener);
|
|
|
+ client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.builder().withLimitedBy(withLimitedBy).build(), listener);
|
|
|
GetApiKeyResponse response = listener.get();
|
|
|
int totalApiKeys = noOfSuperuserApiKeys + noOfApiKeysForUserWithManageApiKeyRole + noOfApiKeysForUserWithManageOwnApiKeyRole;
|
|
|
List<CreateApiKeyResponse> allApiKeys = new ArrayList<>();
|
|
@@ -997,14 +1103,32 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
final List<Map<String, Object>> metadatas = Stream.of(defaultUserTuple.v2(), userWithManageTuple.v2(), userWithManageOwnTuple.v2())
|
|
|
.flatMap(List::stream)
|
|
|
.collect(Collectors.toList());
|
|
|
- verifyGetResponse(
|
|
|
+
|
|
|
+ final Function<String, List<RoleDescriptor>> expectedLimitedByRoleDescriptorsLookup = username -> {
|
|
|
+ if (withLimitedBy) {
|
|
|
+ return switch (username) {
|
|
|
+ case ES_TEST_ROOT_USER -> List.of(ES_TEST_ROOT_ROLE_DESCRIPTOR);
|
|
|
+ case "user_with_manage_api_key_role" -> List.of(
|
|
|
+ new RoleDescriptor("manage_api_key_role", new String[] { "manage_api_key" }, null, null)
|
|
|
+ );
|
|
|
+ case "user_with_manage_own_api_key_role" -> List.of(
|
|
|
+ new RoleDescriptor("manage_own_api_key_role", new String[] { "manage_own_api_key" }, null, null)
|
|
|
+ );
|
|
|
+ default -> throw new IllegalStateException("unknown username: " + username);
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ verifyApiKeyInfos(
|
|
|
new String[] { ES_TEST_ROOT_USER, "user_with_manage_api_key_role", "user_with_manage_own_api_key_role" },
|
|
|
totalApiKeys,
|
|
|
allApiKeys,
|
|
|
metadatas,
|
|
|
List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
|
|
|
- response,
|
|
|
- allApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()),
|
|
|
+ expectedLimitedByRoleDescriptorsLookup,
|
|
|
+ response.getApiKeyInfos(),
|
|
|
+ allApiKeys.stream().map(CreateApiKeyResponse::getId).collect(Collectors.toSet()),
|
|
|
null
|
|
|
);
|
|
|
}
|
|
@@ -1032,7 +1156,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
Collections.singletonMap("Authorization", basicAuthHeaderValue(withUser, TEST_PASSWORD_SECURE_STRING))
|
|
|
);
|
|
|
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, new GetApiKeyRequest(), listener);
|
|
|
+ client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.forAllApiKeys(), listener);
|
|
|
ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, () -> listener.actionGet());
|
|
|
assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", withUser);
|
|
|
}
|
|
@@ -1143,20 +1267,35 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
.encodeToString((responses.get(0).getId() + ":" + responses.get(0).getKey().toString()).getBytes(StandardCharsets.UTF_8));
|
|
|
Client client = client().filterWithHeader(Map.of("Authorization", "ApiKey " + base64ApiKeyKeyValue));
|
|
|
PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
|
|
|
- client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyId(responses.get(0).getId(), randomBoolean()), listener);
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().apiKeyId(responses.get(0).getId()).ownedByAuthenticatedUser(randomBoolean()).build(),
|
|
|
+ listener
|
|
|
+ );
|
|
|
GetApiKeyResponse response = listener.get();
|
|
|
- verifyGetResponse(
|
|
|
+ verifyApiKeyInfos(
|
|
|
1,
|
|
|
responses,
|
|
|
tuple.v2(),
|
|
|
List.of(new RoleDescriptor(DEFAULT_API_KEY_ROLE_DESCRIPTOR.getName(), Strings.EMPTY_ARRAY, null, null)),
|
|
|
- response,
|
|
|
+ null,
|
|
|
+ response.getApiKeyInfos(),
|
|
|
Collections.singleton(responses.get(0).getId()),
|
|
|
null
|
|
|
);
|
|
|
|
|
|
- final PlainActionFuture<GetApiKeyResponse> failureListener = new PlainActionFuture<>();
|
|
|
+ // It cannot retrieve its own limited-by role descriptors
|
|
|
+ final PlainActionFuture<GetApiKeyResponse> future2 = new PlainActionFuture<>();
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().apiKeyId(responses.get(0).getId()).ownedByAuthenticatedUser(randomBoolean()).withLimitedBy().build(),
|
|
|
+ future2
|
|
|
+ );
|
|
|
+ final ElasticsearchSecurityException e2 = expectThrows(ElasticsearchSecurityException.class, future2::actionGet);
|
|
|
+ assertErrorMessage(e2, "cluster:admin/xpack/security/api_key/get", ES_TEST_ROOT_USER, responses.get(0).getId());
|
|
|
+
|
|
|
// for any other API key id, it must deny access
|
|
|
+ final PlainActionFuture<GetApiKeyResponse> failureListener = new PlainActionFuture<>();
|
|
|
client.execute(
|
|
|
GetApiKeyAction.INSTANCE,
|
|
|
GetApiKeyRequest.usingApiKeyId(responses.get(1).getId(), randomBoolean()),
|
|
@@ -1171,6 +1310,117 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", ES_TEST_ROOT_USER, responses.get(0).getId());
|
|
|
}
|
|
|
|
|
|
+ public void testApiKeyViewLimitedBy() {
|
|
|
+ // 1. An API key with manage_own_api_key
|
|
|
+ final Tuple<List<CreateApiKeyResponse>, List<Map<String, Object>>> tuple1 = createApiKeys(
|
|
|
+ ES_TEST_ROOT_USER,
|
|
|
+ 1,
|
|
|
+ null,
|
|
|
+ "manage_own_api_key"
|
|
|
+ );
|
|
|
+ final List<CreateApiKeyResponse> responses1 = tuple1.v1();
|
|
|
+ final String apiKeyId1 = responses1.get(0).getId();
|
|
|
+ final Client client1 = client().filterWithHeader(
|
|
|
+ Map.of(
|
|
|
+ "Authorization",
|
|
|
+ "ApiKey "
|
|
|
+ + Base64.getEncoder()
|
|
|
+ .encodeToString((apiKeyId1 + ":" + responses1.get(0).getKey().toString()).getBytes(StandardCharsets.UTF_8))
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+ // Can view itself without limited-by
|
|
|
+ verifyApiKeyInfos(
|
|
|
+ 1,
|
|
|
+ responses1,
|
|
|
+ tuple1.v2(),
|
|
|
+ List.of(new RoleDescriptor(DEFAULT_API_KEY_ROLE_DESCRIPTOR.getName(), new String[] { "manage_own_api_key" }, null, null)),
|
|
|
+ null,
|
|
|
+ new ApiKey[] { getApiKeyInfo(client1, apiKeyId1, false, randomBoolean()) },
|
|
|
+ Collections.singleton(apiKeyId1),
|
|
|
+ null
|
|
|
+ );
|
|
|
+
|
|
|
+ // Cannot view itself with limited-by
|
|
|
+ final boolean useGetApiKey = randomBoolean();
|
|
|
+ final var e2 = expectThrows(ElasticsearchSecurityException.class, () -> getApiKeyInfo(client1, apiKeyId1, true, useGetApiKey));
|
|
|
+ assertErrorMessage(e2, "cluster:admin/xpack/security/api_key/" + (useGetApiKey ? "get" : "query"), ES_TEST_ROOT_USER, apiKeyId1);
|
|
|
+
|
|
|
+ // 2. An API key with manage_api_key can view its own limited-by or any other key's limited-by
|
|
|
+ final Tuple<List<CreateApiKeyResponse>, List<Map<String, Object>>> tuple3 = createApiKeys(
|
|
|
+ ES_TEST_ROOT_USER,
|
|
|
+ 1,
|
|
|
+ null,
|
|
|
+ "manage_api_key"
|
|
|
+ );
|
|
|
+ final List<CreateApiKeyResponse> responses3 = tuple3.v1();
|
|
|
+ final String apiKeyId3 = responses3.get(0).getId();
|
|
|
+ final Client client3 = client().filterWithHeader(
|
|
|
+ Map.of(
|
|
|
+ "Authorization",
|
|
|
+ "ApiKey "
|
|
|
+ + Base64.getEncoder()
|
|
|
+ .encodeToString((apiKeyId3 + ":" + responses3.get(0).getKey().toString()).getBytes(StandardCharsets.UTF_8))
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+ // View its own limited-by
|
|
|
+ verifyApiKeyInfos(
|
|
|
+ 1,
|
|
|
+ responses3,
|
|
|
+ tuple3.v2(),
|
|
|
+ List.of(new RoleDescriptor(DEFAULT_API_KEY_ROLE_DESCRIPTOR.getName(), new String[] { "manage_api_key" }, null, null)),
|
|
|
+ List.of(ES_TEST_ROOT_ROLE_DESCRIPTOR),
|
|
|
+ new ApiKey[] { getApiKeyInfo(client3, apiKeyId3, true, randomBoolean()) },
|
|
|
+ Collections.singleton(apiKeyId3),
|
|
|
+ null
|
|
|
+ );
|
|
|
+
|
|
|
+ // View other key's limited-by
|
|
|
+ verifyApiKeyInfos(
|
|
|
+ 1,
|
|
|
+ responses1,
|
|
|
+ tuple1.v2(),
|
|
|
+ List.of(new RoleDescriptor(DEFAULT_API_KEY_ROLE_DESCRIPTOR.getName(), new String[] { "manage_own_api_key" }, null, null)),
|
|
|
+ List.of(ES_TEST_ROOT_ROLE_DESCRIPTOR),
|
|
|
+ new ApiKey[] { getApiKeyInfo(client3, apiKeyId1, true, randomBoolean()) },
|
|
|
+ Collections.singleton(apiKeyId1),
|
|
|
+ null
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testLegacySuperuserLimitedByWillBeReturnedAsTransformed() throws Exception {
|
|
|
+ final Tuple<CreateApiKeyResponse, Map<String, Object>> createdApiKey = createApiKey(TEST_USER_NAME, null);
|
|
|
+ final var apiKeyId = createdApiKey.v1().getId();
|
|
|
+ final ServiceWithNodeName serviceWithNodeName = getServiceWithNodeName();
|
|
|
+ final Authentication authentication = Authentication.newRealmAuthentication(
|
|
|
+ new User(TEST_USER_NAME, TEST_ROLE),
|
|
|
+ new Authentication.RealmRef("file", "file", serviceWithNodeName.nodeName())
|
|
|
+ );
|
|
|
+ // Force set user role descriptors to 7.x legacy superuser role descriptors
|
|
|
+ assertSingleUpdate(
|
|
|
+ apiKeyId,
|
|
|
+ updateApiKeys(
|
|
|
+ serviceWithNodeName.service(),
|
|
|
+ authentication,
|
|
|
+ BulkUpdateApiKeyRequest.usingApiKeyIds(apiKeyId),
|
|
|
+ Set.of(ApiKeyService.LEGACY_SUPERUSER_ROLE_DESCRIPTOR)
|
|
|
+ )
|
|
|
+ );
|
|
|
+ // raw document has the legacy superuser role descriptor
|
|
|
+ expectRoleDescriptorsForApiKey(
|
|
|
+ "limited_by_role_descriptors",
|
|
|
+ Set.of(ApiKeyService.LEGACY_SUPERUSER_ROLE_DESCRIPTOR),
|
|
|
+ getApiKeyDocument(apiKeyId)
|
|
|
+ );
|
|
|
+
|
|
|
+ final ApiKey apiKeyInfo = getApiKeyInfo(client(), apiKeyId, true, randomBoolean());
|
|
|
+ assertThat(
|
|
|
+ apiKeyInfo.getLimitedBy().roleDescriptorsList().iterator().next(),
|
|
|
+ equalTo(Set.of(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR))
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
public void testApiKeyWithManageOwnPrivilegeIsAbleToInvalidateItselfButNotAnyOtherKeysCreatedBySameOwner() throws InterruptedException,
|
|
|
ExecutionException {
|
|
|
List<CreateApiKeyResponse> responses = createApiKeys(ES_TEST_ROOT_USER, 2, null, "manage_own_api_key").v1();
|
|
@@ -1285,6 +1535,18 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
assertNotNull(key100Response.getId());
|
|
|
assertNotNull(key100Response.getKey());
|
|
|
|
|
|
+ // Derive keys have empty limited-by role descriptors
|
|
|
+ final PlainActionFuture<GetApiKeyResponse> future = new PlainActionFuture<>();
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().apiKeyId(key100Response.getId()).withLimitedBy().build(),
|
|
|
+ future
|
|
|
+ );
|
|
|
+ assertThat(future.actionGet().getApiKeyInfos().length, equalTo(1));
|
|
|
+ final RoleDescriptorsIntersection limitedBy = future.actionGet().getApiKeyInfos()[0].getLimitedBy();
|
|
|
+ assertThat(limitedBy.roleDescriptorsList().size(), equalTo(1));
|
|
|
+ assertThat(limitedBy.roleDescriptorsList().iterator().next(), emptyIterable());
|
|
|
+
|
|
|
// Check at the end to allow sometime for the operation to happen. Since an erroneous creation is
|
|
|
// asynchronous so that the document is not available immediately.
|
|
|
assertApiKeyNotCreated(client, "key-2");
|
|
@@ -1546,7 +1808,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
final var expectedMetadata = request.getMetadata() != null ? request.getMetadata() : createdApiKey.v2();
|
|
|
final var expectedRoleDescriptors = nullRoleDescriptors ? List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR) : newRoleDescriptors;
|
|
|
|
|
|
- expectAttributesForApiKey(
|
|
|
+ doTestApiKeyHasExpectedAttributes(
|
|
|
apiKeyId,
|
|
|
Map.of(
|
|
|
ApiKeyAttribute.CREATOR,
|
|
@@ -1609,7 +1871,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
)
|
|
|
);
|
|
|
for (String apiKeyId : apiKeyIds) {
|
|
|
- expectAttributesForApiKey(
|
|
|
+ doTestApiKeyHasExpectedAttributes(
|
|
|
apiKeyId,
|
|
|
Map.of(
|
|
|
ApiKeyAttribute.METADATA,
|
|
@@ -1641,7 +1903,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
assertThat(response.getNoops(), containsInAnyOrder(apiKeyIds.toArray()));
|
|
|
assertThat(response.getErrorDetails().keySet(), containsInAnyOrder(notFoundIds.toArray()));
|
|
|
for (String apiKeyId : apiKeyIds) {
|
|
|
- expectAttributesForApiKey(
|
|
|
+ doTestApiKeyHasExpectedAttributes(
|
|
|
apiKeyId,
|
|
|
Map.of(
|
|
|
ApiKeyAttribute.METADATA,
|
|
@@ -1817,7 +2079,10 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
"all"
|
|
|
).v1().get(0);
|
|
|
final String apiKeyId = createdApiKey.getId();
|
|
|
- expectAttributesForApiKey(apiKeyId, Map.of(ApiKeyAttribute.LIMITED_BY_ROLE_DESCRIPTORS, Set.of(roleDescriptorBeforeUpdate)));
|
|
|
+ doTestApiKeyHasExpectedAttributes(
|
|
|
+ apiKeyId,
|
|
|
+ Map.of(ApiKeyAttribute.LIMITED_BY_ROLE_DESCRIPTORS, Set.of(roleDescriptorBeforeUpdate))
|
|
|
+ );
|
|
|
|
|
|
final List<String> newClusterPrivileges = randomValueOtherThan(clusterPrivileges, () -> {
|
|
|
final List<String> privs = new ArrayList<>(randomSubsetOf(ClusterPrivilegeResolver.names()));
|
|
@@ -1839,7 +2104,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
|
|
|
assertNotNull(response);
|
|
|
assertTrue(response.isUpdated());
|
|
|
- expectAttributesForApiKey(apiKeyId, Map.of(ApiKeyAttribute.LIMITED_BY_ROLE_DESCRIPTORS, Set.of(roleDescriptorAfterUpdate)));
|
|
|
+ doTestApiKeyHasExpectedAttributes(apiKeyId, Map.of(ApiKeyAttribute.LIMITED_BY_ROLE_DESCRIPTORS, Set.of(roleDescriptorAfterUpdate)));
|
|
|
|
|
|
// Update user role name only
|
|
|
final RoleDescriptor roleDescriptorWithNewName = putRoleWithClusterPrivileges(
|
|
@@ -1865,7 +2130,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
expectedCreator.put("metadata", updatedUser.metadata());
|
|
|
expectedCreator.put("realm_type", "native");
|
|
|
expectedCreator.put("realm", "index");
|
|
|
- expectAttributesForApiKey(
|
|
|
+ doTestApiKeyHasExpectedAttributes(
|
|
|
apiKeyId,
|
|
|
Map.of(ApiKeyAttribute.CREATOR, expectedCreator, ApiKeyAttribute.LIMITED_BY_ROLE_DESCRIPTORS, Set.of(roleDescriptorWithNewName))
|
|
|
);
|
|
@@ -2181,7 +2446,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
currentSuperuserRoleDescriptors
|
|
|
)
|
|
|
);
|
|
|
- expectAttributesForApiKey(apiKeyId, Map.of(ApiKeyAttribute.LIMITED_BY_ROLE_DESCRIPTORS, currentSuperuserRoleDescriptors));
|
|
|
+ doTestApiKeyHasExpectedAttributes(apiKeyId, Map.of(ApiKeyAttribute.LIMITED_BY_ROLE_DESCRIPTORS, currentSuperuserRoleDescriptors));
|
|
|
// Second update is noop because role descriptors were auto-updated by the previous request
|
|
|
assertSingleNoop(
|
|
|
apiKeyId,
|
|
@@ -2299,35 +2564,40 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
|
|
|
// Check attributes with both the raw document and the get api key response whenever possible
|
|
|
@SuppressWarnings("unchecked")
|
|
|
- private void expectAttributesForApiKey(String apiKeyId, Map<ApiKeyAttribute, Object> attributes) throws IOException {
|
|
|
+ private void doTestApiKeyHasExpectedAttributes(String apiKeyId, Map<ApiKeyAttribute, Object> attributes) throws IOException {
|
|
|
final Map<String, Object> apiKeyDocMap = getApiKeyDocument(apiKeyId);
|
|
|
- final PlainActionFuture<GetApiKeyResponse> future = new PlainActionFuture<>();
|
|
|
- client().execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyId(apiKeyId, false), future);
|
|
|
- final GetApiKeyResponse getApiKeyResponse = future.actionGet();
|
|
|
- assertThat(getApiKeyResponse.getApiKeyInfos(), arrayWithSize(1));
|
|
|
- final ApiKey apiKeyInfo = getApiKeyResponse.getApiKeyInfos()[0];
|
|
|
-
|
|
|
+ final boolean useGetApiKey = randomBoolean();
|
|
|
+ final ApiKey apiKeyInfo = getApiKeyInfo(client(), apiKeyId, true, useGetApiKey);
|
|
|
for (Map.Entry<ApiKeyAttribute, Object> entry : attributes.entrySet()) {
|
|
|
switch (entry.getKey()) {
|
|
|
case CREATOR -> {
|
|
|
final var creatorMap = (Map<String, Object>) entry.getValue();
|
|
|
expectCreatorForApiKey(creatorMap, apiKeyDocMap);
|
|
|
- assertThat(creatorMap.get("principal"), equalTo(apiKeyInfo.getUsername()));
|
|
|
- assertThat(creatorMap.get("realm"), equalTo(apiKeyInfo.getRealm()));
|
|
|
+ assertThat("useGetApiKey: " + useGetApiKey, creatorMap.get("principal"), equalTo(apiKeyInfo.getUsername()));
|
|
|
+ assertThat("useGetApiKey: " + useGetApiKey, creatorMap.get("realm"), equalTo(apiKeyInfo.getRealm()));
|
|
|
}
|
|
|
case METADATA -> {
|
|
|
final var metadata = (Map<String, Object>) entry.getValue();
|
|
|
expectMetadataForApiKey(metadata, apiKeyDocMap);
|
|
|
- assertThat(metadata, equalTo(apiKeyInfo.getMetadata()));
|
|
|
+ assertThat("useGetApiKey: " + useGetApiKey, metadata, equalTo(apiKeyInfo.getMetadata()));
|
|
|
}
|
|
|
case ASSIGNED_ROLE_DESCRIPTORS -> {
|
|
|
final var expectedRoleDescriptors = (Collection<RoleDescriptor>) entry.getValue();
|
|
|
expectRoleDescriptorsForApiKey("role_descriptors", expectedRoleDescriptors, apiKeyDocMap);
|
|
|
- assertThat(expectedRoleDescriptors, containsInAnyOrder(apiKeyInfo.getRoleDescriptors().toArray(RoleDescriptor[]::new)));
|
|
|
+ assertThat(
|
|
|
+ "useGetApiKey: " + useGetApiKey,
|
|
|
+ expectedRoleDescriptors,
|
|
|
+ containsInAnyOrder(apiKeyInfo.getRoleDescriptors().toArray(RoleDescriptor[]::new))
|
|
|
+ );
|
|
|
}
|
|
|
case LIMITED_BY_ROLE_DESCRIPTORS -> {
|
|
|
final var expectedRoleDescriptors = (Collection<RoleDescriptor>) entry.getValue();
|
|
|
expectRoleDescriptorsForApiKey("limited_by_role_descriptors", expectedRoleDescriptors, apiKeyDocMap);
|
|
|
+ assertThat(
|
|
|
+ "useGetApiKey: " + useGetApiKey,
|
|
|
+ expectedRoleDescriptors,
|
|
|
+ containsInAnyOrder(apiKeyInfo.getLimitedBy().roleDescriptorsList().iterator().next().toArray(RoleDescriptor[]::new))
|
|
|
+ );
|
|
|
}
|
|
|
default -> throw new IllegalStateException("unexpected attribute name");
|
|
|
}
|
|
@@ -2336,7 +2606,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
|
|
|
private void expectAttributesForApiKeys(List<String> apiKeyIds, Map<ApiKeyAttribute, Object> attributes) throws IOException {
|
|
|
for (String apiKeyId : apiKeyIds) {
|
|
|
- expectAttributesForApiKey(apiKeyId, attributes);
|
|
|
+ doTestApiKeyHasExpectedAttributes(apiKeyId, attributes);
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -2387,6 +2657,30 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
return client().execute(GetAction.INSTANCE, new GetRequest(SECURITY_MAIN_ALIAS, apiKeyId)).actionGet().getSource();
|
|
|
}
|
|
|
|
|
|
+ private ApiKey getApiKeyInfo(Client client, String apiKeyId, boolean withLimitedBy, boolean useGetApiKey) {
|
|
|
+ if (useGetApiKey) {
|
|
|
+ final PlainActionFuture<GetApiKeyResponse> future = new PlainActionFuture<>();
|
|
|
+ client.execute(
|
|
|
+ GetApiKeyAction.INSTANCE,
|
|
|
+ GetApiKeyRequest.builder().apiKeyId(apiKeyId).withLimitedBy(withLimitedBy).build(),
|
|
|
+ future
|
|
|
+ );
|
|
|
+ final GetApiKeyResponse getApiKeyResponse = future.actionGet();
|
|
|
+ assertThat(getApiKeyResponse.getApiKeyInfos(), arrayWithSize(1));
|
|
|
+ return getApiKeyResponse.getApiKeyInfos()[0];
|
|
|
+ } else {
|
|
|
+ final PlainActionFuture<QueryApiKeyResponse> future = new PlainActionFuture<>();
|
|
|
+ client.execute(
|
|
|
+ QueryApiKeyAction.INSTANCE,
|
|
|
+ new QueryApiKeyRequest(QueryBuilders.idsQuery().addIds(apiKeyId), null, null, null, null, withLimitedBy),
|
|
|
+ future
|
|
|
+ );
|
|
|
+ final QueryApiKeyResponse queryApiKeyResponse = future.actionGet();
|
|
|
+ assertThat(queryApiKeyResponse.getItems(), arrayWithSize(1));
|
|
|
+ return queryApiKeyResponse.getItems()[0].getApiKey();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private ServiceWithNodeName getServiceWithNodeName() {
|
|
|
final var nodeName = randomFrom(internalCluster().getNodeNames());
|
|
|
final var service = internalCluster().getInstance(ApiKeyService.class, nodeName);
|
|
@@ -2439,65 +2733,70 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- private void verifyGetResponse(
|
|
|
+ private void verifyApiKeyInfos(
|
|
|
int expectedNumberOfApiKeys,
|
|
|
List<CreateApiKeyResponse> responses,
|
|
|
List<Map<String, Object>> metadatas,
|
|
|
List<RoleDescriptor> expectedRoleDescriptors,
|
|
|
- GetApiKeyResponse response,
|
|
|
+ List<RoleDescriptor> expectedLimitedByRoleDescriptors,
|
|
|
+ ApiKey[] apiKeyInfos,
|
|
|
Set<String> validApiKeyIds,
|
|
|
List<String> invalidatedApiKeyIds
|
|
|
) {
|
|
|
- verifyGetResponse(
|
|
|
+ verifyApiKeyInfos(
|
|
|
ES_TEST_ROOT_USER,
|
|
|
expectedNumberOfApiKeys,
|
|
|
responses,
|
|
|
metadatas,
|
|
|
expectedRoleDescriptors,
|
|
|
- response,
|
|
|
+ expectedLimitedByRoleDescriptors,
|
|
|
+ apiKeyInfos,
|
|
|
validApiKeyIds,
|
|
|
invalidatedApiKeyIds
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- private void verifyGetResponse(
|
|
|
+ private void verifyApiKeyInfos(
|
|
|
String user,
|
|
|
int expectedNumberOfApiKeys,
|
|
|
List<CreateApiKeyResponse> responses,
|
|
|
List<Map<String, Object>> metadatas,
|
|
|
List<RoleDescriptor> expectedRoleDescriptors,
|
|
|
- GetApiKeyResponse response,
|
|
|
+ List<RoleDescriptor> expectedLimitedByRoleDescriptors,
|
|
|
+ ApiKey[] apiKeyInfos,
|
|
|
Set<String> validApiKeyIds,
|
|
|
List<String> invalidatedApiKeyIds
|
|
|
) {
|
|
|
- verifyGetResponse(
|
|
|
+ verifyApiKeyInfos(
|
|
|
new String[] { user },
|
|
|
expectedNumberOfApiKeys,
|
|
|
responses,
|
|
|
metadatas,
|
|
|
expectedRoleDescriptors,
|
|
|
- response,
|
|
|
+ (ignore) -> expectedLimitedByRoleDescriptors,
|
|
|
+ apiKeyInfos,
|
|
|
validApiKeyIds,
|
|
|
invalidatedApiKeyIds
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- private void verifyGetResponse(
|
|
|
+ private void verifyApiKeyInfos(
|
|
|
String[] user,
|
|
|
int expectedNumberOfApiKeys,
|
|
|
List<CreateApiKeyResponse> responses,
|
|
|
List<Map<String, Object>> metadatas,
|
|
|
List<RoleDescriptor> expectedRoleDescriptors,
|
|
|
- GetApiKeyResponse response,
|
|
|
+ Function<String, List<RoleDescriptor>> expectedLimitedByRoleDescriptorsLookup,
|
|
|
+ ApiKey[] apiKeyInfos,
|
|
|
Set<String> validApiKeyIds,
|
|
|
List<String> invalidatedApiKeyIds
|
|
|
) {
|
|
|
- assertThat(response.getApiKeyInfos().length, equalTo(expectedNumberOfApiKeys));
|
|
|
+ assertThat(apiKeyInfos.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())
|
|
|
+ List<String> actualIds = Arrays.stream(apiKeyInfos)
|
|
|
.filter(o -> o.isInvalidated() == false)
|
|
|
.map(o -> o.getId())
|
|
|
.collect(Collectors.toList());
|
|
@@ -2506,19 +2805,19 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
.filter(o -> validApiKeyIds.contains(o.getId()))
|
|
|
.map(o -> o.getName())
|
|
|
.collect(Collectors.toList());
|
|
|
- List<String> actualNames = Arrays.stream(response.getApiKeyInfos())
|
|
|
+ List<String> actualNames = Arrays.stream(apiKeyInfos)
|
|
|
.filter(o -> o.isInvalidated() == false)
|
|
|
.map(o -> o.getName())
|
|
|
.collect(Collectors.toList());
|
|
|
assertThat(actualNames, containsInAnyOrder(expectedNames.toArray(Strings.EMPTY_ARRAY)));
|
|
|
Set<String> expectedUsernames = (validApiKeyIds.isEmpty()) ? Collections.emptySet() : Set.of(user);
|
|
|
- Set<String> actualUsernames = Arrays.stream(response.getApiKeyInfos())
|
|
|
+ Set<String> actualUsernames = Arrays.stream(apiKeyInfos)
|
|
|
.filter(o -> o.isInvalidated() == false)
|
|
|
.map(o -> o.getUsername())
|
|
|
.collect(Collectors.toSet());
|
|
|
assertThat(actualUsernames, containsInAnyOrder(expectedUsernames.toArray(Strings.EMPTY_ARRAY)));
|
|
|
if (invalidatedApiKeyIds != null) {
|
|
|
- List<String> actualInvalidatedApiKeyIds = Arrays.stream(response.getApiKeyInfos())
|
|
|
+ List<String> actualInvalidatedApiKeyIds = Arrays.stream(apiKeyInfos)
|
|
|
.filter(o -> o.isInvalidated())
|
|
|
.map(o -> o.getId())
|
|
|
.collect(Collectors.toList());
|
|
@@ -2531,18 +2830,25 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
|
|
|
(m, i) -> m.put(responses.get(i).getId(), metadatas.get(i)),
|
|
|
HashMap::putAll
|
|
|
);
|
|
|
- for (ApiKey apiKey : response.getApiKeyInfos()) {
|
|
|
+ for (ApiKey apiKey : apiKeyInfos) {
|
|
|
final Map<String, Object> metadata = idToMetadata.get(apiKey.getId());
|
|
|
assertThat(apiKey.getMetadata(), equalTo(metadata == null ? Map.of() : metadata));
|
|
|
}
|
|
|
}
|
|
|
- Arrays.stream(response.getApiKeyInfos())
|
|
|
- .forEach(
|
|
|
- apiKeyInfo -> assertThat(
|
|
|
- apiKeyInfo.getRoleDescriptors(),
|
|
|
- containsInAnyOrder(expectedRoleDescriptors.toArray(RoleDescriptor[]::new))
|
|
|
- )
|
|
|
+ Arrays.stream(apiKeyInfos).forEach(apiKeyInfo -> {
|
|
|
+ assertThat(apiKeyInfo.getRoleDescriptors(), containsInAnyOrder(expectedRoleDescriptors.toArray(RoleDescriptor[]::new)));
|
|
|
+ final List<RoleDescriptor> expectedLimitedByRoleDescriptors = expectedLimitedByRoleDescriptorsLookup.apply(
|
|
|
+ apiKeyInfo.getUsername()
|
|
|
);
|
|
|
+ if (expectedLimitedByRoleDescriptors == null) {
|
|
|
+ assertThat(apiKeyInfo.getLimitedBy(), nullValue());
|
|
|
+ } else {
|
|
|
+ assertThat(
|
|
|
+ apiKeyInfo.getLimitedBy().roleDescriptorsList().iterator().next(),
|
|
|
+ containsInAnyOrder(expectedLimitedByRoleDescriptors.toArray(RoleDescriptor[]::new))
|
|
|
+ );
|
|
|
+ }
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
private Tuple<CreateApiKeyResponse, Map<String, Object>> createApiKey(String user, TimeValue expiration) {
|