-
Notifications
You must be signed in to change notification settings - Fork 355
Dynamically Enable/Disable multi-tenancy, private tenant and set Defa… #2444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| _meta: | ||
| type: "tenancyconfig" | ||
| config_version: 2 | ||
|
|
||
| tenancy_config: | ||
| multitenancy_enabled: true | ||
| private_tenant_enabled: true | ||
| default_tenant: "" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be moved to a config constants file?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure will do in next commit. Thanks for the suggestion.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With this, would
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was already always true. We just moved the line of declaring this variable. From next commit onwards, we can change this from config constants file. |
||
| 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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this only be populated if the file exists? I see |
||
| } | ||
| 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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<String, Boolean> tenants) { | ||
|
|
||
| final boolean enabled = config.isDashboardsMultitenancyEnabled();//config.dynamic.kibana.multitenancy_enabled; | ||
| final boolean enabled = tenancyconfig.isDashboardsMultitenancyEnabled(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Camel Case
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @anijain-Amazon for finding this miss. Will update in next commit.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With this change, would
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cliu123 going forward we will only use
In this case we will give priority to new settings.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @abhivka7, thanks for this contribution. I see the point that you are trying to extend the original multi-tenancy feature with this change. So do we need a feature flag for this dynamically enable/disable multi-tenancy feature? Or it will be applied directly by enable multi tenancy?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't meed a flag for these changes. We will keep multi-tenancy enabled for new domains and changes can be applied directly. |
||
|
|
||
| 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(); | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove the empty line and avoid format-only changes. |
||
| 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; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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()) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we change the condition to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was added while testing and I forgot to remove it. Thanks @anijain-Amazon for pointing this out. Will remove this check condition in next commit.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see that this PR introduces a validator. When will the validator ever be null? Are the changes in this file needed?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was added while testing and I forgot to remove it. Thanks @cwperks for pointing this out. Will remove this check condition in next commit. |
||
| 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(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Were we not calling
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No we were not doing it. That was a miss I guess. |
||
| channel.sendResponse( | ||
| new BytesRestResponse(RestStatus.OK, builder)); | ||
| } catch (IOException e) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,5 +29,6 @@ public enum Endpoint { | |
| WHITELIST, | ||
| ALLOWLIST, | ||
| NODESDN, | ||
| SSL; | ||
| SSL, | ||
| TENANCYCONFIG; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Route> allRoutes = new ImmutableList.Builder<Route>() | ||
| .addAll(addRoutesPrefix( | ||
| ImmutableList.of( | ||
| new Route(Method.GET, "/tenancyconfig/"), | ||
| new Route(Method.PUT, "/tenancyconfig/"), | ||
| new Route(Method.PATCH, "/tenancyconfig/") | ||
| ) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is DELETE not required?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think there will be a use case where we require DELETE. We should not delete tenancy_config and we can replace existing config using PATCH. |
||
| )) | ||
| .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<Route> 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you want to do anything special for responses you will need to add
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @scrawfor99 We do not want to do anything special with our responses. I don't think we need that logic here. |
||
| } | ||
|
|
||
|
|
||
| @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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is not doing anything, shouldn't it be
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We want to enable PUT request for tenancyConfig. That's why we are not using
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For my understanding, why is it returning successResponse directly? |
||
| } | ||
|
|
||
| @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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<String> 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<String> getAllConfiguredTenantNames() { | |
|
|
||
| public boolean multitenancyEnabled() { | ||
| return privilegesInterceptor.getClass() != PrivilegesInterceptor.class | ||
| && dcm.isDashboardsMultitenancyEnabled(); | ||
| && tcm.isDashboardsMultitenancyEnabled(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we remove this method from DynamicConfigModel and its inheritor DynamicConfigModelV7 since it is not used anymore? |
||
| } | ||
|
|
||
| public boolean privateTenantEnabled(){ return tcm.isDashboardsPrivateTenantEnabled(); } | ||
|
|
||
| public String dashboardsDefaultTenant(){ return tcm.dashboardsDefaultTenant(); } | ||
|
|
||
| public boolean notFailOnForbiddenEnabled() { | ||
| return privilegesInterceptor.getClass() != PrivilegesInterceptor.class | ||
| && dcm.isDnfofEnabled(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the name
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok will make this change. |
||
| builder.field("private_tenant_enabled", evaluator.privateTenantEnabled()); | ||
| builder.field("default_tenant", evaluator.dashboardsDefaultTenant()); | ||
|
|
||
| if(user != null && verbose) { | ||
| try { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use
./gradlew spotlessApplyto automatically correct styleThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the sugestion @scrawfor99 . I used
./gradlew spotlessApplyas per your suggestion. It corrected style for some files but not tenancy_config.yml. Do you need any specific change in this file that you want me to do manually?