-
Notifications
You must be signed in to change notification settings - Fork 300
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
Dynamically Enable/Disable multi-tenancy, private tenant and set Defa… #2444
Dynamically Enable/Disable multi-tenancy, private tenant and set Defa… #2444
Conversation
…ult Tenant Signed-off-by: Abhi Kalra <[email protected]>
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.
Looks good, just a couple comments and general questions. I left a note for cleaning the style stuff. Thank you for you contribution.
tenancy_config: | ||
multitenancy_enabled: true | ||
private_tenant_enabled: true | ||
default_tenant: "" |
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 spotlessApply
to automatically correct style
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.
Thanks for the sugestion @scrawfor99 . I used ./gradlew spotlessApply
as 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?
@@ -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; |
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.
Should this be moved to a config constants file?
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.
Sure will do in next commit. Thanks for the suggestion.
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.
With this, would populateEmptyIfFileMissing
always be true
? When would it be false
?
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.
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.
|
||
final SecurityDynamicConfiguration<?> configuration = load(getConfigName(), true); | ||
filter(configuration); | ||
successResponse(channel,configuration); |
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.
If you want to do anything special for responses you will need to add onResponse
logic here.
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.
@scrawfor99 We do not want to do anything special with our responses. I don't think we need that logic here.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Camel Case
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.
Thanks @anijain-Amazon for finding this miss. Will update in next commit.
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.
With this change, would config.dynamic.kibana.multitenancy_enabled
be ignored? Would this work in mixed clusters? Based on my understanding, security index is node-scoped. If there're 2 settings(the old and the new), could there be 2 different values(false
and true
) of multitenancy_enabled setting in a mixed cluster?
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.
@cliu123 going forward we will only use config.dynamic.kibana.multitenancy_enabled
and this will get priority over config.dynamic.kibana.multitenancy_enabled
.
If there're 2 settings(the old and the new), could there be 2 different values(false and true) of multitenancy_enabled setting in a mixed cluster?
In this case we will give priority to new settings.
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
Were we not calling builder.endObject();
before ?
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.
No we were not doing it. That was a miss I guess.
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
If we change the condition to validator == null || !validator.validate()
then the use of checks in the lines below is not required.
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.
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.
|
||
@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 comment
The reason will be displayed to describe this comment to others. Learn more.
If this is not doing anything, shouldn't it be notImplemented
as well ?
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.
We want to enable PUT request for tenancyConfig. That's why we are not using notImplemented
.
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.
For my understanding, why is it returning successResponse directly?
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.
@abhivka7 Thank you for the PR! Overall the PR looks good, but I wanted to run an idea by you to ensure that introducing a new tenantconfig.yml
file is the right way to go.
I see that config.yml
already provides:
@Override
public boolean isDashboardsMultitenancyEnabled() {
return config.dynamic.kibana.multitenancy_enabled;
}
was any consideration given to also adding private_tenant_enabled
and default_tenant
to this section of config.yml
? I think that could potentially simplify this PR. The beginning of config.yml
could look like:
config:
dynamic:
# Set filtered_alias_mode to 'disallow' to forbid more than 2 filtered aliases per index
# Set filtered_alias_mode to 'warn' to allow more than 2 filtered aliases per index but warns about it (default)
# Set filtered_alias_mode to 'nowarn' to allow more than 2 filtered aliases per index silently
#filtered_alias_mode: warn
#do_not_fail_on_forbidden: false
multitenancy:
enabled: true
private_tenant_enabled: true
default_tenant: ''
then to get these into SecurityInfoAction
you could follow a pattern similar to https://github.com/opensearch-project/security/blob/main/src/main/java/org/opensearch/security/auth/BackendRegistry.java#L152-L169 to subscribe to changes in this config.
@@ -1382,7 +1387,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"}; |
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.
This legacy block is only for CType for ES6. This can be removed from this block.
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.
Thanks for pointing this out @cwperks. Will remove this in next commit.
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); |
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.
Should this only be populated if the file exists? I see tenants.yml
does not create this config entry if no tenants.yml
file is supplied.
@@ -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 comment
The 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?
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.
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.
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.
Thank you for the work, added few comments.
new Route(Method.GET, "/tenancyconfig/"), | ||
new Route(Method.PUT, "/tenancyconfig/"), | ||
new Route(Method.PATCH, "/tenancyconfig/") | ||
) |
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.
Is DELETE not required?
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.
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.
Correct me if I am wrong @devardee
|
||
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; }; |
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.
nit: rename to getDashboardsDefaultTenant
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.
Ok. Will change in next commit.
@@ -184,6 +189,7 @@ public void onChange(Map<CType, SecurityDynamicConfiguration<?>> 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 ; |
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.
nit: extra space
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.
Thanks @shikharj05 for pointing this out. Will fix in next commit.
|
||
import com.fasterxml.jackson.annotation.JsonInclude; | ||
|
||
public class TenancyConfigV7 { |
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.
implement a toString() method?
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.
Sure. Will do in next commit.
} | ||
|
||
@Test | ||
public void testTenancyConfigAPIUpdate() throws Exception { |
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.
these can be split into different tests for each property
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.
Thanks for the suggestion @shikharj05 . I will split these and update in next commit.
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.
I would suggest to keep it in one test, as the test sets up a cluster before executing the test and tears down after. This will impact on the performance of the tests.
@abhivka7 @shikharj05 See comment above. Have you considered doing this inside of i.e.
|
@cwperks We were initially working on the same approach that you suggested. But going forward, we plan on introducing some other fields and features for multi-tenancy. Therefore we plan on separating this file and introducing a new one. |
@@ -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; |
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.
With this, would populateEmptyIfFileMissing
always be true
? When would it be false
?
@@ -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(); | |||
|
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.
Please remove the empty line and avoid format-only changes.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
With this change, would config.dynamic.kibana.multitenancy_enabled
be ignored? Would this work in mixed clusters? Based on my understanding, security index is node-scoped. If there're 2 settings(the old and the new), could there be 2 different values(false
and true
) of multitenancy_enabled setting in a mixed cluster?
@@ -106,7 +106,7 @@ public void accept(RestChannel channel) throws Exception { | |||
try { | |||
|
|||
final User user = (User)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); | |||
|
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.
Please remove format-only changes.
@@ -52,7 +52,7 @@ | |||
import org.opensearch.security.securityconf.StaticDefinable; | |||
|
|||
public class SecurityDynamicConfiguration<T> implements ToXContent { | |||
|
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.
Please remove format-only changes.
@@ -87,7 +87,7 @@ public static <T> SecurityDynamicConfiguration<T> fromJson(String json, CType ct | |||
} else { | |||
sdc = new SecurityDynamicConfiguration<T>(); | |||
} | |||
|
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.
Please remove format-only changes.
@@ -138,6 +138,7 @@ public static class Kibana { | |||
public String server_username = "kibanaserver"; | |||
public String opendistro_role = null; | |||
public String index = ".kibana"; | |||
|
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.
Please remove format-only changes.
@@ -150,8 +151,12 @@ public List<IndexRequest> getDynamicConfig(String folder) { | |||
.id(CType.TENANTS.toLCString()) | |||
.setRefreshPolicy(RefreshPolicy.IMMEDIATE) | |||
.source(CType.TENANTS.toLCString(), FileHelper.readYamlContent(prefix+securityTenants))); | |||
} |
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.
Please remove format-only changes.
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.
Consider reusing the existing configuration and configuration API for tenancy instead of introducing a new one. This would simplify the design.
Could you explain why this new configuration approach was chosen?
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 comment
The 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?
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.
We don't meed a flag for these changes. We will keep multi-tenancy enabled for new domains and changes can be applied directly.
We want to decouple tenancy related configuration from securityconfig for the following reasons:
cc: @peternied , @shikharj05, @abhivka7 |
@devardee, there is already a multi-tenancy key ( I'm not sure I follow the concerns on the migrate API. For this section I am assuming the new section in
What I am thinking is changing the
then inside
This new section could be created in Would this approach be feasible? I think what's presented in this PR will work, but I also like the idea of less configuration files. |
Thank you for the reply @devardee
There is an existing setting already in the security configuration, removing/invalidating this setting is a breaking change - that is a con with this proposal.
I'm not sure how this is a problem. Could you provide more background on what the migrate api is and how it is impacted with this change vs reusing the existing config?
Could you provide reference to what future enhancements are? Without more context this argument does not inform the current choice. I am in aligned with Craig's proposal that reusing the existing systems. |
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.
Thank you @abhivka7 for adding this PR. One generic comment is to add a brief javadoc for the new class added and check/add a new-line at the end of all modified files.
|
||
@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 comment
The reason will be displayed to describe this comment to others. Learn more.
For my understanding, why is it returning successResponse directly?
@@ -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()); |
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.
the name tenancy_enabled
is slightly misleading. Can we have multi_tenancy_enabled
instead?
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.
Ok will make this change.
|
||
import com.fasterxml.jackson.annotation.JsonInclude; | ||
|
||
public class TenancyConfigV7 { |
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.
Why the name V7 here? There is only one version of this class. We can move this class out of v7 folder and rename it to TenancyConfig
@@ -522,9 +529,13 @@ public Set<String> getAllConfiguredTenantNames() { | |||
|
|||
public boolean multitenancyEnabled() { | |||
return privilegesInterceptor.getClass() != PrivilegesInterceptor.class | |||
&& dcm.isDashboardsMultitenancyEnabled(); | |||
&& tcm.isDashboardsMultitenancyEnabled(); |
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.
Can we remove this method from DynamicConfigModel and its inheritor DynamicConfigModelV7 since it is not used anymore?
} | ||
|
||
@Test | ||
public void testTenancyConfigAPIUpdate() throws Exception { |
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.
I would suggest to keep it in one test, as the test sets up a cluster before executing the test and tears down after. This will impact on the performance of the tests.
Checking back in with this pull request. There are still a number of outstanding comments. I have written up a proposal that I think might simplify the issues around proxying config data from place to place. I'll see about getting a proof of concept up and running as I think it would be a far better model isolating intention from the storage medium of the configuration value. Love to get feedback on this - thanks! |
I have created a draft pull request featuring a version of this dynamic configuration that would support this scenario and shore up the limitations that have been called in the comments. |
Created another PR for these changes: #2607. Closing this one. |
I'm closing this PR out since #2607 is where this functionality is being worked on |
…ult Tenant
Description
These changes are required to dynamically enable/disable multi-tenancy, private tenant and set a default tenant.
Earlier users had to change YAMl files in each node and restart node to enable/disable multi-tenancy and private tenant. With our changes, users will be able to change these settings dynamically. We will also give admins the option to set a default tenant for all users for first time log in.
More details can be found here:
[RFC] Dynamically Configurable Tenancy Feature OpenSearch#5853
[FEATURE] Enable/Disable Multi-Tenancy, Private Tenant and set Default Tenant in OS Dashboards. security-dashboards-plugin#1302
Issues Resolved
opensearch-project/security-dashboards-plugin#1302
Is this a backport? If so, please add backport PR # and/or commits #
Nope
Testing
We have done manual testing for all changes on our local server. We have also written some unit tests for our api changes. We will write more unit tests and integration tests based on reviewer feedbacks soon.
Check List
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.