Browse Source

Introduce new `read_security` cluster privilege (#89790)

This introduces a new built-in cluster privilege that allows all
read-only security-related operations. It also allows checking the user
and user profile privileges with the "has privilege" APIs.

Resolves #89245
Albert Zaharovits 3 years ago
parent
commit
c402723948
20 changed files with 248 additions and 52 deletions
  1. 6 0
      docs/changelog/89790.yaml
  2. 3 2
      x-pack/docs/en/rest-api/security/get-api-keys.asciidoc
  3. 3 3
      x-pack/docs/en/rest-api/security/get-app-privileges.asciidoc
  4. 3 2
      x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc
  5. 7 7
      x-pack/docs/en/rest-api/security/get-role-mappings.asciidoc
  6. 5 6
      x-pack/docs/en/rest-api/security/get-roles.asciidoc
  7. 5 4
      x-pack/docs/en/rest-api/security/get-service-credentials.asciidoc
  8. 3 2
      x-pack/docs/en/rest-api/security/get-user-profile.asciidoc
  9. 6 6
      x-pack/docs/en/rest-api/security/get-users.asciidoc
  10. 3 1
      x-pack/docs/en/rest-api/security/has-privileges-user-profile.asciidoc
  11. 3 3
      x-pack/docs/en/rest-api/security/query-api-key.asciidoc
  12. 3 1
      x-pack/docs/en/rest-api/security/suggest-user-profile.asciidoc
  13. 14 2
      x-pack/docs/en/security/authorization/privileges.asciidoc
  14. 33 0
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java
  15. 1 0
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestTests.java
  16. 20 4
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermissionTests.java
  17. 7 3
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolverTests.java
  18. 90 0
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java
  19. 32 5
      x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java
  20. 1 1
      x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml

+ 6 - 0
docs/changelog/89790.yaml

@@ -0,0 +1,6 @@
+pr: 89790
+summary: Introduce the new `read_security` cluster privilege
+area: Authorization
+type: feature
+issues:
+ - 89245

+ 3 - 2
x-pack/docs/en/rest-api/security/get-api-keys.asciidoc

@@ -15,9 +15,10 @@ Retrieves information for one or more API keys.
 [[security-api-get-api-key-prereqs]]
 ==== {api-prereq-title}
 
-* To use this API, you must have at least the `manage_own_api_key` cluster privilege.
+* To use this API, you must have at least the `manage_own_api_key` or the `read_security`
+cluster privileges.
 * If you have only the `manage_own_api_key` privilege, this API returns only
-the API keys that you own. If you have the `manage_api_key` or greater
+the API keys that you own. If you have `read_security`, `manage_api_key` or greater
 privileges (including `manage_security`), this API returns all API keys
 regardless of ownership.
 

+ 3 - 3
x-pack/docs/en/rest-api/security/get-app-privileges.asciidoc

@@ -14,7 +14,7 @@ Retrieves <<application-privileges,application privileges>>.
 
 `GET /_security/privilege/<application>` +
 
-`GET /_security/privilege/<application>/<privilege>` 
+`GET /_security/privilege/<application>/<privilege>`
 
 
 [[security-api-get-privileges-prereqs]]
@@ -22,7 +22,7 @@ Retrieves <<application-privileges,application privileges>>.
 
 To use this API, you must have either:
 
-- the `manage_security` cluster privilege (or a greater privilege such as `all`); _or_
+- the `read_security` cluster privilege (or a greater privilege such as `manage_security` or `all`); _or_
 - the _"Manage Application Privileges"_ global privilege for the application being referenced
   in the request
 
@@ -51,7 +51,7 @@ To check a user's application privileges, use the
 [[security-api-get-privileges-example]]
 ==== {api-examples-title}
 
-The following example retrieves information about the `read` privilege for the 
+The following example retrieves information about the `read` privilege for the
 `app01` application:
 
 [source,console]

+ 3 - 2
x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc

@@ -19,8 +19,8 @@ available in this version of {es}.
 [[security-api-get-builtin-privileges-prereqs]]
 ==== {api-prereq-title}
 
-* To use this API, you must have - the `manage_security` cluster privilege
-(or a greater privilege such as `all`).
+* To use this API, you must have the `read_security` cluster privilege
+(or a greater privilege such as `manage_security` or `all`).
 
 [[security-api-get-builtin-privileges-desc]]
 ==== {api-description-title}
@@ -102,6 +102,7 @@ A successful call returns an object with "cluster" and "index" fields.
     "read_ccr",
     "read_ilm",
     "read_pipeline",
+    "read_security",
     "read_slm",
     "transport_client"
   ],

