From 926eb91e725ff1ca7a46daa57fe678b5aac7ac80 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 3 Feb 2021 15:56:49 +1100 Subject: [PATCH] [Backport] Add more context to cluster access denied messages (#68263) In #60357 we improved the error message when access to perform an action on an index was denied by including the index name and the privileges that would grant the action. This commit extends the second part of that change (the list of privileges that would resolve the problem) to situations when a cluster action is denied. This implementation for cluster privileges is slightly more complex than that of index privileges because cluster privileges can be dependent on parameters in the request, not just the action name. For example, "manage_own_api_key" should be suggested as a matching privilege when a user attempts to create an API key, or delete their own API key, but should not be suggested when that same user attempts to delete another user's API key. Backport of: #66900 --- .../authz/permission/ClusterPermission.java | 9 +- .../privilege/ActionClusterPrivilege.java | 7 + .../privilege/ClusterPrivilegeResolver.java | 46 +++- .../ManageOwnApiKeyClusterPrivilege.java | 8 + .../privilege/NamedClusterPrivilege.java | 13 + .../permission/ClusterPermissionTests.java | 31 +++ .../ClusterPrivilegeResolverTests.java | 39 +++ .../security/authc/ApiKeyIntegTests.java | 233 +++++++++--------- .../security/authz/AuthorizationService.java | 39 +-- .../authz/AuthorizationServiceTests.java | 85 ++++++- .../xpack/sql/qa/security/JdbcSecurityIT.java | 2 +- .../test/api_key/11_invalidation.yml | 4 +- 12 files changed, 356 insertions(+), 160 deletions(-) create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolverTests.java 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 4bbd7f498766a..cb3ac74d59746 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 @@ -207,9 +207,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 f829cb410db21..bf1bc641a2999 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 384a57f845c67..bea2757056941 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.SimulatePipelineAction; import org.elasticsearch.common.Strings; import org.elasticsearch.common.util.set.Sets; +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,15 +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.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +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 @@ -153,8 +160,7 @@ public class ClusterPrivilegeResolver { public static final NamedClusterPrivilege MANAGE_LOGSTASH_PIPELINES = new ActionClusterPrivilege("manage_logstash_pipelines", Collections.unmodifiableSet(Sets.newHashSet("cluster:admin/logstash/pipeline/*"))); - private static final Map VALUES = Collections.unmodifiableMap( - Stream.of( + private static final Map VALUES = sortByAccessLevel(Arrays.asList( NONE, ALL, MONITOR, @@ -193,7 +199,7 @@ public class ClusterPrivilegeResolver { DELEGATE_PKI, MANAGE_OWN_API_KEY, MANAGE_ENRICH, - MANAGE_LOGSTASH_PIPELINES).collect(Collectors.toMap(cp -> cp.name(), cp -> cp))); + MANAGE_LOGSTASH_PIPELINES)); /** * Resolves a {@link NamedClusterPrivilege} from a given name if it exists. @@ -234,4 +240,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 Collections.unmodifiableList(VALUES.entrySet().stream() + .filter(e -> e.getValue().permission().check(action, request, authentication)) + .map(Map.Entry::getKey) + .collect(Collectors.toList())); + } + + /** + * 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 142c527de80c5..a517eb438ba9e 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 @@ -27,7 +27,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 @@ -40,6 +43,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 1d0d70120d5d2..ece76e602f09f 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 @@ -8,6 +8,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 @@ -15,4 +16,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 947540a2016db..22d363b8857ad 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 @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.core.security.authz.permission; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.TransportRequest; @@ -15,13 +16,16 @@ 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.Collections; +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 +234,33 @@ public void testClusterPermissionSubsetIsImpliedByAllClusterPermission() { assertThat(allClusterPermission.implies(otherClusterPermission), is(true)); } + public void testImpliesOnSecurityPrivilegeHierarchy() { + final List highToLow = CollectionUtils.arrayAsArrayList( + 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..ed0dc25643e53 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolverTests.java @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.authz.privilege; + +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.test.ESTestCase; + +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 = CollectionUtils.arrayAsArrayList( + 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 07d6f8ce6739d..d88b6ec0cc5f0 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 @@ -38,7 +38,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; @@ -53,7 +52,6 @@ import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyResponse; 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.core.security.client.SecurityClient; import org.elasticsearch.xpack.security.transport.filter.IPFilter; @@ -81,6 +79,10 @@ import java.util.stream.Stream; import static org.elasticsearch.index.mapper.MapperService.SINGLE_MAPPING_NAME; +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; @@ -141,7 +143,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" + @@ -162,13 +164,12 @@ private void awaitApiKeysRemoverCompletion() throws Exception { } } - public void testCreateApiKey() { + 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))); SecurityClient securityClient = new SecurityClient(client); final CreateApiKeyResponse response = securityClient.prepareCreateApiKey() .setName("test key") @@ -195,6 +196,7 @@ public void testCreateApiKey() { // use the first ApiKey for authorized action final String base64ApiKeyKeyValue = Base64.getEncoder().encodeToString( (response.getId() + ":" + response.getKey().toString()).getBytes(StandardCharsets.UTF_8)); + // Assert that we can authenticate with the API KEY ClusterHealthResponse healthResponse = client() .filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue)) .admin() @@ -219,9 +221,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))); SecurityClient securityClient = new SecurityClient(client); final CreateApiKeyResponse response = securityClient.prepareCreateApiKey().setName(keyName).setExpiration(null) .setRoleDescriptors(Collections.singletonList(descriptor)).get(); @@ -236,9 +238,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))); SecurityClient securityClient = new SecurityClient(client); final ActionRequestValidationException e = expectThrows(ActionRequestValidationException.class, () -> securityClient.prepareCreateApiKey().get()); @@ -248,8 +249,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))); SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); securityClient.invalidateApiKey(InvalidateApiKeyRequest.usingRealmName("file"), listener); @@ -260,8 +261,8 @@ 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))); SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); securityClient.invalidateApiKey(InvalidateApiKeyRequest.usingUserName(SecuritySettingsSource.TEST_SUPERUSER), listener); @@ -271,8 +272,8 @@ public void testInvalidateApiKeysForUser() throws Exception { 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))); SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); securityClient.invalidateApiKey(InvalidateApiKeyRequest.usingRealmAndUserName("file", SecuritySettingsSource.TEST_SUPERUSER), @@ -283,8 +284,8 @@ public void testInvalidateApiKeysForRealmAndUser() throws InterruptedException, 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))); SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); securityClient.invalidateApiKey(InvalidateApiKeyRequest.usingApiKeyId(responses.get(0).getId(), false), listener); @@ -294,8 +295,8 @@ 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))); SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); securityClient.invalidateApiKey(InvalidateApiKeyRequest.usingApiKeyName(responses.get(0).getName(), false), listener); @@ -330,8 +331,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))); SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); securityClient.invalidateApiKey(InvalidateApiKeyRequest.usingApiKeyId(apiKey1.v1(), false), listener); @@ -362,15 +363,14 @@ 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); @@ -402,11 +402,10 @@ 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))); securityClient = new SecurityClient(client); // invalidate API key to trigger remover @@ -432,7 +431,7 @@ public void testInvalidatedApiKeysDeletedByRemover() throws Exception { } } assertThat(getApiKeyResponseListener.get().getApiKeyInfos().length, - is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 1 : 0)); + is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 1 : 0)); } private Client waitForExpiredApiKeysRemoverTriggerReadyAndGetClient() throws Exception { @@ -444,7 +443,7 @@ private Client waitForExpiredApiKeysRemoverTriggerReadyAndGetClient() throws Exc if (apiKeyService.lastTimeWhenApiKeysRemoverWasTriggered() > apiKeyLastTrigger) { nodeWithMostRecentRun = nodeName; apiKeyLastTrigger = apiKeyService.lastTimeWhenApiKeysRemoverWasTriggered(); - } + } } } final ThreadPool threadPool = internalCluster().getInstance(ThreadPool.class, nodeWithMostRecentRun); @@ -457,8 +456,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); @@ -475,10 +473,10 @@ public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() Instant dayBefore = created.minus(1L, ChronoUnit.DAYS); assertTrue(Instant.now().isAfter(dayBefore)); UpdateResponse expirationDateUpdatedResponse = client - .prepareUpdate(SECURITY_MAIN_ALIAS, SINGLE_MAPPING_NAME, createdApiKeys.get(0).getId()) - .setDoc("expiration_time", dayBefore.toEpochMilli()) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .get(); + .prepareUpdate(SECURITY_MAIN_ALIAS, SINGLE_MAPPING_NAME, 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 @@ -486,9 +484,9 @@ public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() Instant weekBefore = created.minus(8L, ChronoUnit.DAYS); assertTrue(Instant.now().isAfter(weekBefore)); expirationDateUpdatedResponse = client.prepareUpdate(SECURITY_MAIN_ALIAS, SINGLE_MAPPING_NAME, 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 @@ -505,7 +503,7 @@ public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() securityClient.getApiKey(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))); @@ -527,7 +525,7 @@ public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() } } assertThat(getApiKeyResponseListener.get().getApiKeyInfos().length, - is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 3 : 2)); + is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 3 : 2)); } private void refreshSecurityIndex() throws Exception { @@ -540,8 +538,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))); SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); // trigger expired keys remover @@ -555,16 +553,16 @@ public void testActiveApiKeysWithNoExpirationNeverGetDeletedByRemover() throws E securityClient.getApiKey(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))); + Client client = client().filterWithHeader( + Collections.singletonMap("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING))); SecurityClient securityClient = new SecurityClient(client); - boolean invalidate= randomBoolean(); + boolean invalidate = randomBoolean(); List invalidatedApiKeyIds = null; Set expectedValidKeyIds = null; if (invalidate) { @@ -573,7 +571,7 @@ public void testGetApiKeysForRealm() throws InterruptedException, ExecutionExcep 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()); @@ -583,38 +581,37 @@ public void testGetApiKeysForRealm() throws InterruptedException, ExecutionExcep securityClient.getApiKey(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))); SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); - securityClient.getApiKey(GetApiKeyRequest.usingUserName(SecuritySettingsSource.TEST_SUPERUSER), listener); + securityClient.getApiKey(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))); SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); - securityClient.getApiKey(GetApiKeyRequest.usingRealmAndUserName("file", SecuritySettingsSource.TEST_SUPERUSER), - listener); + securityClient.getApiKey(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))); SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); securityClient.getApiKey(GetApiKeyRequest.usingApiKeyId(responses.get(0).getId(), false), listener); @@ -625,9 +622,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); @@ -670,8 +665,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))); SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); @@ -719,7 +714,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")); @@ -733,15 +728,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))); final SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); securityClient.getApiKey(new GetApiKeyRequest(), listener); @@ -750,15 +745,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"); @@ -766,8 +761,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))); final SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); securityClient.getApiKey(new GetApiKeyRequest(), listener); @@ -782,8 +777,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))); SecurityClient securityClient = new SecurityClient(client); PlainActionFuture listener = new PlainActionFuture<>(); @@ -830,7 +825,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")); @@ -843,7 +838,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(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue)); @@ -858,19 +853,18 @@ public void testApiKeyAuthorizationApiKeyMustBeAbleToRetrieveItsOwnInformationBu securityClient.getApiKey(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<>(); securityClient.getApiKey(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(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue)); @@ -881,14 +875,13 @@ public void testApiKeyWithManageOwnPrivilegeIsAbleToInvalidateItselfButNotAnyOth securityClient.invalidateApiKey(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<>(); securityClient.invalidateApiKey(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<>(); securityClient.invalidateApiKey(InvalidateApiKeyRequest.usingApiKeyId(responses.get(0).getId(), randomBoolean()), @@ -902,9 +895,9 @@ public void testApiKeyWithManageOwnPrivilegeIsAbleToInvalidateItselfButNotAnyOth } public void testDerivedKeys() throws ExecutionException, InterruptedException { - final 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 SecurityClient(client) .prepareCreateApiKey() @@ -926,7 +919,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, - () -> clientKey1.prepareCreateApiKey().setName("key-2").get()); + () -> clientKey1.prepareCreateApiKey().setName("key-2").get()); assertThat(e1.getMessage(), containsString(expectedMessage)); final IllegalArgumentException e2 = expectThrows(IllegalArgumentException.class, @@ -937,14 +930,14 @@ public void testDerivedKeys() throws ExecutionException, InterruptedException { final IllegalArgumentException e3 = expectThrows(IllegalArgumentException.class, () -> clientKey1.prepareCreateApiKey().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, () -> clientKey1.prepareCreateApiKey().setName("key-5") @@ -974,9 +967,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))); SecurityClient securityClient = new SecurityClient(client); final CreateApiKeyResponse createApiKeyResponse = securityClient.prepareCreateApiKey() .setName("auth only key") @@ -1133,9 +1125,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") @@ -1159,7 +1150,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); } @@ -1172,9 +1163,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()); @@ -1194,33 +1185,31 @@ 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) { + int noOfApiKeys, TimeValue expiration, String... clusterPrivileges) { final Map headers = new MapBuilder() - .put("Authorization", - UsernamePasswordToken.basicAuthHeaderValue( - authenticatingUser, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)) + .put("Authorization", basicAuthHeaderValue(authenticatingUser, TEST_PASSWORD_SECURE_STRING)) .put("es-security-runas-user", owningUser) .immutableMap(); 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); @@ -1249,9 +1238,7 @@ private void createUserWithRunAsRole() throws ExecutionException, InterruptedExc putUserRequest.passwordHash(SecuritySettingsSource.TEST_PASSWORD_HASHED.toCharArray()); PlainActionFuture listener = new PlainActionFuture<>(); final Map headers = new MapBuilder() - .put("Authorization", - UsernamePasswordToken.basicAuthHeaderValue( - SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)) + .put("Authorization", basicAuthHeaderValue(TEST_SUPERUSER, TEST_PASSWORD_SECURE_STRING)) .immutableMap(); final Client client = client().filterWithHeader(headers); new SecurityClient(client).putUser(putUserRequest, listener); @@ -1261,21 +1248,23 @@ private void createUserWithRunAsRole() throws ExecutionException, InterruptedExc private Client getClientForRunAsUser() { final Map headers = new MapBuilder() - .put("Authorization", - UsernamePasswordToken.basicAuthHeaderValue( - "user_with_run_as_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)) + .put("Authorization", basicAuthHeaderValue( "user_with_run_as_role", TEST_PASSWORD_SECURE_STRING)) .put("es-security-runas-user", "user_with_manage_own_api_key_role") .immutableMap(); return client().filterWithHeader(headers); } 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 6828eca0ce403..4b77cda125398 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 @@ -212,7 +212,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; } @@ -252,12 +252,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 { @@ -292,7 +292,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)); } })); }); @@ -304,7 +304,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)); } } @@ -419,7 +419,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)); } } @@ -438,13 +438,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; @@ -470,7 +470,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 @@ -544,7 +544,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, IndexAuthorizationResult.getFailureDescription(singletonList(resolvedIndex)), null)); } else if (audit.get()) { auditTrail.explicitIndexAccessEvent(requestId, AuditLevel.ACCESS_GRANTED, authentication, itemAction, @@ -605,12 +605,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)) { @@ -636,11 +637,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); @@ -691,7 +697,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 11991cbf7c3bd..a724ab6fba7c0 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 @@ -23,8 +23,6 @@ import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; -import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsAction; -import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest; import org.elasticsearch.action.admin.indices.get.GetIndexAction; import org.elasticsearch.action.admin.indices.get.GetIndexRequest; import org.elasticsearch.action.admin.indices.recovery.RecoveryAction; @@ -110,6 +108,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; @@ -189,7 +189,6 @@ import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.ORIGINATING_ACTION_KEY; 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.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.containsString; @@ -668,16 +667,19 @@ public void testUserWithNoRolesCanClosePointInTime() { public void testUnknownRoleCausesDenial() throws IOException { Tuple tuple = randomFrom(asList( new Tuple<>(SearchAction.NAME, new SearchRequest()), - new Tuple<>(IndicesExistsAction.NAME, new IndicesExistsRequest()), new Tuple<>(SqlQueryAction.NAME, new SqlQueryRequest()))); String action = tuple.v1(); TransportRequest request = tuple.v2(); 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); } @@ -702,19 +704,21 @@ public void testThatRoleWithNoIndicesIsDenied() throws IOException { @SuppressWarnings("unchecked") Tuple tuple = randomFrom( new Tuple<>(SearchAction.NAME, new SearchRequest()), - new Tuple<>(IndicesExistsAction.NAME, new IndicesExistsRequest()), new Tuple<>(SqlQueryAction.NAME, new SqlQueryRequest())); - String action = tuple.v1(); - TransportRequest request = tuple.v2(); + final String action = tuple.v1(); + final TransportRequest request = tuple.v2(); final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final Authentication authentication = createAuthentication(new User("test user", "no_indices")); RoleDescriptor role = new RoleDescriptor("no_indices", null, null, null); 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); @@ -949,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 1229c178678a7..c1bb0c8dda971 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 @@ -242,7 +242,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: