diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java index 964cc1275b029..449f5b3a41edf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java @@ -205,9 +205,14 @@ protected boolean extendedCheck(String action, TransportRequest request, Authent @Override protected boolean doImplies(ActionBasedPermissionCheck permissionCheck) { - return permissionCheck instanceof AutomatonPermissionCheck; + /* + * We know that "permissionCheck" has an automaton which is a subset of ours. + * Which means "permissionCheck" _cannot_ grant an action that we don't (see ActionBasedPermissionCheck#check) + * Since we grant _all_ requests on actions within our automaton, we must therefore grant _all_ actions+requests that the other + * permission check grants. + */ + return true; } - } // action, request based permission check diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ActionClusterPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ActionClusterPrivilege.java index 3784797ee1ec9..3a5792bc83123 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ActionClusterPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ActionClusterPrivilege.java @@ -19,6 +19,7 @@ public class ActionClusterPrivilege implements NamedClusterPrivilege { private final String name; private final Set allowedActionPatterns; private final Set excludedActionPatterns; + private final ClusterPermission permission; /** * Constructor for {@link ActionClusterPrivilege} defining what cluster actions are accessible for the user with this privilege. @@ -43,6 +44,7 @@ public ActionClusterPrivilege(final String name, final Set allowedAction this.name = name; this.allowedActionPatterns = allowedActionPatterns; this.excludedActionPatterns = excludedActionPatterns; + this.permission = buildPermission(ClusterPermission.builder()).build(); } @Override @@ -62,4 +64,9 @@ public Set getExcludedActionPatterns() { public ClusterPermission.Builder buildPermission(final ClusterPermission.Builder builder) { return builder.add(this, allowedActionPatterns, excludedActionPatterns); } + + @Override + public ClusterPermission permission() { + return permission; + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java index 20561c801efe0..aeb7cf40d9e7c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java @@ -18,6 +18,7 @@ import org.elasticsearch.action.ingest.GetPipelineAction; import org.elasticsearch.action.ingest.SimulatePipelineAction; import org.elasticsearch.common.Strings; +import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.ilm.action.GetLifecycleAction; import org.elasticsearch.xpack.core.ilm.action.GetStatusAction; import org.elasticsearch.xpack.core.ilm.action.StartILMAction; @@ -28,16 +29,21 @@ import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction; import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; +import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.slm.action.GetSnapshotLifecycleAction; +import java.util.Collection; import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Function; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Translates cluster privilege names into concrete implementations @@ -140,7 +146,7 @@ public class ClusterPrivilegeResolver { public static final NamedClusterPrivilege MANAGE_LOGSTASH_PIPELINES = new ActionClusterPrivilege("manage_logstash_pipelines", Set.of("cluster:admin/logstash/pipeline/*")); - private static final Map VALUES = Stream.of( + private static final Map VALUES = sortByAccessLevel(List.of( NONE, ALL, MONITOR, @@ -178,7 +184,7 @@ public class ClusterPrivilegeResolver { DELEGATE_PKI, MANAGE_OWN_API_KEY, MANAGE_ENRICH, - MANAGE_LOGSTASH_PIPELINES).collect(Collectors.toUnmodifiableMap(NamedClusterPrivilege::name, Function.identity())); + MANAGE_LOGSTASH_PIPELINES)); /** * Resolves a {@link NamedClusterPrivilege} from a given name if it exists. @@ -219,4 +225,36 @@ private static String actionToPattern(String text) { return text + "*"; } + /** + * Returns the names of privileges that grant the specified action and request, for the given authentication context. + * @return A collection of names, ordered (to the extent possible) from least privileged (e.g. {@link #MONITOR}) + * to most privileged (e.g. {@link #ALL}) + * @see #sortByAccessLevel(Collection) + * @see org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission#check(String, TransportRequest, Authentication) + */ + public static Collection findPrivilegesThatGrant(String action, TransportRequest request, Authentication authentication) { + return VALUES.entrySet().stream() + .filter(e -> e.getValue().permission().check(action, request, authentication)) + .map(Map.Entry::getKey) + .collect(Collectors.toUnmodifiableList()); + } + + /** + * Sorts the collection of privileges from least-privilege to most-privilege (to the extent possible), + * returning them in a sorted map keyed by name. + */ + static SortedMap sortByAccessLevel(Collection privileges) { + // How many other privileges does this privilege imply. Those with a higher count are considered to be a higher privilege + final Map impliesCount = new HashMap<>(privileges.size()); + privileges.forEach(priv -> impliesCount.put(priv.name(), + privileges.stream().filter(p2 -> p2 != priv && priv.permission().implies(p2.permission())).count()) + ); + + final Comparator compare = Comparator.comparingLong(key -> impliesCount.getOrDefault(key, 0L)) + .thenComparing(Comparator.naturalOrder()); + final TreeMap tree = new TreeMap<>(compare); + privileges.forEach(p -> tree.put(p.name(), p)); + return Collections.unmodifiableSortedMap(tree); + } + } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilege.java index 965e490eb7d02..c377b9111ce3e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilege.java @@ -28,7 +28,10 @@ public class ManageOwnApiKeyClusterPrivilege implements NamedClusterPrivilege { private static final String PRIVILEGE_NAME = "manage_own_api_key"; private static final String API_KEY_ID_KEY = "_security_api_key_id"; + private final ClusterPermission permission; + private ManageOwnApiKeyClusterPrivilege() { + permission = this.buildPermission(ClusterPermission.builder()).build(); } @Override @@ -41,6 +44,11 @@ public ClusterPermission.Builder buildPermission(ClusterPermission.Builder build return builder.add(this, ManageOwnClusterPermissionCheck.INSTANCE); } + @Override + public ClusterPermission permission() { + return permission; + } + private static final class ManageOwnClusterPermissionCheck extends ClusterPermission.ActionBasedPermissionCheck { public static final ManageOwnClusterPermissionCheck INSTANCE = new ManageOwnClusterPermissionCheck(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/NamedClusterPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/NamedClusterPrivilege.java index fe2e16af62003..3980dfe8174f6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/NamedClusterPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/NamedClusterPrivilege.java @@ -9,6 +9,7 @@ package org.elasticsearch.xpack.core.security.authz.privilege; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission; /** * A {@link ClusterPrivilege} that has a name. The named cluster privileges can be referred simply by name within a @@ -16,4 +17,16 @@ */ public interface NamedClusterPrivilege extends ClusterPrivilege { String name(); + + /** + * Returns a permission that represents this privilege only. + * When building a role (or role-like object) that has many privileges, it is more efficient to build a shared permission using the + * {@link #buildPermission(ClusterPermission.Builder)} method instead. This method is intended to allow callers to interrogate the + * runtime permissions specifically granted by this privilege. + * It is acceptable (and encouraged) for implementations of this method to cache (or precompute) the {@link ClusterPermission} + * and return the same object on each call. + * @see #buildPermission(ClusterPermission.Builder) + */ + ClusterPermission permission(); + } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermissionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermissionTests.java index 5a52519c7b72e..981f069fc37f0 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermissionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermissionTests.java @@ -16,12 +16,15 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.NamedClusterPrivilege; import org.junit.Before; import java.io.IOException; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; @@ -230,6 +233,33 @@ public void testClusterPermissionSubsetIsImpliedByAllClusterPermission() { assertThat(allClusterPermission.implies(otherClusterPermission), is(true)); } + public void testImpliesOnSecurityPrivilegeHierarchy() { + final List highToLow = List.of( + ClusterPrivilegeResolver.ALL.permission(), + ClusterPrivilegeResolver.MANAGE_SECURITY.permission(), + ClusterPrivilegeResolver.MANAGE_API_KEY.permission(), + ClusterPrivilegeResolver.MANAGE_OWN_API_KEY.permission() + ); + + for (int i = 0; i < highToLow.size(); i++) { + ClusterPermission high = highToLow.get(i); + for (int j = i; j < highToLow.size(); j++) { + ClusterPermission low = highToLow.get(j); + assertThat("Permission " + name(high) + " should imply " + name(low), high.implies(low), is(true)); + } + } + } + + private String name(ClusterPermission permission) { + return permission.privileges().stream().map(priv -> { + if (priv instanceof NamedClusterPrivilege) { + return ((NamedClusterPrivilege) priv).name(); + } else { + return priv.toString(); + } + }).collect(Collectors.joining(",")); + } + private static class MockConfigurableClusterPrivilege implements ConfigurableClusterPrivilege { private Predicate requestPredicate; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolverTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolverTests.java new file mode 100644 index 0000000000000..df95348e99e1d --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolverTests.java @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.authz.privilege; + +import org.elasticsearch.test.ESTestCase; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.SortedMap; + +import static org.hamcrest.Matchers.contains; + +public class ClusterPrivilegeResolverTests extends ESTestCase { + + public void testSortByAccessLevel() throws Exception { + final List privileges = new ArrayList<>(List.of( + ClusterPrivilegeResolver.ALL, + ClusterPrivilegeResolver.MONITOR, + ClusterPrivilegeResolver.MANAGE, + ClusterPrivilegeResolver.MANAGE_OWN_API_KEY, + ClusterPrivilegeResolver.MANAGE_API_KEY, + ClusterPrivilegeResolver.MANAGE_SECURITY + )); + Collections.shuffle(privileges, random()); + final SortedMap sorted = ClusterPrivilegeResolver.sortByAccessLevel(privileges); + // This is: + // "manage_own_api_key", "monitor" (neither of which grant anything else in the list), sorted by name + // "manage" and "manage_api_key",(which each grant 1 other privilege in the list), sorted by name + // "manage_security" and "all", sorted by access level ("all" implies "manage_security") + assertThat(sorted.keySet(), contains("manage_own_api_key", "monitor", "manage", "manage_api_key", "manage_security", "all")); + } + +} diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java index b76146e98f146..8c54c78c376b5 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java @@ -36,7 +36,6 @@ import org.elasticsearch.rest.RestStatus; import org.elasticsearch.test.SecurityIntegTestCase; import org.elasticsearch.test.SecuritySettingsSource; -import org.elasticsearch.test.SecuritySettingsSourceField; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.action.ApiKey; @@ -54,7 +53,6 @@ import org.elasticsearch.xpack.core.security.action.user.PutUserAction; import org.elasticsearch.xpack.core.security.action.user.PutUserRequest; import org.elasticsearch.xpack.core.security.action.user.PutUserResponse; -import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.junit.After; @@ -79,6 +77,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.elasticsearch.test.SecuritySettingsSource.TEST_SUPERUSER; +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; import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.INTERNAL_SECURITY_MAIN_INDEX_7; import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS; import static org.elasticsearch.xpack.security.Security.SECURITY_CRYPTO_THREAD_POOL_NAME; @@ -139,7 +141,7 @@ public String configRoles() { @Override public String configUsers() { final String usersPasswdHashed = new String( - getFastStoredHashAlgoForTests().hash(SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)); + getFastStoredHashAlgoForTests().hash(TEST_PASSWORD_SECURE_STRING)); return super.configUsers() + "user_with_no_api_key_role:" + usersPasswdHashed + "\n" + "user_with_manage_api_key_role:" + usersPasswdHashed + "\n" + @@ -160,13 +162,12 @@ private void awaitApiKeysRemoverCompletion() throws Exception { } } - public void testCreateApiKey() throws Exception{ + public void testCreateApiKey() throws Exception { // Get an instant without nanoseconds as the expiration has millisecond precision final Instant start = Instant.ofEpochMilli(Instant.now().toEpochMilli()); 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))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client) .setName("test key") .setExpiration(TimeValue.timeValueHours(TimeUnit.DAYS.toHours(7L))) @@ -196,7 +197,7 @@ public void testCreateApiKey() throws Exception{ final RestHighLevelClient restClient = new TestRestHighLevelClient(); AuthenticateResponse authResponse = restClient.security().authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "ApiKey " + base64ApiKeyKeyValue).build()); - assertThat(authResponse.getUser().getUsername(), equalTo(SecuritySettingsSource.TEST_SUPERUSER)); + assertThat(authResponse.getUser().getUsername(), equalTo(TEST_SUPERUSER)); assertThat(authResponse.getAuthenticationType(), equalTo("api_key")); // use the first ApiKey for an unauthorized action @@ -215,9 +216,9 @@ public void testMultipleApiKeysCanHaveSameName() { int noOfApiKeys = randomIntBetween(2, 5); List 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))); + final RoleDescriptor descriptor = new RoleDescriptor("role", new String[] { "monitor" }, null, null); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client).setName(keyName).setExpiration(null) .setRoleDescriptors(Collections.singletonList(descriptor)).get(); assertNotNull(response.getId()); @@ -231,9 +232,8 @@ public void testMultipleApiKeysCanHaveSameName() { } public void testCreateApiKeyWithoutNameWillFail() { - Client client = client().filterWithHeader(Collections.singletonMap("Authorization", - UsernamePasswordToken.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, - SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); final ActionRequestValidationException e = expectThrows(ActionRequestValidationException.class, () -> new CreateApiKeyRequestBuilder(client).get()); assertThat(e.getMessage(), containsString("api key name is required")); @@ -242,8 +242,8 @@ public void testCreateApiKeyWithoutNameWillFail() { public void testInvalidateApiKeysForRealm() throws InterruptedException, ExecutionException { int noOfApiKeys = randomIntBetween(3, 5); List responses = createApiKeys(noOfApiKeys, null); - Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingRealmName("file"), listener); InvalidateApiKeyResponse invalidateResponse = listener.get(); @@ -253,30 +253,30 @@ public void testInvalidateApiKeysForRealm() throws InterruptedException, Executi public void testInvalidateApiKeysForUser() throws Exception { int noOfApiKeys = randomIntBetween(3, 5); List responses = createApiKeys(noOfApiKeys, null); - Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, - InvalidateApiKeyRequest.usingUserName(SecuritySettingsSource.TEST_SUPERUSER), listener); + InvalidateApiKeyRequest.usingUserName(TEST_SUPERUSER), listener); InvalidateApiKeyResponse invalidateResponse = listener.get(); verifyInvalidateResponse(noOfApiKeys, responses, invalidateResponse); } public void testInvalidateApiKeysForRealmAndUser() throws InterruptedException, ExecutionException { List responses = createApiKeys(1, null); - Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, - InvalidateApiKeyRequest.usingRealmAndUserName("file", SecuritySettingsSource.TEST_SUPERUSER), listener); + InvalidateApiKeyRequest.usingRealmAndUserName("file", TEST_SUPERUSER), listener); InvalidateApiKeyResponse invalidateResponse = listener.get(); verifyInvalidateResponse(1, responses, invalidateResponse); } public void testInvalidateApiKeysForApiKeyId() throws InterruptedException, ExecutionException { List responses = createApiKeys(1, null); - Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyId(responses.get(0).getId(), false), listener); InvalidateApiKeyResponse invalidateResponse = listener.get(); @@ -285,11 +285,11 @@ public void testInvalidateApiKeysForApiKeyId() throws InterruptedException, Exec public void testInvalidateApiKeysForApiKeyName() throws InterruptedException, ExecutionException { List responses = createApiKeys(1, null); - Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyName(responses.get(0).getName(), false), - listener); + listener); InvalidateApiKeyResponse invalidateResponse = listener.get(); verifyInvalidateResponse(1, responses, invalidateResponse); } @@ -319,8 +319,8 @@ public void testInvalidateApiKeyWillClearApiKeyCache() throws IOException, Execu } // Invalidate the first key - Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyId(apiKey1.v1(), false), listener); InvalidateApiKeyResponse invalidateResponse = listener.get(); @@ -350,21 +350,20 @@ private void verifyInvalidateResponse(int noOfApiKeys, List r.getId()).collect(Collectors.toList()).toArray(Strings.EMPTY_ARRAY))); + containsInAnyOrder(responses.stream().map(r -> r.getId()).collect(Collectors.toList()).toArray(Strings.EMPTY_ARRAY))); assertThat(invalidateResponse.getPreviouslyInvalidatedApiKeys().size(), equalTo(0)); assertThat(invalidateResponse.getErrors().size(), equalTo(0)); } public void testInvalidatedApiKeysDeletedByRemover() throws Exception { Client client = waitForExpiredApiKeysRemoverTriggerReadyAndGetClient().filterWithHeader( - Collections.singletonMap("Authorization", UsernamePasswordToken.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, - SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); List createdApiKeys = createApiKeys(2, null); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyId(createdApiKeys.get(0).getId(), false), - listener); + listener); InvalidateApiKeyResponse invalidateResponse = listener.get(); assertThat(invalidateResponse.getInvalidatedApiKeys().size(), equalTo(1)); assertThat(invalidateResponse.getPreviouslyInvalidatedApiKeys().size(), equalTo(0)); @@ -389,16 +388,15 @@ public void testInvalidatedApiKeysDeletedByRemover() throws Exception { } } assertThat(getApiKeyResponseListener.get().getApiKeyInfos().length, - is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 2 : 1)); + is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 2 : 1)); client = waitForExpiredApiKeysRemoverTriggerReadyAndGetClient().filterWithHeader( - Collections.singletonMap("Authorization", UsernamePasswordToken.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, - SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); // invalidate API key to trigger remover listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyId(createdApiKeys.get(1).getId(), false), - listener); + listener); assertThat(listener.get().getInvalidatedApiKeys().size(), is(1)); awaitApiKeysRemoverCompletion(); @@ -419,7 +417,7 @@ public void testInvalidatedApiKeysDeletedByRemover() throws Exception { } } assertThat(getApiKeyResponseListener.get().getApiKeyInfos().length, - is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 1 : 0)); + is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 1 : 0)); } private Client waitForExpiredApiKeysRemoverTriggerReadyAndGetClient() throws Exception { @@ -431,7 +429,7 @@ private Client waitForExpiredApiKeysRemoverTriggerReadyAndGetClient() throws Exc if (apiKeyService.lastTimeWhenApiKeysRemoverWasTriggered() > apiKeyLastTrigger) { nodeWithMostRecentRun = nodeName; apiKeyLastTrigger = apiKeyService.lastTimeWhenApiKeysRemoverWasTriggered(); - } + } } } final ThreadPool threadPool = internalCluster().getInstance(ThreadPool.class, nodeWithMostRecentRun); @@ -444,8 +442,7 @@ private Client waitForExpiredApiKeysRemoverTriggerReadyAndGetClient() throws Exc public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() throws Exception { Client client = waitForExpiredApiKeysRemoverTriggerReadyAndGetClient().filterWithHeader( - Collections.singletonMap("Authorization", UsernamePasswordToken.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, - SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); int noOfKeys = 4; List createdApiKeys = createApiKeys(noOfKeys, null); @@ -460,10 +457,10 @@ public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() Instant dayBefore = created.minus(1L, ChronoUnit.DAYS); assertTrue(Instant.now().isAfter(dayBefore)); UpdateResponse expirationDateUpdatedResponse = client - .prepareUpdate(SECURITY_MAIN_ALIAS, createdApiKeys.get(0).getId()) - .setDoc("expiration_time", dayBefore.toEpochMilli()) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .get(); + .prepareUpdate(SECURITY_MAIN_ALIAS, createdApiKeys.get(0).getId()) + .setDoc("expiration_time", dayBefore.toEpochMilli()) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .get(); assertThat(expirationDateUpdatedResponse.getResult(), is(DocWriteResponse.Result.UPDATED)); // Expire the 2nd key such that it can be deleted by the remover @@ -471,15 +468,15 @@ public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() Instant weekBefore = created.minus(8L, ChronoUnit.DAYS); assertTrue(Instant.now().isAfter(weekBefore)); expirationDateUpdatedResponse = client.prepareUpdate(SECURITY_MAIN_ALIAS, createdApiKeys.get(1).getId()) - .setDoc("expiration_time", weekBefore.toEpochMilli()) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .get(); + .setDoc("expiration_time", weekBefore.toEpochMilli()) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .get(); assertThat(expirationDateUpdatedResponse.getResult(), is(DocWriteResponse.Result.UPDATED)); // Invalidate to trigger the remover PlainActionFuture listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyId(createdApiKeys.get(2).getId(), false), - listener); + listener); assertThat(listener.get().getInvalidatedApiKeys().size(), is(1)); awaitApiKeysRemoverCompletion(); @@ -491,7 +488,7 @@ public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingRealmName("file"), getApiKeyResponseListener); Set expectedKeyIds = Sets.newHashSet(createdApiKeys.get(0).getId(), createdApiKeys.get(2).getId(), - createdApiKeys.get(3).getId()); + createdApiKeys.get(3).getId()); boolean apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover = false; for (ApiKey apiKey : getApiKeyResponseListener.get().getApiKeyInfos()) { assertThat(apiKey.getId(), is(in(expectedKeyIds))); @@ -513,7 +510,7 @@ public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() } } assertThat(getApiKeyResponseListener.get().getApiKeyInfos().length, - is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 3 : 2)); + is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 3 : 2)); } private void refreshSecurityIndex() throws Exception { @@ -526,8 +523,8 @@ private void refreshSecurityIndex() throws Exception { public void testActiveApiKeysWithNoExpirationNeverGetDeletedByRemover() throws Exception { List responses = createApiKeys(2, null); - Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); // trigger expired keys remover client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyId(responses.get(1).getId(), false), listener); @@ -540,25 +537,25 @@ public void testActiveApiKeysWithNoExpirationNeverGetDeletedByRemover() throws E client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingRealmName("file"), getApiKeyResponseListener); GetApiKeyResponse response = getApiKeyResponseListener.get(); verifyGetResponse(2, responses, response, Collections.singleton(responses.get(0).getId()), - Collections.singletonList(responses.get(1).getId())); + Collections.singletonList(responses.get(1).getId())); } public void testGetApiKeysForRealm() throws InterruptedException, ExecutionException { int noOfApiKeys = randomIntBetween(3, 5); List responses = createApiKeys(noOfApiKeys, null); - Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); - boolean invalidate= randomBoolean(); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); + boolean invalidate = randomBoolean(); List invalidatedApiKeyIds = null; Set expectedValidKeyIds = null; if (invalidate) { PlainActionFuture listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyId(responses.get(0).getId(), false), - listener); + listener); InvalidateApiKeyResponse invalidateResponse = listener.get(); invalidatedApiKeyIds = invalidateResponse.getInvalidatedApiKeys(); expectedValidKeyIds = responses.stream().filter(o -> !o.getId().equals(responses.get(0).getId())).map(o -> o.getId()) - .collect(Collectors.toSet()); + .collect(Collectors.toSet()); } else { invalidatedApiKeyIds = Collections.emptyList(); expectedValidKeyIds = responses.stream().map(o -> o.getId()).collect(Collectors.toSet()); @@ -568,36 +565,36 @@ public void testGetApiKeysForRealm() throws InterruptedException, ExecutionExcep client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingRealmName("file"), listener); GetApiKeyResponse response = listener.get(); verifyGetResponse(noOfApiKeys, responses, response, - expectedValidKeyIds, - invalidatedApiKeyIds); + expectedValidKeyIds, + invalidatedApiKeyIds); } public void testGetApiKeysForUser() throws Exception { int noOfApiKeys = randomIntBetween(3, 5); List responses = createApiKeys(noOfApiKeys, null); - Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); - client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingUserName(SecuritySettingsSource.TEST_SUPERUSER), listener); + client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingUserName(TEST_SUPERUSER), listener); GetApiKeyResponse response = listener.get(); verifyGetResponse(noOfApiKeys, responses, response, responses.stream().map(o -> o.getId()).collect(Collectors.toSet()), null); } public void testGetApiKeysForRealmAndUser() throws InterruptedException, ExecutionException { List responses = createApiKeys(1, null); - Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); - client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingRealmAndUserName("file", SecuritySettingsSource.TEST_SUPERUSER), - listener); + client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingRealmAndUserName("file", TEST_SUPERUSER), + listener); GetApiKeyResponse response = listener.get(); verifyGetResponse(1, responses, response, Collections.singleton(responses.get(0).getId()), null); } public void testGetApiKeysForApiKeyId() throws InterruptedException, ExecutionException { List responses = createApiKeys(1, null); - Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyId(responses.get(0).getId(), false), listener); GetApiKeyResponse response = listener.get(); @@ -607,9 +604,7 @@ public void testGetApiKeysForApiKeyId() throws InterruptedException, ExecutionEx public void testGetApiKeysForApiKeyName() throws InterruptedException, ExecutionException { final Map headers = Collections.singletonMap( "Authorization", - UsernamePasswordToken.basicAuthHeaderValue( - SecuritySettingsSource.TEST_SUPERUSER, - SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)); + basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING)); final int noOfApiKeys = randomIntBetween(1, 3); final List createApiKeyResponses1 = createApiKeys(noOfApiKeys, null); @@ -651,8 +646,8 @@ public void testGetApiKeysOwnedByCurrentAuthenticatedUser() throws InterruptedEx String userWithManageApiKeyRole = randomFrom("user_with_manage_api_key_role", "user_with_manage_own_api_key_role"); List userWithManageApiKeyRoleApiKeys = createApiKeys(userWithManageApiKeyRole, noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); - final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(userWithManageApiKeyRole, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + final Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(userWithManageApiKeyRole, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.forOwnedApiKeys(), listener); @@ -699,7 +694,7 @@ public void testGetApiKeysOwnedByRunAsUserWillNotWorkWhenAuthUserInfoIsGiven() t "user_with_run_as_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); PlainActionFuture listener = new PlainActionFuture<>(); @SuppressWarnings("unchecked") - final Tuple invalidRealmAndUserPair = randomFrom( + final Tuple invalidRealmAndUserPair = randomFrom( new Tuple<>("file", "user_with_run_as_role"), new Tuple<>("index", "user_with_manage_own_api_key_role"), new Tuple<>("index", "user_with_run_as_role")); @@ -713,15 +708,15 @@ public void testGetApiKeysOwnedByRunAsUserWillNotWorkWhenAuthUserInfoIsGiven() t public void testGetAllApiKeys() throws InterruptedException, ExecutionException { int noOfSuperuserApiKeys = randomIntBetween(3, 5); int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); - int noOfApiKeysForUserWithManageOwnApiKeyRole = randomIntBetween(3,7); + int noOfApiKeysForUserWithManageOwnApiKeyRole = randomIntBetween(3, 7); List defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null); List userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); List userWithManageOwnApiKeyRoleApiKeys = createApiKeys("user_with_manage_own_api_key_role", noOfApiKeysForUserWithManageOwnApiKeyRole, null, "monitor"); - final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue("user_with_manage_api_key_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + final Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue("user_with_manage_api_key_role", TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, new GetApiKeyRequest(), listener); GetApiKeyResponse response = listener.get(); @@ -729,15 +724,15 @@ public void testGetAllApiKeys() throws InterruptedException, ExecutionException List allApiKeys = new ArrayList<>(); Stream.of(defaultUserCreatedKeys, userWithManageApiKeyRoleApiKeys, userWithManageOwnApiKeyRoleApiKeys).forEach( allApiKeys::addAll); - verifyGetResponse(new String[]{SecuritySettingsSource.TEST_SUPERUSER, "user_with_manage_api_key_role", - "user_with_manage_own_api_key_role"}, totalApiKeys, allApiKeys, response, + verifyGetResponse(new String[] {TEST_SUPERUSER, "user_with_manage_api_key_role", + "user_with_manage_own_api_key_role" }, totalApiKeys, allApiKeys, response, allApiKeys.stream().map(o -> o.getId()).collect(Collectors.toSet()), null); } public void testGetAllApiKeysFailsForUserWithNoRoleOrRetrieveOwnApiKeyRole() throws InterruptedException, ExecutionException { int noOfSuperuserApiKeys = randomIntBetween(3, 5); int noOfApiKeysForUserWithManageApiKeyRole = randomIntBetween(3, 5); - int noOfApiKeysForUserWithManageOwnApiKeyRole = randomIntBetween(3,7); + int noOfApiKeysForUserWithManageOwnApiKeyRole = randomIntBetween(3, 7); List defaultUserCreatedKeys = createApiKeys(noOfSuperuserApiKeys, null); List userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); @@ -745,8 +740,8 @@ public void testGetAllApiKeysFailsForUserWithNoRoleOrRetrieveOwnApiKeyRole() thr noOfApiKeysForUserWithManageOwnApiKeyRole, null, "monitor"); final String withUser = randomFrom("user_with_manage_own_api_key_role", "user_with_no_api_key_role"); - final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(withUser, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + final Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(withUser, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, new GetApiKeyRequest(), listener); ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, () -> listener.actionGet()); @@ -760,8 +755,8 @@ public void testInvalidateApiKeysOwnedByCurrentAuthenticatedUser() throws Interr String userWithManageApiKeyRole = randomFrom("user_with_manage_api_key_role", "user_with_manage_own_api_key_role"); List userWithManageApiKeyRoleApiKeys = createApiKeys(userWithManageApiKeyRole, noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); - final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(userWithManageApiKeyRole, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + final Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(userWithManageApiKeyRole, TEST_PASSWORD_SECURE_STRING))); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.forOwnedApiKeys(), listener); @@ -806,7 +801,7 @@ public void testInvalidateApiKeysOwnedByRunAsUserWillNotWorkWhenAuthUserInfoIsGi "user_with_run_as_role", noOfApiKeysForUserWithManageApiKeyRole, null, "monitor"); PlainActionFuture listener = new PlainActionFuture<>(); @SuppressWarnings("unchecked") - final Tuple invalidRealmAndUserPair = randomFrom( + final Tuple invalidRealmAndUserPair = randomFrom( new Tuple<>("file", "user_with_run_as_role"), new Tuple<>("index", "user_with_manage_own_api_key_role"), new Tuple<>("index", "user_with_run_as_role")); @@ -819,7 +814,7 @@ public void testInvalidateApiKeysOwnedByRunAsUserWillNotWorkWhenAuthUserInfoIsGi public void testApiKeyAuthorizationApiKeyMustBeAbleToRetrieveItsOwnInformationButNotAnyOtherKeysCreatedBySameOwner() throws InterruptedException, ExecutionException { - List responses = createApiKeys(SecuritySettingsSource.TEST_SUPERUSER,2, null, (String[]) null); + List responses = createApiKeys(TEST_SUPERUSER, 2, null, (String[]) null); final String base64ApiKeyKeyValue = Base64.getEncoder().encodeToString( (responses.get(0).getId() + ":" + responses.get(0).getKey().toString()).getBytes(StandardCharsets.UTF_8)); Client client = client().filterWithHeader(Map.of("Authorization", "ApiKey " + base64ApiKeyKeyValue)); @@ -833,19 +828,18 @@ public void testApiKeyAuthorizationApiKeyMustBeAbleToRetrieveItsOwnInformationBu client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.usingApiKeyId(responses.get(1).getId(), randomBoolean()), failureListener); ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, () -> failureListener.actionGet()); - assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", SecuritySettingsSource.TEST_SUPERUSER, + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", TEST_SUPERUSER, responses.get(0).getId()); final PlainActionFuture failureListener1 = new PlainActionFuture<>(); client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.forOwnedApiKeys(), failureListener1); ese = expectThrows(ElasticsearchSecurityException.class, () -> failureListener1.actionGet()); - assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", SecuritySettingsSource.TEST_SUPERUSER, - responses.get(0).getId()); + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", TEST_SUPERUSER, responses.get(0).getId()); } public void testApiKeyWithManageOwnPrivilegeIsAbleToInvalidateItselfButNotAnyOtherKeysCreatedBySameOwner() throws InterruptedException, ExecutionException { - List responses = createApiKeys(SecuritySettingsSource.TEST_SUPERUSER, 2, null, "manage_own_api_key"); + List responses = createApiKeys(TEST_SUPERUSER, 2, null, "manage_own_api_key"); final String base64ApiKeyKeyValue = Base64.getEncoder().encodeToString( (responses.get(0).getId() + ":" + responses.get(0).getKey().toString()).getBytes(StandardCharsets.UTF_8)); Client client = client().filterWithHeader(Map.of("Authorization", "ApiKey " + base64ApiKeyKeyValue)); @@ -855,14 +849,13 @@ public void testApiKeyWithManageOwnPrivilegeIsAbleToInvalidateItselfButNotAnyOth client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyId(responses.get(1).getId(), randomBoolean()), failureListener); ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, () -> failureListener.actionGet()); - assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/invalidate", SecuritySettingsSource.TEST_SUPERUSER, + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/invalidate", TEST_SUPERUSER, responses.get(0).getId()); final PlainActionFuture failureListener1 = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.forOwnedApiKeys(), failureListener1); ese = expectThrows(ElasticsearchSecurityException.class, () -> failureListener1.actionGet()); - assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/invalidate", SecuritySettingsSource.TEST_SUPERUSER, - responses.get(0).getId()); + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/invalidate", TEST_SUPERUSER, responses.get(0).getId()); PlainActionFuture listener = new PlainActionFuture<>(); client.execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyId(responses.get(0).getId(), randomBoolean()), @@ -877,8 +870,8 @@ public void testApiKeyWithManageOwnPrivilegeIsAbleToInvalidateItselfButNotAnyOth public void testDerivedKeys() throws ExecutionException, InterruptedException { Client client = client().filterWithHeader(Collections.singletonMap("Authorization", - UsernamePasswordToken.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, - SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + basicAuthHeaderValue(TEST_SUPERUSER, + TEST_PASSWORD_SECURE_STRING))); final CreateApiKeyResponse response = new CreateApiKeyRequestBuilder(client) .setName("key-1") .setRoleDescriptors(Collections.singletonList( @@ -897,7 +890,7 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { final String expectedMessage = "creating derived api keys requires an explicit role descriptor that is empty"; final IllegalArgumentException e1 = expectThrows(IllegalArgumentException.class, - () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-2").get()); + () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-2").get()); assertThat(e1.getMessage(), containsString(expectedMessage)); final IllegalArgumentException e2 = expectThrows(IllegalArgumentException.class, @@ -908,14 +901,14 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { final IllegalArgumentException e3 = expectThrows(IllegalArgumentException.class, () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-4") .setRoleDescriptors(Collections.singletonList( - new RoleDescriptor("role", new String[] {"manage_own_api_key"}, null, null) + new RoleDescriptor("role", new String[] { "manage_own_api_key" }, null, null) )).get()); assertThat(e3.getMessage(), containsString(expectedMessage)); final List roleDescriptors = randomList(2, 10, () -> new RoleDescriptor("role", null, null, null)); roleDescriptors.set(randomInt(roleDescriptors.size() - 1), - new RoleDescriptor("role", new String[] {"manage_own_api_key"}, null, null)); + new RoleDescriptor("role", new String[] { "manage_own_api_key" }, null, null)); final IllegalArgumentException e4 = expectThrows(IllegalArgumentException.class, () -> new CreateApiKeyRequestBuilder(clientKey1).setName("key-5") @@ -945,9 +938,8 @@ public void testAuthenticationReturns429WhenThreadPoolIsSaturated() throws IOExc final ThreadPool threadPool = internalCluster().getInstance(ThreadPool.class, nodeName); final RoleDescriptor descriptor = new RoleDescriptor("auth_only", new String[] { }, null, null); - final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", - UsernamePasswordToken.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, - SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + final Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); final CreateApiKeyResponse createApiKeyResponse = new CreateApiKeyRequestBuilder(client) .setName("auth only key") .setRoleDescriptors(Collections.singletonList(descriptor)) @@ -1100,9 +1092,8 @@ public void testSecurityIndexStateChangeWillInvalidateApiKeyCaches() throws Exce } private Tuple createApiKeyAndAuthenticateWithIt() throws IOException { - Client client = client().filterWithHeader(Collections.singletonMap("Authorization", - UsernamePasswordToken.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, - SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); final CreateApiKeyResponse createApiKeyResponse = new CreateApiKeyRequestBuilder(client) .setName("test key") @@ -1125,7 +1116,7 @@ private void assertApiKeyNotCreated(Client client, String keyName) throws Execut private void verifyGetResponse(int expectedNumberOfApiKeys, List responses, GetApiKeyResponse response, Set validApiKeyIds, List invalidatedApiKeyIds) { - verifyGetResponse(SecuritySettingsSource.TEST_SUPERUSER, expectedNumberOfApiKeys, responses, response, validApiKeyIds, + verifyGetResponse(TEST_SUPERUSER, expectedNumberOfApiKeys, responses, response, validApiKeyIds, invalidatedApiKeyIds); } @@ -1138,9 +1129,9 @@ private void verifyGetResponse(String[] user, int expectedNumberOfApiKeys, List< GetApiKeyResponse response, Set validApiKeyIds, List invalidatedApiKeyIds) { assertThat(response.getApiKeyInfos().length, equalTo(expectedNumberOfApiKeys)); List expectedIds = responses.stream().filter(o -> validApiKeyIds.contains(o.getId())).map(o -> o.getId()) - .collect(Collectors.toList()); + .collect(Collectors.toList()); List actualIds = Arrays.stream(response.getApiKeyInfos()).filter(o -> o.isInvalidated() == false).map(o -> o.getId()) - .collect(Collectors.toList()); + .collect(Collectors.toList()); assertThat(actualIds, containsInAnyOrder(expectedIds.toArray(Strings.EMPTY_ARRAY))); List expectedNames = responses.stream().filter(o -> validApiKeyIds.contains(o.getId())).map(o -> o.getName()) .collect(Collectors.toList()); @@ -1160,30 +1151,30 @@ private void verifyGetResponse(String[] user, int expectedNumberOfApiKeys, List< } private List createApiKeys(int noOfApiKeys, TimeValue expiration) { - return createApiKeys(SecuritySettingsSource.TEST_SUPERUSER, noOfApiKeys, expiration, "monitor"); + return createApiKeys(TEST_SUPERUSER, noOfApiKeys, expiration, "monitor"); } private List createApiKeys(String user, int noOfApiKeys, TimeValue expiration, String... clusterPrivileges) { - final Map headers = Collections.singletonMap( - "Authorization", UsernamePasswordToken.basicAuthHeaderValue(user, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)); + final Map headers = Collections.singletonMap("Authorization", + basicAuthHeaderValue(user, TEST_PASSWORD_SECURE_STRING)); return createApiKeys(headers, noOfApiKeys, expiration, clusterPrivileges); } private List createApiKeys(String owningUser, String authenticatingUser, - int noOfApiKeys, TimeValue expiration, String... clusterPrivileges) { - final Map headers = Map.of("Authorization", - UsernamePasswordToken.basicAuthHeaderValue(authenticatingUser, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING), + int noOfApiKeys, TimeValue expiration, String... clusterPrivileges) { + final Map headers = Map.of( + "Authorization", basicAuthHeaderValue(authenticatingUser, TEST_PASSWORD_SECURE_STRING), "es-security-runas-user", owningUser); return createApiKeys(headers, noOfApiKeys, expiration, clusterPrivileges); } private List createApiKeys(Map headers, - int noOfApiKeys, TimeValue expiration, String... clusterPrivileges) { + int noOfApiKeys, TimeValue expiration, String... clusterPrivileges) { return createApiKeys(headers, noOfApiKeys, "test-key-", expiration, clusterPrivileges); } - private List createApiKeys(Map headers, - int noOfApiKeys, String namePrefix, TimeValue expiration, String... clusterPrivileges) { + private List createApiKeys(Map headers, int noOfApiKeys, String namePrefix, + TimeValue expiration, String... clusterPrivileges) { List responses = new ArrayList<>(); for (int i = 0; i < noOfApiKeys; i++) { final RoleDescriptor descriptor = new RoleDescriptor("role", clusterPrivileges, null, null); @@ -1210,27 +1201,30 @@ private void createUserWithRunAsRole() throws ExecutionException, InterruptedExc putUserRequest.roles("run_as_role"); putUserRequest.passwordHash(SecuritySettingsSource.TEST_PASSWORD_HASHED.toCharArray()); PlainActionFuture listener = new PlainActionFuture<>(); - final Client client = client().filterWithHeader(Map.of("Authorization", - UsernamePasswordToken.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, - SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + final Client client = client().filterWithHeader( + Map.of("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); client.execute(PutUserAction.INSTANCE, putUserRequest, listener); final PutUserResponse putUserResponse = listener.get(); assertTrue(putUserResponse.created()); } private Client getClientForRunAsUser() { - return client().filterWithHeader(Map.of("Authorization", UsernamePasswordToken - .basicAuthHeaderValue("user_with_run_as_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING), + return client().filterWithHeader(Map.of( + "Authorization", basicAuthHeaderValue("user_with_run_as_role", TEST_PASSWORD_SECURE_STRING), "es-security-runas-user", "user_with_manage_own_api_key_role")); } private void assertErrorMessage(final ElasticsearchSecurityException ese, String action, String userName, String apiKeyId) { - assertThat(ese.getMessage(), - is("action [" + action + "] is unauthorized for API key id [" + apiKeyId + "] of user [" + userName + "]")); + assertThat(ese, throwableWithMessage( + containsString("action [" + action + "] is unauthorized for API key id [" + apiKeyId + "] of user [" + userName + "]"))); + assertThat(ese, throwableWithMessage(containsString(", this action is granted by the privileges ["))); + assertThat(ese, throwableWithMessage(containsString("manage_api_key,manage_security,all]"))); } private void assertErrorMessage(final ElasticsearchSecurityException ese, String action, String userName) { - assertThat(ese.getMessage(), - is("action [" + action + "] is unauthorized for user [" + userName + "]")); + assertThat(ese, throwableWithMessage( + containsString("action [" + action + "] is unauthorized for user [" + userName + "]"))); + assertThat(ese, throwableWithMessage(containsString(", this action is granted by the privileges ["))); + assertThat(ese, throwableWithMessage(containsString("manage_api_key,manage_security,all]"))); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index ba110c185eeee..60af63f313c49 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -210,7 +210,7 @@ public void authorize(final Authentication authentication, final String action, final ElasticsearchSecurityException operatorException = operatorPrivilegesService.check(action, originalRequest, threadContext); if (operatorException != null) { - listener.onFailure(denialException(authentication, action, operatorException)); + listener.onFailure(denialException(authentication, action, originalRequest, operatorException)); return; } @@ -250,12 +250,12 @@ private void maybeAuthorizeRunAs(final RequestInfo requestInfo, final String req auditTrail.runAsDenied(requestId, authentication, action, request, authzInfo.getAuthenticatedUserAuthorizationInfo()); } - listener.onFailure(denialException(authentication, action, null)); + listener.onFailure(denialException(authentication, action, request, null)); } }, e -> { auditTrail.runAsDenied(requestId, authentication, action, request, authzInfo.getAuthenticatedUserAuthorizationInfo()); - listener.onFailure(denialException(authentication, action, null)); + listener.onFailure(denialException(authentication, action, request, null)); }), threadContext); authorizeRunAs(requestInfo, authzInfo, runAsListener); } else { @@ -290,7 +290,7 @@ private void authorizeAction(final RequestInfo requestInfo, final String request if (e instanceof IndexNotFoundException) { listener.onFailure(e); } else { - listener.onFailure(denialException(authentication, action, e)); + listener.onFailure(denialException(authentication, action, request, e)); } })); }); @@ -302,7 +302,7 @@ private void authorizeAction(final RequestInfo requestInfo, final String request } else { logger.warn("denying access as action [{}] is not an index or cluster action", action); auditTrail.accessDenied(requestId, authentication, action, request, authzInfo); - listener.onFailure(denialException(authentication, action, null)); + listener.onFailure(denialException(authentication, action, request, null)); } } @@ -417,7 +417,7 @@ private void authorizeSystemUser(final Authentication authentication, final Stri listener.onResponse(null); } else { auditTrail.accessDenied(requestId, authentication, action, request, SYSTEM_AUTHZ_INFO); - listener.onFailure(denialException(authentication, action, null)); + listener.onFailure(denialException(authentication, action, request, null)); } } @@ -436,13 +436,13 @@ private TransportRequest maybeUnwrapRequest(Authentication authentication, Trans IllegalStateException cause = new IllegalStateException("originalRequest is not a proxy request: [" + originalRequest + "] but action: [" + action + "] is a proxy action"); auditTrail.accessDenied(requestId, authentication, action, request, EmptyAuthorizationInfo.INSTANCE); - throw denialException(authentication, action, cause); + throw denialException(authentication, action, request, cause); } if (TransportActionProxy.isProxyRequest(originalRequest) && TransportActionProxy.isProxyAction(action) == false) { IllegalStateException cause = new IllegalStateException("originalRequest is a proxy request for: [" + request + "] but action: [" + action + "] isn't"); auditTrail.accessDenied(requestId, authentication, action, request, EmptyAuthorizationInfo.INSTANCE); - throw denialException(authentication, action, cause); + throw denialException(authentication, action, request, cause); } } return request; @@ -468,7 +468,7 @@ private void authorizeRunAs(final RequestInfo requestInfo, final AuthorizationIn * and then checks whether that action is allowed on the targeted index. Items * that fail this checks are {@link BulkItemRequest#abort(String, Exception) * aborted}, with an - * {@link #denialException(Authentication, String, Exception) access + * {@link #denialException(Authentication, String, TransportRequest, Exception) access * denied} exception. Because a shard level request is for exactly 1 index, and * there are a small number of possible item {@link DocWriteRequest.OpType * types}, the number of distinct authorization checks that need to be performed @@ -542,7 +542,7 @@ private void authorizeBulkItems(RequestInfo requestInfo, AuthorizationInfo authz if (indexAccessControl == null || indexAccessControl.isGranted() == false) { auditTrail.explicitIndexAccessEvent(requestId, AuditLevel.ACCESS_DENIED, authentication, itemAction, resolvedIndex, item.getClass().getSimpleName(), request.remoteAddress(), authzInfo); - item.abort(resolvedIndex, denialException(authentication, itemAction, + item.abort(resolvedIndex, denialException(authentication, itemAction, request, AuthorizationEngine.IndexAuthorizationResult.getFailureDescription(List.of(resolvedIndex)), null)); } else if (audit.get()) { auditTrail.explicitIndexAccessEvent(requestId, AuditLevel.ACCESS_GRANTED, authentication, itemAction, @@ -603,12 +603,13 @@ private void putTransientIfNonExisting(String key, Object value) { } } - private ElasticsearchSecurityException denialException(Authentication authentication, String action, Exception cause) { - return denialException(authentication, action, null, cause); + private ElasticsearchSecurityException denialException(Authentication authentication, String action, TransportRequest request, + Exception cause) { + return denialException(authentication, action, request, null, cause); } - private ElasticsearchSecurityException denialException(Authentication authentication, String action, @Nullable String context, - Exception cause) { + private ElasticsearchSecurityException denialException(Authentication authentication, String action, TransportRequest request, + @Nullable String context, Exception cause) { final User authUser = authentication.getUser().authenticatedUser(); // Special case for anonymous user if (isAnonymousEnabled && anonymousUser.equals(authUser)) { @@ -634,11 +635,16 @@ private ElasticsearchSecurityException denialException(Authentication authentica message = message + " " + context; } - if(isIndexAction(action)) { + if (isIndexAction(action)) { final Collection privileges = IndexPrivilege.findPrivilegesThatGrant(action); if (privileges != null && privileges.size() > 0) { message = message + ", this action is granted by the privileges [" + collectionToCommaDelimitedString(privileges) + "]"; } + } else if (ClusterPrivilegeResolver.isClusterAction(action)) { + final Collection privileges = ClusterPrivilegeResolver.findPrivilegesThatGrant(action, request, authentication); + if (privileges != null && privileges.size() > 0) { + message = message + ", this action is granted by the privileges [" + collectionToCommaDelimitedString(privileges) + "]"; + } } logger.debug(message); @@ -689,7 +695,8 @@ private void handleFailure(boolean audit, @Nullable String context, @Nullable Ex auditTrailService.get().accessDenied(requestId, requestInfo.getAuthentication(), requestInfo.getAction(), requestInfo.getRequest(), authzInfo); } - failureConsumer.accept(denialException(requestInfo.getAuthentication(), requestInfo.getAction(), context, e)); + failureConsumer.accept( + denialException(requestInfo.getAuthentication(), requestInfo.getAction(), requestInfo.getRequest(), context, e)); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index e39f3b778bb03..03784207a7ff6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -106,6 +106,8 @@ import org.elasticsearch.xpack.core.search.action.ClosePointInTimeRequest; import org.elasticsearch.xpack.core.search.action.OpenPointInTimeAction; import org.elasticsearch.xpack.core.search.action.OpenPointInTimeRequest; +import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; +import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction; import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; @@ -669,9 +671,13 @@ public void testUnknownRoleCausesDenial() throws IOException { final Authentication authentication = createAuthentication(new User("test user", "non-existent-role")); final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); mockEmptyMetadata(); - assertThrowsAuthorizationException( - () -> authorize(authentication, action, request), - action, "test user"); + + ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, + () -> authorize(authentication, action, request)); + assertThat(securityException, + throwableWithMessage(containsString("[" + action + "] is unauthorized for user [test user] on indices ["))); + assertThat(securityException, throwableWithMessage(containsString("this action is granted by the privileges [read,all]"))); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(action), eq(request), authzInfoRoles(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); } @@ -705,9 +711,12 @@ public void testThatRoleWithNoIndicesIsDenied() throws IOException { roleMap.put("no_indices", role); mockEmptyMetadata(); - assertThrowsAuthorizationException( - () -> authorize(authentication, action, request), - action, "test user"); + ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, + () -> authorize(authentication, action, request)); + assertThat(securityException, + throwableWithMessage(containsString("[" + action + "] is unauthorized for user [test user] on indices ["))); + assertThat(securityException, throwableWithMessage(containsString("this action is granted by the privileges [read,all]"))); + verify(auditTrail).accessDenied(eq(requestId), eq(authentication), eq(action), eq(request), authzInfoRoles(new String[]{role.getName()})); verifyNoMoreInteractions(auditTrail); @@ -944,6 +953,61 @@ public void testDenialErrorMessagesForBulkIngest() throws Exception { assertThat(response.getResponses()[2].getFailureMessage(), containsString("[delete,write,all]") ); } + public void testDenialErrorMessagesForClusterHealthAction() throws IOException { + RoleDescriptor role = new RoleDescriptor("role_" + randomAlphaOfLengthBetween(3, 6), + new String[0], // no cluster privileges + new IndicesPrivileges[] { IndicesPrivileges.builder().indices("index-*").privileges("all").build() } , null); + User user = new User(randomAlphaOfLengthBetween(6, 8), role.getName()); + final Authentication authentication = createAuthentication(user); + roleMap.put(role.getName(), role); + + AuditUtil.getOrGenerateRequestId(threadContext); + + TransportRequest request = new ClusterHealthRequest(); + + ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, + () -> authorize(authentication, ClusterHealthAction.NAME, request)); + assertThat(securityException, throwableWithMessage( + containsString("[" + ClusterHealthAction.NAME + "] is unauthorized for user [" + user.principal() + "]"))); + assertThat(securityException, + throwableWithMessage(containsString("this action is granted by the privileges [monitor,manage,all]"))); + } + + public void testDenialErrorMessagesForInvalidateApiKeyAction() throws IOException { + RoleDescriptor role = new RoleDescriptor("role_" + randomAlphaOfLengthBetween(3, 6), + new String[0], // no cluster privileges + new IndicesPrivileges[]{IndicesPrivileges.builder().indices("index-*").privileges("all").build()}, null); + User user = new User(randomAlphaOfLengthBetween(6, 8), role.getName()); + final Authentication authentication = createAuthentication(user); + roleMap.put(role.getName(), role); + + AuditUtil.getOrGenerateRequestId(threadContext); + + // Own API Key + { + TransportRequest request = new InvalidateApiKeyRequest(null, null, null, true, null); + + ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, + () -> authorize(authentication, InvalidateApiKeyAction.NAME, request)); + assertThat(securityException, throwableWithMessage( + containsString("[" + InvalidateApiKeyAction.NAME + "] is unauthorized for user [" + user.principal() + "]"))); + assertThat(securityException, throwableWithMessage( + containsString("this action is granted by the privileges [manage_own_api_key,manage_api_key,manage_security,all]"))); + } + + // All API Keys + { + TransportRequest request = new InvalidateApiKeyRequest(null, null, null, false, null); + + ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, + () -> authorize(authentication, InvalidateApiKeyAction.NAME, request)); + assertThat(securityException, throwableWithMessage( + containsString("[" + InvalidateApiKeyAction.NAME + "] is unauthorized for user [" + user.principal() + "]"))); + assertThat(securityException, throwableWithMessage( + containsString("this action is granted by the privileges [manage_api_key,manage_security,all]"))); + } + } + public void testDenialForAnonymousUser() throws IOException { TransportRequest request = new GetIndexRequest().indices("b"); ClusterState state = mockEmptyMetadata(); diff --git a/x-pack/plugin/sql/qa/server/security/src/test/java/org/elasticsearch/xpack/sql/qa/security/JdbcSecurityIT.java b/x-pack/plugin/sql/qa/server/security/src/test/java/org/elasticsearch/xpack/sql/qa/security/JdbcSecurityIT.java index a19e32ea25e1b..9c04203709b9c 100644 --- a/x-pack/plugin/sql/qa/server/security/src/test/java/org/elasticsearch/xpack/sql/qa/security/JdbcSecurityIT.java +++ b/x-pack/plugin/sql/qa/server/security/src/test/java/org/elasticsearch/xpack/sql/qa/security/JdbcSecurityIT.java @@ -241,7 +241,7 @@ public void checkNoMonitorMain(String user) throws Exception { private void expectUnauthorized(String action, String user, ThrowingRunnable r) { SQLInvalidAuthorizationSpecException e = expectThrows(SQLInvalidAuthorizationSpecException.class, r); - assertEquals("action [" + action + "] is unauthorized for user [" + user + "]", e.getMessage()); + assertThat(e.getMessage(), containsString("action [" + action + "] is unauthorized for user [" + user + "]")); } } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/api_key/11_invalidation.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/api_key/11_invalidation.yml index b1bb2762f8432..06f75cbb8ac16 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/api_key/11_invalidation.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/api_key/11_invalidation.yml @@ -126,7 +126,7 @@ teardown: "username": "api_key_manager" } - match: { "error.type": "security_exception" } - - match: { "error.reason": "action [cluster:admin/xpack/security/api_key/invalidate] is unauthorized for user [api_key_user_1]" } + - match: { "error.reason": "action [cluster:admin/xpack/security/api_key/invalidate] is unauthorized for user [api_key_user_1], this action is granted by the privileges [manage_api_key,manage_security,all]" } - do: headers: @@ -189,7 +189,7 @@ teardown: "realm_name": "default_native" } - match: { "error.type": "security_exception" } - - match: { "error.reason": "action [cluster:admin/xpack/security/api_key/invalidate] is unauthorized for user [api_key_user_1]" } + - match: { "error.reason": "action [cluster:admin/xpack/security/api_key/invalidate] is unauthorized for user [api_key_user_1], this action is granted by the privileges [manage_api_key,manage_security,all]" } - do: headers: