diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index ce64299f13..ffb74021d3 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -115,6 +115,11 @@ 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.MultiTenancyRetrieveAction; +import org.opensearch.security.action.tenancy.MultiTenancyRetrieveTransportAction; +import org.opensearch.security.action.tenancy.MultiTenancyUpdateAction; +import org.opensearch.security.action.tenancy.MultiTenancyUpdateTransportAction; +import org.opensearch.security.action.tenancy.MutliTenancyRestHandler; import org.opensearch.security.action.whoami.TransportWhoAmIAction; import org.opensearch.security.action.whoami.WhoAmIAction; import org.opensearch.security.auditlog.AuditLog; @@ -469,6 +474,7 @@ 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 MutliTenancyRestHandler()); handlers.addAll( SecurityRestApiActions.getHandler( settings, @@ -505,6 +511,9 @@ 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<>(MultiTenancyRetrieveAction.INSTANCE, MultiTenancyRetrieveTransportAction.class)); + actions.add(new ActionHandler<>(MultiTenancyUpdateAction.INSTANCE, MultiTenancyUpdateTransportAction.class)); } return actions; } diff --git a/src/main/java/org/opensearch/security/action/tenancy/BooleanSettingRetrieveResponse.java b/src/main/java/org/opensearch/security/action/tenancy/BooleanSettingRetrieveResponse.java new file mode 100644 index 0000000000..d69ccbee30 --- /dev/null +++ b/src/main/java/org/opensearch/security/action/tenancy/BooleanSettingRetrieveResponse.java @@ -0,0 +1,58 @@ +/* + * 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 BooleanSettingRetrieveResponse extends ActionResponse implements ToXContentObject { + + private Boolean value; + + public BooleanSettingRetrieveResponse(final StreamInput in) throws IOException { + super(in); + this.value = in.readBoolean(); + } + + public BooleanSettingRetrieveResponse(final Boolean value) { + this.value = value; + } + + public Boolean getValue() { + return value; + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeBoolean(getValue()); + } + + @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("value", getValue()); + builder.endObject(); + return builder; + } +} diff --git a/src/main/java/org/opensearch/security/action/tenancy/BooleanSettingUpdateRequest.java b/src/main/java/org/opensearch/security/action/tenancy/BooleanSettingUpdateRequest.java new file mode 100644 index 0000000000..6b83b0c218 --- /dev/null +++ b/src/main/java/org/opensearch/security/action/tenancy/BooleanSettingUpdateRequest.java @@ -0,0 +1,63 @@ +/* + * 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 BooleanSettingUpdateRequest extends ActionRequest { + + private Boolean value; + + public BooleanSettingUpdateRequest(final StreamInput in) throws IOException { + super(in); + in.readBoolean(); + } + + public BooleanSettingUpdateRequest(final Boolean value) { + super(); + this.value = value; + } + + public Boolean getValue() { + return value; + } + + @Override + public ActionRequestValidationException validate() { + if (getValue() == null) { + final ActionRequestValidationException validationException = new ActionRequestValidationException(); + validationException.addValidationError("Missing boolean value"); + return validationException; + } + return null; + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + BooleanSettingUpdateRequest.class.getName(), + args -> new BooleanSettingUpdateRequest((Boolean) args[0]) + ); + + static { + PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), new ParseField("value")); + } + + public static BooleanSettingUpdateRequest fromXContent(final XContentParser parser) { + return PARSER.apply(parser, null); + } +} diff --git a/src/main/java/org/opensearch/security/action/tenancy/EmptyRequest.java b/src/main/java/org/opensearch/security/action/tenancy/EmptyRequest.java new file mode 100644 index 0000000000..d66b62199f --- /dev/null +++ b/src/main/java/org/opensearch/security/action/tenancy/EmptyRequest.java @@ -0,0 +1,34 @@ +/* + * 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/MultiTenancyRetrieveAction.java b/src/main/java/org/opensearch/security/action/tenancy/MultiTenancyRetrieveAction.java new file mode 100644 index 0000000000..56bfbdecf9 --- /dev/null +++ b/src/main/java/org/opensearch/security/action/tenancy/MultiTenancyRetrieveAction.java @@ -0,0 +1,24 @@ +/* + * 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 MultiTenancyRetrieveAction extends ActionType { + + public static final MultiTenancyRetrieveAction INSTANCE = new MultiTenancyRetrieveAction(); + public static final String NAME = "cluster:feature/tenancy/multitenancy_enabled/read"; + + protected MultiTenancyRetrieveAction() { + super(NAME, BooleanSettingRetrieveResponse::new); + } +} diff --git a/src/main/java/org/opensearch/security/action/tenancy/MultiTenancyRetrieveTransportAction.java b/src/main/java/org/opensearch/security/action/tenancy/MultiTenancyRetrieveTransportAction.java new file mode 100644 index 0000000000..7d9605c068 --- /dev/null +++ b/src/main/java/org/opensearch/security/action/tenancy/MultiTenancyRetrieveTransportAction.java @@ -0,0 +1,57 @@ +/* + * 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 MultiTenancyRetrieveTransportAction + extends HandledTransportAction { + + private final ConfigurationRepository config; + + @Inject + public MultiTenancyRetrieveTransportAction(final Settings settings, + final TransportService transportService, + final ActionFilters actionFilters, + final ConfigurationRepository config) { + super(MultiTenancyRetrieveAction.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 Boolean value = config.dynamic.kibana.multitenancy_enabled; + + listener.onResponse(new BooleanSettingRetrieveResponse(value)); + } +} diff --git a/src/main/java/org/opensearch/security/action/tenancy/MultiTenancyUpdateAction.java b/src/main/java/org/opensearch/security/action/tenancy/MultiTenancyUpdateAction.java new file mode 100644 index 0000000000..45b82febaa --- /dev/null +++ b/src/main/java/org/opensearch/security/action/tenancy/MultiTenancyUpdateAction.java @@ -0,0 +1,24 @@ +/* + * 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 MultiTenancyUpdateAction extends ActionType { + + public static final MultiTenancyUpdateAction INSTANCE = new MultiTenancyUpdateAction(); + public static final String NAME = "cluster:feature/tenancy/multitenancy_enabled/update"; + + protected MultiTenancyUpdateAction() { + super(NAME, BooleanSettingRetrieveResponse::new); + } +} diff --git a/src/main/java/org/opensearch/security/action/tenancy/MultiTenancyUpdateTransportAction.java b/src/main/java/org/opensearch/security/action/tenancy/MultiTenancyUpdateTransportAction.java new file mode 100644 index 0000000000..269230c6ab --- /dev/null +++ b/src/main/java/org/opensearch/security/action/tenancy/MultiTenancyUpdateTransportAction.java @@ -0,0 +1,104 @@ +/* + * 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 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 MultiTenancyUpdateTransportAction extends HandledTransportAction { + + private static final Logger log = LogManager.getLogger(MultiTenancyUpdateTransportAction.class); + + private final String securityIndex; + private final ConfigurationRepository config; + private final Client client; + private final ThreadPool pool; + + @Inject + public MultiTenancyUpdateTransportAction(final Settings settings, + final TransportService transportService, + final ActionFilters actionFilters, + final ConfigurationRepository config, + final ThreadPool pool, + final Client client) { + super(MultiTenancyUpdateAction.NAME, transportService, actionFilters, BooleanSettingUpdateRequest::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(); + } + + @Override + protected void doExecute(final Task task, final BooleanSettingUpdateRequest 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"); + config.dynamic.kibana.multitenancy_enabled = request.getValue(); + 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(MultiTenancyRetrieveAction.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/MutliTenancyRestHandler.java b/src/main/java/org/opensearch/security/action/tenancy/MutliTenancyRestHandler.java new file mode 100644 index 0000000000..2dbefb98f5 --- /dev/null +++ b/src/main/java/org/opensearch/security/action/tenancy/MutliTenancyRestHandler.java @@ -0,0 +1,64 @@ +/* + * 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 MutliTenancyRestHandler extends BaseRestHandler { + + public MutliTenancyRestHandler() { + super(); + } + + @Override + public String getName() { + return "Multi Tenancy actions for Retrieve / Update"; + } + + @Override + public List routes() { + return ImmutableList.of( + new Route(GET, "/_plugins/_security/config/tenancy/multitenancy_enabled"), + new Route(PUT, "/_plugins/_security/config/tenancy/multitenancy_enabled") + ); + } + + @Override + protected RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient nodeClient) throws IOException { + + switch (request.method()) { + case GET: + return channel -> nodeClient.execute( + MultiTenancyRetrieveAction.INSTANCE, + new EmptyRequest(), + new RestToXContentListener<>(channel)); + case PUT: + return channel -> nodeClient.execute( + MultiTenancyUpdateAction.INSTANCE, + BooleanSettingUpdateRequest.fromXContent(request.contentParser()), + new RestToXContentListener<>(channel)); + default: + throw new RuntimeException("Not implemented"); + } + } +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java index 0e98124b6f..50e71a0a54 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java @@ -73,7 +73,7 @@ public abstract class AbstractApiAction extends BaseRestHandler { protected final ConfigurationRepository cl; protected final ClusterService cs; final ThreadPool threadPool; - protected String opendistroIndex; + protected String securityIndexName; private final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator; protected final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator; protected final AuditLog auditLog; @@ -85,7 +85,7 @@ protected AbstractApiAction(final Settings settings, final Path configPath, fina ThreadPool threadPool, AuditLog auditLog) { super(); this.settings = settings; - this.opendistroIndex = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + this.securityIndexName = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); this.cl = cl; @@ -153,7 +153,7 @@ protected void handleDelete(final RestChannel channel, final RestRequest request existingConfiguration.remove(name); if (existed) { - saveAnUpdateConfigs(client, request, getConfigName(), existingConfiguration, new OnSucessActionListener(channel) { + AbstractApiAction.saveAndUpdateConfigs(this.securityIndexName, client, getConfigName(), existingConfiguration, new OnSucessActionListener(channel) { @Override public void onResponse(IndexResponse response) { @@ -203,7 +203,7 @@ protected void handlePut(final RestChannel channel, final RestRequest request, f } existingConfiguration.putCObject(name, newContent); - saveAnUpdateConfigs(client, request, getConfigName(), existingConfiguration, new OnSucessActionListener(channel) { + AbstractApiAction.saveAndUpdateConfigs(this.securityIndexName, client, getConfigName(), existingConfiguration, new OnSucessActionListener(channel) { @Override public void onResponse(IndexResponse response) { @@ -266,7 +266,7 @@ protected final SecurityDynamicConfiguration load(final CType config, boolean } protected boolean ensureIndexExists() { - if (!cs.state().metadata().hasConcreteIndex(this.opendistroIndex)) { + if (!cs.state().metadata().hasConcreteIndex(this.securityIndexName)) { return false; } return true; @@ -309,11 +309,8 @@ public final void onFailure(Exception e) { } - protected void saveAnUpdateConfigs(final Client client, final RestRequest request, final CType cType, - final SecurityDynamicConfiguration configuration, OnSucessActionListener actionListener) { - final IndexRequest ir = new IndexRequest(this.opendistroIndex); - - //final String type = "_doc"; + public static void saveAndUpdateConfigs(final String indexName, final Client client, final CType cType, final SecurityDynamicConfiguration configuration, final ActionListener actionListener) { + final IndexRequest ir = new IndexRequest(indexName); final String id = cType.toLCString(); configuration.removeStatic(); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java index 885a5476af..5e4799d655 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java @@ -224,7 +224,7 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C internalUserEntry.setHash(hash); - saveAnUpdateConfigs(client, request, CType.INTERNALUSERS, internalUser, new OnSucessActionListener(channel) { + AccountApiAction.saveAndUpdateConfigs(this.securityIndexName, client, CType.INTERNALUSERS, internalUser, new OnSucessActionListener(channel) { @Override public void onResponse(IndexResponse response) { successResponse(channel, "'" + username + "' updated."); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java index b37375a461..cefaeb5c6c 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java @@ -141,7 +141,7 @@ protected void handlePut(final RestChannel channel, final RestRequest request, f boolean existed = existingConfiguration.exists(name); existingConfiguration.putCObject(name, DefaultObjectMapper.readTree(content, existingConfiguration.getImplementingClass())); - saveAnUpdateConfigs(client, request, getConfigName(), existingConfiguration, new OnSucessActionListener(channel) { + saveAndUpdateConfigs(this.securityIndexName,client, getConfigName(), existingConfiguration, new OnSucessActionListener(channel) { @Override public void onResponse(IndexResponse response) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java index 417465e353..646e1f81d6 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java @@ -172,7 +172,7 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C // checks complete, create or update the user internalUsersConfiguration.putCObject(username, DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass())); - saveAnUpdateConfigs(client, request, CType.INTERNALUSERS, internalUsersConfiguration, new OnSucessActionListener(channel) { + saveAndUpdateConfigs(this.securityIndexName,client, CType.INTERNALUSERS, internalUsersConfiguration, new OnSucessActionListener(channel) { @Override public void onResponse(IndexResponse response) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java index 1a1092e2fb..d252515f72 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java @@ -140,8 +140,8 @@ protected void handlePost(RestChannel channel, RestRequest request, Client clien final SecurityDynamicConfiguration auditConfigV7 = Migration.migrateAudit(auditConfigV6); builder.add(auditConfigV7); - final int replicas = cs.state().metadata().index(opendistroIndex).getNumberOfReplicas(); - final String autoExpandReplicas = cs.state().metadata().index(opendistroIndex).getSettings().get(IndexMetadata.SETTING_AUTO_EXPAND_REPLICAS); + final int replicas = cs.state().metadata().index(securityIndexName).getNumberOfReplicas(); + final String autoExpandReplicas = cs.state().metadata().index(securityIndexName).getSettings().get(IndexMetadata.SETTING_AUTO_EXPAND_REPLICAS); final Builder securityIndexSettings = Settings.builder(); @@ -153,7 +153,7 @@ protected void handlePost(RestChannel channel, RestRequest request, Client clien securityIndexSettings.put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1); - client.admin().indices().prepareDelete(this.opendistroIndex).execute(new ActionListener() { + client.admin().indices().prepareDelete(this.securityIndexName).execute(new ActionListener() { @Override public void onResponse(AcknowledgedResponse response) { @@ -161,14 +161,14 @@ public void onResponse(AcknowledgedResponse response) { if (response.isAcknowledged()) { log.debug("opendistro_security index deleted successfully"); - client.admin().indices().prepareCreate(opendistroIndex).setSettings(securityIndexSettings) + client.admin().indices().prepareCreate(securityIndexName).setSettings(securityIndexSettings) .execute(new ActionListener() { @Override public void onResponse(CreateIndexResponse response) { final List> dynamicConfigurations = builder.build(); final ImmutableList.Builder cTypes = ImmutableList.builderWithExpectedSize(dynamicConfigurations.size()); - final BulkRequestBuilder br = client.prepareBulk(opendistroIndex); + final BulkRequestBuilder br = client.prepareBulk(securityIndexName); br.setRefreshPolicy(RefreshPolicy.IMMEDIATE); try { for (SecurityDynamicConfiguration dynamicConfiguration : dynamicConfigurations) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java index 6d644c1eae..deb56a69c7 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java @@ -162,7 +162,7 @@ private void handleSinglePatch(RestChannel channel, RestRequest request, Client } } - saveAnUpdateConfigs(client, request, getConfigName(), mdc, new OnSucessActionListener(channel){ + saveAndUpdateConfigs(this.securityIndexName,client, getConfigName(), mdc, new OnSucessActionListener(channel){ @Override public void onResponse(IndexResponse response) { @@ -244,7 +244,7 @@ private void handleBulkPatch(RestChannel channel, RestRequest request, Client cl } } - saveAnUpdateConfigs(client, request, getConfigName(), mdc, new OnSucessActionListener(channel) { + saveAndUpdateConfigs(this.securityIndexName,client, getConfigName(), mdc, new OnSucessActionListener(channel) { @Override public void onResponse(IndexResponse response) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java index b056a25ad2..72928cd0ad 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java @@ -82,7 +82,7 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C } rolesMappingConfiguration.putCObject(name, DefaultObjectMapper.readTree(content, rolesMappingConfiguration.getImplementingClass())); - saveAnUpdateConfigs(client, request, getConfigName(), rolesMappingConfiguration, new OnSucessActionListener(channel) { + saveAndUpdateConfigs(this.securityIndexName,client, getConfigName(), rolesMappingConfiguration, new OnSucessActionListener(channel) { @Override public void onResponse(IndexResponse response) { diff --git a/src/test/java/org/opensearch/security/multitenancy/test/TenancyActionsTests.java b/src/test/java/org/opensearch/security/multitenancy/test/TenancyActionsTests.java new file mode 100644 index 0000000000..4ce11a3953 --- /dev/null +++ b/src/test/java/org/opensearch/security/multitenancy/test/TenancyActionsTests.java @@ -0,0 +1,83 @@ +/* + * 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.apache.hc.core5.http.message.BasicHeader; +import org.junit.Test; + +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 TenancyActionsTests 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 String createIndexPatternDoc(final String title) { + return "{"+ + "\"type\" : \"index-pattern\","+ + "\"updated_at\" : \"2018-09-29T08:56:59.066Z\","+ + "\"index-pattern\" : {"+ + "\"title\" : \"" + title + "\""+ + "}}"; + } + + @Override + protected String getResourceFolder() { + return "multitenancy"; + } + + @Test + public void testMultitenancyDisabled_endToEndTest() throws Exception { + setup(); + + final HttpResponse getSettingResponse = nonSslRestHelper().executeGetRequest("/_plugins/_security/config/tenancy/multitenancy_enabled", asAdminUser); + assertThat(getSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + assertThat(getSettingResponse.findValueInJson("value"), equalTo("true")); + + final HttpResponse createDocInGlobalTenantResponse = nonSslRestHelper().executePostRequest(".kibana/_doc?refresh=true", createIndexPatternDoc("globalIndex"), asAdminUser); + assertThat(createDocInGlobalTenantResponse.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); + final HttpResponse createDocInUserTenantResponse = nonSslRestHelper().executePostRequest(".kibana/_doc?refresh=true", createIndexPatternDoc("userIndex"), onUserTenant, asAdminUser); + assertThat(createDocInUserTenantResponse.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); + + final HttpResponse searchInUserTenantWithMutlitenancyEnabled = nonSslRestHelper().executeGetRequest(".kibana/_search", onUserTenant, asAdminUser); + assertThat(getSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + assertThat(searchInUserTenantWithMutlitenancyEnabled.findValueInJson("hits.hits[0]._source.index-pattern.title"), equalTo("userIndex")); + + final HttpResponse updateMutlitenancyToDisabled = nonSslRestHelper().executePutRequest("/_plugins/_security/config/tenancy/multitenancy_enabled", "{\"value\": \"false\"}", asAdminUser); + assertThat(getSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + assertThat(updateMutlitenancyToDisabled.findValueInJson("value"), equalTo("false")); + + final HttpResponse searchInUserTenantWithMutlitenancyDisabled = nonSslRestHelper().executeGetRequest(".kibana/_search", onUserTenant, asAdminUser); + assertThat(getSettingResponse.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/config/tenancy/multitenancy_enabled", asUser); + assertThat(getSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + assertThat(getSettingResponse.findValueInJson("error.reason"), containsString("no permissions for [cluster:feature/tenancy/multitenancy_enabled/read]")); + + final HttpResponse updateSettingResponse = nonSslRestHelper().executePutRequest("/_plugins/_security/config/tenancy/multitenancy_enabled", "{\"value\": \"false\"}", asUser); + assertThat(updateSettingResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + assertThat(updateSettingResponse.findValueInJson("error.reason"), containsString("no permissions for [cluster:feature/tenancy/multitenancy_enabled/update]")); + } +} diff --git a/src/test/java/org/opensearch/security/test/helper/rest/RestHelper.java b/src/test/java/org/opensearch/security/test/helper/rest/RestHelper.java index ff9a5b536b..367332f160 100644 --- a/src/test/java/org/opensearch/security/test/helper/rest/RestHelper.java +++ b/src/test/java/org/opensearch/security/test/helper/rest/RestHelper.java @@ -92,8 +92,6 @@ import org.opensearch.security.test.helper.cluster.ClusterInfo; import org.opensearch.security.test.helper.file.FileHelper; -import static org.junit.jupiter.api.Assertions.fail; - public class RestHelper { protected final Logger log = LogManager.getLogger(RestHelper.class); @@ -480,7 +478,7 @@ public String toString() { public String findValueInJson(final String jsonDotPath) { // Make sure its json / then parse it if (!isJsonContentType()) { - fail("Response was expected to be JSON, body was: \n" + body); + throw new RuntimeException("Response was expected to be JSON, body was: \n" + body); } JsonNode currentNode = null; try { @@ -492,7 +490,7 @@ public String findValueInJson(final String jsonDotPath) { // Break the path into parts, and scan into the json object try (final Scanner jsonPathScanner = new Scanner(jsonDotPath).useDelimiter("\\.")) { if (!jsonPathScanner.hasNext()) { - fail("Invalid json dot path '" + jsonDotPath + "', rewrite with '.' characters between path elements."); + throw new RuntimeException("Invalid json dot path '" + jsonDotPath + "', rewrite with '.' characters between path elements."); } do { String pathEntry = jsonPathScanner.next(); @@ -510,23 +508,23 @@ public String findValueInJson(final String jsonDotPath) { } if (!currentNode.has(pathEntry)) { - fail("Unable to resolve '" + jsonDotPath + "', on path entry '" + pathEntry + "' from available fields " + currentNode.toPrettyString()); + throw new RuntimeException("Unable to resolve '" + jsonDotPath + "', on path entry '" + pathEntry + "' from available fields " + currentNode.toPrettyString()); } currentNode = currentNode.get(pathEntry); // if it's an Array lookup we get the requested index item if (arrayEntryIdx > -1) { if(!currentNode.isArray()) { - fail("Unable to resolve '" + jsonDotPath + "', the '" + pathEntry + "' field is not an array " + currentNode.toPrettyString()); + throw new RuntimeException("Unable to resolve '" + jsonDotPath + "', the '" + pathEntry + "' field is not an array " + currentNode.toPrettyString()); } else if (!currentNode.has(arrayEntryIdx)) { - fail("Unable to resolve '" + jsonDotPath + "', index '" + arrayEntryIdx + "' is out of bounds for array '" + pathEntry + "' \n" + currentNode.toPrettyString()); + throw new RuntimeException("Unable to resolve '" + jsonDotPath + "', index '" + arrayEntryIdx + "' is out of bounds for array '" + pathEntry + "' \n" + currentNode.toPrettyString()); } currentNode = currentNode.get(arrayEntryIdx); } } while (jsonPathScanner.hasNext()); if (!currentNode.isValueNode()) { - fail("Unexpected value note, index directly to the object to reference, object\n" + currentNode.toPrettyString()); + throw new RuntimeException("Unexpected value note, index directly to the object to reference, object\n" + currentNode.toPrettyString()); } return currentNode.asText(); }