+ 7 - 7
x-pack/docs/en/rest-api/security/get-role-mappings.asciidoc

@@ -12,17 +12,17 @@ Retrieves role mappings.
 
 `GET /_security/role_mapping` +
 
-`GET /_security/role_mapping/<name>` 
+`GET /_security/role_mapping/<name>`
 
 [[security-api-get-role-mapping-prereqs]]
 ==== {api-prereq-title}
 
-* To use this API, you must have at least the `manage_security` cluster privilege.
+* To use this API, you must have at least the `read_security` cluster privilege.
 
 [[security-api-get-role-mapping-desc]]
 ==== {api-description-title}
 
-Role mappings define which roles are assigned to each user. For more information, 
+Role mappings define which roles are assigned to each user. For more information,
 see <<mapping-roles>>.
 
 The role mapping APIs are generally the preferred way to manage role mappings
@@ -36,16 +36,16 @@ in role mapping files.
 `name`::
   (Optional, string) The distinct name that identifies the role mapping. The name
   is used solely as an identifier to facilitate interaction via the API; it does
-  not affect the behavior of the mapping in any way. You can specify multiple 
+  not affect the behavior of the mapping in any way. You can specify multiple
   mapping names as a comma-separated list. If you do not specify this
-  parameter, the API returns information about all role mappings. 
+  parameter, the API returns information about all role mappings.
 
 [[security-api-get-role-mapping-response-body]]
 ==== {api-response-body-title}
 
 A successful call retrieves an object, where the keys are the
-names of the request mappings, and the values are the JSON representation of 
-those mappings. For more information, see 
+names of the request mappings, and the values are the JSON representation of
+those mappings. For more information, see
 <<role-mapping-resources>>.
 
 [[security-api-get-role-mapping-response-codes]]

+ 5 - 6
x-pack/docs/en/rest-api/security/get-roles.asciidoc

@@ -17,8 +17,7 @@ Retrieves roles in the native realm.
 [[security-api-get-role-prereqs]]
 ==== {api-prereq-title}
 
-* To use this API, you must have at least the `manage_security` cluster
-privilege.
+* To use this API, you must have at least the `read_security` cluster privilege.
 
 [[security-api-get-role-desc]]
 ==== {api-description-title}
@@ -31,10 +30,10 @@ API cannot retrieve roles that are defined in roles files.
 ==== {api-path-parms-title}
 
 `name`::
-  (Optional, string) The name of the role. You can specify multiple roles as a 
-  comma-separated list. If you do not specify this parameter, the API 
+  (Optional, string) The name of the role. You can specify multiple roles as a
+  comma-separated list. If you do not specify this parameter, the API
   returns information about all roles.
-  
+
 [[security-api-get-role-response-body]]
 ==== {api-response-body-title}
 
@@ -49,7 +48,7 @@ If the role is not defined in the native realm, the request returns 404.
 [[security-api-get-role-example]]
 ==== {api-examples-title}
 
-The following example retrieves information about the `my_admin_role` role in 
+The following example retrieves information about the `my_admin_role` role in
 the native realm:
 
 [source,console]

+ 5 - 4
x-pack/docs/en/rest-api/security/get-service-credentials.asciidoc

@@ -16,16 +16,17 @@ Retrieves all service credentials for a  <<service-accounts,service account>>.
 [[security-api-get-service-credentials-prereqs]]
 ==== {api-prereq-title}
 
-* To use this API, you must have at least the `manage_service_account`
-<<privileges-list-cluster,cluster privilege>>.
+* To use this API, you must have at least the `read_security`
+<<privileges-list-cluster,cluster privilege>> (or a greater privilege
+such as `manage_service_account` or `manage_security`).
 
 [[security-api-get-service-credentials-desc]]
 ==== {api-description-title}
 
 Use this API to retrieve a list of credentials for a service account.
 The response includes service account tokens that were created with the
