diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesRequest.java index 1fdf8ee35d1b6..bbcd2bbe255ce 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesRequest.java @@ -8,19 +8,12 @@ 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 diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponse.java index d4d99d0b25b7d..6b2b202d17840 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponse.java +++ b/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; @@ -37,12 +37,6 @@ 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 +47,6 @@ public String[] getIndexPrivileges() { @Override public void writeTo(StreamOutput out) throws IOException { - out.writeStringArray(clusterPrivileges); - out.writeStringArray(indexPrivileges); + TransportAction.localOnly(); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTranslator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTranslator.java new file mode 100644 index 0000000000000..2d018ae2f1b2f --- /dev/null +++ b/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; + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTests.java deleted file mode 100644 index c8d14a4d71db1..0000000000000 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTests.java +++ /dev/null @@ -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)); - } - -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 763eb2616175c..3beff69849a58 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -135,6 +135,7 @@ 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 scriptServiceReference = new SetOnce<>(); private final SetOnce operatorOnlyRegistry = new SetOnce<>(); private final SetOnce putRoleRequestBuilderFactory = new SetOnce<>(); + private final SetOnce getBuiltinPrivilegesResponseTranslator = new SetOnce<>(); private final SetOnce fileRolesStore = new SetOnce<>(); private final SetOnce operatorPrivilegesService = new SetOnce<>(); private final SetOnce reservedRoleMappingAction = new SetOnce<>(); @@ -820,6 +822,10 @@ Collection createComponents( putRoleRequestBuilderFactory.set(new PutRoleRequestBuilderFactory.Default()); } + if (getBuiltinPrivilegesResponseTranslator.get() == null) { + getBuiltinPrivilegesResponseTranslator.set(new GetBuiltinPrivilegesResponseTranslator.Default()); + } + final Map, ActionListener>>> customRoleProviders = new LinkedHashMap<>(); for (SecurityExtension extension : securityExtensions) { final List, ActionListener>> providers = extension.getRolesProviders( @@ -1446,7 +1452,7 @@ public List getRestHandlers( 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 void accept(DiscoveryNode node, ClusterState state) { @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 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 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 void loadSingletonExtensionAndSetOnce(ExtensionLoader loader, SetOnce setOnce, Class clazz) { + final List 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); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetBuiltinPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetBuiltinPrivilegesAction.java index 6494c5b7c9230..8ea8ec3e0dcd9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetBuiltinPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetBuiltinPrivilegesAction.java @@ -8,9 +8,8 @@ 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 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 { +public class TransportGetBuiltinPrivilegesAction extends TransportAction { @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 diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java index fe3b5cab38444..7751ad0d114ce 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java +++ b/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.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 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 String getName() { @Override public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + final boolean restrictResponse = request.hasParam(RestRequest.RESPONSE_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 RestResponse buildResponse(GetBuiltinPrivilegesResponse response, XConten ); } + @Override + protected Exception innerCheckFeatureAvailable(RestRequest request) { + final boolean restrictPath = request.hasParam(RestRequest.RESPONSE_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; + } + } + }