From b7ed8648984bd37c19c1a7a5d77a53ded42166e9 Mon Sep 17 00:00:00 2001 From: Andrey Pleskach Date: Wed, 10 May 2023 11:42:47 +0200 Subject: [PATCH] Fix multitency config update Moved multi-tenancy to REST API implementation Signed-off-by: Andrey Pleskach --- .../security/OpenSearchSecurityPlugin.java | 10 - .../security/action/tenancy/EmptyRequest.java | 35 --- .../tenancy/TenancyConfigRestHandler.java | 65 ------ .../tenancy/TenancyConfigRetrieveActions.java | 24 -- .../TenancyConfigRetrieveResponse.java | 70 ------ .../TenancyConfigRetrieveTransportAction.java | 63 ----- .../tenancy/TenancyConfigUpdateAction.java | 26 --- .../tenancy/TenancyConfigUpdateRequest.java | 69 ------ .../TenancyConfigUpdateTransportAction.java | 155 ------------- .../action/tenancy/TenancyConfigs.java | 18 -- .../rest/api/MultiTenancyConfigApiAction.java | 217 ++++++++++++++++++ .../dlic/rest/api/SecurityRestApiActions.java | 1 + .../MultiTenancyConfigValidator.java | 32 +++ .../security/support/ConfigConstants.java | 4 +- .../rest/api/MultiTenancyConfigApiTest.java | 170 ++++++++++++++ .../test/TenancyDefaultTenantTests.java | 96 -------- .../test/TenancyMultitenancyEnabledTests.java | 43 ++-- .../TenancyPrivateTenantEnabledTests.java | 55 ++--- .../resources/multitenancy/internal_users.yml | 6 + src/test/resources/multitenancy/roles.yml | 2 + .../resources/multitenancy/roles_mapping.yml | 5 + 21 files changed, 472 insertions(+), 694 deletions(-) delete mode 100644 src/main/java/org/opensearch/security/action/tenancy/EmptyRequest.java delete mode 100644 src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRestHandler.java delete mode 100644 src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRetrieveActions.java delete mode 100644 src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRetrieveResponse.java delete mode 100644 src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRetrieveTransportAction.java delete mode 100644 src/main/java/org/opensearch/security/action/tenancy/TenancyConfigUpdateAction.java delete mode 100644 src/main/java/org/opensearch/security/action/tenancy/TenancyConfigUpdateRequest.java delete mode 100644 src/main/java/org/opensearch/security/action/tenancy/TenancyConfigUpdateTransportAction.java delete mode 100644 src/main/java/org/opensearch/security/action/tenancy/TenancyConfigs.java create mode 100644 src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java create mode 100644 src/main/java/org/opensearch/security/dlic/rest/validation/MultiTenancyConfigValidator.java create mode 100644 src/test/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiTest.java delete mode 100644 src/test/java/org/opensearch/security/multitenancy/test/TenancyDefaultTenantTests.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 815f96f0c5..2db8b5d2eb 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -117,11 +117,6 @@ import org.opensearch.search.query.QuerySearchResult; import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.TransportConfigUpdateAction; -import org.opensearch.security.action.tenancy.TenancyConfigRestHandler; -import org.opensearch.security.action.tenancy.TenancyConfigRetrieveActions; -import org.opensearch.security.action.tenancy.TenancyConfigRetrieveTransportAction; -import org.opensearch.security.action.tenancy.TenancyConfigUpdateAction; -import org.opensearch.security.action.tenancy.TenancyConfigUpdateTransportAction; import org.opensearch.security.action.whoami.TransportWhoAmIAction; import org.opensearch.security.action.whoami.WhoAmIAction; import org.opensearch.security.auditlog.AuditLog; @@ -480,7 +475,6 @@ public List getRestHandlers(Settings settings, RestController restC Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); handlers.add(new SecurityConfigUpdateAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); handlers.add(new SecurityWhoAmIAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); - handlers.add(new TenancyConfigRestHandler()); handlers.addAll( SecurityRestApiActions.getHandler( settings, @@ -518,10 +512,6 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre if(!disabled && !SSLConfig.isSslOnlyMode()) { actions.add(new ActionHandler<>(ConfigUpdateAction.INSTANCE, TransportConfigUpdateAction.class)); actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class)); - - actions.add(new ActionHandler<>(TenancyConfigRetrieveActions.INSTANCE, TenancyConfigRetrieveTransportAction.class)); - actions.add(new ActionHandler<>(TenancyConfigUpdateAction.INSTANCE, TenancyConfigUpdateTransportAction.class)); - } return actions; } diff --git a/src/main/java/org/opensearch/security/action/tenancy/EmptyRequest.java b/src/main/java/org/opensearch/security/action/tenancy/EmptyRequest.java deleted file mode 100644 index 607216a8c3..0000000000 --- a/src/main/java/org/opensearch/security/action/tenancy/EmptyRequest.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.action.tenancy; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.common.io.stream.StreamInput; - -public class EmptyRequest extends ActionRequest { - - public EmptyRequest(final StreamInput in) throws IOException { - super(in); - } - - public EmptyRequest() throws IOException { - super(); - } - - @Override - public ActionRequestValidationException validate() - { - return null; - } -} diff --git a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRestHandler.java b/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRestHandler.java deleted file mode 100644 index 0a00d16694..0000000000 --- a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRestHandler.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.action.tenancy; - -import java.io.IOException; -import java.util.List; - -import com.google.common.collect.ImmutableList; - -import org.opensearch.client.node.NodeClient; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; - -import static org.opensearch.rest.RestRequest.Method.GET; -import static org.opensearch.rest.RestRequest.Method.PUT; - -public class TenancyConfigRestHandler extends BaseRestHandler { - - public TenancyConfigRestHandler() { - super(); - } - - @Override - public String getName() { - return "Multi Tenancy actions to Retrieve / Update configs."; - } - - @Override - public List routes() { - return ImmutableList.of( - new Route(GET, "/_plugins/_security/api/tenancy/config"), - new Route(PUT, "/_plugins/_security/api/tenancy/config") - ); - } - - @Override - protected RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient nodeClient) throws IOException { - - switch (request.method()) { - case GET: - return channel -> nodeClient.execute( - TenancyConfigRetrieveActions.INSTANCE, - new EmptyRequest(), - new RestToXContentListener<>(channel)); - case PUT: - return channel -> nodeClient.execute( - TenancyConfigUpdateAction.INSTANCE, - TenancyConfigUpdateRequest.fromXContent(request.contentParser()), - new RestToXContentListener<>(channel)); - default: - throw new RuntimeException("Not implemented"); - } - } - -} diff --git a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRetrieveActions.java b/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRetrieveActions.java deleted file mode 100644 index 796f233f13..0000000000 --- a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRetrieveActions.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.action.tenancy; - -import org.opensearch.action.ActionType; - -public class TenancyConfigRetrieveActions extends ActionType { - - public static final TenancyConfigRetrieveActions INSTANCE = new TenancyConfigRetrieveActions(); - public static final String NAME = "cluster:feature/tenancy/config/read"; - - protected TenancyConfigRetrieveActions() { - super(NAME, TenancyConfigRetrieveResponse::new); - } -} diff --git a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRetrieveResponse.java b/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRetrieveResponse.java deleted file mode 100644 index 463cc7d831..0000000000 --- a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRetrieveResponse.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.action.tenancy; - -import java.io.IOException; - -import org.opensearch.action.ActionResponse; -import org.opensearch.common.Strings; -import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.common.io.stream.StreamOutput; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; - -public class TenancyConfigRetrieveResponse extends ActionResponse implements ToXContentObject { - - public TenancyConfigs tenancyConfigs = new TenancyConfigs(); - - public TenancyConfigRetrieveResponse(final StreamInput in) throws IOException { - super(in); - this.tenancyConfigs.multitenancy_enabled = in.readOptionalBoolean(); - this.tenancyConfigs.private_tenant_enabled = in.readOptionalBoolean(); - this.tenancyConfigs.default_tenant = in.readOptionalString(); - } - - public TenancyConfigRetrieveResponse(final TenancyConfigs tenancyConfigs) { - this.tenancyConfigs = tenancyConfigs; - } - - public TenancyConfigs getMultitenancyConfig() { - return tenancyConfigs; - } - - public Boolean getMultitenancyEnabled() { return tenancyConfigs.multitenancy_enabled; } - - public Boolean getPrivateTenantEnabled() { return tenancyConfigs.private_tenant_enabled; } - - public String getDefaultTenant() { return tenancyConfigs.default_tenant; } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeBoolean(getMultitenancyEnabled()); - out.writeBoolean(getPrivateTenantEnabled()); - out.writeString(getDefaultTenant()); - } - - @Override - public String toString() { - return Strings.toString(XContentType.JSON, this, true, true); - } - - @Override - public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { - builder.startObject(); - builder.field("multitenancy_enabled", getMultitenancyEnabled()); - builder.field("private_tenant_enabled", getPrivateTenantEnabled()); - builder.field("default_tenant", getDefaultTenant()); - builder.endObject(); - return builder; - } -} diff --git a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRetrieveTransportAction.java b/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRetrieveTransportAction.java deleted file mode 100644 index a68bbae85e..0000000000 --- a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigRetrieveTransportAction.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.action.tenancy; - -import java.util.Collections; - -import org.opensearch.action.ActionListener; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.common.inject.Inject; -import org.opensearch.common.settings.Settings; -import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import org.opensearch.security.securityconf.impl.v7.ConfigV7; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -public class TenancyConfigRetrieveTransportAction - extends HandledTransportAction { - - private final ConfigurationRepository config; - - @Inject - public TenancyConfigRetrieveTransportAction(final Settings settings, - final TransportService transportService, - final ActionFilters actionFilters, - final ConfigurationRepository config) { - super(TenancyConfigRetrieveActions.NAME, transportService, actionFilters, EmptyRequest::new); - - this.config = config; - } - - /** Load the configuration from the security index and return a copy */ - protected final SecurityDynamicConfiguration load() { - return config.getConfigurationsFromIndex(Collections.singleton(CType.CONFIG), false).get(CType.CONFIG).deepClone(); - } - - @Override - protected void doExecute(final Task task, final EmptyRequest request, final ActionListener listener) { - - // Get the security configuration and lookup the config setting state - final SecurityDynamicConfiguration dynamicConfig = load(); - ConfigV7 config = (ConfigV7)dynamicConfig.getCEntry("config"); - - final TenancyConfigs tenancyConfigs= new TenancyConfigs(); - - tenancyConfigs.multitenancy_enabled = config.dynamic.kibana.multitenancy_enabled; - tenancyConfigs.private_tenant_enabled = config.dynamic.kibana.private_tenant_enabled; - tenancyConfigs.default_tenant = config.dynamic.kibana.default_tenant; - - listener.onResponse(new TenancyConfigRetrieveResponse(tenancyConfigs)); - } -} diff --git a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigUpdateAction.java b/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigUpdateAction.java deleted file mode 100644 index 73d515b7d8..0000000000 --- a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigUpdateAction.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.action.tenancy; - -import org.opensearch.action.ActionType; - -public class TenancyConfigUpdateAction extends ActionType { - - public static final TenancyConfigUpdateAction INSTANCE = new TenancyConfigUpdateAction(); - public static final String NAME = "cluster:feature/tenancy/config/update"; - - - protected TenancyConfigUpdateAction() - { - super(NAME, TenancyConfigRetrieveResponse::new); - } -} diff --git a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigUpdateRequest.java b/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigUpdateRequest.java deleted file mode 100644 index 2d03f698a6..0000000000 --- a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigUpdateRequest.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.action.tenancy; -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.core.ParseField; -import org.opensearch.core.xcontent.ConstructingObjectParser; -import org.opensearch.core.xcontent.XContentParser; - -public class TenancyConfigUpdateRequest extends ActionRequest { - - private TenancyConfigs tenancyConfigs = new TenancyConfigs(); - - public TenancyConfigUpdateRequest(final StreamInput in) throws IOException { - super(in); - in.readOptionalBoolean(); - in.readOptionalBoolean(); - in.readOptionalString(); - } - - public TenancyConfigUpdateRequest(final Boolean multitenancy_enabled, final Boolean private_tenant_enabled, final String default_tenant) { - super(); - this.tenancyConfigs.multitenancy_enabled = multitenancy_enabled; - this.tenancyConfigs.private_tenant_enabled = private_tenant_enabled; - this.tenancyConfigs.default_tenant = default_tenant; - } - - public TenancyConfigs getTenancyConfigs() { - return tenancyConfigs; - } - - @Override - public ActionRequestValidationException validate() { - if (getTenancyConfigs() == null) { - final ActionRequestValidationException validationException = new ActionRequestValidationException(); - validationException.addValidationError("Missing tenancy configs"); - return validationException; - } - return null; - } - - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - TenancyConfigUpdateRequest.class.getName(), - args -> new TenancyConfigUpdateRequest((Boolean)args[0], (Boolean) args[1], (String) args[2]) - ); - - static { - PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), new ParseField("multitenancy_enabled")); - PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), new ParseField("private_tenant_enabled")); - PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("default_tenant")); - - } - - public static TenancyConfigUpdateRequest fromXContent(final XContentParser parser) { - return PARSER.apply(parser, null); - } -} diff --git a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigUpdateTransportAction.java b/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigUpdateTransportAction.java deleted file mode 100644 index 1d4b563ca4..0000000000 --- a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigUpdateTransportAction.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.action.tenancy; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.action.ActionListener; -import org.opensearch.action.index.IndexResponse; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.client.Client; -import org.opensearch.common.inject.Inject; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.dlic.rest.api.AbstractApiAction; -import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import org.opensearch.security.securityconf.impl.v7.ConfigV7; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.tasks.Task; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.TransportService; - -public class TenancyConfigUpdateTransportAction extends HandledTransportAction { - - private static final Logger log = LogManager.getLogger(TenancyConfigUpdateTransportAction.class); - - private final String securityIndex; - private final ConfigurationRepository config; - private final Client client; - private final ThreadPool pool; - - @Inject - public TenancyConfigUpdateTransportAction(final Settings settings, - final TransportService transportService, - final ActionFilters actionFilters, - final ConfigurationRepository config, - final ThreadPool pool, - final Client client) { - super(TenancyConfigUpdateAction.NAME, transportService, actionFilters, TenancyConfigUpdateRequest::new); - - this.securityIndex = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); - - this.config = config; - this.client = client; - this.pool = pool; - } - - /** Load the configuration from the security index and return a copy */ - protected final SecurityDynamicConfiguration load() { - return config.getConfigurationsFromIndex(Collections.singleton(CType.CONFIG), false).get(CType.CONFIG).deepClone(); - } - - private Set getAcceptableDefaultTenants() { - Set acceptableDefaultTenants = new HashSet(); - acceptableDefaultTenants.add(ConfigConstants.TENANCY_GLOBAL_TENANT_DEFAULT_NAME); - acceptableDefaultTenants.add(ConfigConstants.TENANCY_GLOBAL_TENANT_NAME); - acceptableDefaultTenants.add(ConfigConstants.TENANCY_PRIVATE_TENANT_NAME); - return acceptableDefaultTenants; - } - - private Set getAllConfiguredTenantNames() { - - return this.config.getConfiguration(CType.TENANTS).getCEntries().keySet(); - } - - protected void validate(ConfigV7 updatedConfig) { - if(!updatedConfig.dynamic.kibana.private_tenant_enabled && (updatedConfig.dynamic.kibana.default_tenant).equals(ConfigConstants.TENANCY_PRIVATE_TENANT_NAME)) { - throw new IllegalArgumentException("Private tenant can not be disabled if it is the default tenant."); - } - - Set acceptableDefaultTenants = getAcceptableDefaultTenants(); - - if(acceptableDefaultTenants.contains(updatedConfig.dynamic.kibana.default_tenant)) { - return; - } - - Set availableTenants = getAllConfiguredTenantNames(); - - if(!availableTenants.contains(updatedConfig.dynamic.kibana.default_tenant)){ - throw new IllegalArgumentException(updatedConfig.dynamic.kibana.default_tenant + " can not be set to default tenant. Default tenant should be selected from one of the available tenants."); - } - - } - - @Override - protected void doExecute(final Task task, final TenancyConfigUpdateRequest request, final ActionListener listener) { - - // Get the current security config and prepare the config with the updated value - final SecurityDynamicConfiguration dynamicConfig = load(); - final ConfigV7 config = (ConfigV7)dynamicConfig.getCEntry("config"); - - final TenancyConfigs tenancyConfigs = request.getTenancyConfigs(); - if(tenancyConfigs.multitenancy_enabled != null) - { - config.dynamic.kibana.multitenancy_enabled = tenancyConfigs.multitenancy_enabled; - } - - if(tenancyConfigs.private_tenant_enabled != null) - { - config.dynamic.kibana.private_tenant_enabled = tenancyConfigs.private_tenant_enabled; - } - - if(tenancyConfigs.default_tenant != null) - { - config.dynamic.kibana.default_tenant = tenancyConfigs.default_tenant; - } - - validate(config); - - dynamicConfig.putCEntry("config", config); - - // When performing an update to the configuration run as admin - try (final ThreadContext.StoredContext stashedContext = pool.getThreadContext().stashContext()) { - // Update the security configuration and make sure the cluster has fully refreshed - AbstractApiAction.saveAndUpdateConfigs(this.securityIndex, this.client, CType.CONFIG, dynamicConfig, new ActionListener(){ - - @Override - public void onResponse(final IndexResponse response) { - // After processing the request, restore the user context - stashedContext.close(); - try { - // Lookup the current value and notify the listener - client.execute(TenancyConfigRetrieveActions.INSTANCE, new EmptyRequest(), listener); - } catch (IOException ioe) { - log.error(ioe); - listener.onFailure(ioe); - } - } - - @Override - public void onFailure(Exception e) { - log.error(e); - listener.onFailure(e); - } - }); - } - } -} diff --git a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigs.java b/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigs.java deleted file mode 100644 index 4e8fc41ef4..0000000000 --- a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigs.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.action.tenancy; - -public class TenancyConfigs { - public Boolean multitenancy_enabled; - public Boolean private_tenant_enabled; - public String default_tenant; -} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java new file mode 100644 index 0000000000..e5ec82c245 --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java @@ -0,0 +1,217 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import org.opensearch.action.index.IndexResponse; +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestController; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestStatus; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.dlic.rest.validation.AbstractConfigurationValidator; +import org.opensearch.security.dlic.rest.validation.MultiTenancyConfigValidator; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; +import org.opensearch.security.ssl.transport.PrincipalExtractor; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.threadpool.ThreadPool; + +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.PUT; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + + +public class MultiTenancyConfigApiAction extends AbstractApiAction { + + private static final List ROUTES = addRoutesPrefix( + ImmutableList.of( + new Route(GET, "/tenancy/config"), + new Route(PUT, "/tenancy/config") + ) + ); + + private final static Set ACCEPTABLE_DEFAULT_TENANTS = ImmutableSet.of( + ConfigConstants.TENANCY_GLOBAL_TENANT_DEFAULT_NAME, + ConfigConstants.TENANCY_GLOBAL_TENANT_NAME, + ConfigConstants.TENANCY_PRIVATE_TENANT_NAME + ); + + @Override + public String getName() { + return "Multi Tenancy actions to Retrieve / Update configs."; + } + + @Override + public List routes() { + return ROUTES; + } + + public MultiTenancyConfigApiAction( + final Settings settings, final Path configPath, + final RestController controller, final Client client, + final AdminDNs adminDNs, final ConfigurationRepository cl, + final ClusterService cs, final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, final ThreadPool threadPool, + final AuditLog auditLog) { + super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + } + + @Override + protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... params) { + return new MultiTenancyConfigValidator(request, ref, settings, params); + } + + @Override + protected Endpoint getEndpoint() { + return Endpoint.TENANTS; + } + + @Override + protected String getResourceName() { + return null; + } + + @Override + protected CType getConfigName() { + return CType.CONFIG; + } + + @Override + protected void handleDelete(final RestChannel channel, + final RestRequest request, + final Client client, + final JsonNode content) throws IOException { + notImplemented(channel, RestRequest.Method.DELETE); + } + + private void multitenancyResponse(final ConfigV7 config, final RestChannel channel) { + try (final XContentBuilder contentBuilder = channel.newBuilder()) { + channel.sendResponse( + new BytesRestResponse( + RestStatus.OK, + contentBuilder + .startObject() + .field( + MultiTenancyConfigValidator.DEFAULT_TENANT_JSON_PROPERTY, + config.dynamic.kibana.default_tenant + ).field( + MultiTenancyConfigValidator.PRIVATE_TENANT_ENABLED_JSON_PROPERTY, + config.dynamic.kibana.private_tenant_enabled + ).field( + MultiTenancyConfigValidator.MULTITENANCY_ENABLED_JSON_PROPERTY, + config.dynamic.kibana.multitenancy_enabled + ).endObject() + ) + ); + } catch (final Exception e) { + internalErrorResponse(channel, e.getMessage()); + log.error("Error handle request ", e); + } + } + + + @Override + protected void handleGet(final RestChannel channel, + final RestRequest request, + final Client client, + final JsonNode content) throws IOException { + final SecurityDynamicConfiguration dynamicConfiguration = load(CType.CONFIG, false); + final ConfigV7 config = (ConfigV7) dynamicConfiguration.getCEntry(CType.CONFIG.toLCString()); + multitenancyResponse(config, channel); + } + + @Override + protected void handlePut(final RestChannel channel, + final RestRequest request, + final Client client, + final JsonNode content) throws IOException { + final SecurityDynamicConfiguration dynamicConfiguration = (SecurityDynamicConfiguration) + load(CType.CONFIG, false); + final ConfigV7 config = dynamicConfiguration.getCEntry(CType.CONFIG.toLCString()); + updateAndValidatesValues(config, content); + dynamicConfiguration.putCEntry(CType.CONFIG.toLCString(), config); + saveAndUpdateConfigs( + this.securityIndexName, + client, + getConfigName(), + dynamicConfiguration, + new OnSucessActionListener<>(channel) { + @Override + public void onResponse(IndexResponse response) { + multitenancyResponse(config, channel); + } + } + ); + } + + private void updateAndValidatesValues(final ConfigV7 config, final JsonNode jsonContent) { + if (Objects.nonNull(jsonContent.findValue(MultiTenancyConfigValidator.DEFAULT_TENANT_JSON_PROPERTY))) { + config.dynamic.kibana.default_tenant = + jsonContent.findValue(MultiTenancyConfigValidator.DEFAULT_TENANT_JSON_PROPERTY).asText(); + } + if (Objects.nonNull(jsonContent.findValue(MultiTenancyConfigValidator.PRIVATE_TENANT_ENABLED_JSON_PROPERTY))) { + config.dynamic.kibana.private_tenant_enabled = + jsonContent.findValue(MultiTenancyConfigValidator.PRIVATE_TENANT_ENABLED_JSON_PROPERTY).booleanValue(); + } + if (Objects.nonNull(jsonContent.findValue(MultiTenancyConfigValidator.MULTITENANCY_ENABLED_JSON_PROPERTY))) { + config.dynamic.kibana.multitenancy_enabled = + jsonContent.findValue(MultiTenancyConfigValidator.MULTITENANCY_ENABLED_JSON_PROPERTY).asBoolean(); + } + final String defaultTenant = + Optional.ofNullable(config.dynamic.kibana.default_tenant) + .map(String::toLowerCase) + .orElse(""); + + if (!config.dynamic.kibana.private_tenant_enabled + && ConfigConstants.TENANCY_PRIVATE_TENANT_NAME.equals(defaultTenant)) { + throw new IllegalArgumentException("Private tenant can not be disabled if it is the default tenant."); + } + + if (ACCEPTABLE_DEFAULT_TENANTS.contains(defaultTenant)) { + return; + } + + final Set availableTenants = + cl.getConfiguration(CType.TENANTS) + .getCEntries() + .keySet() + .stream() + .map(String::toLowerCase) + .collect(Collectors.toSet()); + if (!availableTenants.contains(defaultTenant)) { + throw new IllegalArgumentException(config.dynamic.kibana.default_tenant + " can not be set to default tenant. Default tenant should be selected from one of the available tenants."); + } + } + +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index e73d28e865..cd489cab4d 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -64,6 +64,7 @@ public static Collection getHandler(final Settings settings, handlers.add(new WhitelistApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new AllowlistApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new AuditApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); + handlers.add(new MultiTenancyConfigApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new SecuritySSLCertsAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog, securityKeyStore, certificatesReloadEnabled)); return Collections.unmodifiableCollection(handlers); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/MultiTenancyConfigValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/MultiTenancyConfigValidator.java new file mode 100644 index 0000000000..42870f1c13 --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/MultiTenancyConfigValidator.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.dlic.rest.validation; + +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.settings.Settings; +import org.opensearch.rest.RestRequest; + +public class MultiTenancyConfigValidator extends AbstractConfigurationValidator { + + public static final String DEFAULT_TENANT_JSON_PROPERTY = "default_tenant"; + public static final String PRIVATE_TENANT_ENABLED_JSON_PROPERTY = "private_tenant_enabled"; + public static final String MULTITENANCY_ENABLED_JSON_PROPERTY = "multitenancy_enabled"; + + + public MultiTenancyConfigValidator(RestRequest request, BytesReference ref, Settings opensearchSettings, Object... param) { + super(request, ref, opensearchSettings, param); + this.payloadMandatory = true; + allowedKeys.put(DEFAULT_TENANT_JSON_PROPERTY, DataType.STRING); + allowedKeys.put(PRIVATE_TENANT_ENABLED_JSON_PROPERTY, DataType.BOOLEAN); + allowedKeys.put(MULTITENANCY_ENABLED_JSON_PROPERTY, DataType.BOOLEAN); + } + +} diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 7ed780f413..a58639bc0a 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -290,8 +290,8 @@ public enum RolesMappingResolution { public static final String SECURITY_SYSTEM_INDICES_KEY = "plugins.security.system_indices.indices"; public static final List SECURITY_SYSTEM_INDICES_DEFAULT = Collections.emptyList(); - public static final String TENANCY_PRIVATE_TENANT_NAME = "Private"; - public static final String TENANCY_GLOBAL_TENANT_NAME = "Global"; + public static final String TENANCY_PRIVATE_TENANT_NAME = "private"; + public static final String TENANCY_GLOBAL_TENANT_NAME = "global"; public static final String TENANCY_GLOBAL_TENANT_DEFAULT_NAME = ""; public static Set getSettingAsSet(final Settings settings, final String key, final List defaultList, final boolean ignoreCaseForNone) { diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiTest.java new file mode 100644 index 0000000000..24aa8737c6 --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiTest.java @@ -0,0 +1,170 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.rest.api; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpStatus; +import org.junit.Test; + +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.core.StringContains.containsString; + +public class MultiTenancyConfigApiTest extends AbstractRestApiUnitTest { + + private static final Header ADMIN_FULL_ACCESS_USER = encodeBasicHeader("admin_all_access", "admin_all_access"); + private static final Header USER_NO_REST_API_ACCESS = encodeBasicHeader("admin", "admin"); + + private void verifyTenantUpdate(final Header... header) throws Exception { + final HttpResponse getSettingResponse = rh.executeGetRequest("/_plugins/_security/api/tenancy/config", header); + assertThat(getSettingResponse.getBody(), getSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + assertThat( + getSettingResponse.getBody(), + getSettingResponse.findValueInJson("default_tenant"), + equalTo(ConfigConstants.TENANCY_GLOBAL_TENANT_DEFAULT_NAME) + ); + + HttpResponse getDashboardsinfoResponse = rh.executeGetRequest("/_plugins/_security/dashboardsinfo", header); + assertThat(getDashboardsinfoResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + assertThat( + getDashboardsinfoResponse.getBody(), + getDashboardsinfoResponse.findValueInJson("default_tenant"), + equalTo(ConfigConstants.TENANCY_GLOBAL_TENANT_DEFAULT_NAME) + ); + + final HttpResponse setPrivateTenantAsDefaultResponse = rh.executePutRequest( + "/_plugins/_security/api/tenancy/config", + "{\"default_tenant\": \"Private\"}", header + ); + assertThat( + setPrivateTenantAsDefaultResponse.getBody(), + setPrivateTenantAsDefaultResponse.getStatusCode(), + equalTo(HttpStatus.SC_OK) + ); + getDashboardsinfoResponse = rh.executeGetRequest("/_plugins/_security/dashboardsinfo", ADMIN_FULL_ACCESS_USER); + assertThat(getDashboardsinfoResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + assertThat(getDashboardsinfoResponse.findValueInJson("default_tenant"), equalTo("Private")); + } + + @Test + public void testUpdateSuperAdmin() throws Exception { + setupWithRestRoles(); + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = true; + verifyTenantUpdate(); + } + + @Test + public void testUpdateRestAPIAdmin() throws Exception { + setupWithRestRoles(); + rh.sendAdminCertificate = false; + verifyTenantUpdate(ADMIN_FULL_ACCESS_USER); + } + + + private void verifyTenantUpdateFailed(final Header... header) throws Exception { + final HttpResponse disablePrivateTenantResponse = rh.executePutRequest( + "/_plugins/_security/api/tenancy/config", + "{\"private_tenant_enabled\":false}", header + ); + assertThat(disablePrivateTenantResponse.getBody(), disablePrivateTenantResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + final HttpResponse setPrivateTenantAsDefaultFailResponse = rh.executePutRequest( + "/_plugins/_security/api/tenancy/config", + "{\"default_tenant\": \"Private\"}", header + ); + assertThat(setPrivateTenantAsDefaultFailResponse.getBody(), setPrivateTenantAsDefaultFailResponse.getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); + assertThat( + setPrivateTenantAsDefaultFailResponse.getBody(), + setPrivateTenantAsDefaultFailResponse.findValueInJson("error.reason"), + containsString("Private tenant can not be disabled if it is the default tenant.") + ); + + final HttpResponse enablePrivateTenantResponse = rh.executePutRequest( + "/_plugins/_security/api/tenancy/config", + "{\"private_tenant_enabled\":true}", + header + ); + assertThat(enablePrivateTenantResponse.getBody(), enablePrivateTenantResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + final HttpResponse setPrivateTenantAsDefaultResponse = rh.executePutRequest( + "/_plugins/_security/api/tenancy/config", + "{\"default_tenant\": \"Private\"}", + header + ); + assertThat(setPrivateTenantAsDefaultResponse.getBody(), setPrivateTenantAsDefaultResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + final HttpResponse updatePrivateSettingResponse = + rh.executePutRequest("/_plugins/_security/api/tenancy/config", "{\"private_tenant_enabled\":false}", header); + assertThat(updatePrivateSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); + assertThat(updatePrivateSettingResponse.findValueInJson("error.reason"), containsString("Private tenant can not be disabled if it is the default tenant.")); + + final HttpResponse getSettingResponseAfterUpdate = rh.executeGetRequest("/_plugins/_security/api/tenancy/config", header); + assertThat(getSettingResponseAfterUpdate.getBody(), getSettingResponseAfterUpdate.getStatusCode(), equalTo(HttpStatus.SC_OK)); + assertThat( + getSettingResponseAfterUpdate.getBody(), + getSettingResponseAfterUpdate.findValueInJson("default_tenant"), + equalTo("Private") + ); + + final HttpResponse getDashboardsinfoResponse = rh.executeGetRequest("/_plugins/_security/dashboardsinfo", header); + assertThat( + getDashboardsinfoResponse.getBody(), + getDashboardsinfoResponse.findValueInJson("default_tenant"), + equalTo("Private") + ); + + final HttpResponse setRandomStringAsDefaultTenant = rh.executePutRequest( + "/_plugins/_security/api/tenancy/config", + "{\"default_tenant\": \"NonExistentTenant\"}", + header + ); + assertThat(setRandomStringAsDefaultTenant.getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); + assertThat(setPrivateTenantAsDefaultFailResponse.getBody(), + setRandomStringAsDefaultTenant.findValueInJson("error.reason"), + containsString("Default tenant should be selected from one of the available tenants.") + ); + } + + @Test + public void testDefaultTenantUpdateFailedSuperAdmin() throws Exception { + setupWithRestRoles(); + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = true; + verifyTenantUpdateFailed(); + } + + @Test + public void testDefaultTenantUpdateFailedRestAPIAdmin() throws Exception { + setupWithRestRoles(); + rh.sendAdminCertificate = false; + verifyTenantUpdateFailed(ADMIN_FULL_ACCESS_USER); + } + + @Test + public void testForbiddenAccess() throws Exception { + setupWithRestRoles(); + + rh.sendAdminCertificate = false; + HttpResponse getSettingResponse = rh.executeGetRequest("/_plugins/_security/api/tenancy/config", USER_NO_REST_API_ACCESS); + assertThat(getSettingResponse.getBody(), getSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + HttpResponse updateSettingResponse = rh.executePutRequest( + "/_plugins/_security/api/tenancy/config", + "{\"default_tenant\": \"Private\"}", USER_NO_REST_API_ACCESS + ); + assertThat(getSettingResponse.getBody(), updateSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + } + + +} diff --git a/src/test/java/org/opensearch/security/multitenancy/test/TenancyDefaultTenantTests.java b/src/test/java/org/opensearch/security/multitenancy/test/TenancyDefaultTenantTests.java deleted file mode 100644 index b4d6aa4b59..0000000000 --- a/src/test/java/org/opensearch/security/multitenancy/test/TenancyDefaultTenantTests.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.multitenancy.test; - -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; -import org.junit.Test; - -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.test.SingleClusterTest; -import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.hamcrest.core.StringContains.containsString; - -public class TenancyDefaultTenantTests extends SingleClusterTest { - private final Header asAdminUser = encodeBasicHeader("admin", "admin"); - private final Header asUser = encodeBasicHeader("kirk", "kirk"); - - @Override - protected String getResourceFolder() { - return "multitenancy"; - } - - @Test - public void testDefaultTenantUpdate() throws Exception { - setup(); - - final HttpResponse getSettingResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/api/tenancy/config", asAdminUser); - assertThat(getSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); - assertThat(getSettingResponse.findValueInJson("default_tenant"), equalTo(ConfigConstants.TENANCY_GLOBAL_TENANT_DEFAULT_NAME)); - - HttpResponse getDashboardsinfoResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/dashboardsinfo", asAdminUser); - assertThat(getDashboardsinfoResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); - assertThat(getDashboardsinfoResponse.findValueInJson("default_tenant"), equalTo(ConfigConstants.TENANCY_GLOBAL_TENANT_DEFAULT_NAME)); - - final HttpResponse setPrivateTenantAsDefaultResponse = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"default_tenant\": \"Private\"}", asAdminUser); - assertThat(setPrivateTenantAsDefaultResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); - getDashboardsinfoResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/dashboardsinfo", asAdminUser); - assertThat(getDashboardsinfoResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); - assertThat(getDashboardsinfoResponse.findValueInJson("default_tenant"), equalTo(ConfigConstants.TENANCY_PRIVATE_TENANT_NAME)); - } - - @Test - public void testDefaultTenant_UpdateFailed() throws Exception { - setup(); - - final HttpResponse disablePrivateTenantResponse = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"private_tenant_enabled\":false}", asAdminUser); - assertThat(disablePrivateTenantResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); - - - final HttpResponse setPrivateTenantAsDefaultFailResponse = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"default_tenant\": \"Private\"}", asAdminUser); - assertThat(setPrivateTenantAsDefaultFailResponse.getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); - assertThat(setPrivateTenantAsDefaultFailResponse.findValueInJson("error.reason"), containsString("Private tenant can not be disabled if it is the default tenant.")); - - final HttpResponse enablePrivateTenantResponse = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"private_tenant_enabled\":true}", asAdminUser); - assertThat(enablePrivateTenantResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); - - final HttpResponse setPrivateTenantAsDefaultResponse = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"default_tenant\": \"Private\"}", asAdminUser); - assertThat(setPrivateTenantAsDefaultResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); - - final HttpResponse getSettingResponseAfterUpdate = nonSslRestHelper().executeGetRequest("/_plugins/_security/api/tenancy/config", asAdminUser); - assertThat(getSettingResponseAfterUpdate.getStatusCode(), equalTo(HttpStatus.SC_OK)); - assertThat(getSettingResponseAfterUpdate.findValueInJson("default_tenant"), equalTo(ConfigConstants.TENANCY_PRIVATE_TENANT_NAME)); - - HttpResponse getDashboardsinfoResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/dashboardsinfo", asAdminUser); - assertThat(getDashboardsinfoResponse.findValueInJson("default_tenant"),equalTo(ConfigConstants.TENANCY_PRIVATE_TENANT_NAME)); - - final HttpResponse setRandomStringAsDefaultTenant = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"default_tenant\": \"NonExistentTenant\"}", asAdminUser); - assertThat(setRandomStringAsDefaultTenant.getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); - assertThat(setRandomStringAsDefaultTenant.findValueInJson("error.reason"), containsString("Default tenant should be selected from one of the available tenants.")); - - } - @Test - public void testForbiddenAccess() throws Exception { - setup(); - - final HttpResponse getSettingResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/api/tenancy/config", asUser); - assertThat(getSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); - assertThat(getSettingResponse.findValueInJson("error.reason"), containsString("no permissions for [cluster:feature/tenancy/config/read]")); - - final HttpResponse updateSettingResponse = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"default_tenant\": \"Private\"}", asUser); - assertThat(updateSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); - assertThat(updateSettingResponse.findValueInJson("error.reason"), containsString("no permissions for [cluster:feature/tenancy/config/update]")); - } -} diff --git a/src/test/java/org/opensearch/security/multitenancy/test/TenancyMultitenancyEnabledTests.java b/src/test/java/org/opensearch/security/multitenancy/test/TenancyMultitenancyEnabledTests.java index 033d38ed41..bd9664a84c 100644 --- a/src/test/java/org/opensearch/security/multitenancy/test/TenancyMultitenancyEnabledTests.java +++ b/src/test/java/org/opensearch/security/multitenancy/test/TenancyMultitenancyEnabledTests.java @@ -16,17 +16,19 @@ import org.apache.hc.core5.http.message.BasicHeader; import org.junit.Test; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.test.DynamicSecurityConfig; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsEqual.equalTo; -import static org.hamcrest.core.StringContains.containsString; public class TenancyMultitenancyEnabledTests extends SingleClusterTest { - private final Header asAdminUser = encodeBasicHeader("admin", "admin"); - private final Header asUser = encodeBasicHeader("kirk", "kirk"); - private final Header onUserTenant = new BasicHeader("securitytenant", "__user__"); + + private static final Header AS_REST_API_USER = encodeBasicHeader("user_rest_api_access", "user_rest_api_access"); + private static final Header AS_USER = encodeBasicHeader("admin", "admin"); + private static final Header ON_USER_TENANT = new BasicHeader("securitytenant", "__user__"); private static String createIndexPatternDoc(final String title) { return "{"+ @@ -44,46 +46,37 @@ protected String getResourceFolder() { @Test public void testMultitenancyDisabled_endToEndTest() throws Exception { - setup(); + setup(Settings.EMPTY, + new DynamicSecurityConfig(), + Settings.builder().put("plugins.security.restapi.roles_enabled.0", "security_rest_api_access").build(), + true); - final HttpResponse getSettingResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/api/tenancy/config", asAdminUser); + final HttpResponse getSettingResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/api/tenancy/config", AS_REST_API_USER); assertThat(getSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); assertThat(getSettingResponse.findValueInJson("multitenancy_enabled"), equalTo("true")); - HttpResponse getDashboardsinfoResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/dashboardsinfo", asAdminUser); + HttpResponse getDashboardsinfoResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/dashboardsinfo", AS_USER); assertThat(getDashboardsinfoResponse.findValueInJson("multitenancy_enabled"),equalTo("true")); - final HttpResponse createDocInGlobalTenantResponse = nonSslRestHelper().executePostRequest(".kibana/_doc?refresh=true", createIndexPatternDoc("globalIndex"), asAdminUser); + final HttpResponse createDocInGlobalTenantResponse = nonSslRestHelper().executePostRequest(".kibana/_doc?refresh=true", createIndexPatternDoc("globalIndex"), AS_USER); assertThat(createDocInGlobalTenantResponse.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); - final HttpResponse createDocInUserTenantResponse = nonSslRestHelper().executePostRequest(".kibana/_doc?refresh=true", createIndexPatternDoc("userIndex"), onUserTenant, asAdminUser); + final HttpResponse createDocInUserTenantResponse = nonSslRestHelper().executePostRequest(".kibana/_doc?refresh=true", createIndexPatternDoc("userIndex"), ON_USER_TENANT, AS_USER); assertThat(createDocInUserTenantResponse.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); - final HttpResponse searchInUserTenantWithMutlitenancyEnabled = nonSslRestHelper().executeGetRequest(".kibana/_search", onUserTenant, asAdminUser); + final HttpResponse searchInUserTenantWithMutlitenancyEnabled = nonSslRestHelper().executeGetRequest(".kibana/_search", ON_USER_TENANT, AS_USER); assertThat(searchInUserTenantWithMutlitenancyEnabled.getStatusCode(), equalTo(HttpStatus.SC_OK)); assertThat(searchInUserTenantWithMutlitenancyEnabled.findValueInJson("hits.hits[0]._source.index-pattern.title"), equalTo("userIndex")); - final HttpResponse updateMutlitenancyToDisabled = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"multitenancy_enabled\": \"false\"}", asAdminUser); + final HttpResponse updateMutlitenancyToDisabled = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"multitenancy_enabled\": \"false\"}", AS_REST_API_USER); assertThat(updateMutlitenancyToDisabled.getStatusCode(), equalTo(HttpStatus.SC_OK)); assertThat(updateMutlitenancyToDisabled.findValueInJson("multitenancy_enabled"), equalTo("false")); - getDashboardsinfoResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/dashboardsinfo", asAdminUser); + getDashboardsinfoResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/dashboardsinfo", AS_USER); assertThat(getDashboardsinfoResponse.findValueInJson("multitenancy_enabled"),equalTo("false")); - final HttpResponse searchInUserTenantWithMutlitenancyDisabled = nonSslRestHelper().executeGetRequest(".kibana/_search", onUserTenant, asAdminUser); + final HttpResponse searchInUserTenantWithMutlitenancyDisabled = nonSslRestHelper().executeGetRequest(".kibana/_search", ON_USER_TENANT, AS_USER); assertThat(searchInUserTenantWithMutlitenancyDisabled.getStatusCode(), equalTo(HttpStatus.SC_OK)); assertThat(searchInUserTenantWithMutlitenancyDisabled.findValueInJson("hits.hits[0]._source.index-pattern.title"), equalTo("globalIndex")); } - @Test - public void testForbiddenAccess() throws Exception { - setup(); - - final HttpResponse getSettingResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/api/tenancy/config", asUser); - assertThat(getSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); - assertThat(getSettingResponse.findValueInJson("error.reason"), containsString("no permissions for [cluster:feature/tenancy/config/read]")); - - final HttpResponse updateSettingResponse = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"multitenancy_enabled\": \"false\"}", asUser); - assertThat(updateSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); - assertThat(updateSettingResponse.findValueInJson("error.reason"), containsString("no permissions for [cluster:feature/tenancy/config/update]")); - } } diff --git a/src/test/java/org/opensearch/security/multitenancy/test/TenancyPrivateTenantEnabledTests.java b/src/test/java/org/opensearch/security/multitenancy/test/TenancyPrivateTenantEnabledTests.java index c6927dbbf8..599586239c 100644 --- a/src/test/java/org/opensearch/security/multitenancy/test/TenancyPrivateTenantEnabledTests.java +++ b/src/test/java/org/opensearch/security/multitenancy/test/TenancyPrivateTenantEnabledTests.java @@ -16,6 +16,8 @@ import org.apache.hc.core5.http.message.BasicHeader; import org.junit.Test; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.test.DynamicSecurityConfig; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; @@ -24,9 +26,10 @@ import static org.hamcrest.core.StringContains.containsString; public class TenancyPrivateTenantEnabledTests extends SingleClusterTest { - private final Header asAdminUser = encodeBasicHeader("admin", "admin"); - private final Header asUser = encodeBasicHeader("kirk", "kirk"); - private final Header onUserTenant = new BasicHeader("securitytenant", "__user__"); + private static final Header AS_REST_API_USER = encodeBasicHeader("user_rest_api_access", "user_rest_api_access"); + private static final Header AS_ADMIN_USER = encodeBasicHeader("admin", "admin"); + private static final Header AS_USER = encodeBasicHeader("kirk", "kirk"); + private static final Header ON_USER_TENANT = new BasicHeader("securitytenant", "__user__"); private static String createIndexPatternDoc(final String title) { return "{"+ @@ -43,59 +46,39 @@ protected String getResourceFolder() { } @Test - public void testPrivateTenantDisabled_Update() throws Exception { - setup(); + public void testPrivateTenantDisabled_Update_EndToEnd() throws Exception { + setup(Settings.EMPTY, + new DynamicSecurityConfig(), + Settings.builder().put("plugins.security.restapi.roles_enabled.0", "security_rest_api_access").build(), + true); - final HttpResponse getSettingResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/api/tenancy/config", asAdminUser); + final HttpResponse getSettingResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/api/tenancy/config", AS_REST_API_USER); assertThat(getSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); assertThat(getSettingResponse.findValueInJson("private_tenant_enabled"), equalTo("true")); - HttpResponse getDashboardsinfoResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/dashboardsinfo", asAdminUser); + HttpResponse getDashboardsinfoResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/dashboardsinfo", AS_ADMIN_USER); assertThat(getDashboardsinfoResponse.findValueInJson("private_tenant_enabled"), equalTo("true")); - final HttpResponse createDocInGlobalTenantResponse = nonSslRestHelper().executePostRequest(".kibana/_doc?refresh=true", createIndexPatternDoc("globalIndex"), asAdminUser); + final HttpResponse createDocInGlobalTenantResponse = nonSslRestHelper().executePostRequest(".kibana/_doc?refresh=true", createIndexPatternDoc("globalIndex"), AS_ADMIN_USER); assertThat(createDocInGlobalTenantResponse.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); - final HttpResponse createDocInUserTenantResponse = nonSslRestHelper().executePostRequest(".kibana/_doc?refresh=true", createIndexPatternDoc("userIndex"), onUserTenant, asUser); + final HttpResponse createDocInUserTenantResponse = nonSslRestHelper().executePostRequest(".kibana/_doc?refresh=true", createIndexPatternDoc("userIndex"), ON_USER_TENANT, AS_USER); assertThat(createDocInUserTenantResponse.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); - final HttpResponse searchInUserTenantWithPrivateTenantEnabled = nonSslRestHelper().executeGetRequest(".kibana/_search", onUserTenant, asUser); + final HttpResponse searchInUserTenantWithPrivateTenantEnabled = nonSslRestHelper().executeGetRequest(".kibana/_search", ON_USER_TENANT, AS_USER); assertThat(searchInUserTenantWithPrivateTenantEnabled.getStatusCode(), equalTo(HttpStatus.SC_OK)); assertThat(searchInUserTenantWithPrivateTenantEnabled.findValueInJson("hits.hits[0]._source.index-pattern.title"), equalTo("userIndex")); - final HttpResponse disablePrivateTenantResponse = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"private_tenant_enabled\": \"false\"}", asAdminUser); + final HttpResponse disablePrivateTenantResponse = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"private_tenant_enabled\": \"false\"}", AS_REST_API_USER); assertThat(disablePrivateTenantResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); assertThat(disablePrivateTenantResponse.findValueInJson("private_tenant_enabled"), equalTo("false")); - getDashboardsinfoResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/dashboardsinfo", asAdminUser); + getDashboardsinfoResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/dashboardsinfo", AS_ADMIN_USER); assertThat(getDashboardsinfoResponse.findValueInJson("private_tenant_enabled"),equalTo("false")); - final HttpResponse searchInUserTenantWithPrivateTenantDisabled = nonSslRestHelper().executeGetRequest(".kibana/_search", onUserTenant, asUser); + final HttpResponse searchInUserTenantWithPrivateTenantDisabled = nonSslRestHelper().executeGetRequest(".kibana/_search", ON_USER_TENANT, AS_USER); assertThat(searchInUserTenantWithPrivateTenantDisabled.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); assertThat(searchInUserTenantWithPrivateTenantDisabled.findValueInJson("error.reason"), containsString("no permissions for [indices:data/read/search] and User")); } - @Test - public void testPrivateTenantDisabled_UpdateFailed() throws Exception { - setup(); - - final HttpResponse setPrivateTenantAsDefaultResponse = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"default_tenant\": \"Private\"}", asAdminUser); - assertThat(setPrivateTenantAsDefaultResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); - final HttpResponse updateSettingResponse = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"private_tenant_enabled\":false}", asAdminUser); - assertThat(updateSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); - assertThat(updateSettingResponse.findValueInJson("error.reason"), containsString("Private tenant can not be disabled if it is the default tenant.")); - } - - @Test - public void testForbiddenAccess() throws Exception { - setup(); - - final HttpResponse getSettingResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/api/tenancy/config", asUser); - assertThat(getSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); - assertThat(getSettingResponse.findValueInJson("error.reason"), containsString("no permissions for [cluster:feature/tenancy/config/read]")); - - final HttpResponse updateSettingResponse = nonSslRestHelper().executePutRequest("/_plugins/_security/api/tenancy/config", "{\"private_tenant_enabled\": false}", asUser); - assertThat(updateSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); - assertThat(updateSettingResponse.findValueInJson("error.reason"), containsString("no permissions for [cluster:feature/tenancy/config/update]")); - } } diff --git a/src/test/resources/multitenancy/internal_users.yml b/src/test/resources/multitenancy/internal_users.yml index 809d268710..af96b2b8ee 100644 --- a/src/test/resources/multitenancy/internal_users.yml +++ b/src/test/resources/multitenancy/internal_users.yml @@ -156,3 +156,9 @@ user_tenant_parameters_substitution: attributes: attribute1: "tenant_parameters_substitution" description: "PR# 819 / Issue#817" +user_rest_api_access: + hash: "$2y$12$aHkyhk95XbrMCByYYVAlrek1thXpTDuVKJW01vdLYPh6kyR36j7x6" + reserved: false + hidden: false + backend_roles: [] + description: "REST API Access" diff --git a/src/test/resources/multitenancy/roles.yml b/src/test/resources/multitenancy/roles.yml index efc17f7464..1baa77cc8b 100644 --- a/src/test/resources/multitenancy/roles.yml +++ b/src/test/resources/multitenancy/roles.yml @@ -2,6 +2,8 @@ _meta: type: "roles" config_version: 2 +security_rest_api_access: + reserved: true opendistro_security_own_index: reserved: false hidden: false diff --git a/src/test/resources/multitenancy/roles_mapping.yml b/src/test/resources/multitenancy/roles_mapping.yml index a7d2867db1..397171a360 100644 --- a/src/test/resources/multitenancy/roles_mapping.yml +++ b/src/test/resources/multitenancy/roles_mapping.yml @@ -2,6 +2,11 @@ _meta: type: "rolesmapping" config_version: 2 +security_rest_api_access: + reserved: false + hidden: false + users: + - "user_rest_api_access" opendistro_security_human_resources: reserved: false hidden: false