-<< create service account API >> as well as file-backed tokens from all
-nodes of the cluster.
+<<security-api-create-service-token,create service account tokens API>>
+as well as file-backed tokens from all nodes of the cluster.
 
 NOTE: For tokens backed by the `service_tokens` file, the API collects
 them from all nodes of the cluster. Tokens with the same name from

+ 3 - 2
x-pack/docs/en/rest-api/security/get-user-profile.asciidoc

@@ -17,8 +17,9 @@ Retrieves user profiles using a list of unique profile ID.
 [[security-api-get-user-profile-prereqs]]
 ==== {api-prereq-title}
 
-* To use this API, you must have _at least_ the `manage_user_profile` cluster privilege.
-
+To use this API, you must have _at least_ the `read_security`
+<<privileges-list-cluster,cluster privilege>> (or a greater privilege
+such as `manage_user_profile` or `manage_security`).
 
 [[security-api-get-user-profile-desc]]
 ==== {api-description-title}

+ 6 - 6
x-pack/docs/en/rest-api/security/get-users.asciidoc

@@ -5,7 +5,7 @@
 <titleabbrev>Get users</titleabbrev>
 ++++
 
-Retrieves information about users in the native realm and built-in users. 
+Retrieves information about users in the native realm and built-in users.
 
 
 [[security-api-get-user-request]]
@@ -13,19 +13,19 @@ Retrieves information about users in the native realm and built-in users.
 
 `GET /_security/user` +
 
-`GET /_security/user/<username>` 
+`GET /_security/user/<username>`
 
 [[security-api-get-user-prereqs]]
 ==== {api-prereq-title}
 
-* To use this API, you must have at least the `manage_security` cluster privilege.
+* To use this API, you must have at least the `read_security` cluster privilege.
 
 
 [[security-api-get-user-desc]]
 ==== {api-description-title}
 
-For more information about the native realm, see 
-<<realms>> and <<native-realm>>. 
+For more information about the native realm, see
+<<realms>> and <<native-realm>>.
 
 [[security-api-get-user-path-params]]
 ==== {api-path-parms-title}
@@ -60,7 +60,7 @@ GET /_security/user/jacknich
 
 [source,console-result]
 --------------------------------------------------
