diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index eacad44f54..1d89928d2b 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -116,11 +116,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; @@ -179,6 +174,7 @@ import org.opensearch.security.transport.InterClusterRequestEvaluator; import org.opensearch.security.transport.SecurityInterceptor; import org.opensearch.security.user.User; +import org.opensearch.security.user.UserService; import org.opensearch.tasks.Task; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.RemoteClusterService; @@ -207,6 +203,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile SecurityRestFilter securityRestHandler; private volatile SecurityInterceptor si; private volatile PrivilegesEvaluator evaluator; + private volatile UserService userService; private volatile ThreadPool threadPool; private volatile ConfigurationRepository cr; private volatile AdminDNs adminDns; @@ -364,7 +361,9 @@ public List run() { final List files = AccessController.doPrivileged(new PrivilegedAction>() { @Override public List run() { + final Path confPath = new Environment(settings, configPath).configFile().toAbsolutePath(); + if(Files.isDirectory(confPath, LinkOption.NOFOLLOW_LINKS)) { try (Stream s = Files.walk(confPath)) { return s.distinct().map(p -> sha256(p)).collect(Collectors.toList()); @@ -477,15 +476,26 @@ public List getRestHandlers(Settings settings, RestController restC handlers.add(new DashboardsInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool))); handlers.add(new TenantInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool), Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); - handlers.add(new TenancyConfigRestHandler()); handlers.add(new SecurityConfigUpdateAction(settings, restController,Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); handlers.add(new SecurityWhoAmIAction(settings ,restController,Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); if (sslCertReloadEnabled) { handlers.add(new SecuritySSLReloadCertsAction(settings, restController, sks, Objects.requireNonNull(threadPool), Objects.requireNonNull(adminDns))); } - final Collection apiHandlers = SecurityRestApiActions.getHandler(settings, configPath, restController, localClient, adminDns, cr, cs, principalExtractor, evaluator, threadPool, Objects.requireNonNull(auditLog)); - handlers.addAll(apiHandlers); - log.debug("Added {} management rest handler(s)", apiHandlers.size()); + + handlers.addAll( + SecurityRestApiActions.getHandler( + settings, + configPath, + restController, + localClient, + adminDns, + cr, cs, principalExtractor, + evaluator, + threadPool, + Objects.requireNonNull(auditLog), + Objects.requireNonNull(userService)) + ); + log.debug("Added {} rest handler(s)", handlers.size()); } } @@ -508,8 +518,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; } @@ -810,6 +818,8 @@ public Collection createComponents(Client localClient, ClusterService cl cr = ConfigurationRepository.create(settings, this.configPath, threadPool, localClient, clusterService, auditLog); + userService = new UserService(cs, cr, settings, localClient); + final XFFResolver xffResolver = new XFFResolver(threadPool); backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool); @@ -866,6 +876,7 @@ public Collection createComponents(Client localClient, ClusterService cl components.add(evaluator); components.add(si); components.add(dcf); + components.add(userService); return components; @@ -1172,7 +1183,6 @@ public static class GuiceHolder implements LifecycleComponent { private static RepositoriesService repositoriesService; private static RemoteClusterService remoteClusterService; private static IndicesService indicesService; - private static PitService pitService; private static ExtensionsManager extensionsManager; 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/dlic/rest/api/InternalUsersApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java index 2d6512dd34..e0cc5d2301 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 @@ -14,7 +14,6 @@ import java.io.IOException; import java.nio.file.Path; import java.util.List; -import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -44,6 +43,8 @@ import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.support.SecurityJsonNode; +import org.opensearch.security.user.UserService; +import org.opensearch.security.user.UserServiceException; import org.opensearch.threadpool.ThreadPool; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; @@ -51,31 +52,36 @@ public class InternalUsersApiAction extends PatchableResourceApiAction { static final List RESTRICTED_FROM_USERNAME = ImmutableList.of( - ":" // Not allowed in basic auth, see https://stackoverflow.com/a/33391003/533057 + ":" // Not allowed in basic auth, see https://stackoverflow.com/a/33391003/533057 ); private static final List routes = addRoutesPrefix(ImmutableList.of( new Route(Method.GET, "/user/{name}"), new Route(Method.GET, "/user/"), + new Route(Method.POST, "/user/{name}/authtoken"), new Route(Method.DELETE, "/user/{name}"), new Route(Method.PUT, "/user/{name}"), // corrected mapping, introduced in OpenSearch Security new Route(Method.GET, "/internalusers/{name}"), new Route(Method.GET, "/internalusers/"), + new Route(Method.POST, "/internalusers/{name}/authtoken"), new Route(Method.DELETE, "/internalusers/{name}"), new Route(Method.PUT, "/internalusers/{name}"), new Route(Method.PATCH, "/internalusers/"), new Route(Method.PATCH, "/internalusers/{name}") )); + UserService userService; + @Inject public InternalUsersApiAction(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, - ThreadPool threadPool, AuditLog auditLog) { + ThreadPool threadPool, UserService userService, AuditLog auditLog) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + this.userService = userService; } @Override @@ -93,22 +99,7 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C final String username = request.param("name"); - if (username == null || username.length() == 0) { - badRequestResponse(channel, "No " + getResourceName() + " specified."); - return; - } - - final List foundRestrictedContents = RESTRICTED_FROM_USERNAME.stream().filter(username::contains).collect(Collectors.toList()); - if (!foundRestrictedContents.isEmpty()) { - final String restrictedContents = foundRestrictedContents.stream().map(s -> "'" + s + "'").collect(Collectors.joining(",")); - badRequestResponse(channel, "Username has restricted characters " + restrictedContents + " that are not permitted."); - return; - } - - // TODO it might be sensible to consolidate this with the overridden method in - // order to minimize duplicated logic - - final SecurityDynamicConfiguration internalUsersConfiguration = load(getConfigName(), false); + SecurityDynamicConfiguration internalUsersConfiguration = load(getConfigName(), false); if (!isWriteable(channel, internalUsersConfiguration, username)) { return; @@ -121,32 +112,35 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C final List securityRoles = securityJsonNode.get("opendistro_security_roles").asList(); if (securityRoles != null) { for (final String role: securityRoles) { - if (!isValidRolesMapping(channel, role)) return; + if (!isValidRolesMapping(channel, role)) { + return; + } } } - // if password is set, it takes precedence over hash - final String plainTextPassword = securityJsonNode.get("password").asString(); - final String origHash = securityJsonNode.get("hash").asString(); - if (plainTextPassword != null && plainTextPassword.length() > 0) { - contentAsNode.remove("password"); - contentAsNode.put("hash", hash(plainTextPassword.toCharArray())); - } else if (origHash != null && origHash.length() > 0) { - contentAsNode.remove("password"); - } else if (plainTextPassword != null && plainTextPassword.isEmpty() && origHash == null) { - contentAsNode.remove("password"); - } - final boolean userExisted = internalUsersConfiguration.exists(username); // when updating an existing user password hash can be blank, which means no // changes - // sanity checks, hash is mandatory for newly created users - if (!userExisted && securityJsonNode.get("hash").asString() == null) { - badRequestResponse(channel, "Please specify either 'hash' or 'password' when creating a new internal user."); + try { + if (request.hasParam("service")) { + ((ObjectNode) content).put("service", request.param("service")); + } + if (request.hasParam("enabled")) { + ((ObjectNode) content).put("enabled", request.param("enabled")); + } + ((ObjectNode) content).put("name", username); + internalUsersConfiguration = userService.createOrUpdateAccount((ObjectNode) content); + } + catch (UserServiceException ex) { + badRequestResponse(channel, ex.getMessage()); + return; } + catch (IOException ex) { + throw new IOException(ex); + } // for existing users, hash is optional if (userExisted && securityJsonNode.get("hash").asString() == null) { @@ -154,7 +148,7 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C final String hash = ((Hashed) internalUsersConfiguration.getCEntry(username)).getHash(); if (hash == null || hash.length() == 0) { internalErrorResponse(channel, - "Existing user " + username + " has no password, and no new password or hash was specified."); + "Existing user " + username + " has no password, and no new password or hash was specified."); return; } contentAsNode.put("hash", hash); @@ -163,10 +157,13 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C internalUsersConfiguration.remove(username); // checks complete, create or update the user - internalUsersConfiguration.putCObject(username, DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass())); + Object userData = DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass()); + internalUsersConfiguration.putCObject(username, userData); + saveAndUpdateConfigs(this.securityIndexName,client, CType.INTERNALUSERS, internalUsersConfiguration, new OnSucessActionListener(channel) { + @Override public void onResponse(IndexResponse response) { if (userExisted) { @@ -179,6 +176,63 @@ public void onResponse(IndexResponse response) { }); } + /** + * Overrides the GET request functionality to allow for the special case of requesting an auth token. + * + * @param channel The channel the request is coming through + * @param request The request itself + * @param client The client executing the request + * @param content The content of the request parsed into a node + * @throws IOException when parsing of configuration files fails (should not happen) + */ + @Override + protected void handlePost(final RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException{ + + final String username = request.param("name"); + + final SecurityDynamicConfiguration internalUsersConfiguration = load(getConfigName(), true); + filter(internalUsersConfiguration); // Hides hashes + + // no specific resource requested + if (username == null || username.length() == 0) { + + notImplemented(channel, Method.POST); + return; + } + + final boolean userExisted = internalUsersConfiguration.exists(username); + + if (!userExisted) { + notFound(channel, "Resource '" + username + "' not found."); + return; + } + + String authToken = ""; + try { + if (request.uri().contains("/internalusers/" + username + "/authtoken") && request.uri().endsWith("/authtoken")) { // Handle auth token fetching + + authToken = userService.generateAuthToken(username); + } else { // Not an auth token request + + notImplemented(channel, Method.POST); + return; + } + } catch (UserServiceException ex) { + badRequestResponse(channel, ex.getMessage()); + return; + } + catch (IOException ex) { + throw new IOException(ex); + } + + if (!authToken.isEmpty()) { + createdResponse(channel, "'" + username + "' authtoken generated " + authToken); + } else { + badRequestResponse(channel, "'" + username + "' authtoken failed to be created."); + } + } + + @Override protected void filter(SecurityDynamicConfiguration builder) { super.filter(builder); 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 54ea46efd1..bbe653d3ec 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 @@ -27,15 +27,25 @@ import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.ssl.transport.PrincipalExtractor; +import org.opensearch.security.user.UserService; import org.opensearch.threadpool.ThreadPool; public class SecurityRestApiActions { - public static Collection getHandler(Settings settings, Path configPath, RestController controller, Client client, - AdminDNs adminDns, ConfigurationRepository cr, ClusterService cs, PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { - final List handlers = new ArrayList(15); - handlers.add(new InternalUsersApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); + public static Collection getHandler(final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDns, + final ConfigurationRepository cr, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + final ThreadPool threadPool, + final AuditLog auditLog, + final UserService userService) { + final List handlers = new ArrayList(16); + handlers.add(new InternalUsersApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, userService, auditLog)); handlers.add(new RolesMappingApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new RolesApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new ActionGroupsApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); @@ -51,6 +61,7 @@ public static Collection getHandler(Settings settings, Path configP 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)); 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/securityconf/impl/v7/InternalUserV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/InternalUserV7.java index 4a1467f3a6..b697a9485b 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/InternalUserV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/InternalUserV7.java @@ -44,6 +44,8 @@ public class InternalUserV7 implements Hideable, Hashed, StaticDefinable { private String hash; private boolean reserved; private boolean hidden; + private boolean service; + private boolean enabled; @JsonProperty(value = "static") private boolean _static; private List backend_roles = Collections.emptyList(); @@ -58,7 +60,20 @@ private InternalUserV7(String hash, boolean reserved, boolean hidden, List backend_roles, Map attributes, Boolean enabled, Boolean service) { + super(); + this.hash = hash; + this.reserved = reserved; + this.hidden = hidden; + this.backend_roles = backend_roles; + this.attributes = attributes; + this.enabled = enabled; + this.service = service; + } public InternalUserV7() { super(); @@ -80,7 +95,6 @@ public String getHash() { public void setHash(String hash) { this.hash = hash; } - public boolean isHidden() { @@ -114,9 +128,17 @@ public void setAttributes(Map attributes) { this.attributes = attributes; } + public boolean enabled() { + return this.enabled; + } + + public boolean service() { + return this.service; + } + @Override public String toString() { - return "InternalUserV7 [hash=" + hash + ", reserved=" + reserved + ", hidden=" + hidden + ", _static=" + _static + ", backend_roles=" + return "InternalUserV7 [hash=" + hash + ", enabled=" + enabled + ", service=" + service + ", reserved=" + reserved + ", hidden=" + hidden + ", _static=" + _static + ", backend_roles=" + backend_roles + ", attributes=" + attributes + ", description=" + description + "]"; } @@ -134,6 +156,14 @@ public void setDescription(String description) { this.description = description; } + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setService(boolean service) { + this.service = service; + } + public boolean isReserved() { return reserved; } diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 56d23e22aa..cc70d904a9 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -289,8 +289,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/main/java/org/opensearch/security/user/UserService.java b/src/main/java/org/opensearch/security/user/UserService.java new file mode 100644 index 0000000000..fb25c85dc3 --- /dev/null +++ b/src/main/java/org/opensearch/security/user/UserService.java @@ -0,0 +1,278 @@ +/* + * 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.user; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.ExceptionsHelper; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.XContentHelper; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.Hashed; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.SecurityJsonNode; + +import static org.opensearch.security.dlic.rest.support.Utils.hash; + +/** + * This class handles user registration and operations on behalf of the Security Plugin. + */ +public class UserService { + + protected final Logger log = LogManager.getLogger(this.getClass()); + ClusterService clusterService; + static ConfigurationRepository configurationRepository; + String securityIndex; + Client client; + + final static String NO_PASSWORD_OR_HASH_MESSAGE = "Please specify either 'hash' or 'password' when creating a new internal user."; + final static String RESTRICTED_CHARACTER_USE_MESSAGE = "A restricted character(s) was detected in the account name. Please remove: "; + + final static String SERVICE_ACCOUNT_PASSWORD_MESSAGE = "A password cannot be provided for a service account. Failed to register service account: "; + + final static String SERVICE_ACCOUNT_HASH_MESSAGE = "A password hash cannot be provided for service account. Failed to register service account: "; + + final static String NO_ACCOUNT_NAME_MESSAGE = "No account name was specified in the request."; + + final static String FAILED_ACCOUNT_RETRIEVAL_MESSAGE = "The account specified could not be accessed at this time."; + final static String AUTH_TOKEN_GENERATION_MESSAGE = "An auth token could not be generated for the specified account."; + private static CType getUserConfigName() { + return CType.INTERNALUSERS; + } + + static final List RESTRICTED_FROM_USERNAME = ImmutableList.of( + ":" // Not allowed in basic auth, see https://stackoverflow.com/a/33391003/533057 + ); + + @Inject + public UserService( + ClusterService clusterService, + ConfigurationRepository configurationRepository, + Settings settings, + Client client + ) { + this.clusterService = clusterService; + this.configurationRepository = configurationRepository; + this.securityIndex = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + this.client = client; + } + + /** + * Load data for a given CType + * @param config CType whose data is to be loaded in-memory + * @return configuration loaded with given CType data + */ + protected static final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { + SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex(Collections.singleton(config), logComplianceEvent).get(config).deepClone(); + return DynamicConfigFactory.addStatics(loaded); + } + + /** + * This function will handle the creation or update of a user account. + * + * @param contentAsNode An object node of different account configurations. + * @return InternalUserConfiguration with the new/updated user + * @throws UserServiceException + * @throws IOException + */ + public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentAsNode) throws IOException { + + SecurityJsonNode securityJsonNode = new SecurityJsonNode(contentAsNode); + + final SecurityDynamicConfiguration internalUsersConfiguration = load(getUserConfigName(), false); + + String accountName = securityJsonNode.get("name").asString(); + + if (accountName == null || accountName.length() == 0) { // Fail if field is present but empty + throw new UserServiceException(NO_ACCOUNT_NAME_MESSAGE); + } + + SecurityJsonNode attributeNode = securityJsonNode.get("attributes"); + + if (!attributeNode.get("service").isNull() && attributeNode.get("service").asString().equalsIgnoreCase("true")) + { // If this is a service account + verifyServiceAccount(securityJsonNode, accountName); + String password = generatePassword(); + contentAsNode.put("hash", hash(password.toCharArray())); + contentAsNode.put("service", "true"); + } else{ + contentAsNode.put("service", "false"); + } + + securityJsonNode = new SecurityJsonNode(contentAsNode); + final List foundRestrictedContents = RESTRICTED_FROM_USERNAME.stream().filter(accountName::contains).collect(Collectors.toList()); + if (!foundRestrictedContents.isEmpty()) { + final String restrictedContents = foundRestrictedContents.stream().map(s -> "'" + s + "'").collect(Collectors.joining(",")); + throw new UserServiceException(RESTRICTED_CHARACTER_USE_MESSAGE + restrictedContents); + } + + // if password is set, it takes precedence over hash + final String plainTextPassword = securityJsonNode.get("password").asString(); + final String origHash = securityJsonNode.get("hash").asString(); + if (plainTextPassword != null && plainTextPassword.length() > 0) { + contentAsNode.remove("password"); + contentAsNode.put("hash", hash(plainTextPassword.toCharArray())); + } else if (origHash != null && origHash.length() > 0) { + contentAsNode.remove("password"); + } else if (plainTextPassword != null && plainTextPassword.isEmpty() && origHash == null) { + contentAsNode.remove("password"); + } + + if (!attributeNode.get("enabled").isNull()) { + contentAsNode.put("enabled", securityJsonNode.get("enabled").asString()); + } + + final boolean userExisted = internalUsersConfiguration.exists(accountName); + + // sanity checks, hash is mandatory for newly created users + if (!userExisted && securityJsonNode.get("hash").asString() == null) { + throw new UserServiceException(NO_PASSWORD_OR_HASH_MESSAGE); + } + + // for existing users, hash is optional + if (userExisted && securityJsonNode.get("hash").asString() == null) { + // sanity check, this should usually not happen + final String hash = ((Hashed) internalUsersConfiguration.getCEntry(accountName)).getHash(); + if (hash == null || hash.length() == 0) { + throw new UserServiceException("Existing user " + accountName + " has no password, and no new password or hash was specified."); + } + contentAsNode.put("hash", hash); + } + + internalUsersConfiguration.remove(accountName); + contentAsNode.remove("name"); + + internalUsersConfiguration.putCObject(accountName, DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass())); + return internalUsersConfiguration; + } + + private void verifyServiceAccount(SecurityJsonNode securityJsonNode, String accountName) { + + final String plainTextPassword = securityJsonNode.get("password").asString(); + final String origHash = securityJsonNode.get("hash").asString(); + + if (plainTextPassword != null && plainTextPassword.length() > 0) { + throw new UserServiceException(SERVICE_ACCOUNT_PASSWORD_MESSAGE + accountName); + } + + if (origHash != null && origHash.length() > 0) { + throw new UserServiceException(SERVICE_ACCOUNT_HASH_MESSAGE + accountName); + } + } + + /** + * This will be swapped in for a real solution once one is decided on. + * + * @return A password for a service account. + */ + private String generatePassword() { + String generatedPassword = "superSecurePassword"; + return generatedPassword; + } + + /** + * This function retrieves the auth token associated with a service account. + * Fails if the provided account is not a service account or account is not enabled. + * + * @param accountName A string representing the name of the account + * @return A string auth token + */ + public String generateAuthToken(String accountName) throws IOException { + + final SecurityDynamicConfiguration internalUsersConfiguration = load(getUserConfigName(), false); + + if (!internalUsersConfiguration.exists(accountName)) { + throw new UserServiceException(FAILED_ACCOUNT_RETRIEVAL_MESSAGE); + } + + String authToken = null; + try { + DefaultObjectMapper mapper = new DefaultObjectMapper(); + JsonNode accountDetails = mapper.readTree(internalUsersConfiguration.getCEntry(accountName).toString()); + final ObjectNode contentAsNode = (ObjectNode) accountDetails; + SecurityJsonNode securityJsonNode = new SecurityJsonNode(contentAsNode); + + Optional.ofNullable(securityJsonNode.get("service")) + .map(SecurityJsonNode::asString) + .filter("true"::equalsIgnoreCase) + .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); + + + Optional.ofNullable(securityJsonNode.get("enabled")) + .map(SecurityJsonNode::asString) + .filter("true"::equalsIgnoreCase) + .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); + + // Generate a new password for the account and store the hash of it + String plainTextPassword = generatePassword(); + contentAsNode.put("hash", hash(plainTextPassword.toCharArray())); + contentAsNode.put("enabled", "true"); + contentAsNode.put("service", "true"); + + // Update the internal user associated with the auth token + internalUsersConfiguration.remove(accountName); + contentAsNode.remove("name"); + internalUsersConfiguration.putCObject(accountName, DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass())); + saveAndUpdateConfigs(getUserConfigName().toString(), client, CType.INTERNALUSERS, internalUsersConfiguration); + + + authToken = Base64.getUrlEncoder().encodeToString((accountName + ":" + plainTextPassword).getBytes(StandardCharsets.UTF_8)); + return authToken; + + } catch (JsonProcessingException ex) { + throw new UserServiceException(FAILED_ACCOUNT_RETRIEVAL_MESSAGE); + } catch (Exception e) { + throw new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE); + } + } + + public static void saveAndUpdateConfigs(final String indexName, final Client client, final CType cType, final SecurityDynamicConfiguration configuration) { + final IndexRequest ir = new IndexRequest(indexName); + final String id = cType.toLCString(); + + configuration.removeStatic(); + + try { + client.index(ir.id(id) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .setIfSeqNo(configuration.getSeqNo()) + .setIfPrimaryTerm(configuration.getPrimaryTerm()) + .source(id, XContentHelper.toXContent(configuration, XContentType.JSON, false))); + } catch (IOException e) { + throw ExceptionsHelper.convertToOpenSearchException(e); + } + } +} diff --git a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigs.java b/src/main/java/org/opensearch/security/user/UserServiceException.java similarity index 60% rename from src/main/java/org/opensearch/security/action/tenancy/TenancyConfigs.java rename to src/main/java/org/opensearch/security/user/UserServiceException.java index 4e8fc41ef4..b8e6843751 100644 --- a/src/main/java/org/opensearch/security/action/tenancy/TenancyConfigs.java +++ b/src/main/java/org/opensearch/security/user/UserServiceException.java @@ -9,10 +9,12 @@ * GitHub history for details. */ -package org.opensearch.security.action.tenancy; +package org.opensearch.security.user; + +public class UserServiceException extends RuntimeException { + + public UserServiceException(String message) { + super(message); + } -public class TenancyConfigs { - public Boolean multitenancy_enabled; - public Boolean private_tenant_enabled; - public String default_tenant; } diff --git a/src/test/java/org/opensearch/security/TransportUserInjectorIntegTest.java b/src/test/java/org/opensearch/security/TransportUserInjectorIntegTest.java index bb339b6fc7..092c6968d9 100644 --- a/src/test/java/org/opensearch/security/TransportUserInjectorIntegTest.java +++ b/src/test/java/org/opensearch/security/TransportUserInjectorIntegTest.java @@ -73,7 +73,7 @@ public void testSecurityUserInjection() throws Exception { .put(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, true) .build(); setup(clusterNodeSettings, new DynamicSecurityConfig().setSecurityRolesMapping("roles_transport_inject_user.yml"), Settings.EMPTY); - final Settings tcSettings = AbstractSecurityUnitTest.nodeRolesSettings(Settings.builder(), false, false) + final Settings tcSettings = AbstractSecurityUnitTest.nodeRolesSettings(Settings.builder(), false, false) .put(minimumSecuritySettings(Settings.EMPTY).get(0)) .put("cluster.name", clusterInfo.clustername) .put("path.data", "./target/data/" + clusterInfo.clustername + "/cert/data") @@ -106,11 +106,12 @@ public void testSecurityUserInjection() throws Exception { Assert.fail("Expecting exception"); } catch (OpenSearchSecurityException ex) { exception = ex; - log.warn(ex.toString()); + log.debug(ex.toString()); Assert.assertNotNull(exception); - Assert.assertTrue(exception.getMessage().contains("indices:admin/create")); + Assert.assertTrue(exception.getMessage().toString().contains("no permissions for [indices:admin/create]")); } + // 3. with valid backend roles for injected user UserInjectorPlugin.injectedUser = "injectedadmin|injecttest"; try (Node node = new PluginAwareNode(false, tcSettings, Netty4Plugin.class, @@ -127,7 +128,7 @@ public void testSecurityUserInjectionWithConfigDisabled() throws Exception { .put(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false) .build(); setup(clusterNodeSettings, new DynamicSecurityConfig().setSecurityRolesMapping("roles_transport_inject_user.yml"), Settings.EMPTY); - final Settings tcSettings = AbstractSecurityUnitTest.nodeRolesSettings(Settings.builder(), false, false) + final Settings tcSettings = AbstractSecurityUnitTest.nodeRolesSettings(Settings.builder(), false, false) .put(minimumSecuritySettings(Settings.EMPTY).get(0)) .put("cluster.name", clusterInfo.clustername) .put("path.data", "./target/data/" + clusterInfo.clustername + "/cert/data") @@ -147,7 +148,7 @@ public void testSecurityUserInjectionWithConfigDisabled() throws Exception { CreateIndexResponse cir = node.client().admin().indices().create(new CreateIndexRequest("captain-logs-1")).actionGet(); Assert.assertTrue(cir.isAcknowledged()); } - + // with invalid backend roles UserInjectorPlugin.injectedUser = "ttt|kkk"; try (Node node = new PluginAwareNode(false, tcSettings, Netty4Plugin.class, @@ -157,6 +158,5 @@ public void testSecurityUserInjectionWithConfigDisabled() throws Exception { // Should pass as the user injection is disabled Assert.assertTrue(cir.isAcknowledged()); } - } } diff --git a/src/test/java/org/opensearch/security/auditlog/integration/TestAuditlogImpl.java b/src/test/java/org/opensearch/security/auditlog/integration/TestAuditlogImpl.java index 4677bc37a9..bc763b4c33 100644 --- a/src/test/java/org/opensearch/security/auditlog/integration/TestAuditlogImpl.java +++ b/src/test/java/org/opensearch/security/auditlog/integration/TestAuditlogImpl.java @@ -119,13 +119,13 @@ public List getFoundMessages() { private static String createDetailMessage(final int expectedCount, final List foundMessages) { return new StringBuilder() - .append("Did not receive all " + expectedCount + " audit messages after a short wait. ") - .append("Missing " + (expectedCount - foundMessages.size()) + " messages.") - .append("Messages found during this time: \n\n") - .append(foundMessages.stream() - .map(AuditMessage::toString) - .collect(Collectors.joining("\n"))) - .toString(); + .append("Did not receive all " + expectedCount + " audit messages after a short wait. ") + .append("Missing " + (expectedCount - foundMessages.size()) + " messages.") + .append("Messages found during this time: \n\n") + .append(foundMessages.stream() + .map(AuditMessage::toString) + .collect(Collectors.joining("\n"))) + .toString(); } } } 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..76f9e4f224 --- /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.http.Header; +import org.apache.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/dlic/rest/api/UserApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java index cb844c7b62..ea6101e429 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/UserApiTest.java @@ -12,6 +12,7 @@ package org.opensearch.security.dlic.rest.api; import java.net.URLEncoder; +import java.util.Base64; import java.util.List; import org.apache.http.Header; @@ -34,13 +35,41 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.dlic.rest.api.InternalUsersApiAction.RESTRICTED_FROM_USERNAME; - public class UserApiTest extends AbstractRestApiUnitTest { - private final String ENDPOINT; + private final String ENDPOINT; protected String getEndpointPrefix() { return PLUGINS_PREFIX; } + + final int USER_SETTING_SIZE = 56; // Lines per account entry * number of accounts + + private static final String ENABLED_SERVICE_ACCOUNT_BODY = "{" + + " \"attributes\": { \"service\": \"true\", " + + "\"enabled\": \"true\"}" + + " }\n"; + + private static final String DISABLED_SERVICE_ACCOUNT_BODY = "{" + + " \"attributes\": { \"service\": \"true\", " + + "\"enabled\": \"false\"}" + + " }\n"; + private static final String ENABLED_NOT_SERVICE_ACCOUNT_BODY = "{" + + " \"attributes\": { \"service\": \"false\", " + + "\"enabled\": \"true\"}" + + " }\n"; + private static final String PASSWORD_SERVICE = "{ \"password\" : \"test\"," + + " \"attributes\": { \"service\": \"true\", " + + "\"enabled\": \"true\"}" + + " }\n"; + private static final String HASH_SERVICE = "{ \"owner\" : \"test_owner\"," + + " \"attributes\": { \"service\": \"true\", " + + "\"enabled\": \"true\"}" + + " }\n"; + private static final String PASSWORD_HASH_SERVICE = "{ \"password\" : \"test\", \"hash\" : \"123\"," + + " \"attributes\": { \"service\": \"true\", " + + "\"enabled\": \"true\"}" + + " }\n"; + public UserApiTest(){ ENDPOINT = getEndpointPrefix() + "/api"; } @@ -55,10 +84,10 @@ public void testSecurityRoles() throws Exception { // initial configuration, 6 users HttpResponse response = rh - .executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); + .executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(56, settings.size()); + Assert.assertEquals(USER_SETTING_SIZE, settings.size()); response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/newuser\", \"value\": {\"password\": \"newuser\", \"opendistro_security_roles\": [\"opendistro_security_all_access\"] } }]", new Header[0]); Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); @@ -71,9 +100,9 @@ public void testSecurityRoles() throws Exception { @Test public void testParallelPutRequests() throws Exception { - + setup(); - + rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; @@ -104,50 +133,58 @@ public void testUserApi() throws Exception { rh.keystore = "restapi/kirk-keystore.jks"; rh.sendAdminCertificate = true; - // initial configuration, 6 users - HttpResponse response = rh - .executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); + // initial configuration + HttpResponse response = rh.executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(56, settings.size()); - // --- GET + Assert.assertEquals(USER_SETTING_SIZE, settings.size()); + verifyGet(); + verifyPut(); + verifyPatch(true); + // create index first + setupStarfleetIndex(); + verifyRoles(true); + } + private void verifyGet(final Header... header) throws Exception { + // --- GET // GET, user admin, exists - response = rh.executeGetRequest(ENDPOINT + "/internalusers/admin", new Header[0]); + HttpResponse response = rh.executeGetRequest(ENDPOINT + "/internalusers/admin", header); Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(7, settings.size()); // hash must be filtered Assert.assertEquals("", settings.get("admin.hash")); // GET, user does not exist - response = rh.executeGetRequest(ENDPOINT + "/internalusers/nothinghthere", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/internalusers/nothinghthere", header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // GET, new URL endpoint in security - response = rh.executeGetRequest(ENDPOINT + "/user/", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/user/", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // GET, new URL endpoint in security - response = rh.executeGetRequest(ENDPOINT + "/user", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/user", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - // -- PUT + } + private void verifyPut(final Header... header) throws Exception { + // -- PUT // no username given - response = rh.executePutRequest(ENDPOINT + "/internalusers/", "{\"hash\": \"123\"}", new Header[0]); + HttpResponse response = rh.executePutRequest(ENDPOINT + "/internalusers/", "{\"hash\": \"123\"}", header); Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); // Faulty JSON payload response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{some: \"thing\" asd other: \"thing\"}", - new Header[0]); + header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(settings.get("reason"), AbstractConfigurationValidator.ErrorType.BODY_NOT_PARSEABLE.getMessage()); // Missing quotes in JSON - parseable in 6.x, but wrong config keys - response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{some: \"thing\", other: \"thing\"}", - new Header[0]); + response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{some: \"thing\", other: \"thing\"}", header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); //JK: this should be "Could not parse content of request." because JSON is truly invalid @@ -156,102 +193,106 @@ public void testUserApi() throws Exception { //Assert.assertTrue(settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY + ".keys").contains("other")); // Get hidden role - response = rh.executeGetRequest(ENDPOINT + "/internalusers/hide" , new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/internalusers/hide" , header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("\"hidden\":true")); // Associating with hidden role is allowed (for superadmin) response = rh.executePutRequest(ENDPOINT + "/internalusers/test", "{ \"opendistro_security_roles\": " + - "[\"opendistro_security_hidden\"]}", new Header[0]); + "[\"opendistro_security_hidden\"]}", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // Associating with reserved role is allowed (for superadmin) response = rh.executePutRequest(ENDPOINT + "/internalusers/test", "{ \"opendistro_security_roles\": [\"opendistro_security_reserved\"], " + "\"hash\": \"123\"}", - new Header[0]); + header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // Associating with non-existent role is not allowed response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{ \"opendistro_security_roles\": [\"non_existent\"]}", - new Header[0]); + header); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(settings.get("message"), "Role 'non_existent' is not available for role-mapping."); // Wrong config keys response = rh.executePutRequest(ENDPOINT + "/internalusers/nagilum", "{\"some\": \"thing\", \"other\": \"thing\"}", - new Header[0]); + header); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(settings.get("reason"), AbstractConfigurationValidator.ErrorType.INVALID_CONFIGURATION.getMessage()); Assert.assertTrue(settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY + ".keys").contains("some")); Assert.assertTrue(settings.get(AbstractConfigurationValidator.INVALID_KEYS_KEY + ".keys").contains("other")); + } + + private void verifyPatch(final boolean sendAdminCert, Header... restAdminHeader) throws Exception { // -- PATCH // PATCH on non-existing resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers/imnothere", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + HttpResponse response = rh.executePatchRequest(ENDPOINT + "/internalusers/imnothere", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // PATCH read only resource, must be forbidden, // but SuperAdmin can PATCH read-only resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers/sarek", "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers/sarek", "[{ \"op\": \"add\", \"path\": \"/description\", \"value\": \"foo\" }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // PATCH hidden resource, must be not found, can be found for super admin - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers/q", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers/q", "[{ \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH value of hidden flag, must fail with validation error - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers/test", "[{ \"op\": \"add\", \"path\": \"/hidden\", \"value\": true }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers/test", "[{ \"op\": \"add\", \"path\": \"/hidden\", \"value\": true }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); // PATCH password - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers/test", "[{ \"op\": \"add\", \"path\": \"/password\", \"value\": \"neu\" }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers/test", "[{ \"op\": \"add\", \"path\": \"/password\", \"value\": \"neu\" }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/internalusers/test", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/internalusers/test", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertFalse(settings.hasValue("test.password")); Assert.assertTrue(settings.hasValue("test.hash")); // -- PATCH on whole config resource // PATCH on non-existing resource - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/imnothere/a\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/imnothere/a\", \"value\": [ \"foo\", \"bar\" ] }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH read only resource, must be forbidden, // but SuperAdmin can PATCH read only resouce - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/sarek/description\", \"value\": \"foo\" }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/sarek/description\", \"value\": \"foo\" }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); rh.sendAdminCertificate = false; - response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/sarek/a\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/sarek/a\", \"value\": [ \"foo\", \"bar\" ] }]"); Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); // PATCH hidden resource, must be bad request - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/q/a\", \"value\": [ \"foo\", \"bar\" ] }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/q/a\", \"value\": [ \"foo\", \"bar\" ] }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // PATCH value of hidden flag, must fail with validation error - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/test/hidden\", \"value\": true }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/test/hidden\", \"value\": true }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertTrue(response.getBody().matches(".*\"invalid_keys\"\\s*:\\s*\\{\\s*\"keys\"\\s*:\\s*\"hidden\"\\s*\\}.*")); // PATCH - rh.sendAdminCertificate = true; - response = rh.executePatchRequest(ENDPOINT + "/internalusers", "[{ \"op\": \"add\", \"path\": \"/bulknew1\", \"value\": {\"password\": \"bla\", \"backend_roles\": [\"vulcan\"] } }]", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePatchRequest(ENDPOINT + "/internalusers", + "[{ \"op\": \"add\", \"path\": \"/bulknew1\", \"value\": {\"password\": \"bla\", \"backend_roles\": [\"vulcan\"] } }]", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - response = rh.executeGetRequest(ENDPOINT + "/internalusers/bulknew1", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/internalusers/bulknew1", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertFalse(settings.hasValue("bulknew1.password")); @@ -267,38 +308,67 @@ public void testUserApi() throws Exception { // add/update user, user is read only, forbidden // SuperAdmin can add read only users - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; addUserWithHash("sarek", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", - HttpStatus.SC_OK); + HttpStatus.SC_OK); // add/update user, user is hidden, forbidden, allowed for super admin - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; addUserWithHash("q", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", - HttpStatus.SC_OK); + HttpStatus.SC_OK); // add users - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; addUserWithHash("nagilum", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", - HttpStatus.SC_CREATED); + HttpStatus.SC_CREATED); + + // Add enabled service account then get it + response = rh.executePutRequest(ENDPOINT + "/internalusers/happyServiceLive", + ENABLED_SERVICE_ACCOUNT_BODY, restAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); + response = rh.executeGetRequest(ENDPOINT + "/internalusers/happyServiceLive", restAdminHeader); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + + + // Add disabled service account + response = rh.executePutRequest(ENDPOINT + "/internalusers/happyServiceDead", + DISABLED_SERVICE_ACCOUNT_BODY, restAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); + + + // Add service account with password -- Should Fail + response = rh.executePutRequest(ENDPOINT + "/internalusers/passwordService", + PASSWORD_SERVICE, restAdminHeader); + Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + + //Add service with hash -- should fail + response = rh.executePutRequest(ENDPOINT + "/internalusers/hashService", + HASH_SERVICE, restAdminHeader); + Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + + // Add Service account with password & Hash -- should fail + response = rh.executePutRequest(ENDPOINT + "/internalusers/passwordHashService", + PASSWORD_HASH_SERVICE, restAdminHeader); + Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); // access must be allowed now checkGeneralAccess(HttpStatus.SC_OK, "nagilum", "nagilum"); // try remove user, no username - rh.sendAdminCertificate = true; - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executeDeleteRequest(ENDPOINT + "/internalusers", restAdminHeader); Assert.assertEquals(HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode()); // try remove user, nonexisting user - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/picard", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/picard", restAdminHeader); Assert.assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); // try remove readonly user - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/sarek", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/sarek", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // try remove hidden user, allowed for super admin - response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/q", new Header[0]); + response = rh.executeDeleteRequest(ENDPOINT + "/internalusers/q", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertTrue(response.getBody().contains("'q' deleted.")); // now really remove user @@ -309,7 +379,7 @@ public void testUserApi() throws Exception { checkGeneralAccess(HttpStatus.SC_UNAUTHORIZED, "nagilum", "nagilum"); // use password instead of hash - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; addUserWithPassword("nagilum", "correctpassword", HttpStatus.SC_CREATED); rh.sendAdminCertificate = false; @@ -319,45 +389,87 @@ public void testUserApi() throws Exception { deleteUser("nagilum"); // Check unchanged password functionality - rh.sendAdminCertificate = true; + rh.sendAdminCertificate = sendAdminCert; // new user, password or hash is mandatory addUserWithoutPasswordOrHash("nagilum", new String[]{"starfleet"}, HttpStatus.SC_BAD_REQUEST); // new user, add hash addUserWithHash("nagilum", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", - HttpStatus.SC_CREATED); + HttpStatus.SC_CREATED); // update user, do not specify hash or password, hash must remain the same addUserWithoutPasswordOrHash("nagilum", new String[]{"starfleet"}, HttpStatus.SC_OK); // get user, check hash, must be untouched - response = rh.executeGetRequest(ENDPOINT + "/internalusers/nagilum", new Header[0]); + response = rh.executeGetRequest(ENDPOINT + "/internalusers/nagilum", restAdminHeader); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertTrue(settings.get("nagilum.hash").equals("")); + } + private void verifyAuthToken(final boolean sendAdminCert, Header... restAdminHeader) throws Exception { - // ROLES - // create index first - setupStarfleetIndex(); + // Add enabled service account then generate auth token + + rh.sendAdminCertificate = sendAdminCert; + HttpResponse response = rh.executePutRequest(ENDPOINT + "/internalusers/happyServiceLive", + ENABLED_SERVICE_ACCOUNT_BODY, restAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executeGetRequest(ENDPOINT + "/internalusers/happyServiceLive", restAdminHeader); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + + response = rh.executePostRequest(ENDPOINT + "/internalusers/happyServiceLive/authtoken", + ENABLED_SERVICE_ACCOUNT_BODY, restAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); + String tokenFromResponse = response.getBody(); + byte[] decodedResponse = Base64.getUrlDecoder().decode(tokenFromResponse); + String[] decodedResponseString = new String(decodedResponse).split(":", 2); + String username = decodedResponseString[0]; + String password = decodedResponseString[1]; + Assert.assertEquals("Username is: " + username,username, "happyServiceLive"); + + // Add disabled service account then try to get its auth token + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePutRequest(ENDPOINT + "/internalusers/happyServiceDead", + DISABLED_SERVICE_ACCOUNT_BODY, restAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); + + response = rh.executePostRequest(ENDPOINT + "/internalusers/happyServiceDead/authtoken", + ENABLED_SERVICE_ACCOUNT_BODY, restAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + + + // Add enabled non-service account + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePutRequest(ENDPOINT + "/internalusers/user_is_owner_1", + ENABLED_NOT_SERVICE_ACCOUNT_BODY, restAdminHeader); + Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); + + response = rh.executePostRequest(ENDPOINT + "/internalusers/user_is_owner_1/authtoken", + ENABLED_SERVICE_ACCOUNT_BODY, restAdminHeader); + Assert.assertEquals(response.getBody(), HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + + } + private void verifyRoles(final boolean sendAdminCert, Header... header) throws Exception { // wrong datatypes in roles file - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes.json"), new Header[0]); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); + rh.sendAdminCertificate = sendAdminCert; + HttpResponse response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes.json"), header); + Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); Assert.assertTrue(settings.get("backend_roles").equals("Array expected")); rh.sendAdminCertificate = false; - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes.json"), new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes.json"), header); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); Assert.assertTrue(settings.get("backend_roles").equals("Array expected")); rh.sendAdminCertificate = false; - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes2.json"), new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes2.json"), header); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); @@ -365,8 +477,8 @@ public void testUserApi() throws Exception { Assert.assertTrue(settings.get("backend_roles") == null); rh.sendAdminCertificate = false; - rh.sendAdminCertificate = true; - response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes3.json"), new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", FileHelper.loadFile("restapi/users_wrong_datatypes3.json"), header); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.WRONG_DATATYPE.getMessage(), settings.get("reason")); @@ -395,12 +507,12 @@ public void testUserApi() throws Exception { checkReadAccess(HttpStatus.SC_OK, "picard", "picard", "sf", "_doc", 0); checkWriteAccess(HttpStatus.SC_CREATED, "picard", "picard", "sf", "_doc", 1); - rh.sendAdminCertificate = true; - response = rh.executeGetRequest(ENDPOINT + "/internalusers/picard", new Header[0]); + rh.sendAdminCertificate = sendAdminCert; + response = rh.executeGetRequest(ENDPOINT + "/internalusers/picard", header); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals("", settings.get("picard.hash")); - roles = settings.getAsList("picard.backend_roles"); + List roles = settings.getAsList("picard.backend_roles"); Assert.assertNotNull(roles); Assert.assertEquals(2, roles.size()); Assert.assertTrue(roles.contains("starfleet")); @@ -409,9 +521,8 @@ public void testUserApi() throws Exception { addUserWithPassword("$1aAAAAAAAAC", "$1aAAAAAAAAC", HttpStatus.SC_CREATED); addUserWithPassword("abc", "abc", HttpStatus.SC_CREATED); - // check tabs in json - response = rh.executePutRequest(ENDPOINT + "/internalusers/userwithtabs", "\t{\"hash\": \t \"123\"\t} ", new Header[0]); + response = rh.executePutRequest(ENDPOINT + "/internalusers/userwithtabs", "\t{\"hash\": \t \"123\"\t} ", header); Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); } @@ -419,11 +530,11 @@ public void testUserApi() throws Exception { public void testPasswordRules() throws Exception { Settings nodeSettings = - Settings.builder() - .put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, "xxx") - .put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, - "(?=.*[A-Z])(?=.*[^a-zA-Z\\\\d])(?=.*[0-9])(?=.*[a-z]).{8,}") - .build(); + Settings.builder() + .put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, "xxx") + .put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, + "(?=.*[A-Z])(?=.*[^a-zA-Z\\\\d])(?=.*[0-9])(?=.*[a-z]).{8,}") + .build(); setup(nodeSettings); @@ -432,11 +543,10 @@ public void testPasswordRules() throws Exception { // initial configuration, 6 users HttpResponse response = rh - .executeGetRequest("_plugins/_security/api/" + CType.INTERNALUSERS.toLCString()); + .executeGetRequest("_plugins/_security/api/" + CType.INTERNALUSERS.toLCString()); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - System.out.println(response.getBody()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(56, settings.size()); + Assert.assertEquals(USER_SETTING_SIZE, settings.size()); addUserWithPassword("tooshoort", "", HttpStatus.SC_BAD_REQUEST); addUserWithPassword("tooshoort", "123", HttpStatus.SC_BAD_REQUEST); @@ -480,9 +590,9 @@ public void testPasswordRules() throws Exception { Assert.assertTrue(response.getBody().contains("NOT_FOUND")); String patchPayload = "[ " + - "{ \"op\": \"add\", \"path\": \"/testuser1\", \"value\": { \"password\": \"$aA123456789\", \"backend_roles\": [\"testrole1\"] } }," + - "{ \"op\": \"add\", \"path\": \"/testuser2\", \"value\": { \"password\": \"testpassword2\", \"backend_roles\": [\"testrole2\"] } }" + - "]"; + "{ \"op\": \"add\", \"path\": \"/testuser1\", \"value\": { \"password\": \"$aA123456789\", \"backend_roles\": [\"testrole1\"] } }," + + "{ \"op\": \"add\", \"path\": \"/testuser2\", \"value\": { \"password\": \"testpassword2\", \"backend_roles\": [\"testrole2\"] } }" + + "]"; response = rh.executePatchRequest(PLUGINS_PREFIX + "/api/internalusers", patchPayload, new BasicHeader("Content-Type", "application/json")); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); @@ -499,7 +609,7 @@ public void testPasswordRules() throws Exception { Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); response = rh.executePutRequest(PLUGINS_PREFIX + "/api/internalusers/ok1", "{\"backend_roles\":[\"my-backend-role\"],\"attributes\":{},\"password\":\"bla\"}", - new Header[0]); + new Header[0]); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); } @@ -513,22 +623,22 @@ public void testUserApiWithDots() throws Exception { // initial configuration, 6 users HttpResponse response = rh - .executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); + .executeGetRequest(ENDPOINT + "/" + CType.INTERNALUSERS.toLCString()); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(56, settings.size()); + Assert.assertEquals(USER_SETTING_SIZE, settings.size()); addUserWithPassword(".my.dotuser0", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", - HttpStatus.SC_CREATED); + HttpStatus.SC_CREATED); addUserWithPassword(".my.dot.user0", "12345678", - HttpStatus.SC_CREATED); + HttpStatus.SC_CREATED); addUserWithHash(".my.dotuser1", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", - HttpStatus.SC_CREATED); + HttpStatus.SC_CREATED); addUserWithPassword(".my.dot.user2", "12345678", - HttpStatus.SC_CREATED); + HttpStatus.SC_CREATED); } @@ -544,7 +654,7 @@ public void testUserApiNoPasswordChange() throws Exception { HttpResponse response; addUserWithHash("user1", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", - HttpStatus.SC_CREATED); + HttpStatus.SC_CREATED); response = rh.executePutRequest(ENDPOINT + "/internalusers/user1", "{\"hash\":\"$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m\",\"password\":\"\",\"backend_roles\":[\"admin\",\"rolea\"]}"); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); @@ -556,7 +666,7 @@ public void testUserApiNoPasswordChange() throws Exception { Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); addUserWithHash("user2", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", - HttpStatus.SC_CREATED); + HttpStatus.SC_CREATED); response = rh.executePutRequest(ENDPOINT + "/internalusers/user2", "{\"password\":\"\",\"backend_roles\":[\"admin\",\"rolex\"]}"); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); @@ -656,7 +766,7 @@ public void checkNullElementsInArray() throws Exception{ rh.sendAdminCertificate = true; String body = FileHelper.loadFile("restapi/users_null_array_element.json"); - HttpResponse response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", body, new Header[0]); + HttpResponse response = rh.executePutRequest(ENDPOINT + "/internalusers/picard", body); Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); Assert.assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); Assert.assertEquals(AbstractConfigurationValidator.ErrorType.NULL_ARRAY_ELEMENT.getMessage(), settings.get("reason")); 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 27f02a90b7..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.http.Header; -import org.apache.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 e33842d433..63a647fc29 100644 --- a/src/test/java/org/opensearch/security/multitenancy/test/TenancyMultitenancyEnabledTests.java +++ b/src/test/java/org/opensearch/security/multitenancy/test/TenancyMultitenancyEnabledTests.java @@ -16,26 +16,28 @@ import org.apache.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 "{"+ - "\"type\" : \"index-pattern\","+ - "\"updated_at\" : \"2018-09-29T08:56:59.066Z\","+ - "\"index-pattern\" : {"+ + "\"type\" : \"index-pattern\","+ + "\"updated_at\" : \"2018-09-29T08:56:59.066Z\","+ + "\"index-pattern\" : {"+ "\"title\" : \"" + title + "\""+ "}}"; - } + } @Override protected String getResourceFolder() { @@ -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 d1db4117a1..ead43008f8 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.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,17 +26,18 @@ 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 "{"+ - "\"type\" : \"index-pattern\","+ - "\"updated_at\" : \"2018-09-29T08:56:59.066Z\","+ - "\"index-pattern\" : {"+ - "\"title\" : \"" + title + "\""+ - "}}"; + "\"type\" : \"index-pattern\","+ + "\"updated_at\" : \"2018-09-29T08:56:59.066Z\","+ + "\"index-pattern\" : {"+ + "\"title\" : \"" + title + "\""+ + "}}"; } @Override @@ -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