diff --git a/config/tenancy_config.yml b/config/tenancy_config.yml new file mode 100644 index 0000000000..be496e7db5 --- /dev/null +++ b/config/tenancy_config.yml @@ -0,0 +1,8 @@ +_meta: + type: "tenancyconfig" + config_version: 2 + +tenancy_config: + multitenancy_enabled: true + private_tenant_enabled: true + default_tenant: "" \ No newline at end of file diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index ce64299f13..d3d123efdd 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -913,6 +913,7 @@ public List> getSettings() { settings.add(Setting.simpleString(ConfigConstants.SECURITY_CERT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS, Property.NodeScope, Property.Filtered)); settings.add(Setting.listSetting(ConfigConstants.SECURITY_NODES_DN, Collections.emptyList(), Function.identity(), Property.NodeScope));//not filtered here + settings.add(Setting.listSetting(ConfigConstants.SECURITY_TENANCY_CONFIG, Collections.emptyList(), Function.identity(), Property.NodeScope)); settings.add(Setting.boolSetting(ConfigConstants.SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED, false, Property.NodeScope));//not filtered here diff --git a/src/main/java/org/opensearch/security/configuration/ConfigurationLoaderSecurity7.java b/src/main/java/org/opensearch/security/configuration/ConfigurationLoaderSecurity7.java index 3019c76462..cdfd25ee21 100644 --- a/src/main/java/org/opensearch/security/configuration/ConfigurationLoaderSecurity7.java +++ b/src/main/java/org/opensearch/security/configuration/ConfigurationLoaderSecurity7.java @@ -139,6 +139,19 @@ public void noData(String id) { } } + // Since TenancyConfig is newly introduced data-type applying for existing clusters as well, we make it backward compatible by returning valid empty + // SecurityDynamicConfiguration. + if (cType == CType.TENANCYCONFIG) { + try { + SecurityDynamicConfiguration empty = ConfigHelper.createEmptySdc(cType, ConfigurationRepository.getDefaultConfigVersion()); + rs.put(cType, empty); + latch.countDown(); + return; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + if(cType == CType.AUDIT) { // Audit configuration doc is not available in the index. // Configuration cannot be hot-reloaded. @@ -233,8 +246,6 @@ private SecurityDynamicConfiguration toConfig(GetResponse singleGetResponse, final long seqNo = singleGetResponse.getSeqNo(); final long primaryTerm = singleGetResponse.getPrimaryTerm(); - - if (ref == null || ref.length() == 0) { log.error("Empty or null byte reference for {}", id); return null; diff --git a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java index 19e036b5e9..cd8448a74b 100644 --- a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java +++ b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java @@ -136,10 +136,11 @@ public void run() { ConfigHelper.uploadFile(client, cd+"roles_mapping.yml", securityIndex, CType.ROLESMAPPING, DEFAULT_CONFIG_VERSION); ConfigHelper.uploadFile(client, cd+"internal_users.yml", securityIndex, CType.INTERNALUSERS, DEFAULT_CONFIG_VERSION); ConfigHelper.uploadFile(client, cd+"action_groups.yml", securityIndex, CType.ACTIONGROUPS, DEFAULT_CONFIG_VERSION); + final boolean populateEmptyIfFileMissing = true; if(DEFAULT_CONFIG_VERSION == 2) { ConfigHelper.uploadFile(client, cd+"tenants.yml", securityIndex, CType.TENANTS, DEFAULT_CONFIG_VERSION); + ConfigHelper.uploadFile(client, cd+"tenancy_config.yml", securityIndex, CType.TENANCYCONFIG, DEFAULT_CONFIG_VERSION, populateEmptyIfFileMissing); } - final boolean populateEmptyIfFileMissing = true; ConfigHelper.uploadFile(client, cd+"nodes_dn.yml", securityIndex, CType.NODESDN, DEFAULT_CONFIG_VERSION, populateEmptyIfFileMissing); ConfigHelper.uploadFile(client, cd + "whitelist.yml", securityIndex, CType.WHITELIST, DEFAULT_CONFIG_VERSION, populateEmptyIfFileMissing); ConfigHelper.uploadFile(client, cd + "allowlist.yml", securityIndex, CType.ALLOWLIST, DEFAULT_CONFIG_VERSION, populateEmptyIfFileMissing); diff --git a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java index 262aadf424..5ed7567115 100644 --- a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java +++ b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java @@ -48,6 +48,7 @@ import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.securityconf.TenancyConfigModel; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; @@ -95,9 +96,10 @@ private boolean isTenantAllowed(final ActionRequest request, final String action */ @Override public ReplaceResult replaceDashboardsIndex(final ActionRequest request, final String action, final User user, final DynamicConfigModel config, + final TenancyConfigModel tenancyconfig, final Resolved requestedResolved, final Map tenants) { - final boolean enabled = config.isDashboardsMultitenancyEnabled();//config.dynamic.kibana.multitenancy_enabled; + final boolean enabled = tenancyconfig.isDashboardsMultitenancyEnabled(); if (!enabled) { return CONTINUE_EVALUATION_REPLACE_RESULT; @@ -116,6 +118,7 @@ public ReplaceResult replaceDashboardsIndex(final ActionRequest request, final S //intercept when requests are not made by the kibana server and if the kibana index/alias (.kibana) is the only index/alias involved final boolean dashboardsIndexOnly = !user.getName().equals(dashboardsServerUsername) && resolveToDashboardsIndexOrAlias(requestedResolved, dashboardsIndexName); final boolean isTraceEnabled = log.isTraceEnabled(); + if (requestedTenant == null || requestedTenant.length() == 0) { if (isTraceEnabled) { log.trace("No tenant, will resolve to " + dashboardsIndexName); @@ -124,7 +127,6 @@ public ReplaceResult replaceDashboardsIndex(final ActionRequest request, final S if (dashboardsIndexOnly && !isTenantAllowed(request, action, user, tenants, "global_tenant")) { return ACCESS_DENIED_REPLACE_RESULT; } - return CONTINUE_EVALUATION_REPLACE_RESULT; } @@ -175,7 +177,6 @@ public ReplaceResult replaceDashboardsIndex(final ActionRequest request, final S } } - return CONTINUE_EVALUATION_REPLACE_RESULT; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java index 0e98124b6f..0887bc0fbe 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AbstractApiAction.java @@ -109,20 +109,21 @@ protected void handleApiRequest(final RestChannel channel, final RestRequest req try { // validate additional settings, if any AbstractConfigurationValidator validator = getValidator(request, request.content()); - if (!validator.validate()) { + if (validator!= null && !validator.validate()) { request.params().clear(); badRequestResponse(channel, validator); return; } + JsonNode jsonNode = validator == null ? null : validator.getContentAsNode(); switch (request.method()) { case DELETE: - handleDelete(channel,request, client, validator.getContentAsNode()); break; + handleDelete(channel,request, client, jsonNode); break; case POST: - handlePost(channel,request, client, validator.getContentAsNode());break; + handlePost(channel,request, client, jsonNode);break; case PUT: - handlePut(channel,request, client, validator.getContentAsNode());break; + handlePut(channel,request, client, jsonNode);break; case GET: - handleGet(channel,request, client, validator.getContentAsNode());break; + handleGet(channel,request, client, jsonNode);break; default: throw new IllegalArgumentException(request.method() + " not supported"); } @@ -481,6 +482,7 @@ protected void successResponse(RestChannel channel) { try { final XContentBuilder builder = channel.newBuilder(); builder.startObject(); + builder.endObject(); channel.sendResponse( new BytesRestResponse(RestStatus.OK, builder)); } catch (IOException e) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/Endpoint.java b/src/main/java/org/opensearch/security/dlic/rest/api/Endpoint.java index 84a447bcac..a8d3cd473c 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/Endpoint.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/Endpoint.java @@ -29,5 +29,6 @@ public enum Endpoint { WHITELIST, ALLOWLIST, NODESDN, - SSL; + SSL, + TENANCYCONFIG; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java index c3449e99bb..ad04ded124 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java @@ -68,6 +68,7 @@ default String build() { .put(Endpoint.ROLES, action -> buildEndpointPermission(Endpoint.ROLES)) .put(Endpoint.ROLESMAPPING, action -> buildEndpointPermission(Endpoint.ROLESMAPPING)) .put(Endpoint.TENANTS, action -> buildEndpointPermission(Endpoint.TENANTS)) + .put(Endpoint.TENANCYCONFIG, action -> buildEndpointPermission(Endpoint.TENANCYCONFIG)) .put(Endpoint.SSL, action -> { switch (action) { case CERTS_INFO_ACTION: 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 9655ba67ea..d003d5948f 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 @@ -59,6 +59,7 @@ public static Collection getHandler(final Settings settings, handlers.add(new ValidateApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new AccountApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); handlers.add(new NodesDnApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); + handlers.add(new TenancyConfigAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); 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)); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/TenancyConfigAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/TenancyConfigAction.java new file mode 100644 index 0000000000..08041420a7 --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/TenancyConfigAction.java @@ -0,0 +1,104 @@ +package org.opensearch.security.dlic.rest.api; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; + +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestController; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestRequest.Method; +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.TenancyConfigValidator; +import org.opensearch.security.dlic.rest.validation.SecurityConfigValidator; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.ssl.transport.PrincipalExtractor; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.threadpool.ThreadPool; + +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +public class TenancyConfigAction extends PatchableResourceApiAction{ + + private static final List allRoutes = new ImmutableList.Builder() + .addAll(addRoutesPrefix( + ImmutableList.of( + new Route(Method.GET, "/tenancyconfig/"), + new Route(Method.PUT, "/tenancyconfig/"), + new Route(Method.PATCH, "/tenancyconfig/") + ) + )) + .build(); + + @Inject + public TenancyConfigAction(Settings settings, Path configPath, RestController controller, Client client, + AdminDNs adminDNs, ConfigurationRepository cl, ClusterService cs, + PrincipalExtractor principalExtractor, PrivilegesEvaluator evaluator, + ThreadPool threadPool, AuditLog auditLog) { + super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + } + + @Override + public List routes() { + return allRoutes; + } + + @Override + protected void handleGet(RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException{ + + final SecurityDynamicConfiguration configuration = load(getConfigName(), true); + filter(configuration); + successResponse(channel,configuration); + } + + + @Override + protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... params) { + return new TenancyConfigValidator(request, ref, this.settings, params); + } + + @Override + protected void handleDelete(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException{ + notImplemented(channel, Method.DELETE); + } + + @Override + protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException{ + successResponse(channel); + } + + @Override + protected String getResourceName() { + return null; + } + + @Override + protected CType getConfigName() { + return CType.TENANCYCONFIG; + } + + @Override + protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, final String resourceName) { + return true; + } + + @Override + protected Endpoint getEndpoint() { + return Endpoint.TENANCYCONFIG; + } +} \ No newline at end of file diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/TenancyConfigValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/TenancyConfigValidator.java new file mode 100644 index 0000000000..217f9638c9 --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/TenancyConfigValidator.java @@ -0,0 +1,28 @@ +package org.opensearch.security.dlic.rest.validation; + +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.settings.Settings; +import org.opensearch.rest.RestRequest; + +/* + * 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. + */ + +public class TenancyConfigValidator extends AbstractConfigurationValidator { + + public TenancyConfigValidator(final RestRequest request, final BytesReference ref, final Settings opensearchSettings, Object... param) { + super(request, ref, opensearchSettings, param); + this.payloadMandatory = true; + + allowedKeys.put("multitenancy_enabled", DataType.BOOLEAN); + allowedKeys.put("private_tenant_enabled", DataType.BOOLEAN); + allowedKeys.put("default_tenant", DataType.STRING); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index cceaeb4cb0..6fb818e7d4 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -91,6 +91,7 @@ import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.securityconf.TenancyConfigModel; import org.opensearch.security.securityconf.SecurityRoles; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; @@ -134,6 +135,7 @@ public class PrivilegesEvaluator { private final boolean dlsFlsEnabled; private final boolean dfmEmptyOverwritesAll; private DynamicConfigModel dcm; + private TenancyConfigModel tcm; private final NamedXContentRegistry namedXContentRegistry; public PrivilegesEvaluator(final ClusterService clusterService, final ThreadPool threadPool, @@ -175,6 +177,11 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { this.dcm = dcm; } + @Subscribe + public void onTenancyConfigModelChanged(TenancyConfigModel tcm) { + this.tcm = tcm; + } + private SecurityRoles getSecurityRoles(Set roles) { return configModel.getSecurityRoles().filter(roles); } @@ -328,7 +335,7 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin } else { if(privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { - final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex(request, action0, user, dcm, requestedResolved, + final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex(request, action0, user, dcm, tcm, requestedResolved, mapTenants(user, mappedRoles)); if (isDebugEnabled) { @@ -412,7 +419,7 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin if(privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { - final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex(request, action0, user, dcm, requestedResolved, mapTenants(user, mappedRoles)); + final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex(request, action0, user, dcm, tcm, requestedResolved, mapTenants(user, mappedRoles)); if (isDebugEnabled) { log.debug("Result from privileges interceptor: {}", replaceResult); @@ -522,9 +529,13 @@ public Set getAllConfiguredTenantNames() { public boolean multitenancyEnabled() { return privilegesInterceptor.getClass() != PrivilegesInterceptor.class - && dcm.isDashboardsMultitenancyEnabled(); + && tcm.isDashboardsMultitenancyEnabled(); } + public boolean privateTenantEnabled(){ return tcm.isDashboardsPrivateTenantEnabled(); } + + public String dashboardsDefaultTenant(){ return tcm.dashboardsDefaultTenant(); } + public boolean notFailOnForbiddenEnabled() { return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDnfofEnabled(); diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java index c76910474f..8b1b3b442a 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java @@ -36,6 +36,7 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.securityconf.TenancyConfigModel; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; @@ -74,10 +75,11 @@ public PrivilegesInterceptor(final IndexNameExpressionResolver resolver, final C } public ReplaceResult replaceDashboardsIndex(final ActionRequest request, final String action, final User user, final DynamicConfigModel config, + final TenancyConfigModel tenancyconfig, final Resolved requestedResolved, final Map tenants) { throw new RuntimeException("not implemented"); } - + protected final ThreadContext getThreadContext() { return threadPool.getThreadContext(); } diff --git a/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java b/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java index f8e03da5d2..0c91bb0ca6 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java @@ -112,6 +112,9 @@ public void accept(RestChannel channel) throws Exception { builder.field("principal", (String)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL)); builder.field("peer_certificates", certs != null && certs.length > 0 ? certs.length + "" : "0"); builder.field("sso_logout_url", (String)threadContext.getTransient(ConfigConstants.SSO_LOGOUT_URL)); + builder.field("tenancy_enabled", evaluator.multitenancyEnabled()); + builder.field("private_tenant_enabled", evaluator.privateTenantEnabled()); + builder.field("default_tenant", evaluator.dashboardsDefaultTenant()); if(user != null && verbose) { try { diff --git a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java index 266d2edf49..d7bb27b744 100644 --- a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java @@ -106,7 +106,7 @@ public void accept(RestChannel channel) throws Exception { try { final User user = (User)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - + //only allowed for admins or the kibanaserveruser if(!isAuthorized()) { response = new BytesRestResponse(RestStatus.FORBIDDEN,""); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java index 262eb37cf8..6be39440c1 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java @@ -56,6 +56,7 @@ import org.opensearch.security.securityconf.impl.AllowlistingSettings; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.NodesDn; +import org.opensearch.security.securityconf.TenancyConfigModel; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.WhitelistingSettings; import org.opensearch.security.securityconf.impl.v6.ActionGroupsV6; @@ -69,11 +70,13 @@ import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.securityconf.impl.v7.TenantV7; +import org.opensearch.security.securityconf.impl.v7.TenancyConfigV7; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.threadpool.ThreadPool; public class DynamicConfigFactory implements Initializable, ConfigurationChangeListener { + protected final Logger log = LogManager.getLogger(this.getClass()); public static final EventBusBuilder EVENT_BUS_BUILDER = EventBus.builder(); private static SecurityDynamicConfiguration staticRoles = SecurityDynamicConfiguration.empty(); @@ -117,8 +120,7 @@ public final static SecurityDynamicConfiguration addStatics(SecurityDynamicCo return original; } - - protected final Logger log = LogManager.getLogger(this.getClass()); + private final ConfigurationRepository cr; private final AtomicBoolean initialized = new AtomicBoolean(); private final EventBus eventBus = EVENT_BUS_BUILDER.logger(new JavaLogger(DynamicConfigFactory.class.getCanonicalName())).build(); @@ -161,6 +163,8 @@ public void onChange(Map> typeToConfig) { SecurityDynamicConfiguration nodesDn = cr.getConfiguration(CType.NODESDN); SecurityDynamicConfiguration whitelistingSetting = cr.getConfiguration(CType.WHITELIST); SecurityDynamicConfiguration allowlistingSetting = cr.getConfiguration(CType.ALLOWLIST); + SecurityDynamicConfiguration tenancyConfig = cr.getConfiguration(CType.TENANCYCONFIG); + if (log.isDebugEnabled()) { @@ -173,7 +177,8 @@ public void onChange(Map> typeToConfig) { " tenants: " + tenants.getImplementingClass() + " with " + tenants.getCEntries().size() + " entries\n" + " nodesdn: " + nodesDn.getImplementingClass() + " with " + nodesDn.getCEntries().size() + " entries\n" + " whitelist " + whitelistingSetting.getImplementingClass() + " with " + whitelistingSetting.getCEntries().size() + " entries\n" + - " allowlist " + allowlistingSetting.getImplementingClass() + " with " + allowlistingSetting.getCEntries().size() + " entries\n"; + " allowlist " + allowlistingSetting.getImplementingClass() + " with " + allowlistingSetting.getCEntries().size() + " entries\n" + + " tenancyConfig: " + tenancyConfig.getImplementingClass() + " with " + tenancyConfig.getCEntries().size() + " entries\n" ; log.debug(logmsg); } @@ -184,6 +189,7 @@ public void onChange(Map> typeToConfig) { final WhitelistingSettings whitelist = (WhitelistingSettings) cr.getConfiguration(CType.WHITELIST).getCEntry("config"); final AllowlistingSettings allowlist = (AllowlistingSettings) cr.getConfiguration(CType.ALLOWLIST).getCEntry("config"); final AuditConfig audit = (AuditConfig)cr.getConfiguration(CType.AUDIT).getCEntry("config"); + final TenancyConfigModel tcm ; if(config.getImplementingClass() == ConfigV7.class) { //statics @@ -226,6 +232,7 @@ public void onChange(Map> typeToConfig) { //rebuild v7 Models dcm = new DynamicConfigModelV7(getConfigV7(config), opensearchSettings, configPath, iab); + tcm = new TenancyConfigModel(getTenancyConfig(tenancyConfig)); ium = new InternalUsersModelV7((SecurityDynamicConfiguration) internalusers, (SecurityDynamicConfiguration) roles, (SecurityDynamicConfiguration) rolesmapping); @@ -235,6 +242,7 @@ public void onChange(Map> typeToConfig) { //rebuild v6 Models dcm = new DynamicConfigModelV6(getConfigV6(config), opensearchSettings, configPath, iab); + tcm = new TenancyConfigModel(getTenancyConfig(tenancyConfig)); ium = new InternalUsersModelV6((SecurityDynamicConfiguration) internalusers); cm = new ConfigModelV6((SecurityDynamicConfiguration) roles, (SecurityDynamicConfiguration)actionGroups, (SecurityDynamicConfiguration)rolesmapping, dcm, opensearchSettings); @@ -245,6 +253,7 @@ public void onChange(Map> typeToConfig) { eventBus.post(dcm); eventBus.post(ium); eventBus.post(nm); + eventBus.post(tcm); eventBus.post(whitelist==null? defaultWhitelistingSettings: whitelist); eventBus.post(allowlist==null? defaultAllowlistingSettings: allowlist); if (cr.isAuditHotReloadingEnabled()) { @@ -266,6 +275,12 @@ private static ConfigV7 getConfigV7(SecurityDynamicConfiguration sdc) { SecurityDynamicConfiguration c = (SecurityDynamicConfiguration) sdc; return c.getCEntry("config"); } + + private static TenancyConfigV7 getTenancyConfig(SecurityDynamicConfiguration sdc) { + @SuppressWarnings("unchecked") + SecurityDynamicConfiguration c = (SecurityDynamicConfiguration) sdc; + return c.getCEntry("tenancy_config"); + } @Override public final boolean isInitialized() { diff --git a/src/main/java/org/opensearch/security/securityconf/TenancyConfigModel.java b/src/main/java/org/opensearch/security/securityconf/TenancyConfigModel.java new file mode 100644 index 0000000000..65cbb7a667 --- /dev/null +++ b/src/main/java/org/opensearch/security/securityconf/TenancyConfigModel.java @@ -0,0 +1,26 @@ +/* + * 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.securityconf; +import org.opensearch.security.securityconf.impl.v7.TenancyConfigV7; + +public class TenancyConfigModel { + private final TenancyConfigV7 tenancyConfig; + + public TenancyConfigModel(TenancyConfigV7 tenancyConfig) { + this.tenancyConfig = tenancyConfig; + } + + public boolean isDashboardsMultitenancyEnabled() { return this.tenancyConfig.multitenancy_enabled; }; + public boolean isDashboardsPrivateTenantEnabled() { return this.tenancyConfig.private_tenant_enabled; }; + public String dashboardsDefaultTenant() { return this.tenancyConfig.default_tenant; }; +} + diff --git a/src/main/java/org/opensearch/security/securityconf/impl/CType.java b/src/main/java/org/opensearch/security/securityconf/impl/CType.java index 5d9f1f307b..2d83232330 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/CType.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/CType.java @@ -47,6 +47,7 @@ import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.securityconf.impl.v7.TenantV7; +import org.opensearch.security.securityconf.impl.v7.TenancyConfigV7; public enum CType { @@ -61,7 +62,8 @@ public enum CType { NODESDN(toMap(1, NodesDn.class, 2, NodesDn.class)), WHITELIST(toMap(1, WhitelistingSettings.class, 2, WhitelistingSettings.class)), ALLOWLIST(toMap(1, AllowlistingSettings.class, 2, AllowlistingSettings.class)), - AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class)); + AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class)), + TENANCYCONFIG(toMap(2,TenancyConfigV7.class)); private Map> implementations; diff --git a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java index 09eeee41e3..d769a7fa57 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java @@ -52,7 +52,7 @@ import org.opensearch.security.securityconf.StaticDefinable; public class SecurityDynamicConfiguration implements ToXContent { - + private static final TypeReference> typeRefMSO = new TypeReference>() {}; @JsonIgnore @@ -87,7 +87,7 @@ public static SecurityDynamicConfiguration fromJson(String json, CType ct } else { sdc = new SecurityDynamicConfiguration(); } - + sdc.ctype = ctype; sdc.seqNo = seqNo; sdc.primaryTerm = primaryTerm; diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java index 0e83590d3e..499106dc0a 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java @@ -138,6 +138,7 @@ public static class Kibana { public String server_username = "kibanaserver"; public String opendistro_role = null; public String index = ".kibana"; + @Override public String toString() { return "Kibana [multitenancy_enabled=" + multitenancy_enabled + ", server_username=" + server_username + ", opendistro_role=" + opendistro_role diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/TenancyConfigV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/TenancyConfigV7.java new file mode 100644 index 0000000000..8219dc017e --- /dev/null +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/TenancyConfigV7.java @@ -0,0 +1,16 @@ +package org.opensearch.security.securityconf.impl.v7; + +import com.fasterxml.jackson.annotation.JsonInclude; + +public class TenancyConfigV7 { + + @JsonInclude(JsonInclude.Include.NON_NULL) + public boolean multitenancy_enabled = true; + + @JsonInclude(JsonInclude.Include.NON_NULL) + public boolean private_tenant_enabled = true; + + @JsonInclude(JsonInclude.Include.NON_NULL) + public String default_tenant = ""; + +} diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index ee83284ca4..d0f45ff35d 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -208,6 +208,7 @@ public class ConfigConstants { public static final String SECURITY_CERT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = "plugins.security.cert.intercluster_request_evaluator_class"; public static final String SECURITY_ADVANCED_MODULES_ENABLED = "plugins.security.advanced_modules_enabled"; public static final String SECURITY_NODES_DN = "plugins.security.nodes_dn"; + public static final String SECURITY_TENANCY_CONFIG = "plugins.security.tenancyconfig"; public static final String SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED = "plugins.security.nodes_dn_dynamic_config_enabled"; public static final String SECURITY_DISABLED = "plugins.security.disabled"; public static final String SECURITY_CACHE_TTL_MINUTES = "plugins.security.cache.ttl_minutes"; diff --git a/src/main/java/org/opensearch/security/tools/SecurityAdmin.java b/src/main/java/org/opensearch/security/tools/SecurityAdmin.java index 161ad72528..c8fe9b12b5 100644 --- a/src/main/java/org/opensearch/security/tools/SecurityAdmin.java +++ b/src/main/java/org/opensearch/security/tools/SecurityAdmin.java @@ -139,6 +139,7 @@ import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.securityconf.impl.v7.TenantV7; +import org.opensearch.security.securityconf.impl.v7.TenancyConfigV7; import org.opensearch.security.ssl.util.ExceptionUtils; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.ConfigHelper; @@ -741,6 +742,7 @@ public static int execute(final String[] args) throws Exception { if(!legacy) { success = retrieveFile(restHighLevelClient, cd + "security_tenants_" + date + ".yml", index, "tenants", legacy) && success; + success = retrieveFile(restHighLevelClient, cd + "tenancy_config_" + date + ".yml", index, "tenants", legacy) && success; } final boolean populateFileIfEmpty = true; @@ -1203,6 +1205,7 @@ private static int backup(RestHighLevelClient tc, String index, File backupDir, if(!legacy) { success = retrieveFile(tc, backupDir.getAbsolutePath()+"/tenants.yml", index, "tenants", legacy) && success; + success = retrieveFile(tc, backupDir.getAbsolutePath()+"/tenancy_config.yml", index, "tenancyconfig", legacy) && success; } success = retrieveFile(tc, backupDir.getAbsolutePath()+"/nodes_dn.yml", index, "nodesdn", legacy, true) && success; success = retrieveFile(tc, backupDir.getAbsolutePath()+"/whitelist.yml", index, "whitelist", legacy, true) && success; @@ -1223,6 +1226,8 @@ private static int upload(RestHighLevelClient tc, String index, String cd, boole if(!legacy) { success = uploadFile(tc, cd+"tenants.yml", index, "tenants", legacy, resolveEnvVars) && success; + success = uploadFile(tc, cd+"tenancy_config.yml", index, "tenancyconfig", legacy, resolveEnvVars, true) && success; + } success = uploadFile(tc, cd+"nodes_dn.yml", index, "nodesdn", legacy, resolveEnvVars, true) && success; @@ -1383,7 +1388,7 @@ private static boolean validateConfigFile(String file, CType cType, int version) private static String[] getTypes(boolean legacy) { if (legacy) { - return new String[]{"config", "roles", "rolesmapping", "internalusers", "actiongroups", "nodesdn", "audit"}; + return new String[]{"config", "roles", "rolesmapping", "internalusers", "actiongroups", "nodesdn", "audit","tenancyconfig"}; } return CType.lcStringValues().toArray(new String[0]); } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/TenancyConfigApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/TenancyConfigApiTest.java new file mode 100644 index 0000000000..a57a206d39 --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/TenancyConfigApiTest.java @@ -0,0 +1,93 @@ +package org.opensearch.security.dlic.rest.api; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpStatus; +import org.junit.Assert; +import org.junit.Test; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.test.helper.file.FileHelper; +import org.opensearch.security.test.helper.rest.RestHelper; + +import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; + +public class TenancyConfigApiTest extends AbstractRestApiUnitTest { + private final String BASE_ENDPOINT; + private final String ENDPOINT; + protected String getEndpointPrefix() { + return PLUGINS_PREFIX; + } + + public TenancyConfigApiTest(){ + BASE_ENDPOINT = getEndpointPrefix(); + ENDPOINT = getEndpointPrefix() + "/api/tenancyconfig"; + } + + @Test + public void testTenancyConfigAPIAccess() throws Exception { + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true).build(); + setup(settings); + + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendAdminCertificate = true; + RestHelper.HttpResponse response = rh.executeGetRequest(ENDPOINT); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + + rh.sendAdminCertificate = false; + response = rh.executeGetRequest(ENDPOINT); + Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); + + rh.sendHTTPClientCredentials = true; + response = rh.executeGetRequest(ENDPOINT); + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } + + @Test + public void testTenancyConfigAPIUpdate() throws Exception { + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true).build(); + setup(settings); + + rh.keystore = "restapi/kirk-keystore.jks"; + rh.sendHTTPClientCredentials = true; + rh.sendAdminCertificate = true; + + //update security config + RestHelper.HttpResponse response = rh.executePutRequest(ENDPOINT, FileHelper.loadFile("restapi/tenancyconfig.json"), new Header[0]); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + + RestHelper.HttpResponse authinfo_response = rh.executeGetRequest(BASE_ENDPOINT + "/authinfo"); + + Assert.assertEquals(authinfo_response.findValueInJson("tenancy_enabled"),"true"); + Assert.assertEquals(authinfo_response.findValueInJson("private_tenant_enabled"),"true"); + Assert.assertEquals(authinfo_response.findValueInJson("default_tenant"),""); + + response = rh.executePatchRequest(ENDPOINT, "[{\"op\": \"add\",\"path\": \"/tenancy_config/multitenancy_enabled\"," + + "\"value\": false}]", new Header[0]); + + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + authinfo_response = rh.executeGetRequest(BASE_ENDPOINT + "/authinfo"); + + Assert.assertEquals(authinfo_response.findValueInJson("tenancy_enabled"),"false"); + + response = rh.executePatchRequest(ENDPOINT, "[{\"op\": \"add\",\"path\": \"/tenancy_config/private_tenant_enabled\"," + + "\"value\": false}]", new Header[0]); + + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + authinfo_response = rh.executeGetRequest(BASE_ENDPOINT + "/authinfo"); + + Assert.assertEquals(authinfo_response.findValueInJson("private_tenant_enabled"),"false"); + + response = rh.executePatchRequest(ENDPOINT, "[{\"op\": \"add\",\"path\": \"/tenancy_config/default_tenant\"," + + "\"value\": \"Private\"}]", new Header[0]); + + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + authinfo_response = rh.executeGetRequest(BASE_ENDPOINT + "/authinfo"); + + Assert.assertEquals(authinfo_response.findValueInJson("default_tenant"),"Private"); + + response = rh.executeGetRequest(ENDPOINT); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + } + +} diff --git a/src/test/java/org/opensearch/security/test/DynamicSecurityConfig.java b/src/test/java/org/opensearch/security/test/DynamicSecurityConfig.java index 3573c7c274..ec4ba21bd4 100644 --- a/src/test/java/org/opensearch/security/test/DynamicSecurityConfig.java +++ b/src/test/java/org/opensearch/security/test/DynamicSecurityConfig.java @@ -45,6 +45,7 @@ public class DynamicSecurityConfig { private String securityInternalUsers = "internal_users.yml"; private String securityActionGroups = "action_groups.yml"; private String securityNodesDn = "nodes_dn.yml"; + private String tenancyconfig = "tenancy_config.yml"; private String securityWhitelist= "whitelist.yml"; private String securityAllowlist= "allowlist.yml"; private String securityAudit = "audit.yml"; @@ -150,8 +151,12 @@ public List getDynamicConfig(String folder) { .id(CType.TENANTS.toLCString()) .setRefreshPolicy(RefreshPolicy.IMMEDIATE) .source(CType.TENANTS.toLCString(), FileHelper.readYamlContent(prefix+securityTenants))); - } + ret.add(new IndexRequest(securityIndexName) + .id(CType.TENANCYCONFIG.toLCString()) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(CType.TENANCYCONFIG.toLCString(), FileHelper.readYamlContent(prefix + tenancyconfig))); + } if (null != FileHelper.getAbsoluteFilePathFromClassPath(prefix + securityNodesDn)) { ret.add(new IndexRequest(securityIndexName) .id(CType.NODESDN.toLCString()) diff --git a/src/test/resources/restapi/tenancy_config.yml b/src/test/resources/restapi/tenancy_config.yml new file mode 100644 index 0000000000..be496e7db5 --- /dev/null +++ b/src/test/resources/restapi/tenancy_config.yml @@ -0,0 +1,8 @@ +_meta: + type: "tenancyconfig" + config_version: 2 + +tenancy_config: + multitenancy_enabled: true + private_tenant_enabled: true + default_tenant: "" \ No newline at end of file diff --git a/src/test/resources/restapi/tenancyconfig.json b/src/test/resources/restapi/tenancyconfig.json new file mode 100644 index 0000000000..a3359c586f --- /dev/null +++ b/src/test/resources/restapi/tenancyconfig.json @@ -0,0 +1,5 @@ +{ + "multitenancy_enabled": true, + "private_tenant_enabled": true, + "default_tenant": "Global" +} \ No newline at end of file diff --git a/src/test/resources/tenancy_config.yml b/src/test/resources/tenancy_config.yml new file mode 100644 index 0000000000..be496e7db5 --- /dev/null +++ b/src/test/resources/tenancy_config.yml @@ -0,0 +1,8 @@ +_meta: + type: "tenancyconfig" + config_version: 2 + +tenancy_config: + multitenancy_enabled: true + private_tenant_enabled: true + default_tenant: "" \ No newline at end of file