-{  
+{
   "jacknich": {
     "username": "jacknich",
     "roles": [

+ 3 - 1
x-pack/docs/en/rest-api/security/has-privileges-user-profile.asciidoc

@@ -21,7 +21,9 @@ have all the requested privileges.
 [[security-api-has-privileges-user-profile-prereqs]]
 ==== {api-prereq-title}
 
-To use this API, you must have the `manage_user_profile` cluster privilege.
+To use this API, you must have _at least_ the `read_security`
+<<privileges-list-cluster,cluster privilege>> (or a greater privilege
+such as `manage_user_profile` or `manage_security`).
 
 [[security-api-has-privileges-user-profile-desc]]
 ==== {api-description-title}

+ 3 - 3
x-pack/docs/en/rest-api/security/query-api-key.asciidoc

@@ -19,10 +19,10 @@ in a <<paginate-search-results,paginated>> fashion.
 [[security-api-query-api-key-prereqs]]
 ==== {api-prereq-title}
 
-* To use this API, you must have at least the `manage_own_api_key` cluster
-privilege.
+* To use this API, you must have at least the `manage_own_api_key` or the `read_security`
+cluster privileges.
 * If you have only the `manage_own_api_key` privilege, this API returns only
-the API keys that you own. If you have the `manage_api_key` or greater
+the API keys that you own. If you have the `read_security`, `manage_api_key` or greater
 privileges (including `manage_security`), this API returns all API keys
 regardless of ownership.
 

+ 3 - 1
x-pack/docs/en/rest-api/security/suggest-user-profile.asciidoc

@@ -19,7 +19,9 @@ Get suggestions for user profiles that match specified search criteria.
 [[security-api-suggest-user-profile-prereqs]]
 ==== {api-prereq-title}
 
-To use this API, you must have the `manage_user_profile` cluster privilege.
+To use this API, you must have _at least_ the `read_security`
+<<privileges-list-cluster,cluster privilege>> (or a greater privilege
+such as `manage_user_profile` or `manage_security`).
 
 [[security-api-suggest-user-profile-query-params]]
 ==== {api-query-parms-title}

+ 14 - 2
x-pack/docs/en/security/authorization/privileges.asciidoc

@@ -32,7 +32,10 @@ ability to manage security.
 `manage_api_key`::
 All security-related operations on {es} API keys including
 <<security-api-create-api-key,creating new API keys>>,
-<<security-api-get-api-key,retrieving information about API keys>>, and
+<<security-api-get-api-key,retrieving information about API keys>>,
+<<security-api-query-api-key,querying API keys>>,
+<<security-api-update-api-key,updating API key>>,
+<<security-api-bulk-update-api-keys,bulk updating API keys>>, and
 <<security-api-invalidate-api-key,invalidating API keys>>.
 +
 --
@@ -89,7 +92,10 @@ to initiate and manage OpenID Connect authentication on behalf of other users.
 All security-related operations on {es} API keys that are owned by the current
 authenticated user. The operations include
 <<security-api-create-api-key,creating new API keys>>,
-<<security-api-get-api-key,retrieving information about API keys>>, and
+<<security-api-get-api-key,retrieving information about API keys>>,
+<<security-api-query-api-key,querying API keys>>,
+<<security-api-update-api-key,updating API key>>,
+<<security-api-bulk-update-api-keys,bulk updating API keys>>, and
 <<security-api-invalidate-api-key,invalidating API keys>>.
 
 `manage_pipeline`::
@@ -176,6 +182,12 @@ Read-only access to ingest pipline (get, simulate).
 All read-only {slm-init} actions, such as getting policies and checking the
 {slm-init} status.
 
+`read_security`::
+All read-only security-related operations, such as getting users, user profiles,
+{es} API keys, {es} service accounts, roles and role mappings.
+Allows <<security-api-query-api-key,querying>> and <<security-api-get-api-key,retrieving information>>
+on all {es} API keys.
+
 `transport_client`::
 All privileges necessary for a transport client to connect. Required by the remote
 cluster to enable <<cross-cluster-configuring,{ccs}>>.

+ 33 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java

@@ -28,11 +28,24 @@ import org.elasticsearch.xpack.core.ilm.action.GetStatusAction;
 import org.elasticsearch.xpack.core.ilm.action.StartILMAction;
 import org.elasticsearch.xpack.core.ilm.action.StopILMAction;
 import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationAction;
+import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyAction;
 import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesAction;
+import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction;
+import org.elasticsearch.xpack.core.security.action.profile.GetProfilesAction;
+import org.elasticsearch.xpack.core.security.action.profile.SuggestProfilesAction;
+import org.elasticsearch.xpack.core.security.action.role.GetRolesAction;
+import org.elasticsearch.xpack.core.security.action.rolemapping.GetRoleMappingsAction;
 import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction;
+import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountAction;
+import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsAction;
 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.GetUserPrivilegesAction;
+import org.elasticsearch.xpack.core.security.action.user.GetUsersAction;
 import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction;
+import org.elasticsearch.xpack.core.security.action.user.ProfileHasPrivilegesAction;
 import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.slm.action.GetSnapshotLifecycleAction;
 
@@ -174,6 +187,25 @@ public class ClusterPrivilegeResolver {
         ALL_SECURITY_PATTERN,
         Set.of(DelegatePkiAuthenticationAction.NAME)
     );
+    public static final NamedClusterPrivilege READ_SECURITY = new ActionClusterPrivilege(
+        "read_security",
+        Set.of(
+            GetApiKeyAction.NAME,
+            QueryApiKeyAction.NAME,
+            GetBuiltinPrivilegesAction.NAME,
+            GetPrivilegesAction.NAME,
+            GetProfilesAction.NAME,
+            ProfileHasPrivilegesAction.NAME,
+            SuggestProfilesAction.NAME,
+            GetRolesAction.NAME,
+            GetRoleMappingsAction.NAME,
+            GetServiceAccountAction.NAME,
+            GetServiceAccountCredentialsAction.NAME + "*",
+            GetUsersAction.NAME,
+            GetUserPrivilegesAction.NAME, // normally authorized under the "same-user" authz check, but added here for uniformity
+            HasPrivilegesAction.NAME
+        )
+    );
     public static final NamedClusterPrivilege MANAGE_SAML = new ActionClusterPrivilege("manage_saml", MANAGE_SAML_PATTERN);
     public static final NamedClusterPrivilege MANAGE_OIDC = new ActionClusterPrivilege("manage_oidc", MANAGE_OIDC_PATTERN);
     public static final NamedClusterPrivilege MANAGE_API_KEY = new ActionClusterPrivilege("manage_api_key", MANAGE_API_KEY_PATTERN);
@@ -239,6 +271,7 @@ public class ClusterPrivilegeResolver {
             READ_PIPELINE,
             TRANSPORT_CLIENT,
             MANAGE_SECURITY,
+            READ_SECURITY,
             MANAGE_SAML,
             MANAGE_OIDC,
             MANAGE_API_KEY,

+ 1 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestTests.java

@@ -94,6 +94,7 @@ public class HasPrivilegesRequestTests extends ESTestCase {
                 ClusterPrivilegeResolver.MANAGE,
                 ClusterPrivilegeResolver.MANAGE_ML,
                 ClusterPrivilegeResolver.MANAGE_SECURITY,
+                ClusterPrivilegeResolver.READ_SECURITY,
                 ClusterPrivilegeResolver.MANAGE_PIPELINE,
                 ClusterPrivilegeResolver.ALL
             )

+ 20 - 4
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermissionTests.java

@@ -51,7 +51,7 @@ public class ClusterPermissionTests extends ESTestCase {
         assertNotNull(builder);
         assertThat(builder.build(), is(ClusterPermission.NONE));
 
-        builder = ClusterPrivilegeResolver.MANAGE_SECURITY.buildPermission(builder);
+        builder = ClusterPrivilegeResolver.READ_SECURITY.buildPermission(builder);
         builder = ClusterPrivilegeResolver.MANAGE_ILM.buildPermission(builder);
         final MockConfigurableClusterPrivilege mockConfigurableClusterPrivilege1 = new MockConfigurableClusterPrivilege(
             r -> r == mockTransportRequest
@@ -69,7 +69,7 @@ public class ClusterPermissionTests extends ESTestCase {
         assertThat(
             privileges,
             containsInAnyOrder(
-                ClusterPrivilegeResolver.MANAGE_SECURITY,
+                ClusterPrivilegeResolver.READ_SECURITY,
                 ClusterPrivilegeResolver.MANAGE_ILM,
                 mockConfigurableClusterPrivilege1,
                 mockConfigurableClusterPrivilege2
@@ -156,7 +156,7 @@ public class ClusterPermissionTests extends ESTestCase {
 
     public void testNoneClusterPermissionIsImpliedByAny() {
         ClusterPermission.Builder builder = ClusterPermission.builder();
-        builder = ClusterPrivilegeResolver.MANAGE_SECURITY.buildPermission(builder);
+        builder = ClusterPrivilegeResolver.READ_SECURITY.buildPermission(builder);
         builder = ClusterPrivilegeResolver.MANAGE_ILM.buildPermission(builder);
         final MockConfigurableClusterPrivilege mockConfigurableClusterPrivilege1 = new MockConfigurableClusterPrivilege(
             r -> r == mockTransportRequest
@@ -253,14 +253,30 @@ public class ClusterPermissionTests extends ESTestCase {
         assertThat(allClusterPermission.implies(otherClusterPermission), is(true));
     }
 
+    public void testReadSecurityPrivilegeNoImplyApiKeyManagement() {
+        assertFalse(ClusterPrivilegeResolver.READ_SECURITY.permission().implies(ClusterPrivilegeResolver.MANAGE_API_KEY.permission()));
+        assertFalse(ClusterPrivilegeResolver.MANAGE_API_KEY.permission().implies(ClusterPrivilegeResolver.READ_SECURITY.permission()));
+        assertFalse(ClusterPrivilegeResolver.READ_SECURITY.permission().implies(ClusterPrivilegeResolver.MANAGE_OWN_API_KEY.permission()));
+        assertFalse(ClusterPrivilegeResolver.MANAGE_OWN_API_KEY.permission().implies(ClusterPrivilegeResolver.READ_SECURITY.permission()));
+    }
+
     public void testImpliesOnSecurityPrivilegeHierarchy() {
-        final List<ClusterPermission> highToLow = List.of(
+        List<ClusterPermission> highToLow = List.of(
             ClusterPrivilegeResolver.ALL.permission(),
             ClusterPrivilegeResolver.MANAGE_SECURITY.permission(),
             ClusterPrivilegeResolver.MANAGE_API_KEY.permission(),
             ClusterPrivilegeResolver.MANAGE_OWN_API_KEY.permission()
         );
+        assertImpliesHierarchy(highToLow);
+        highToLow = List.of(
+            ClusterPrivilegeResolver.ALL.permission(),
+            ClusterPrivilegeResolver.MANAGE_SECURITY.permission(),
+            ClusterPrivilegeResolver.READ_SECURITY.permission()
+        );
+        assertImpliesHierarchy(highToLow);
+    }
 
+    private void assertImpliesHierarchy(List<ClusterPermission> highToLow) {
         for (int i = 0; i < highToLow.size(); i++) {
             ClusterPermission high = highToLow.get(i);
             for (int j = i; j < highToLow.size(); j++) {

+ 7 - 3
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolverTests.java

@@ -18,7 +18,7 @@ import static org.hamcrest.Matchers.contains;
 
 public class ClusterPrivilegeResolverTests extends ESTestCase {
 
-    public void testSortByAccessLevel() throws Exception {
+    public void testSortByAccessLevel() {
         final List<NamedClusterPrivilege> privileges = new ArrayList<>(
             List.of(
                 ClusterPrivilegeResolver.ALL,
@@ -26,16 +26,20 @@ public class ClusterPrivilegeResolverTests extends ESTestCase {
                 ClusterPrivilegeResolver.MANAGE,
                 ClusterPrivilegeResolver.MANAGE_OWN_API_KEY,
                 ClusterPrivilegeResolver.MANAGE_API_KEY,
+                ClusterPrivilegeResolver.READ_SECURITY,
                 ClusterPrivilegeResolver.MANAGE_SECURITY
             )
         );
         Collections.shuffle(privileges, random());
         final SortedMap<String, NamedClusterPrivilege> sorted = ClusterPrivilegeResolver.sortByAccessLevel(privileges);
         // This is:
-        // "manage_own_api_key", "monitor" (neither of which grant anything else in the list), sorted by name
+        // "manage_own_api_key", "monitor", "read_security" (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"));
+        assertThat(
+            sorted.keySet(),
+            contains("manage_own_api_key", "monitor", "read_security", "manage", "manage_api_key", "manage_security", "all")
+        );
     }
 
 }

+ 90 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java

@@ -22,6 +22,41 @@ import org.elasticsearch.xpack.core.enrich.action.DeleteEnrichPolicyAction;
 import org.elasticsearch.xpack.core.enrich.action.ExecuteEnrichPolicyAction;
 import org.elasticsearch.xpack.core.enrich.action.GetEnrichPolicyAction;
 import org.elasticsearch.xpack.core.enrich.action.PutEnrichPolicyAction;
+import org.elasticsearch.xpack.core.security.action.ClearSecurityCacheAction;
+import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationAction;
+import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.apikey.UpdateApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentAction;
+import org.elasticsearch.xpack.core.security.action.enrollment.NodeEnrollmentAction;
+import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesAction;
+import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction;
+import org.elasticsearch.xpack.core.security.action.profile.ActivateProfileAction;
+import org.elasticsearch.xpack.core.security.action.profile.GetProfilesAction;
+import org.elasticsearch.xpack.core.security.action.profile.SetProfileEnabledAction;
+import org.elasticsearch.xpack.core.security.action.profile.SuggestProfilesAction;
+import org.elasticsearch.xpack.core.security.action.profile.UpdateProfileDataAction;
+import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheAction;
+import org.elasticsearch.xpack.core.security.action.role.ClearRolesCacheAction;
+import org.elasticsearch.xpack.core.security.action.role.DeleteRoleAction;
+import org.elasticsearch.xpack.core.security.action.role.GetRolesAction;
+import org.elasticsearch.xpack.core.security.action.role.PutRoleAction;
+import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappingAction;
+import org.elasticsearch.xpack.core.security.action.rolemapping.GetRoleMappingsAction;
+import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingAction;
+import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenAction;
+import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountAction;
+import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsAction;
+import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountNodesCredentialsAction;
+import org.elasticsearch.xpack.core.security.action.user.DeleteUserAction;
+import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction;
+import org.elasticsearch.xpack.core.security.action.user.GetUsersAction;
+import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction;
+import org.elasticsearch.xpack.core.security.action.user.ProfileHasPrivilegesAction;
+import org.elasticsearch.xpack.core.security.action.user.PutUserAction;
 import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper;
 import org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission;
 import org.elasticsearch.xpack.core.security.support.Automatons;
@@ -197,6 +232,61 @@ public class PrivilegeTests extends ESTestCase {
         verifyClusterActionAllowed(ClusterPrivilegeResolver.MANAGE_AUTOSCALING, "cluster:admin/autoscaling/get_decision");
     }
 
+    public void testReadSecurityPrivilege() {
+        verifyClusterActionAllowed(
+            ClusterPrivilegeResolver.READ_SECURITY,
+            GetApiKeyAction.NAME,
+            QueryApiKeyAction.NAME,
+            GetBuiltinPrivilegesAction.NAME,
+            GetPrivilegesAction.NAME,
+            GetProfilesAction.NAME,
+            ProfileHasPrivilegesAction.NAME,
+            SuggestProfilesAction.NAME,
+            GetRolesAction.NAME,
+            GetRoleMappingsAction.NAME,
+            GetServiceAccountAction.NAME,
+            GetServiceAccountCredentialsAction.NAME,
+            GetUsersAction.NAME,
+            HasPrivilegesAction.NAME,
+            GetUserPrivilegesAction.NAME
+        );
+        verifyClusterActionAllowed(
+            ClusterPrivilegeResolver.READ_SECURITY,
+            GetServiceAccountNodesCredentialsAction.NAME,
+            GetServiceAccountCredentialsAction.NAME + randomFrom("", "whatever")
+        );
+        verifyClusterActionDenied(
+            ClusterPrivilegeResolver.READ_SECURITY,
+            PutUserAction.NAME,
+            DeleteUserAction.NAME,
+            PutRoleAction.NAME,
+            DeleteRoleAction.NAME,
+            PutRoleMappingAction.NAME,
+            DeleteRoleMappingAction.NAME,
+            CreateServiceAccountTokenAction.NAME,
+            CreateApiKeyAction.NAME,
+            InvalidateApiKeyAction.NAME,
+            ClusterHealthAction.NAME,
+            ClusterStateAction.NAME,
+            ClusterStatsAction.NAME,
+            NodeEnrollmentAction.NAME,
+            KibanaEnrollmentAction.NAME,
+            PutIndexTemplateAction.NAME,
+            GetIndexTemplatesAction.NAME,
+            ClusterRerouteAction.NAME,
+            ClusterUpdateSettingsAction.NAME,
+            ClearRealmCacheAction.NAME,
+            ClearSecurityCacheAction.NAME,
+            ClearRolesCacheAction.NAME,
+            UpdateApiKeyAction.NAME,
+            BulkUpdateApiKeyAction.NAME,
+            DelegatePkiAuthenticationAction.NAME,
+            ActivateProfileAction.NAME,
+            SetProfileEnabledAction.NAME,
+            UpdateProfileDataAction.NAME
+        );
+    }
+
     public void testManageUserProfilePrivilege() {
         verifyClusterActionAllowed(
             ClusterPrivilegeResolver.MANAGE_USER_PROFILE,

+ 32 - 5
x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java

@@ -198,6 +198,8 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
 
             no_api_key_role:
               cluster: ["manage_token"]
+            read_security_role:
+              cluster: ["read_security"]
             manage_api_key_role:
               cluster: ["manage_api_key"]
             manage_own_api_key_role:
@@ -214,6 +216,9 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
             + "user_with_no_api_key_role:"
             + usersPasswdHashed
             + "\n"
+            + "user_with_read_security_role:"
+            + usersPasswdHashed
+            + "\n"
             + "user_with_manage_api_key_role:"
             + usersPasswdHashed
             + "\n"
@@ -226,6 +231,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
     public String configUsersRoles() {
         return super.configUsersRoles() + """
             no_api_key_role:user_with_no_api_key_role
+            read_security_role:user_with_read_security_role
             manage_api_key_role:user_with_manage_api_key_role
             manage_own_api_key_role:user_with_manage_own_api_key_role
             """;
@@ -1092,12 +1098,15 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
         List<CreateApiKeyResponse> userWithManageOwnApiKeyRoleApiKeys = userWithManageOwnTuple.v1();
 
         final Client client = client().filterWithHeader(
-            Collections.singletonMap("Authorization", basicAuthHeaderValue("user_with_manage_api_key_role", TEST_PASSWORD_SECURE_STRING))
+            Collections.singletonMap(
+                "Authorization",
+                basicAuthHeaderValue(
+                    randomFrom("user_with_read_security_role", "user_with_manage_api_key_role"),
+                    TEST_PASSWORD_SECURE_STRING
+                )
+            )
         );
         final boolean withLimitedBy = randomBoolean();
-        PlainActionFuture<GetApiKeyResponse> listener = new PlainActionFuture<>();
-        client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.builder().withLimitedBy(withLimitedBy).build(), listener);
-        GetApiKeyResponse response = listener.get();
         int totalApiKeys = noOfSuperuserApiKeys + noOfApiKeysForUserWithManageApiKeyRole + noOfApiKeysForUserWithManageOwnApiKeyRole;
         List<CreateApiKeyResponse> allApiKeys = new ArrayList<>();
         Stream.of(defaultUserCreatedKeys, userWithManageApiKeyRoleApiKeys, userWithManageOwnApiKeyRoleApiKeys).forEach(allApiKeys::addAll);
@@ -1128,7 +1137,7 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
             metadatas,
             List.of(DEFAULT_API_KEY_ROLE_DESCRIPTOR),
             expectedLimitedByRoleDescriptorsLookup,
-            response.getApiKeyInfos(),
+            getAllApiKeyInfo(client, withLimitedBy),
             allApiKeys.stream().map(CreateApiKeyResponse::getId).collect(Collectors.toSet()),
             null
         );
@@ -2682,6 +2691,24 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase {
         }
     }
 
+    private ApiKey[] getAllApiKeyInfo(Client client, boolean withLimitedBy) {
+        if (randomBoolean()) {
+            final PlainActionFuture<GetApiKeyResponse> future = new PlainActionFuture<>();
+            client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.builder().withLimitedBy(withLimitedBy).build(), future);
+            final GetApiKeyResponse getApiKeyResponse = future.actionGet();
+            return getApiKeyResponse.getApiKeyInfos();
+        } else {
+            final PlainActionFuture<QueryApiKeyResponse> future = new PlainActionFuture<>();
+            client.execute(
+                QueryApiKeyAction.INSTANCE,
+                new QueryApiKeyRequest(QueryBuilders.matchAllQuery(), null, 1000, null, null, withLimitedBy),
+                future
+            );
+            final QueryApiKeyResponse queryApiKeyResponse = future.actionGet();
+            return Arrays.stream(queryApiKeyResponse.getItems()).map(QueryApiKeyResponse.Item::getApiKey).toArray(ApiKey[]::new);
+        }
+    }
+
     private ServiceWithNodeName getServiceWithNodeName() {
         final var nodeName = randomFrom(internalCluster().getNodeNames());
         final var service = internalCluster().getInstance(ApiKeyService.class, nodeName);

+ 1 - 1
x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml

@@ -15,5 +15,5 @@ setup:
   # This is fragile - it needs to be updated every time we add a new cluster/index privilege
   # I would much prefer we could just check that specific entries are in the array, but we don't have
   # an assertion for that
-  - length: { "cluster" : 42 }
+  - length: { "cluster" : 43 }
   - length: { "index" : 19 }