Pārlūkot izejas kodu

Additional roles and privileges APIs customization (#105503)

This PR folds together the following:

* Support fetching native-only roles, i.e., excluding reserved roles for the Get Roles API
* Switch the Delete Roles API to public protection scope
* Support injecting a response translator for the Get Builtin Privileges API

Depends on: https://github.com/elastic/elasticsearch/pull/105336

Relates: ES-7826, ES-7828, ES-7845
Nikolaj Volgushev 1 gadu atpakaļ
vecāks
revīzija
48ceed0661
14 mainītis faili ar 285 papildinājumiem un 163 dzēšanām
  1. 0 7
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesRequest.java
  2. 10 18
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponse.java
  3. 20 0
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTranslator.java
  4. 11 7
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesRequest.java
  5. 5 0
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesRequestBuilder.java
  6. 3 15
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java
  7. 0 32
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTests.java
  8. 20 26
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
  9. 4 11
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetBuiltinPrivilegesAction.java
  10. 36 18
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesAction.java
  11. 44 5
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java
  12. 1 1
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestDeleteRoleAction.java
  13. 42 23
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestGetRolesAction.java
  14. 89 0
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java

+ 0 - 7
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesRequest.java

@@ -8,19 +8,12 @@ package org.elasticsearch.xpack.core.security.action.privilege;
 
 import org.elasticsearch.action.ActionRequest;
 import org.elasticsearch.action.ActionRequestValidationException;
-import org.elasticsearch.common.io.stream.StreamInput;
-
-import java.io.IOException;
 
 /**
  * Request to retrieve built-in (cluster/index) privileges.
  */
 public final class GetBuiltinPrivilegesRequest extends ActionRequest {
 
-    public GetBuiltinPrivilegesRequest(StreamInput in) throws IOException {
-        super(in);
-    }
-
     public GetBuiltinPrivilegesRequest() {}
 
     @Override

+ 10 - 18
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponse.java

@@ -7,8 +7,8 @@
 package org.elasticsearch.xpack.core.security.action.privilege;
 
 import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.action.support.TransportAction;
 import org.elasticsearch.common.Strings;
-import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 
 import java.io.IOException;
@@ -17,32 +17,25 @@ import java.util.Collections;
 import java.util.Objects;
 
 /**
- * Response containing one or more application privileges retrieved from the security index
+ * Response containing built-in (cluster/index) privileges
  */
 public final class GetBuiltinPrivilegesResponse extends ActionResponse {
 
-    private String[] clusterPrivileges;
-    private String[] indexPrivileges;
-
-    public GetBuiltinPrivilegesResponse(String[] clusterPrivileges, String[] indexPrivileges) {
-        this.clusterPrivileges = Objects.requireNonNull(clusterPrivileges, "Cluster privileges cannot be null");
-        this.indexPrivileges = Objects.requireNonNull(indexPrivileges, "Index privileges cannot be null");
-    }
+    private final String[] clusterPrivileges;
+    private final String[] indexPrivileges;
 
     public GetBuiltinPrivilegesResponse(Collection<String> clusterPrivileges, Collection<String> indexPrivileges) {
-        this(clusterPrivileges.toArray(Strings.EMPTY_ARRAY), indexPrivileges.toArray(Strings.EMPTY_ARRAY));
+        this.clusterPrivileges = Objects.requireNonNull(
+            clusterPrivileges.toArray(Strings.EMPTY_ARRAY),
+            "Cluster privileges cannot be null"
+        );
+        this.indexPrivileges = Objects.requireNonNull(indexPrivileges.toArray(Strings.EMPTY_ARRAY), "Index privileges cannot be null");
     }
 
     public GetBuiltinPrivilegesResponse() {
         this(Collections.emptySet(), Collections.emptySet());
     }
 
-    public GetBuiltinPrivilegesResponse(StreamInput in) throws IOException {
-        super(in);
-        this.clusterPrivileges = in.readStringArray();
-        this.indexPrivileges = in.readStringArray();
-    }
-
     public String[] getClusterPrivileges() {
         return clusterPrivileges;
     }
@@ -53,7 +46,6 @@ public final class GetBuiltinPrivilegesResponse extends ActionResponse {
 
     @Override
     public void writeTo(StreamOutput out) throws IOException {
-        out.writeStringArray(clusterPrivileges);
-        out.writeStringArray(indexPrivileges);
+        TransportAction.localOnly();
     }
 }

+ 20 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTranslator.java

@@ -0,0 +1,20 @@
+/*
+ * 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.action.privilege;
+
+public interface GetBuiltinPrivilegesResponseTranslator {
+
+    GetBuiltinPrivilegesResponse translate(GetBuiltinPrivilegesResponse response, boolean restrictResponse);
+
+    class Default implements GetBuiltinPrivilegesResponseTranslator {
+        public GetBuiltinPrivilegesResponse translate(GetBuiltinPrivilegesResponse response, boolean restrictResponse) {
+            assert false == restrictResponse;
+            return response;
+        }
+    }
+}

+ 11 - 7
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesRequest.java

@@ -9,12 +9,12 @@ package org.elasticsearch.xpack.core.security.action.role;
 import org.elasticsearch.action.ActionRequest;
 import org.elasticsearch.action.ActionRequestValidationException;
 import org.elasticsearch.common.Strings;
-import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 
 import java.io.IOException;
 
 import static org.elasticsearch.action.ValidateActions.addValidationError;
+import static org.elasticsearch.action.support.TransportAction.localOnly;
 
 /**
  * Request to retrieve roles from the security index
@@ -23,10 +23,7 @@ public class GetRolesRequest extends ActionRequest {
 
     private String[] names = Strings.EMPTY_ARRAY;
 
-    public GetRolesRequest(StreamInput in) throws IOException {
-        super(in);
-        names = in.readStringArray();
-    }
+    private boolean nativeOnly = false;
 
     public GetRolesRequest() {}
 
@@ -47,9 +44,16 @@ public class GetRolesRequest extends ActionRequest {
         return names;
     }
 
+    public void nativeOnly(boolean nativeOnly) {
+        this.nativeOnly = nativeOnly;
+    }
+
+    public boolean nativeOnly() {
+        return this.nativeOnly;
+    }
+
     @Override
     public void writeTo(StreamOutput out) throws IOException {
-        super.writeTo(out);
-        out.writeStringArray(names);
+        localOnly();
     }
 }

+ 5 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesRequestBuilder.java

@@ -22,4 +22,9 @@ public class GetRolesRequestBuilder extends ActionRequestBuilder<GetRolesRequest
         request.names(names);
         return this;
     }
+
+    public GetRolesRequestBuilder nativeOnly(boolean nativeOnly) {
+        request.nativeOnly(nativeOnly);
+        return this;
+    }
 }

+ 3 - 15
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java

@@ -7,7 +7,7 @@
 package org.elasticsearch.xpack.core.security.action.role;
 
 import org.elasticsearch.action.ActionResponse;
-import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.action.support.TransportAction;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
 
@@ -18,16 +18,7 @@ import java.io.IOException;
  */
 public class GetRolesResponse extends ActionResponse {
 
-    private RoleDescriptor[] roles;
-
-    public GetRolesResponse(StreamInput in) throws IOException {
-        super(in);
-        int size = in.readVInt();
-        roles = new RoleDescriptor[size];
-        for (int i = 0; i < size; i++) {
-            roles[i] = new RoleDescriptor(in);
-        }
-    }
+    private final RoleDescriptor[] roles;
 
     public GetRolesResponse(RoleDescriptor... roles) {
         this.roles = roles;
@@ -43,9 +34,6 @@ public class GetRolesResponse extends ActionResponse {
 
     @Override
     public void writeTo(StreamOutput out) throws IOException {
-        out.writeVInt(roles.length);
-        for (RoleDescriptor role : roles) {
-            role.writeTo(out);
-        }
+        TransportAction.localOnly();
     }
 }

+ 0 - 32
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTests.java

@@ -1,32 +0,0 @@
-/*
- * 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.action.privilege;
-
-import org.elasticsearch.common.io.stream.BytesStreamOutput;
-import org.elasticsearch.test.ESTestCase;
-import org.hamcrest.Matchers;
-
-import java.io.IOException;
-
-public class GetBuiltinPrivilegesResponseTests extends ESTestCase {
-
-    public void testSerialization() throws IOException {
-        final String[] cluster = generateRandomStringArray(8, randomIntBetween(3, 8), false, true);
-        final String[] index = generateRandomStringArray(8, randomIntBetween(3, 8), false, true);
-        final GetBuiltinPrivilegesResponse original = new GetBuiltinPrivilegesResponse(cluster, index);
-
-        final BytesStreamOutput out = new BytesStreamOutput();
-        original.writeTo(out);
-
-        final GetBuiltinPrivilegesResponse copy = new GetBuiltinPrivilegesResponse(out.bytes().streamInput());
-
-        assertThat(copy.getClusterPrivileges(), Matchers.equalTo(cluster));
-        assertThat(copy.getIndexPrivileges(), Matchers.equalTo(index));
-    }
-
-}

+ 20 - 26
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java

@@ -135,6 +135,7 @@ import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAut
 import org.elasticsearch.xpack.core.security.action.privilege.ClearPrivilegesCacheAction;
 import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction;
 import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesAction;
+import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesResponseTranslator;
 import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction;
 import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction;
 import org.elasticsearch.xpack.core.security.action.profile.ActivateProfileAction;
@@ -560,6 +561,7 @@ public class Security extends Plugin
     private final SetOnce<ScriptService> scriptServiceReference = new SetOnce<>();
     private final SetOnce<OperatorOnlyRegistry> operatorOnlyRegistry = new SetOnce<>();
     private final SetOnce<PutRoleRequestBuilderFactory> putRoleRequestBuilderFactory = new SetOnce<>();
+    private final SetOnce<GetBuiltinPrivilegesResponseTranslator> getBuiltinPrivilegesResponseTranslator = new SetOnce<>();
     private final SetOnce<FileRolesStore> fileRolesStore = new SetOnce<>();
     private final SetOnce<OperatorPrivileges.OperatorPrivilegesService> operatorPrivilegesService = new SetOnce<>();
     private final SetOnce<ReservedRoleMappingAction> reservedRoleMappingAction = new SetOnce<>();
@@ -820,6 +822,10 @@ public class Security extends Plugin
             putRoleRequestBuilderFactory.set(new PutRoleRequestBuilderFactory.Default());
         }
 
+        if (getBuiltinPrivilegesResponseTranslator.get() == null) {
+            getBuiltinPrivilegesResponseTranslator.set(new GetBuiltinPrivilegesResponseTranslator.Default());
+        }
+
         final Map<String, List<BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>>>> customRoleProviders = new LinkedHashMap<>();
         for (SecurityExtension extension : securityExtensions) {
             final List<BiConsumer<Set<String>, ActionListener<RoleRetrievalResult>>> providers = extension.getRolesProviders(
@@ -1446,7 +1452,7 @@ public class Security extends Plugin
             new RestOpenIdConnectPrepareAuthenticationAction(settings, getLicenseState()),
             new RestOpenIdConnectAuthenticateAction(settings, getLicenseState()),
             new RestOpenIdConnectLogoutAction(settings, getLicenseState()),
-            new RestGetBuiltinPrivilegesAction(settings, getLicenseState()),
+            new RestGetBuiltinPrivilegesAction(settings, getLicenseState(), getBuiltinPrivilegesResponseTranslator.get()),
             new RestGetPrivilegesAction(settings, getLicenseState()),
             new RestPutPrivilegesAction(settings, getLicenseState()),
             new RestDeletePrivilegesAction(settings, getLicenseState()),
@@ -2030,33 +2036,21 @@ public class Security extends Plugin
     @Override
     public void loadExtensions(ExtensionLoader loader) {
         securityExtensions.addAll(loader.loadExtensions(SecurityExtension.class));
+        loadSingletonExtensionAndSetOnce(loader, operatorOnlyRegistry, OperatorOnlyRegistry.class);
+        loadSingletonExtensionAndSetOnce(loader, putRoleRequestBuilderFactory, PutRoleRequestBuilderFactory.class);
+        loadSingletonExtensionAndSetOnce(loader, getBuiltinPrivilegesResponseTranslator, GetBuiltinPrivilegesResponseTranslator.class);
+    }
 
-        // operator registry SPI
-        List<OperatorOnlyRegistry> operatorOnlyRegistries = loader.loadExtensions(OperatorOnlyRegistry.class);
-        if (operatorOnlyRegistries.size() > 1) {
-            throw new IllegalStateException(OperatorOnlyRegistry.class + " may not have multiple implementations");
-        } else if (operatorOnlyRegistries.size() == 1) {
-            OperatorOnlyRegistry operatorOnlyRegistry = operatorOnlyRegistries.get(0);
-            this.operatorOnlyRegistry.set(operatorOnlyRegistry);
-            logger.debug(
-                "Loaded implementation [{}] for interface OperatorOnlyRegistry",
-                operatorOnlyRegistry.getClass().getCanonicalName()
-            );
-        }
-
-        List<PutRoleRequestBuilderFactory> builderFactories = loader.loadExtensions(PutRoleRequestBuilderFactory.class);
-        if (builderFactories.size() > 1) {
-            throw new IllegalStateException(PutRoleRequestBuilderFactory.class + " may not have multiple implementations");
-        } else if (builderFactories.size() == 1) {
-            PutRoleRequestBuilderFactory builderFactory = builderFactories.get(0);
-            this.putRoleRequestBuilderFactory.set(builderFactory);
-            logger.debug(
-                "Loaded implementation [{}] for interface [{}]",
-                builderFactory.getClass().getCanonicalName(),
-                PutRoleRequestBuilderFactory.class
-            );
+    private <T> void loadSingletonExtensionAndSetOnce(ExtensionLoader loader, SetOnce<T> setOnce, Class<T> clazz) {
+        final List<T> loaded = loader.loadExtensions(clazz);
+        if (loaded.size() > 1) {
+            throw new IllegalStateException(clazz + " may not have multiple implementations");
+        } else if (loaded.size() == 1) {
+            final T singleLoaded = loaded.get(0);
+            setOnce.set(singleLoaded);
+            logger.debug("Loaded implementation [{}] for interface [{}]", singleLoaded.getClass().getCanonicalName(), clazz);
         } else {
-            logger.debug("Will fall back on default implementation for interface [{}]", PutRoleRequestBuilderFactory.class);
+            logger.debug("Will fall back on default implementation for interface [{}]", clazz);
         }
     }
 

+ 4 - 11
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetBuiltinPrivilegesAction.java

@@ -8,9 +8,8 @@ package org.elasticsearch.xpack.security.action.privilege;
 
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.support.ActionFilters;
-import org.elasticsearch.action.support.HandledTransportAction;
+import org.elasticsearch.action.support.TransportAction;
 import org.elasticsearch.common.inject.Inject;
-import org.elasticsearch.common.util.concurrent.EsExecutors;
 import org.elasticsearch.tasks.Task;
 import org.elasticsearch.transport.TransportService;
 import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesAction;
@@ -22,19 +21,13 @@ import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege;
 import java.util.TreeSet;
 
 /**
- * Transport action to retrieve one or more application privileges from the security index
+ * Transport action to retrieve built-in (cluster/index) privileges
  */
-public class TransportGetBuiltinPrivilegesAction extends HandledTransportAction<GetBuiltinPrivilegesRequest, GetBuiltinPrivilegesResponse> {
+public class TransportGetBuiltinPrivilegesAction extends TransportAction<GetBuiltinPrivilegesRequest, GetBuiltinPrivilegesResponse> {
 
     @Inject
     public TransportGetBuiltinPrivilegesAction(ActionFilters actionFilters, TransportService transportService) {
-        super(
-            GetBuiltinPrivilegesAction.NAME,
-            transportService,
-            actionFilters,
-            GetBuiltinPrivilegesRequest::new,
-            EsExecutors.DIRECT_EXECUTOR_SERVICE
-        );
+        super(GetBuiltinPrivilegesAction.NAME, actionFilters, transportService.getTaskManager());
     }
 
     @Override

+ 36 - 18
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesAction.java

@@ -8,9 +8,8 @@ package org.elasticsearch.xpack.security.action.role;
 
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.support.ActionFilters;
-import org.elasticsearch.action.support.HandledTransportAction;
+import org.elasticsearch.action.support.TransportAction;
 import org.elasticsearch.common.inject.Inject;
-import org.elasticsearch.common.util.concurrent.EsExecutors;
 import org.elasticsearch.tasks.Task;
 import org.elasticsearch.transport.TransportService;
 import org.elasticsearch.xpack.core.security.action.role.GetRolesAction;
@@ -21,11 +20,14 @@ import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore;
 import org.elasticsearch.xpack.security.authz.store.NativeRolesStore;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
-public class TransportGetRolesAction extends HandledTransportAction<GetRolesRequest, GetRolesResponse> {
+public class TransportGetRolesAction extends TransportAction<GetRolesRequest, GetRolesResponse> {
 
     private final NativeRolesStore nativeRolesStore;
     private final ReservedRolesStore reservedRolesStore;
@@ -37,7 +39,7 @@ public class TransportGetRolesAction extends HandledTransportAction<GetRolesRequ
         TransportService transportService,
         ReservedRolesStore reservedRolesStore
     ) {
-        super(GetRolesAction.NAME, transportService, actionFilters, GetRolesRequest::new, EsExecutors.DIRECT_EXECUTOR_SERVICE);
+        super(GetRolesAction.NAME, actionFilters, transportService.getTaskManager());
         this.nativeRolesStore = nativeRolesStore;
         this.reservedRolesStore = reservedRolesStore;
     }
@@ -46,15 +48,23 @@ public class TransportGetRolesAction extends HandledTransportAction<GetRolesRequ
     protected void doExecute(Task task, final GetRolesRequest request, final ActionListener<GetRolesResponse> listener) {
         final String[] requestedRoles = request.names();
         final boolean specificRolesRequested = requestedRoles != null && requestedRoles.length > 0;
-        final Set<String> rolesToSearchFor = new HashSet<>();
-        final List<RoleDescriptor> roles = new ArrayList<>();
 
+        if (request.nativeOnly()) {
+            final Set<String> rolesToSearchFor = specificRolesRequested
+                ? Arrays.stream(requestedRoles).collect(Collectors.toSet())
+                : Collections.emptySet();
+            getNativeRoles(rolesToSearchFor, listener);
+            return;
+        }
+
+        final Set<String> rolesToSearchFor = new HashSet<>();
+        final List<RoleDescriptor> reservedRoles = new ArrayList<>();
         if (specificRolesRequested) {
             for (String role : requestedRoles) {
                 if (ReservedRolesStore.isReserved(role)) {
                     RoleDescriptor rd = ReservedRolesStore.roleDescriptor(role);
                     if (rd != null) {
-                        roles.add(rd);
+                        reservedRoles.add(rd);
                     } else {
                         listener.onFailure(new IllegalStateException("unable to obtain reserved role [" + role + "]"));
                         return;
@@ -64,21 +74,29 @@ public class TransportGetRolesAction extends HandledTransportAction<GetRolesRequ
                 }
             }
         } else {
-            roles.addAll(ReservedRolesStore.roleDescriptors());
+            reservedRoles.addAll(ReservedRolesStore.roleDescriptors());
         }
 
         if (specificRolesRequested && rolesToSearchFor.isEmpty()) {
-            // specific roles were requested but they were built in only, no need to hit the store
-            listener.onResponse(new GetRolesResponse(roles.toArray(new RoleDescriptor[roles.size()])));
+            // specific roles were requested, but they were built in only, no need to hit the store
+            listener.onResponse(new GetRolesResponse(reservedRoles.toArray(new RoleDescriptor[0])));
         } else {
-            nativeRolesStore.getRoleDescriptors(rolesToSearchFor, ActionListener.wrap((retrievalResult) -> {
-                if (retrievalResult.isSuccess()) {
-                    roles.addAll(retrievalResult.getDescriptors());
-                    listener.onResponse(new GetRolesResponse(roles.toArray(new RoleDescriptor[roles.size()])));
-                } else {
-                    listener.onFailure(retrievalResult.getFailure());
-                }
-            }, listener::onFailure));
+            getNativeRoles(rolesToSearchFor, reservedRoles, listener);
         }
     }
+
+    private void getNativeRoles(Set<String> rolesToSearchFor, ActionListener<GetRolesResponse> listener) {
+        getNativeRoles(rolesToSearchFor, new ArrayList<>(), listener);
+    }
+
+    private void getNativeRoles(Set<String> rolesToSearchFor, List<RoleDescriptor> foundRoles, ActionListener<GetRolesResponse> listener) {
+        nativeRolesStore.getRoleDescriptors(rolesToSearchFor, ActionListener.wrap((retrievalResult) -> {
+            if (retrievalResult.isSuccess()) {
+                foundRoles.addAll(retrievalResult.getDescriptors());
+                listener.onResponse(new GetRolesResponse(foundRoles.toArray(new RoleDescriptor[0])));
+            } else {
+                listener.onFailure(retrievalResult.getFailure());
+            }
+        }, listener::onFailure));
+    }
 }

+ 44 - 5
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java

@@ -6,7 +6,11 @@
  */
 package org.elasticsearch.xpack.security.rest.action.privilege;
 
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.ElasticsearchStatusException;
 import org.elasticsearch.client.internal.node.NodeClient;
+import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.rest.RestRequest;
@@ -19,6 +23,8 @@ import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesAction;
 import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesRequest;
 import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesResponse;
+import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesResponseTranslator;
+import org.elasticsearch.xpack.security.authz.store.NativeRolesStore;
 import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler;
 
 import java.io.IOException;
@@ -27,13 +33,21 @@ import java.util.List;
 import static org.elasticsearch.rest.RestRequest.Method.GET;
 
 /**
- * Rest action to retrieve an application privilege from the security index
+ * Rest action to retrieve built-in (cluster/index) privileges
  */
-@ServerlessScope(Scope.INTERNAL)
+@ServerlessScope(Scope.PUBLIC)
 public class RestGetBuiltinPrivilegesAction extends SecurityBaseRestHandler {
 
-    public RestGetBuiltinPrivilegesAction(Settings settings, XPackLicenseState licenseState) {
+    private static final Logger logger = LogManager.getLogger(RestGetBuiltinPrivilegesAction.class);
+    private final GetBuiltinPrivilegesResponseTranslator responseTranslator;
+
+    public RestGetBuiltinPrivilegesAction(
+        Settings settings,
+        XPackLicenseState licenseState,
+        GetBuiltinPrivilegesResponseTranslator responseTranslator
+    ) {
         super(settings, licenseState);
+        this.responseTranslator = responseTranslator;
     }
 
     @Override
@@ -48,15 +62,17 @@ public class RestGetBuiltinPrivilegesAction extends SecurityBaseRestHandler {
 
     @Override
     public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException {
+        final boolean restrictResponse = request.hasParam(RestRequest.PATH_RESTRICTED);
         return channel -> client.execute(
             GetBuiltinPrivilegesAction.INSTANCE,
             new GetBuiltinPrivilegesRequest(),
             new RestBuilderListener<>(channel) {
                 @Override
                 public RestResponse buildResponse(GetBuiltinPrivilegesResponse response, XContentBuilder builder) throws Exception {
+                    final var translatedResponse = responseTranslator.translate(response, restrictResponse);
                     builder.startObject();
-                    builder.array("cluster", response.getClusterPrivileges());
-                    builder.array("index", response.getIndexPrivileges());
+                    builder.array("cluster", translatedResponse.getClusterPrivileges());
+                    builder.array("index", translatedResponse.getIndexPrivileges());
                     builder.endObject();
                     return new RestResponse(RestStatus.OK, builder);
                 }
@@ -64,4 +80,27 @@ public class RestGetBuiltinPrivilegesAction extends SecurityBaseRestHandler {
         );
     }
 
+    @Override
+    protected Exception innerCheckFeatureAvailable(RestRequest request) {
+        final boolean restrictPath = request.hasParam(RestRequest.PATH_RESTRICTED);
+        assert false == restrictPath || DiscoveryNode.isStateless(settings);
+        if (false == restrictPath) {
+            return super.innerCheckFeatureAvailable(request);
+        }
+        // This is a temporary hack: we are re-using the native roles setting as an overall feature flag for custom roles.
+        final Boolean nativeRolesEnabled = settings.getAsBoolean(NativeRolesStore.NATIVE_ROLES_ENABLED, true);
+        if (nativeRolesEnabled == false) {
+            logger.debug(
+                "Attempt to call [{} {}] but [{}] is [{}]",
+                request.method(),
+                request.rawPath(),
+                NativeRolesStore.NATIVE_ROLES_ENABLED,
+                settings.get(NativeRolesStore.NATIVE_ROLES_ENABLED)
+            );
+            return new ElasticsearchStatusException("This API is not enabled on this Elasticsearch instance", RestStatus.GONE);
+        } else {
+            return null;
+        }
+    }
+
 }

+ 1 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestDeleteRoleAction.java

@@ -28,7 +28,7 @@ import static org.elasticsearch.rest.RestRequest.Method.DELETE;
 /**
  * Rest endpoint to delete a Role from the security index
  */
-@ServerlessScope(Scope.INTERNAL)
+@ServerlessScope(Scope.PUBLIC)
 public class RestDeleteRoleAction extends NativeRoleBaseRestHandler {
 
     public RestDeleteRoleAction(Settings settings, XPackLicenseState licenseState) {

+ 42 - 23
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestGetRolesAction.java

@@ -7,6 +7,7 @@
 package org.elasticsearch.xpack.security.rest.action.role;
 
 import org.elasticsearch.client.internal.node.NodeClient;
+import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.RestApiVersion;
@@ -21,7 +22,6 @@ import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xpack.core.security.action.role.GetRolesRequestBuilder;
 import org.elasticsearch.xpack.core.security.action.role.GetRolesResponse;
 import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
-import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler;
 
 import java.io.IOException;
 import java.util.List;
@@ -30,12 +30,9 @@ import static org.elasticsearch.rest.RestRequest.Method.GET;
 
 /**
  * Rest endpoint to retrieve a Role from the security index
- *
- * <strong>Note:</strong> This class does not extend {@link NativeRoleBaseRestHandler} because it handles both reserved roles and native
- * roles, and should still be available even if native role management is disabled.
  */
-@ServerlessScope(Scope.INTERNAL)
-public class RestGetRolesAction extends SecurityBaseRestHandler {
+@ServerlessScope(Scope.PUBLIC)
+public class RestGetRolesAction extends NativeRoleBaseRestHandler {
 
     public RestGetRolesAction(Settings settings, XPackLicenseState licenseState) {
         super(settings, licenseState);
@@ -57,25 +54,47 @@ public class RestGetRolesAction extends SecurityBaseRestHandler {
     @Override
     public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException {
         final String[] roles = request.paramAsStringArray("name", Strings.EMPTY_ARRAY);
-        return channel -> new GetRolesRequestBuilder(client).names(roles).execute(new RestBuilderListener<>(channel) {
-            @Override
-            public RestResponse buildResponse(GetRolesResponse response, XContentBuilder builder) throws Exception {
-                builder.startObject();
-                for (RoleDescriptor role : response.roles()) {
-                    builder.field(role.getName(), role);
-                }
-                builder.endObject();
+        final boolean restrictRequest = isPathRestricted(request);
+        return channel -> new GetRolesRequestBuilder(client).names(roles)
+            .nativeOnly(restrictRequest)
+            .execute(new RestBuilderListener<>(channel) {
+                @Override
+                public RestResponse buildResponse(GetRolesResponse response, XContentBuilder builder) throws Exception {
+                    builder.startObject();
+                    for (RoleDescriptor role : response.roles()) {
+                        builder.field(role.getName(), role);
+                    }
+                    builder.endObject();
+
+                    // if the user asked for specific roles, but none of them were found
+                    // we'll return an empty result and 404 status code
+                    if (roles.length != 0 && response.roles().length == 0) {
+                        return new RestResponse(RestStatus.NOT_FOUND, builder);
+                    }
 
-                // if the user asked for specific roles, but none of them were found
-                // we'll return an empty result and 404 status code
-                if (roles.length != 0 && response.roles().length == 0) {
-                    return new RestResponse(RestStatus.NOT_FOUND, builder);
+                    // either the user asked for all roles, or at least one of the roles
+                    // the user asked for was found
+                    return new RestResponse(RestStatus.OK, builder);
                 }
+            });
+    }
+
+    @Override
+    protected Exception innerCheckFeatureAvailable(RestRequest request) {
+        // Note: For non-restricted requests this action handles both reserved roles and native
+        // roles, and should still be available even if native role management is disabled.
+        // For restricted requests it should only be available if native role management is enabled
+        final boolean restrictPath = isPathRestricted(request);
+        if (false == restrictPath) {
+            return null;
+        } else {
+            return super.innerCheckFeatureAvailable(request);
+        }
+    }
 
-                // either the user asked for all roles, or at least one of the roles
-                // the user asked for was found
-                return new RestResponse(RestStatus.OK, builder);
-            }
-        });
+    private boolean isPathRestricted(RestRequest request) {
+        final boolean restrictRequest = request.hasParam(RestRequest.PATH_RESTRICTED);
+        assert false == restrictRequest || DiscoveryNode.isStateless(settings);
+        return restrictRequest;
     }
 }

+ 89 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java

@@ -276,6 +276,95 @@ public class TransportGetRolesActionTests extends ESTestCase {
         }
     }
 
+    public void testGetWithNativeOnly() {
+        final boolean all = randomBoolean();
+        final List<RoleDescriptor> storeRoleDescriptors = randomRoleDescriptors();
+        final List<String> storeNames = storeRoleDescriptors.stream().map(RoleDescriptor::getName).collect(Collectors.toList());
+
+        final List<String> requestedNames = new ArrayList<>();
+        final List<String> requestedStoreNames = new ArrayList<>();
+        if (all == false) {
+            // Add some reserved roles; we don't expect these to be returned by the native role store
+            requestedNames.addAll(randomSubsetOf(randomIntBetween(1, ReservedRolesStore.names().size()), ReservedRolesStore.names()));
+            requestedStoreNames.addAll(randomSubsetOf(randomIntBetween(1, storeNames.size()), storeNames));
+            requestedNames.addAll(requestedStoreNames);
+        }
+
+        final NativeRolesStore rolesStore = mockNativeRolesStore(requestedNames, storeRoleDescriptors);
+
+        final TransportService transportService = new TransportService(
+            Settings.EMPTY,
+            mock(Transport.class),
+            mock(ThreadPool.class),
+            TransportService.NOOP_TRANSPORT_INTERCEPTOR,
+            x -> null,
+            null,
+            Collections.emptySet()
+        );
+        final TransportGetRolesAction action = new TransportGetRolesAction(
+            mock(ActionFilters.class),
+            rolesStore,
+            transportService,
+            new ReservedRolesStore()
+        );
+
+        final GetRolesRequest request = new GetRolesRequest();
+        request.names(requestedNames.toArray(Strings.EMPTY_ARRAY));
+        request.nativeOnly(true);
+
+        final List<String> actualRoleNames = doExecuteSuccessfully(action, request);
+        if (all) {
+            assertThat(actualRoleNames, containsInAnyOrder(storeNames.toArray(Strings.EMPTY_ARRAY)));
+            verify(rolesStore, times(1)).getRoleDescriptors(eq(new HashSet<>()), anyActionListener());
+        } else {
+            assertThat(actualRoleNames, containsInAnyOrder(requestedStoreNames.toArray(Strings.EMPTY_ARRAY)));
+            verify(rolesStore, times(1)).getRoleDescriptors(eq(new HashSet<>(requestedNames)), anyActionListener());
+        }
+    }
+
+    private List<String> doExecuteSuccessfully(TransportGetRolesAction action, GetRolesRequest request) {
+        final AtomicReference<Throwable> throwableRef = new AtomicReference<>();
+        final AtomicReference<GetRolesResponse> responseRef = new AtomicReference<>();
+        action.doExecute(mock(Task.class), request, new ActionListener<>() {
+            @Override
+            public void onResponse(GetRolesResponse response) {
+                responseRef.set(response);
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                throwableRef.set(e);
+            }
+        });
+
+        assertThat(throwableRef.get(), is(nullValue()));
+        assertThat(responseRef.get(), is(notNullValue()));
+        return Arrays.stream(responseRef.get().roles()).map(RoleDescriptor::getName).collect(Collectors.toList());
+    }
+
+    private NativeRolesStore mockNativeRolesStore(List<String> expectedStoreNames, List<RoleDescriptor> storeRoleDescriptors) {
+        NativeRolesStore rolesStore = mock(NativeRolesStore.class);
+        doAnswer(invocation -> {
+            Object[] args = invocation.getArguments();
+            assert args.length == 2;
+            @SuppressWarnings("unchecked")
+            Set<String> requestedNames = (Set<String>) args[0];
+            @SuppressWarnings("unchecked")
+            ActionListener<RoleRetrievalResult> listener = (ActionListener<RoleRetrievalResult>) args[1];
+            if (requestedNames.size() == 0) {
+                listener.onResponse(RoleRetrievalResult.success(new HashSet<>(storeRoleDescriptors)));
+            } else {
+                listener.onResponse(
+                    RoleRetrievalResult.success(
+                        storeRoleDescriptors.stream().filter(r -> requestedNames.contains(r.getName())).collect(Collectors.toSet())
+                    )
+                );
+            }
+            return null;
+        }).when(rolesStore).getRoleDescriptors(eq(new HashSet<>(expectedStoreNames)), anyActionListener());
+        return rolesStore;
+    }
+
     public void testException() {
         final Exception e = randomFrom(new ElasticsearchSecurityException(""), new IllegalStateException());
         final List<RoleDescriptor> storeRoleDescriptors = randomRoleDescriptors();