From b80485ae7a5ec50f65e00ded63044cbfb8385900 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 20 Nov 2025 14:12:31 -0500 Subject: [PATCH 1/6] WIP on Creating Kibana admin level that's different then write and allows settings to be updated Signed-off-by: Craig Perkins --- .../PrivilegesInterceptorImpl.java | 25 ++++++++++++++++++- .../security/privileges/TenantPrivileges.java | 8 ++++-- .../static_config/static_action_groups.yml | 11 +++++++- .../resources/static_config/static_roles.yml | 5 ++++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java index e362c7a8eb..47175b91ea 100644 --- a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java +++ b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java @@ -32,6 +32,7 @@ import org.opensearch.action.admin.indices.refresh.RefreshRequest; import org.opensearch.action.bulk.BulkRequest; import org.opensearch.action.delete.DeleteRequest; +import org.opensearch.action.get.GetRequest; import org.opensearch.action.get.MultiGetRequest; import org.opensearch.action.get.MultiGetRequest.Item; import org.opensearch.action.index.IndexRequest; @@ -141,6 +142,7 @@ public ReplaceResult replaceDashboardsIndex( && resolveToDashboardsIndexOrAlias(requestedResolved, dashboardsIndexName); final boolean isTraceEnabled = log.isTraceEnabled(); + TenantPrivileges.ActionType actionType = getActionTypeForAction(action); if (requestedTenant == null || requestedTenant.length() == 0) { @@ -148,6 +150,11 @@ public ReplaceResult replaceDashboardsIndex( log.trace("No tenant, will resolve to " + dashboardsIndexName); } + // Intercept when request is dashboards user and request is to get advanced settings. No replacement. + if ("osd:admin/advanced_settings".equals(action)) { + return ACCESS_GRANTED_REPLACE_RESULT; + } + if (dashboardsIndexOnly && !tenantPrivileges.hasTenantPrivilege(context, "global_tenant", actionType)) { return ACCESS_DENIED_REPLACE_RESULT; } @@ -199,6 +206,20 @@ public ReplaceResult replaceDashboardsIndex( final String tenantIndexName = toUserIndexName(dashboardsIndexName, requestedTenant); + System.out.println("tenantIndexName: " + tenantIndexName); + System.out.println("user: " + user); + System.out.println("action: " + action); + System.out.println("requestResolved: " + requestedResolved.getAllIndices()); + if (request instanceof GetRequest gr) { + System.out.println("GetRequest: " + gr.id()); + } else if (request instanceof SearchRequest sr) { + System.out.println("SearchRequest: " + sr.source().toString()); + } + // Intercept when request is dashboards user and request is to get advanced settings + if ("osd:admin/advanced_settings".equals(action)) { + return newAccessGrantedReplaceResult(replaceIndex(request, dashboardsIndexName, tenantIndexName, action)); + } + // The new DLS/FLS implementation defaults to a "deny all" pattern in case no roles are configured // for an index. As the PrivilegeInterceptor grants access to indices bypassing index privileges, // we need to allow-list these indices. @@ -233,7 +254,9 @@ private void applyDocumentAllowList(String indexName) { } static TenantPrivileges.ActionType getActionTypeForAction(String action) { - if (READ_ONLY_ALLOWED_ACTIONS.contains(action)) { + if ("osd:admin/advanced_settings".equals(action)) { + return TenantPrivileges.ActionType.ADMIN; + } else if (READ_ONLY_ALLOWED_ACTIONS.contains(action)) { return TenantPrivileges.ActionType.READ; } else { return TenantPrivileges.ActionType.WRITE; diff --git a/src/main/java/org/opensearch/security/privileges/TenantPrivileges.java b/src/main/java/org/opensearch/security/privileges/TenantPrivileges.java index 9320abfb45..7905bbe261 100644 --- a/src/main/java/org/opensearch/security/privileges/TenantPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/TenantPrivileges.java @@ -50,7 +50,8 @@ public class TenantPrivileges { */ public enum ActionType { READ, - WRITE; + WRITE, + ADMIN; } public static final TenantPrivileges EMPTY = new TenantPrivileges( @@ -61,6 +62,7 @@ public enum ActionType { private static final List READ = ImmutableList.of(ActionType.READ); private static final List READ_WRITE = ImmutableList.of(ActionType.READ, ActionType.WRITE); + private static final List READ_WRITE_ADMIN = ImmutableList.of(ActionType.READ, ActionType.WRITE, ActionType.ADMIN); private static final Logger log = LogManager.getLogger(TenantPrivileges.class); @@ -245,7 +247,9 @@ public Map tenantMap(PrivilegesEvaluationContext context) { static List resolveActionType(Collection allowedActions, FlattenedActionGroups actionGroups) { ImmutableSet permissions = actionGroups.resolve(allowedActions); - if (permissions.contains("kibana:saved_objects/*/write")) { + if (permissions.contains("osd:admin/advanced_settings")) { + return READ_WRITE_ADMIN; + } else if (permissions.contains("kibana:saved_objects/*/write")) { return READ_WRITE; } else { return READ; diff --git a/src/main/resources/static_config/static_action_groups.yml b/src/main/resources/static_config/static_action_groups.yml index e1d3e0aece..eab60fec3a 100644 --- a/src/main/resources/static_config/static_action_groups.yml +++ b/src/main/resources/static_config/static_action_groups.yml @@ -8,8 +8,17 @@ kibana_all_write: static: true allowed_actions: - "kibana:saved_objects/*/write" + - "osd:admin/advanced_settings" type: "kibana" - description: "Allow writing in all OpenSearch Dashboards apps" + description: "Allow writing in all OpenSearch Dashboards apps including advanced settings" +kibana_only_write: + reserved: true + hidden: false + static: true + allowed_actions: + - "kibana:saved_objects/*/write" + type: "kibana" + description: "Allow writing in OpenSearch Dashboards apps except config (advanced settings)" kibana_all_read: reserved: true hidden: false diff --git a/src/main/resources/static_config/static_roles.yml b/src/main/resources/static_config/static_roles.yml index c7820ab627..6a05a3007a 100644 --- a/src/main/resources/static_config/static_roles.yml +++ b/src/main/resources/static_config/static_roles.yml @@ -116,6 +116,11 @@ kibana_server: - "*" allowed_actions: - "indices:admin/aliases*" + tenant_permissions: + - tenant_patterns: + - "*" + allowed_actions: + - "kibana_all_write" logstash: reserved: true From 4b138b0db2759f2e4a243b2debf0d8dd3223c2a7 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 20 Nov 2025 16:28:43 -0500 Subject: [PATCH 2/6] Get it working Signed-off-by: Craig Perkins --- .../PrivilegesInterceptorImpl.java | 22 +++++++++---------- .../security/privileges/TenantPrivileges.java | 5 ++++- .../static_config/static_action_groups.yml | 5 ++++- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java index 47175b91ea..8f581eee03 100644 --- a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java +++ b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java @@ -142,7 +142,6 @@ public ReplaceResult replaceDashboardsIndex( && resolveToDashboardsIndexOrAlias(requestedResolved, dashboardsIndexName); final boolean isTraceEnabled = log.isTraceEnabled(); - TenantPrivileges.ActionType actionType = getActionTypeForAction(action); if (requestedTenant == null || requestedTenant.length() == 0) { @@ -150,11 +149,6 @@ public ReplaceResult replaceDashboardsIndex( log.trace("No tenant, will resolve to " + dashboardsIndexName); } - // Intercept when request is dashboards user and request is to get advanced settings. No replacement. - if ("osd:admin/advanced_settings".equals(action)) { - return ACCESS_GRANTED_REPLACE_RESULT; - } - if (dashboardsIndexOnly && !tenantPrivileges.hasTenantPrivilege(context, "global_tenant", actionType)) { return ACCESS_DENIED_REPLACE_RESULT; } @@ -209,21 +203,27 @@ public ReplaceResult replaceDashboardsIndex( System.out.println("tenantIndexName: " + tenantIndexName); System.out.println("user: " + user); System.out.println("action: " + action); + System.out.println("actionType: " + actionType); System.out.println("requestResolved: " + requestedResolved.getAllIndices()); if (request instanceof GetRequest gr) { System.out.println("GetRequest: " + gr.id()); } else if (request instanceof SearchRequest sr) { System.out.println("SearchRequest: " + sr.source().toString()); } - // Intercept when request is dashboards user and request is to get advanced settings - if ("osd:admin/advanced_settings".equals(action)) { - return newAccessGrantedReplaceResult(replaceIndex(request, dashboardsIndexName, tenantIndexName, action)); - } // The new DLS/FLS implementation defaults to a "deny all" pattern in case no roles are configured // for an index. As the PrivilegeInterceptor grants access to indices bypassing index privileges, // we need to allow-list these indices. applyDocumentAllowList(tenantIndexName); + + // Intercept when request is dashboards user and request is to get advanced settings + System.out.println( + "tenantPrivileges.hasTenantPrivilege: " + tenantPrivileges.hasTenantPrivilege(context, requestedTenant, actionType) + ); + if (action.startsWith("osd:admin/advanced_settings") + && tenantPrivileges.hasTenantPrivilege(context, requestedTenant, actionType)) { + return newAccessGrantedReplaceResult(replaceIndex(request, dashboardsIndexName, tenantIndexName, action)); + } return newAccessGrantedReplaceResult(replaceIndex(request, dashboardsIndexName, tenantIndexName, action)); } else if (!user.getName().equals(dashboardsServerUsername)) { @@ -254,7 +254,7 @@ private void applyDocumentAllowList(String indexName) { } static TenantPrivileges.ActionType getActionTypeForAction(String action) { - if ("osd:admin/advanced_settings".equals(action)) { + if ("osd:admin/advanced_settings/write".equals(action)) { return TenantPrivileges.ActionType.ADMIN; } else if (READ_ONLY_ALLOWED_ACTIONS.contains(action)) { return TenantPrivileges.ActionType.READ; diff --git a/src/main/java/org/opensearch/security/privileges/TenantPrivileges.java b/src/main/java/org/opensearch/security/privileges/TenantPrivileges.java index 7905bbe261..762922cc99 100644 --- a/src/main/java/org/opensearch/security/privileges/TenantPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/TenantPrivileges.java @@ -120,6 +120,9 @@ public TenantPrivileges( // tenant names in advance for (String tenant : WildcardMatcher.from(tenantPattern).iterateMatching(this.allTenantNames)) { for (ActionType actionType : actionTypes) { + System.out.println("role: " + roleName); + System.out.println("actionType: " + actionType); + System.out.println("tenant: " + tenant); tenantToActionTypeToRoles.computeIfAbsent(tenant, (k) -> new EnumMap<>(ActionType.class)) .computeIfAbsent(actionType, (k) -> roleSetBuilder.createSubSetBuilder()) .add(roleName); @@ -247,7 +250,7 @@ public Map tenantMap(PrivilegesEvaluationContext context) { static List resolveActionType(Collection allowedActions, FlattenedActionGroups actionGroups) { ImmutableSet permissions = actionGroups.resolve(allowedActions); - if (permissions.contains("osd:admin/advanced_settings")) { + if (permissions.contains("osd:admin/advanced_settings/write")) { return READ_WRITE_ADMIN; } else if (permissions.contains("kibana:saved_objects/*/write")) { return READ_WRITE; diff --git a/src/main/resources/static_config/static_action_groups.yml b/src/main/resources/static_config/static_action_groups.yml index eab60fec3a..6d7588a9f8 100644 --- a/src/main/resources/static_config/static_action_groups.yml +++ b/src/main/resources/static_config/static_action_groups.yml @@ -8,7 +8,8 @@ kibana_all_write: static: true allowed_actions: - "kibana:saved_objects/*/write" - - "osd:admin/advanced_settings" + - "osd:admin/advanced_settings/get" + - "osd:admin/advanced_settings/write" type: "kibana" description: "Allow writing in all OpenSearch Dashboards apps including advanced settings" kibana_only_write: @@ -17,6 +18,7 @@ kibana_only_write: static: true allowed_actions: - "kibana:saved_objects/*/write" + - "osd:admin/advanced_settings/get" type: "kibana" description: "Allow writing in OpenSearch Dashboards apps except config (advanced settings)" kibana_all_read: @@ -25,6 +27,7 @@ kibana_all_read: static: true allowed_actions: - "kibana:saved_objects/*/read" + - "osd:admin/advanced_settings/get" type: "kibana" description: "Allow reading in all OpenSearch Dashboards apps" cluster_all: From 4b0d56f0a9219baf19bcd4369e71cffbacbcbd6b Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 21 Nov 2025 09:11:37 -0500 Subject: [PATCH 3/6] Ensure header not set twice Signed-off-by: Craig Perkins --- .../org/opensearch/security/privileges/DocumentAllowList.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/privileges/DocumentAllowList.java b/src/main/java/org/opensearch/security/privileges/DocumentAllowList.java index 6e41857737..6684decf61 100644 --- a/src/main/java/org/opensearch/security/privileges/DocumentAllowList.java +++ b/src/main/java/org/opensearch/security/privileges/DocumentAllowList.java @@ -64,7 +64,7 @@ public boolean isEmpty() { } public void applyTo(ThreadContext threadContext) { - if (!isEmpty()) { + if (!isEmpty() && threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER) != null) { threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER, toString()); } } From ab71b5c5256435d91f2949a366042cdf3e0147c1 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 21 Nov 2025 11:42:23 -0500 Subject: [PATCH 4/6] Simplify PrivilegesInterceptorImpl Signed-off-by: Craig Perkins --- build.gradle | 3 +- .../PrivilegesInterceptorImpl.java | 36 ++++++------------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/build.gradle b/build.gradle index 00c003fc77..c7a129d471 100644 --- a/build.gradle +++ b/build.gradle @@ -394,7 +394,7 @@ opensearchplugin { name 'opensearch-security' description 'Provide access control related features for OpenSearch' classname 'org.opensearch.security.OpenSearchSecurityPlugin' - extendedPlugins = ['workload-management;optional=true', 'rule-framework'] + extendedPlugins = ['workload-management;optional=true', 'rule-framework', 'opensearch-dashboards'] } // This requires an additional Jar not published as part of build-tools @@ -688,6 +688,7 @@ dependencies { implementation "com.github.seancfoley:ipaddress:5.5.1" compileOnly "org.opensearch.plugin:workload-management-wlm-spi:${opensearch_version}" compileOnly "org.opensearch.plugin:autotagging-commons-spi:${opensearch_version}" + compileOnly "org.opensearch.plugin:opensearch-dashboards:${opensearch_version}" // Action privileges: check tables and compact collections implementation 'com.selectivem.collections:special-collections-complete:1.4.0' diff --git a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java index 8f581eee03..d558569766 100644 --- a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java +++ b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java @@ -32,7 +32,6 @@ import org.opensearch.action.admin.indices.refresh.RefreshRequest; import org.opensearch.action.bulk.BulkRequest; import org.opensearch.action.delete.DeleteRequest; -import org.opensearch.action.get.GetRequest; import org.opensearch.action.get.MultiGetRequest; import org.opensearch.action.get.MultiGetRequest.Item; import org.opensearch.action.index.IndexRequest; @@ -47,6 +46,7 @@ import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.dashboards.action.WriteAdvancedSettingsRequest; import org.opensearch.security.privileges.DashboardsMultiTenancyConfiguration; import org.opensearch.security.privileges.DocumentAllowList; import org.opensearch.security.privileges.PrivilegesEvaluationContext; @@ -142,7 +142,7 @@ public ReplaceResult replaceDashboardsIndex( && resolveToDashboardsIndexOrAlias(requestedResolved, dashboardsIndexName); final boolean isTraceEnabled = log.isTraceEnabled(); - TenantPrivileges.ActionType actionType = getActionTypeForAction(action); + TenantPrivileges.ActionType actionType = getActionTypeForAction(action, request); if (requestedTenant == null || requestedTenant.length() == 0) { if (isTraceEnabled) { @@ -200,30 +200,11 @@ public ReplaceResult replaceDashboardsIndex( final String tenantIndexName = toUserIndexName(dashboardsIndexName, requestedTenant); - System.out.println("tenantIndexName: " + tenantIndexName); - System.out.println("user: " + user); - System.out.println("action: " + action); - System.out.println("actionType: " + actionType); - System.out.println("requestResolved: " + requestedResolved.getAllIndices()); - if (request instanceof GetRequest gr) { - System.out.println("GetRequest: " + gr.id()); - } else if (request instanceof SearchRequest sr) { - System.out.println("SearchRequest: " + sr.source().toString()); - } - // The new DLS/FLS implementation defaults to a "deny all" pattern in case no roles are configured // for an index. As the PrivilegeInterceptor grants access to indices bypassing index privileges, // we need to allow-list these indices. applyDocumentAllowList(tenantIndexName); - // Intercept when request is dashboards user and request is to get advanced settings - System.out.println( - "tenantPrivileges.hasTenantPrivilege: " + tenantPrivileges.hasTenantPrivilege(context, requestedTenant, actionType) - ); - if (action.startsWith("osd:admin/advanced_settings") - && tenantPrivileges.hasTenantPrivilege(context, requestedTenant, actionType)) { - return newAccessGrantedReplaceResult(replaceIndex(request, dashboardsIndexName, tenantIndexName, action)); - } return newAccessGrantedReplaceResult(replaceIndex(request, dashboardsIndexName, tenantIndexName, action)); } else if (!user.getName().equals(dashboardsServerUsername)) { @@ -253,10 +234,15 @@ private void applyDocumentAllowList(String indexName) { documentAllowList.applyTo(threadPool.getThreadContext()); } - static TenantPrivileges.ActionType getActionTypeForAction(String action) { - if ("osd:admin/advanced_settings/write".equals(action)) { - return TenantPrivileges.ActionType.ADMIN; - } else if (READ_ONLY_ALLOWED_ACTIONS.contains(action)) { + static TenantPrivileges.ActionType getActionTypeForAction(String action, ActionRequest request) { + if (request instanceof WriteAdvancedSettingsRequest wasa) { + if (wasa.isCreateOperation()) { + return TenantPrivileges.ActionType.READ; + } else { + return TenantPrivileges.ActionType.ADMIN; + } + } + if (READ_ONLY_ALLOWED_ACTIONS.contains(action)) { return TenantPrivileges.ActionType.READ; } else { return TenantPrivileges.ActionType.WRITE; From e631294853e6964f845dfd7c3d5f9bf17e386c64 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 21 Nov 2025 11:44:41 -0500 Subject: [PATCH 5/6] Cleanup Signed-off-by: Craig Perkins --- .../security/configuration/PrivilegesInterceptorImpl.java | 1 - .../org/opensearch/security/privileges/TenantPrivileges.java | 3 --- src/main/resources/static_config/static_roles.yml | 5 ----- 3 files changed, 9 deletions(-) diff --git a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java index d558569766..c9e632cace 100644 --- a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java +++ b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java @@ -204,7 +204,6 @@ public ReplaceResult replaceDashboardsIndex( // for an index. As the PrivilegeInterceptor grants access to indices bypassing index privileges, // we need to allow-list these indices. applyDocumentAllowList(tenantIndexName); - return newAccessGrantedReplaceResult(replaceIndex(request, dashboardsIndexName, tenantIndexName, action)); } else if (!user.getName().equals(dashboardsServerUsername)) { diff --git a/src/main/java/org/opensearch/security/privileges/TenantPrivileges.java b/src/main/java/org/opensearch/security/privileges/TenantPrivileges.java index 762922cc99..9924ee1f9e 100644 --- a/src/main/java/org/opensearch/security/privileges/TenantPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/TenantPrivileges.java @@ -120,9 +120,6 @@ public TenantPrivileges( // tenant names in advance for (String tenant : WildcardMatcher.from(tenantPattern).iterateMatching(this.allTenantNames)) { for (ActionType actionType : actionTypes) { - System.out.println("role: " + roleName); - System.out.println("actionType: " + actionType); - System.out.println("tenant: " + tenant); tenantToActionTypeToRoles.computeIfAbsent(tenant, (k) -> new EnumMap<>(ActionType.class)) .computeIfAbsent(actionType, (k) -> roleSetBuilder.createSubSetBuilder()) .add(roleName); diff --git a/src/main/resources/static_config/static_roles.yml b/src/main/resources/static_config/static_roles.yml index 6a05a3007a..c7820ab627 100644 --- a/src/main/resources/static_config/static_roles.yml +++ b/src/main/resources/static_config/static_roles.yml @@ -116,11 +116,6 @@ kibana_server: - "*" allowed_actions: - "indices:admin/aliases*" - tenant_permissions: - - tenant_patterns: - - "*" - allowed_actions: - - "kibana_all_write" logstash: reserved: true From efb6b2878c69e2e89e1e27bbd46429d289b9c144 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 21 Nov 2025 15:58:29 -0500 Subject: [PATCH 6/6] Fix classpath issue Signed-off-by: Craig Perkins --- tools/install_demo_configuration.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/install_demo_configuration.sh b/tools/install_demo_configuration.sh index d3a3ae8f75..e28414416a 100755 --- a/tools/install_demo_configuration.sh +++ b/tools/install_demo_configuration.sh @@ -61,4 +61,4 @@ if [ ! -x "$JAVA" ]; then exit 1 fi -"$JAVA" -Dorg.apache.logging.log4j.simplelog.StatusLogger.level=OFF -cp "$DIR/../*:$DIR/../../../lib/*:$DIR/../deps/*" org.opensearch.security.tools.democonfig.Installer "$DIR" "$@" 2>/dev/null +"$JAVA" -Dorg.apache.logging.log4j.simplelog.StatusLogger.level=OFF -cp "$DIR/../*:$OPENSEARCH_HOME/lib/*:$OPENSEARCH_HOME/modules/opensearch-dashboards/*" org.opensearch.security.tools.democonfig.Installer "$DIR" "$@" 2>/dev/null