From 831dca30df9eb0dd28c6053641c424633141a90f Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 30 Aug 2024 16:16:19 -0400 Subject: [PATCH 001/201] Add a base setup for resource access evaluation Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 55 +++++++++++++++++-- .../resources/ResourceAccessEvaluator.java | 50 +++++++++++++++++ .../security/resources/package-info.java | 12 ++++ 3 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/opensearch/security/resources/ResourceAccessEvaluator.java create mode 100644 src/main/java/org/opensearch/security/resources/package-info.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 57ffc4df6f..f43e7a931b 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -68,6 +68,8 @@ import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; import org.opensearch.Version; +import org.opensearch.accesscontrol.resources.EntityType; +import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.action.ActionRequest; import org.opensearch.action.search.PitService; import org.opensearch.action.search.SearchScrollAction; @@ -120,6 +122,7 @@ import org.opensearch.plugins.IdentityPlugin; import org.opensearch.plugins.MapperPlugin; import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.ResourceAccessControlPlugin; import org.opensearch.plugins.SecureHttpTransportSettingsProvider; import org.opensearch.plugins.SecureSettingsFactory; import org.opensearch.plugins.SecureTransportSettingsProvider; @@ -173,6 +176,7 @@ import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.resources.ResourceAccessEvaluator; import org.opensearch.security.rest.DashboardsInfoAction; import org.opensearch.security.rest.SecurityConfigUpdateAction; import org.opensearch.security.rest.SecurityHealthAction; @@ -232,7 +236,8 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin MapperPlugin, // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings ExtensionAwarePlugin, - IdentityPlugin + IdentityPlugin, + ResourceAccessControlPlugin // CS-ENFORCE-SINGLE { @@ -268,6 +273,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile OpensearchDynamicSetting transportPassiveAuthSetting; private volatile PasswordHasher passwordHasher; private volatile DlsFlsBaseContext dlsFlsBaseContext; + private ResourceAccessEvaluator resourceAccessEvaluator; public static boolean isActionTraceEnabled() { @@ -481,6 +487,8 @@ public List run() { } } + + this.resourceAccessEvaluator = new ResourceAccessEvaluator(); } private void verifyTLSVersion(final String settings, final List configuredProtocols) { @@ -1367,7 +1375,7 @@ public List> getSettings() { settings.add(Setting.simpleString(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, Property.NodeScope, Property.Filtered)); settings.add(Setting.groupSetting(ConfigConstants.SECURITY_AUTHCZ_IMPERSONATION_DN + ".", Property.NodeScope)); // not filtered - // here + // here settings.add(Setting.simpleString(ConfigConstants.SECURITY_CERT_OID, Property.NodeScope, Property.Filtered)); @@ -1383,8 +1391,8 @@ public List> getSettings() { );// not filtered here settings.add(Setting.boolSetting(ConfigConstants.SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED, false, Property.NodeScope));// not - // filtered - // here + // filtered + // here settings.add( Setting.boolSetting( @@ -1428,8 +1436,8 @@ public List> getSettings() { Setting.boolSetting(ConfigConstants.SECURITY_DFM_EMPTY_OVERRIDES_ALL, false, Property.NodeScope, Property.Filtered) ); settings.add(Setting.groupSetting(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + ".", Property.NodeScope)); // not - // filtered - // here + // filtered + // here settings.add(Setting.simpleString(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, Property.NodeScope, Property.Filtered)); settings.add( @@ -2166,6 +2174,41 @@ private void tryAddSecurityProvider() { }); } + @Override + public Map> listAccessibleResources() { + return this.resourceAccessEvaluator.listAccessibleResources(); + } + + @Override + public List listAccessibleResourcesForPlugin(String systemIndexName) { + return this.resourceAccessEvaluator.listAccessibleResourcesForPlugin(systemIndexName); + } + + @Override + public boolean hasPermission(String resourceId, String systemIndexName) { + return this.resourceAccessEvaluator.hasPermission(resourceId, systemIndexName); + } + + @Override + public ResourceSharing shareWith(String resourceId, String systemIndexName, Map> entities) { + return this.resourceAccessEvaluator.shareWith(resourceId, systemIndexName, entities); + } + + @Override + public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> entities) { + return this.resourceAccessEvaluator.revokeAccess(resourceId, systemIndexName, entities); + } + + @Override + public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { + return this.resourceAccessEvaluator.deleteResourceSharingRecord(resourceId, systemIndexName); + } + + @Override + public boolean deleteAllResourceSharingRecordsFor(String entity) { + return this.resourceAccessEvaluator.deleteAllResourceSharingRecordsFor(entity); + } + public static class GuiceHolder implements LifecycleComponent { private static RepositoriesService repositoriesService; diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessEvaluator.java b/src/main/java/org/opensearch/security/resources/ResourceAccessEvaluator.java new file mode 100644 index 0000000000..3e4d73eb03 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessEvaluator.java @@ -0,0 +1,50 @@ +/* + * 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.resources; + +import java.util.List; +import java.util.Map; + +import org.opensearch.accesscontrol.resources.EntityType; +import org.opensearch.accesscontrol.resources.ResourceSharing; + +public class ResourceAccessEvaluator { + + public Map> listAccessibleResources() { + return Map.of(); + } + + public List listAccessibleResourcesForPlugin(String s) { + return List.of(); + } + + public boolean hasPermission(String resourceId, String systemIndexName) { + return false; + } + + public ResourceSharing shareWith(String resourceId, String systemIndexName, Map> map) { + return null; + } + + public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> map) { + return null; + } + + public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { + return false; + } + + public boolean deleteAllResourceSharingRecordsFor(String entity) { + return false; + } + +} diff --git a/src/main/java/org/opensearch/security/resources/package-info.java b/src/main/java/org/opensearch/security/resources/package-info.java new file mode 100644 index 0000000000..855bdf81af --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/package-info.java @@ -0,0 +1,12 @@ +/* + * 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.resources; From 1c65eff05e2a9687df3333ec68251d29b439260b Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 6 Sep 2024 13:17:35 -0400 Subject: [PATCH 002/201] Adds handler and other access management components for resource sharing Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 43 ++++-- .../resources/ResourceAccessEvaluator.java | 50 ------- .../resources/ResourceAccessHandler.java | 94 ++++++++++++ .../ResourceManagementRepository.java | 47 ++++++ .../ResourceSharingIndexHandler.java | 134 ++++++++++++++++++ .../ResourceSharingIndexListener.java | 82 +++++++++++ .../security/support/ConfigConstants.java | 3 + 7 files changed, 388 insertions(+), 65 deletions(-) delete mode 100644 src/main/java/org/opensearch/security/resources/ResourceAccessEvaluator.java create mode 100644 src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java create mode 100644 src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java create mode 100644 src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java create mode 100644 src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index f43e7a931b..27e89f5c31 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -64,12 +64,10 @@ import org.apache.lucene.search.QueryCachingPolicy; import org.apache.lucene.search.Weight; -import org.opensearch.OpenSearchException; -import org.opensearch.OpenSearchSecurityException; -import org.opensearch.SpecialPermission; -import org.opensearch.Version; +import org.opensearch.*; import org.opensearch.accesscontrol.resources.EntityType; import org.opensearch.accesscontrol.resources.ResourceSharing; +import org.opensearch.accesscontrol.resources.ShareWith; import org.opensearch.action.ActionRequest; import org.opensearch.action.search.PitService; import org.opensearch.action.search.SearchScrollAction; @@ -176,7 +174,9 @@ import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.resources.ResourceAccessEvaluator; +import org.opensearch.security.resources.ResourceAccessHandler; +import org.opensearch.security.resources.ResourceManagementRepository; +import org.opensearch.security.resources.ResourceSharingIndexListener; import org.opensearch.security.rest.DashboardsInfoAction; import org.opensearch.security.rest.SecurityConfigUpdateAction; import org.opensearch.security.rest.SecurityHealthAction; @@ -274,6 +274,9 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile PasswordHasher passwordHasher; private volatile DlsFlsBaseContext dlsFlsBaseContext; private ResourceAccessEvaluator resourceAccessEvaluator; + private ResourceManagementRepository rmr; + private ResourceAccessHandler resourceAccessHandler; + private final Set indicesToListen = new HashSet<>(); public static boolean isActionTraceEnabled() { @@ -488,7 +491,7 @@ public List run() { } - this.resourceAccessEvaluator = new ResourceAccessEvaluator(); + this.resourceAccessHandler = new ResourceAccessHandler(threadPool); } private void verifyTLSVersion(final String settings, final List configuredProtocols) { @@ -716,6 +719,12 @@ public void onIndexModule(IndexModule indexModule) { dlsFlsBaseContext ) ); + + if (this.indicesToListen.contains(indexModule.getIndex().getName())) { + indexModule.addIndexOperationListener(ResourceSharingIndexListener.getInstance()); + log.warn("Security plugin started listening to operations on index {}", indexModule.getIndex().getName()); + } + indexModule.forceQueryCacheProvider((indexSettings, nodeCache) -> new QueryCache() { @Override @@ -1199,6 +1208,8 @@ public Collection createComponents( e.subscribeForChanges(dcf); } + rmr = ResourceManagementRepository.create(settings, threadPool, localClient); + components.add(adminDns); components.add(cr); components.add(xffResolver); @@ -2073,6 +2084,8 @@ public void onNodeStarted(DiscoveryNode localNode) { if (!SSLConfig.isSslOnlyMode() && !client && !disabled && !useClusterStateToInitSecurityConfig(settings)) { cr.initOnNodeStart(); } + // create resource sharing index if absent + rmr.createResourceSharingIndexIfAbsent(); final Set securityModules = ReflectionHelper.getModulesLoaded(); log.info("{} OpenSearch Security modules loaded so far: {}", securityModules.size(), securityModules); } @@ -2176,37 +2189,37 @@ private void tryAddSecurityProvider() { @Override public Map> listAccessibleResources() { - return this.resourceAccessEvaluator.listAccessibleResources(); + return this.resourceAccessHandler.listAccessibleResources(); } @Override public List listAccessibleResourcesForPlugin(String systemIndexName) { - return this.resourceAccessEvaluator.listAccessibleResourcesForPlugin(systemIndexName); + return this.resourceAccessHandler.listAccessibleResourcesForPlugin(systemIndexName); } @Override public boolean hasPermission(String resourceId, String systemIndexName) { - return this.resourceAccessEvaluator.hasPermission(resourceId, systemIndexName); + return this.resourceAccessHandler.hasPermission(resourceId, systemIndexName); } @Override - public ResourceSharing shareWith(String resourceId, String systemIndexName, Map> entities) { - return this.resourceAccessEvaluator.shareWith(resourceId, systemIndexName, entities); + public ResourceSharing shareWith(String resourceId, String systemIndexName, ShareWith shareWith) { + return this.resourceAccessHandler.shareWith(resourceId, systemIndexName, shareWith); } @Override public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> entities) { - return this.resourceAccessEvaluator.revokeAccess(resourceId, systemIndexName, entities); + return this.resourceAccessHandler.revokeAccess(resourceId, systemIndexName, entities); } @Override public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { - return this.resourceAccessEvaluator.deleteResourceSharingRecord(resourceId, systemIndexName); + return this.resourceAccessHandler.deleteResourceSharingRecord(resourceId, systemIndexName); } @Override - public boolean deleteAllResourceSharingRecordsFor(String entity) { - return this.resourceAccessEvaluator.deleteAllResourceSharingRecordsFor(entity); + public boolean deleteAllResourceSharingRecordsForCurrentUser() { + return this.resourceAccessHandler.deleteAllResourceSharingRecordsForCurrentUser(); } public static class GuiceHolder implements LifecycleComponent { diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessEvaluator.java b/src/main/java/org/opensearch/security/resources/ResourceAccessEvaluator.java deleted file mode 100644 index 3e4d73eb03..0000000000 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessEvaluator.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.resources; - -import java.util.List; -import java.util.Map; - -import org.opensearch.accesscontrol.resources.EntityType; -import org.opensearch.accesscontrol.resources.ResourceSharing; - -public class ResourceAccessEvaluator { - - public Map> listAccessibleResources() { - return Map.of(); - } - - public List listAccessibleResourcesForPlugin(String s) { - return List.of(); - } - - public boolean hasPermission(String resourceId, String systemIndexName) { - return false; - } - - public ResourceSharing shareWith(String resourceId, String systemIndexName, Map> map) { - return null; - } - - public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> map) { - return null; - } - - public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { - return false; - } - - public boolean deleteAllResourceSharingRecordsFor(String entity) { - return false; - } - -} diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java new file mode 100644 index 0000000000..0861854e13 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -0,0 +1,94 @@ +/* + * 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.resources; + +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.accesscontrol.resources.EntityType; +import org.opensearch.accesscontrol.resources.ResourceSharing; +import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + +public class ResourceAccessHandler { + private static final Logger LOGGER = LogManager.getLogger(ResourceAccessHandler.class); + + private final ThreadContext threadContext; + + public ResourceAccessHandler(final ThreadPool threadPool) { + super(); + this.threadContext = threadPool.getThreadContext(); + } + + public Map> listAccessibleResources() { + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + LOGGER.info("Listing accessible resource for: {}", user.getName()); + + // TODO add concrete implementation + return Map.of(); + } + + public List listAccessibleResourcesForPlugin(String systemIndex) { + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + LOGGER.info("Listing accessible resource within a system index {} for : {}", systemIndex, user.getName()); + + // TODO add concrete implementation + return List.of(); + } + + public boolean hasPermission(String resourceId, String systemIndexName) { + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + LOGGER.info("Checking if {} has permission to resource {}", user.getName(), resourceId); + + // TODO add concrete implementation + return false; + } + + public ResourceSharing shareWith(String resourceId, String systemIndexName, ShareWith shareWith) { + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith); + + // TODO add concrete implementation + return null; + } + + public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> revokeAccess) { + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + LOGGER.info("Revoking access to resource {} created by {} for {}", resourceId, user.getName(), revokeAccess); + + // TODO add concrete implementation + return null; + } + + public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + LOGGER.info("Deleting resource sharing record for resource {} in {} created by {}", resourceId, systemIndexName, user.getName()); + + // TODO add concrete implementation + return false; + } + + public boolean deleteAllResourceSharingRecordsForCurrentUser() { + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + LOGGER.info("Deleting all resource sharing records for resource {}", user.getName()); + + // TODO add concrete implementation + return false; + } + +} diff --git a/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java b/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java new file mode 100644 index 0000000000..df59516a41 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java @@ -0,0 +1,47 @@ +package org.opensearch.security.resources; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.threadpool.ThreadPool; + +public class ResourceManagementRepository { + + private static final Logger LOGGER = LogManager.getLogger(ConfigurationRepository.class); + + private final Client client; + + private final ThreadPool threadPool; + + private final ResourceSharingIndexHandler resourceSharingIndexHandler; + + protected ResourceManagementRepository( + final ThreadPool threadPool, + final Client client, + final ResourceSharingIndexHandler resourceSharingIndexHandler + ) { + this.client = client; + this.threadPool = threadPool; + this.resourceSharingIndexHandler = resourceSharingIndexHandler; + } + + public static ResourceManagementRepository create(Settings settings, final ThreadPool threadPool, Client client) { + final var resourceSharingIndex = ConfigConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; + return new ResourceManagementRepository( + threadPool, + client, + new ResourceSharingIndexHandler(resourceSharingIndex, settings, client, threadPool) + ); + } + + public void createResourceSharingIndexIfAbsent() { + // TODO check if this should be wrapped in an atomic completable future + + this.resourceSharingIndexHandler.createResourceSharingIndexIfAbsent(() -> null); + } + +} diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java new file mode 100644 index 0000000000..79ef85e7eb --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -0,0 +1,134 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security.resources; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.Callable; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.accesscontrol.resources.CreatedBy; +import org.opensearch.accesscontrol.resources.ResourceSharing; +import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.index.IndexResponse; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.threadpool.ThreadPool; + +import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; + +public class ResourceSharingIndexHandler { + + private final static int MINIMUM_HASH_BITS = 128; + + private static final Logger LOGGER = LogManager.getLogger(ResourceSharingIndexHandler.class); + + private final Settings settings; + + private final Client client; + + private final String resourceSharingIndex; + + private final ThreadPool threadPool; + + public ResourceSharingIndexHandler(final String indexName, final Settings settings, final Client client, ThreadPool threadPool) { + this.resourceSharingIndex = indexName; + this.settings = settings; + this.client = client; + this.threadPool = threadPool; + } + + public final static Map INDEX_SETTINGS = Map.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); + + public void createIndex(ActionListener listener) { + try (final ThreadContext.StoredContext threadContext = client.threadPool().getThreadContext().stashContext()) { + client.admin() + .indices() + .create( + new CreateIndexRequest(resourceSharingIndex).settings(INDEX_SETTINGS).waitForActiveShards(1), + ActionListener.runBefore(ActionListener.wrap(r -> { + if (r.isAcknowledged()) { + listener.onResponse(true); + } else listener.onFailure(new SecurityException("Couldn't create resource sharing index " + resourceSharingIndex)); + }, listener::onFailure), threadContext::restore) + ); + } + } + + // public void createIndexIfAbsent() { + // try { + // final Map indexSettings = ImmutableMap.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); + // final CreateIndexRequest createIndexRequest = new CreateIndexRequest(resourceSharingIndex).settings(indexSettings); + // final boolean ok = client.admin().indices().create(createIndexRequest).actionGet().isAcknowledged(); + // LOGGER.info("Resource sharing index {} created?: {}", resourceSharingIndex, ok); + // } catch (ResourceAlreadyExistsException resourceAlreadyExistsException) { + // LOGGER.info("Index {} already exists", resourceSharingIndex); + // } + // } + + public void createResourceSharingIndexIfAbsent(Callable callable) { + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + CreateIndexRequest cir = new CreateIndexRequest(resourceSharingIndex); + ActionListener cirListener = ActionListener.wrap(response -> { + LOGGER.info("Resource sharing index {} created.", resourceSharingIndex); + callable.call(); + }, (failResponse) -> { + /* Index already exists, ignore and continue */ + LOGGER.info("Index {} already exists.", resourceSharingIndex); + try { + callable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + this.client.admin().indices().create(cir, cirListener); + } + } + + public boolean indexResourceSharing( + String resourceId, + String resourceIndex, + CreatedBy createdBy, + ShareWith shareWith, + ActionListener listener + ) throws IOException { + createResourceSharingIndexIfAbsent(() -> { + ResourceSharing entry = new ResourceSharing(resourceIndex, resourceId, createdBy, shareWith); + + IndexRequest ir = client.prepareIndex(resourceSharingIndex) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .setSource(entry.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) + .request(); + + LOGGER.info("Index Request: {}", ir.toString()); + + ActionListener irListener = ActionListener.wrap(idxResponse -> { + LOGGER.info("Created {} entry.", resourceSharingIndex); + listener.onResponse(idxResponse); + }, (failResponse) -> { + LOGGER.error(failResponse.getMessage()); + LOGGER.info("Failed to create {} entry.", resourceSharingIndex); + listener.onFailure(failResponse); + }); + client.index(ir, irListener); + return null; + }); + return true; + } +} diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java new file mode 100644 index 0000000000..7a2af9f3bd --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -0,0 +1,82 @@ +/* + * 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. + */ + +package org.opensearch.security.resources; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.client.Client; +import org.opensearch.core.index.shard.ShardId; +import org.opensearch.index.engine.Engine; +import org.opensearch.index.shard.IndexingOperationListener; +import org.opensearch.threadpool.ThreadPool; + +/** + * This class implements an index operation listener for operations performed on resources stored in plugin's indices + * These indices are defined on bootstrap and configured to listen in OpenSearchSecurityPlugin.java + */ +public class ResourceSharingIndexListener implements IndexingOperationListener { + + private final static Logger log = LogManager.getLogger(ResourceSharingIndexListener.class); + + private static final ResourceSharingIndexListener INSTANCE = new ResourceSharingIndexListener(); + + private boolean initialized; + + private ThreadPool threadPool; + + private Client client; + + private ResourceSharingIndexListener() {} + + public static ResourceSharingIndexListener getInstance() { + + return ResourceSharingIndexListener.INSTANCE; + + } + + public void initialize(ThreadPool threadPool, Client client) { + + if (initialized) { + return; + } + + initialized = true; + + this.threadPool = threadPool; + + this.client = client; + + } + + public boolean isInitialized() { + return initialized; + } + + @Override + + public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { + + // implement a check to see if a resource was updated + log.warn("postIndex called on " + shardId.getIndexName()); + + String resourceId = index.id(); + + String resourceIndex = shardId.getIndexName(); + } + + @Override + + public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { + + // implement a check to see if a resource was deleted + log.warn("postDelete called on " + shardId.getIndexName()); + } + +} diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index f35afc6489..456e9586ca 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -370,6 +370,9 @@ public enum RolesMappingResolution { // Variable for initial admin password support public static final String OPENSEARCH_INITIAL_ADMIN_PASSWORD = "OPENSEARCH_INITIAL_ADMIN_PASSWORD"; + // Resource sharing index + public static final String OPENSEARCH_RESOURCE_SHARING_INDEX = ".opensearch_resource_sharing"; + public static Set getSettingAsSet( final Settings settings, final String key, From 118cb07b9ff45a6722e3d5e4d222106c6da66de6 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 6 Sep 2024 13:18:12 -0400 Subject: [PATCH 003/201] Adds sample resource plugin Signed-off-by: Darshit Chanpura --- sample-resource-plugin/build.gradle | 167 ++++++++++++++++++ .../opensearch/security/sample/Resource.java | 8 + .../security/sample/SampleResourcePlugin.java | 165 +++++++++++++++++ .../create/CreateSampleResourceAction.java | 30 ++++ .../create/CreateSampleResourceRequest.java | 55 ++++++ .../create/CreateSampleResourceResponse.java | 55 ++++++ .../CreateSampleResourceRestAction.java | 56 ++++++ .../CreateSampleResourceTransportAction.java | 32 ++++ .../sample/actions/create/SampleResource.java | 45 +++++ .../list/ListSampleResourceAction.java | 29 +++ .../list/ListSampleResourceRequest.java | 39 ++++ .../list/ListSampleResourceResponse.java | 55 ++++++ .../list/ListSampleResourceRestAction.java | 44 +++++ .../ListSampleResourceTransportAction.java | 52 ++++++ .../transport/CreateResourceRequest.java | 50 ++++++ .../transport/CreateResourceResponse.java | 55 ++++++ .../CreateResourceTransportAction.java | 103 +++++++++++ .../plugin-metadata/plugin-security.policy | 3 + .../test/resources/security/esnode-key.pem | 28 +++ .../src/test/resources/security/esnode.pem | 25 +++ .../src/test/resources/security/kirk-key.pem | 28 +++ .../src/test/resources/security/kirk.pem | 27 +++ .../src/test/resources/security/root-ca.pem | 28 +++ .../src/test/resources/security/sample.pem | 25 +++ .../src/test/resources/security/test-kirk.jks | Bin 0 -> 3766 bytes settings.gradle | 3 + 26 files changed, 1207 insertions(+) create mode 100644 sample-resource-plugin/build.gradle create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/Resource.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourcePlugin.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/SampleResource.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceTransportAction.java create mode 100644 sample-resource-plugin/src/main/plugin-metadata/plugin-security.policy create mode 100644 sample-resource-plugin/src/test/resources/security/esnode-key.pem create mode 100644 sample-resource-plugin/src/test/resources/security/esnode.pem create mode 100644 sample-resource-plugin/src/test/resources/security/kirk-key.pem create mode 100644 sample-resource-plugin/src/test/resources/security/kirk.pem create mode 100644 sample-resource-plugin/src/test/resources/security/root-ca.pem create mode 100644 sample-resource-plugin/src/test/resources/security/sample.pem create mode 100644 sample-resource-plugin/src/test/resources/security/test-kirk.jks diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle new file mode 100644 index 0000000000..6d4b084580 --- /dev/null +++ b/sample-resource-plugin/build.gradle @@ -0,0 +1,167 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'opensearch.opensearchplugin' +apply plugin: 'opensearch.testclusters' +apply plugin: 'opensearch.java-rest-test' + +import org.opensearch.gradle.test.RestIntegTestTask + + +opensearchplugin { + name 'opensearch-security-sample-resource-plugin' + description 'Sample plugin that extends OpenSearch Resource Plugin' + classname 'org.opensearch.security.sampleresourceplugin.SampleResourcePlugin' + extendedPlugins = ['opensearch-security'] +} + +ext { + projectSubstitutions = [:] + licenseFile = rootProject.file('LICENSE.txt') + noticeFile = rootProject.file('NOTICE.txt') +} + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } +} + +dependencies { +} + +def es_tmp_dir = rootProject.file('build/private/es_tmp').absoluteFile +es_tmp_dir.mkdirs() + +File repo = file("$buildDir/testclusters/repo") +def _numNodes = findProperty('numNodes') as Integer ?: 1 + +licenseHeaders.enabled = true +validateNebulaPom.enabled = false +testingConventions.enabled = false +loggerUsageCheck.enabled = false + +javaRestTest.dependsOn(rootProject.assemble) +javaRestTest { + systemProperty 'tests.security.manager', 'false' +} +testClusters.javaRestTest { + testDistribution = 'INTEG_TEST' +} + +task integTest(type: RestIntegTestTask) { + description = "Run tests against a cluster" + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath +} +tasks.named("check").configure { dependsOn(integTest) } + +integTest { + if (project.hasProperty('excludeTests')) { + project.properties['excludeTests']?.replaceAll('\\s', '')?.split('[,;]')?.each { + exclude "${it}" + } + } + systemProperty 'tests.security.manager', 'false' + systemProperty 'java.io.tmpdir', es_tmp_dir.absolutePath + + systemProperty "https", System.getProperty("https") + systemProperty "user", System.getProperty("user") + systemProperty "password", System.getProperty("password") + // Tell the test JVM if the cluster JVM is running under a debugger so that tests can use longer timeouts for + // requests. The 'doFirst' delays reading the debug setting on the cluster till execution time. + doFirst { + // Tell the test JVM if the cluster JVM is running under a debugger so that tests can + // use longer timeouts for requests. + def isDebuggingCluster = getDebug() || System.getProperty("test.debug") != null + systemProperty 'cluster.debug', isDebuggingCluster + // Set number of nodes system property to be used in tests + systemProperty 'cluster.number_of_nodes', "${_numNodes}" + // There seems to be an issue when running multi node run or integ tasks with unicast_hosts + // not being written, the waitForAllConditions ensures it's written + getClusters().forEach { cluster -> + cluster.waitForAllConditions() + } + } + + // The -Dcluster.debug option makes the cluster debuggable; this makes the tests debuggable + if (System.getProperty("test.debug") != null) { + jvmArgs '-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=8000' + } + if (System.getProperty("tests.rest.bwcsuite") == null) { + filter { + excludeTestsMatching "org.opensearch.security.sampleextension.bwc.*IT" + } + } +} +project.getTasks().getByName('bundlePlugin').dependsOn(rootProject.tasks.getByName('build')) +Zip bundle = (Zip) project.getTasks().getByName("bundlePlugin"); +Zip rootBundle = (Zip) rootProject.getTasks().getByName("bundlePlugin"); +integTest.dependsOn(bundle) +integTest.getClusters().forEach{c -> { + c.plugin(rootProject.getObjects().fileProperty().value(rootBundle.getArchiveFile())) + c.plugin(project.getObjects().fileProperty().value(bundle.getArchiveFile())) +}} + +testClusters.integTest { + testDistribution = 'INTEG_TEST' + + // Cluster shrink exception thrown if we try to set numberOfNodes to 1, so only apply if > 1 + if (_numNodes > 1) numberOfNodes = _numNodes + // When running integration tests it doesn't forward the --debug-jvm to the cluster anymore + // i.e. we have to use a custom property to flag when we want to debug OpenSearch JVM + // since we also support multi node integration tests we increase debugPort per node + if (System.getProperty("cluster.debug") != null) { + def debugPort = 5005 + nodes.forEach { node -> + node.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=*:${debugPort}") + debugPort += 1 + } + } + setting 'path.repo', repo.absolutePath +} + +afterEvaluate { + testClusters.integTest.nodes.each { node -> + def plugins = node.plugins + def firstPlugin = plugins.get(0) + if (firstPlugin.provider == project.bundlePlugin.archiveFile) { + plugins.remove(0) + plugins.add(firstPlugin) + } + + node.extraConfigFile("kirk.pem", file("src/test/resources/security/kirk.pem")) + node.extraConfigFile("kirk-key.pem", file("src/test/resources/security/kirk-key.pem")) + node.extraConfigFile("esnode.pem", file("src/test/resources/security/esnode.pem")) + node.extraConfigFile("esnode-key.pem", file("src/test/resources/security/esnode-key.pem")) + node.extraConfigFile("root-ca.pem", file("src/test/resources/security/root-ca.pem")) + node.setting("plugins.security.ssl.transport.pemcert_filepath", "esnode.pem") + node.setting("plugins.security.ssl.transport.pemkey_filepath", "esnode-key.pem") + node.setting("plugins.security.ssl.transport.pemtrustedcas_filepath", "root-ca.pem") + node.setting("plugins.security.ssl.transport.enforce_hostname_verification", "false") + node.setting("plugins.security.ssl.http.enabled", "true") + node.setting("plugins.security.ssl.http.pemcert_filepath", "esnode.pem") + node.setting("plugins.security.ssl.http.pemkey_filepath", "esnode-key.pem") + node.setting("plugins.security.ssl.http.pemtrustedcas_filepath", "root-ca.pem") + node.setting("plugins.security.allow_unsafe_democertificates", "true") + node.setting("plugins.security.allow_default_init_securityindex", "true") + node.setting("plugins.security.authcz.admin_dn", "\n - CN=kirk,OU=client,O=client,L=test,C=de") + node.setting("plugins.security.audit.type", "internal_opensearch") + node.setting("plugins.security.enable_snapshot_restore_privilege", "true") + node.setting("plugins.security.check_snapshot_restore_write_privileges", "true") + node.setting("plugins.security.restapi.roles_enabled", "[\"all_access\", \"security_rest_api_access\"]") + } +} + +run { + doFirst { + // There seems to be an issue when running multi node run or integ tasks with unicast_hosts + // not being written, the waitForAllConditions ensures it's written + getClusters().forEach { cluster -> + cluster.waitForAllConditions() + } + } + useCluster testClusters.integTest +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/Resource.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/Resource.java new file mode 100644 index 0000000000..6126fdb092 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/Resource.java @@ -0,0 +1,8 @@ +package org.opensearch.security.sample; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.xcontent.ToXContentFragment; + +public abstract class Resource implements NamedWriteable, ToXContentFragment { + protected abstract String getResourceIndex(); +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourcePlugin.java new file mode 100644 index 0000000000..58e4daa95c --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourcePlugin.java @@ -0,0 +1,165 @@ +/* + * Copyright OpenSearch Contributors + * 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. + */ +package org.opensearch.security.sample; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.accesscontrol.resources.ResourceService; +import org.opensearch.action.ActionRequest; +import org.opensearch.client.Client; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.node.DiscoveryNodes; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.lifecycle.Lifecycle; +import org.opensearch.common.lifecycle.LifecycleComponent; +import org.opensearch.common.lifecycle.LifecycleListener; +import org.opensearch.common.settings.ClusterSettings; +import org.opensearch.common.settings.IndexScopedSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.settings.SettingsFilter; +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.NamedWriteableRegistry; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.env.Environment; +import org.opensearch.env.NodeEnvironment; +import org.opensearch.indices.SystemIndexDescriptor; +import org.opensearch.plugins.ActionPlugin; +import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.ResourcePlugin; +import org.opensearch.plugins.SystemIndexPlugin; +import org.opensearch.repositories.RepositoriesService; +import org.opensearch.rest.RestController; +import org.opensearch.rest.RestHandler; +import org.opensearch.script.ScriptService; +import org.opensearch.security.sample.actions.create.CreateSampleResourceAction; +import org.opensearch.security.sample.actions.create.CreateSampleResourceRestAction; +import org.opensearch.security.sample.actions.create.CreateSampleResourceTransportAction; +import org.opensearch.security.sample.actions.list.ListSampleResourceAction; +import org.opensearch.security.sample.actions.list.ListSampleResourceRestAction; +import org.opensearch.security.sample.actions.list.ListSampleResourceTransportAction; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.watcher.ResourceWatcherService; + +/** + * Sample Resource plugin. + * It uses ".sample_resources" index to manage its resources, and exposes a REST API + * + */ +public class SampleResourcePlugin extends Plugin implements ActionPlugin, SystemIndexPlugin, ResourcePlugin { + private static final Logger log = LogManager.getLogger(SampleResourcePlugin.class); + + public static final String RESOURCE_INDEX_NAME = ".sample_resources"; + + private Client client; + + @Override + public Collection createComponents( + Client client, + ClusterService clusterService, + ThreadPool threadPool, + ResourceWatcherService resourceWatcherService, + ScriptService scriptService, + NamedXContentRegistry xContentRegistry, + Environment environment, + NodeEnvironment nodeEnvironment, + NamedWriteableRegistry namedWriteableRegistry, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier repositoriesServiceSupplier + ) { + this.client = client; + return Collections.emptyList(); + } + + @Override + public List getRestHandlers( + Settings settings, + RestController restController, + ClusterSettings clusterSettings, + IndexScopedSettings indexScopedSettings, + SettingsFilter settingsFilter, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier nodesInCluster + ) { + return List.of(new CreateSampleResourceRestAction(), new ListSampleResourceRestAction()); + } + + @Override + public List> getActions() { + return List.of( + new ActionHandler<>(CreateSampleResourceAction.INSTANCE, CreateSampleResourceTransportAction.class), + new ActionHandler<>(ListSampleResourceAction.INSTANCE, ListSampleResourceTransportAction.class) + ); + } + + @Override + public Collection getSystemIndexDescriptors(Settings settings) { + final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(RESOURCE_INDEX_NAME, "Example index with resources"); + return Collections.singletonList(systemIndexDescriptor); + } + + @Override + public String getResourceType() { + return ""; + } + + @Override + public String getResourceIndex() { + return ""; + } + + @Override + public Collection> getGuiceServiceClasses() { + final List> services = new ArrayList<>(1); + services.add(GuiceHolder.class); + return services; + } + + public static class GuiceHolder implements LifecycleComponent { + + private static ResourceService resourceService; + + @Inject + public GuiceHolder(final ResourceService resourceService) { + GuiceHolder.resourceService = resourceService; + } + + public static ResourceService getResourceService() { + return resourceService; + } + + @Override + public void close() {} + + @Override + public Lifecycle.State lifecycleState() { + return null; + } + + @Override + public void addLifecycleListener(LifecycleListener listener) {} + + @Override + public void removeLifecycleListener(LifecycleListener listener) {} + + @Override + public void start() {} + + @Override + public void stop() {} + + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceAction.java new file mode 100644 index 0000000000..1e106d1a47 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceAction.java @@ -0,0 +1,30 @@ +/* + * 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. + */ + +package org.opensearch.security.sample.actions.create; + +import org.opensearch.action.ActionType; +import org.opensearch.security.sample.transport.CreateResourceResponse; + +/** + * Action to create a sample resource + */ +public class CreateSampleResourceAction extends ActionType { + /** + * Create sample resource action instance + */ + public static final CreateSampleResourceAction INSTANCE = new CreateSampleResourceAction(); + /** + * Create sample resource action name + */ + public static final String NAME = "cluster:admin/sampleresource/create"; + + private CreateSampleResourceAction() { + super(NAME, CreateResourceResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRequest.java new file mode 100644 index 0000000000..35815f9a17 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRequest.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package org.opensearch.security.sample.actions.create; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.security.sample.Resource; + +/** + * Request object for CreateSampleResource transport action + */ +public class CreateSampleResourceRequest extends ActionRequest { + + private final Resource resource; + + /** + * Default constructor + */ + public CreateSampleResourceRequest(Resource resource) { + this.resource = resource; + } + + /** + * Constructor with stream input + * @param in the stream input + * @throws IOException IOException + */ + public CreateSampleResourceRequest(final StreamInput in) throws IOException { + this.resource = new SampleResource(in); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + resource.writeTo(out); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public Resource getResource() { + return this.resource; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceResponse.java new file mode 100644 index 0000000000..476d63d5fe --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceResponse.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package org.opensearch.security.sample.actions.create; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +/** + * Response to a CreateSampleResourceRequest + */ +public class CreateSampleResourceResponse extends ActionResponse implements ToXContentObject { + private final String message; + + /** + * Default constructor + * + * @param message The message + */ + public CreateSampleResourceResponse(String message) { + this.message = message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(message); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public CreateSampleResourceResponse(final StreamInput in) throws IOException { + message = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("message", message); + builder.endObject(); + return builder; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRestAction.java new file mode 100644 index 0000000000..00e41bbdf9 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRestAction.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package org.opensearch.security.sample.actions.create; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.security.sample.transport.CreateResourceRequest; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.POST; + +public class CreateSampleResourceRestAction extends BaseRestHandler { + + public CreateSampleResourceRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(POST, "/_plugins/resource_sharing_example/resource")); + } + + @Override + public String getName() { + return "create_sample_resource"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String name = (String) source.get("name"); + SampleResource resource = new SampleResource(); + resource.setName(name); + final CreateResourceRequest createSampleResourceRequest = new CreateResourceRequest<>(resource); + return channel -> client.executeLocally( + CreateSampleResourceAction.INSTANCE, + createSampleResourceRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceTransportAction.java new file mode 100644 index 0000000000..23c84aec82 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceTransportAction.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.sample.actions.create; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.support.ActionFilters; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.security.sample.transport.CreateResourceTransportAction; +import org.opensearch.transport.TransportService; + +import static org.opensearch.security.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; + +/** + * Transport action for CreateSampleResource. + */ +public class CreateSampleResourceTransportAction extends CreateResourceTransportAction { + private static final Logger log = LogManager.getLogger(CreateSampleResourceTransportAction.class); + + @Inject + public CreateSampleResourceTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { + super(transportService, actionFilters, nodeClient, CreateSampleResourceAction.NAME, RESOURCE_INDEX_NAME, SampleResource::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/SampleResource.java new file mode 100644 index 0000000000..6bc91c369a --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/SampleResource.java @@ -0,0 +1,45 @@ +package org.opensearch.security.sample.actions.create; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.security.sample.Resource; + +import static org.opensearch.security.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; + +public class SampleResource extends Resource { + + private String name; + + public SampleResource() {} + + SampleResource(StreamInput in) throws IOException { + this.name = in.readString(); + } + + @Override + public String getResourceIndex() { + return RESOURCE_INDEX_NAME; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().field("name", name).endObject(); + } + + @Override + public void writeTo(StreamOutput streamOutput) throws IOException { + streamOutput.writeString(name); + } + + @Override + public String getWriteableName() { + return "sampled_resource"; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceAction.java new file mode 100644 index 0000000000..89bee6c093 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceAction.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package org.opensearch.security.sample.actions.list; + +import org.opensearch.action.ActionType; + +/** + * Action to list sample resources + */ +public class ListSampleResourceAction extends ActionType { + /** + * List sample resource action instance + */ + public static final ListSampleResourceAction INSTANCE = new ListSampleResourceAction(); + /** + * List sample resource action name + */ + public static final String NAME = "cluster:admin/sampleresource/list"; + + private ListSampleResourceAction() { + super(NAME, ListSampleResourceResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRequest.java new file mode 100644 index 0000000000..27d1cd6cfd --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRequest.java @@ -0,0 +1,39 @@ +/* + * 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. + */ + +package org.opensearch.security.sample.actions.list; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +/** + * Request object for ListSampleResource transport action + */ +public class ListSampleResourceRequest extends ActionRequest { + + public ListSampleResourceRequest() {} + + /** + * Constructor with stream input + * @param in the stream input + * @throws IOException IOException + */ + public ListSampleResourceRequest(final StreamInput in) throws IOException {} + + @Override + public void writeTo(final StreamOutput out) throws IOException {} + + @Override + public ActionRequestValidationException validate() { + return null; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceResponse.java new file mode 100644 index 0000000000..021d456cab --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceResponse.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package org.opensearch.security.sample.actions.list; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +/** + * Response to a ListSampleResourceRequest + */ +public class ListSampleResourceResponse extends ActionResponse implements ToXContentObject { + private final String message; + + /** + * Default constructor + * + * @param message The message + */ + public ListSampleResourceResponse(String message) { + this.message = message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(message); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public ListSampleResourceResponse(final StreamInput in) throws IOException { + message = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("message", message); + builder.endObject(); + return builder; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRestAction.java new file mode 100644 index 0000000000..e56fd08179 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRestAction.java @@ -0,0 +1,44 @@ +/* + * 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. + */ + +package org.opensearch.security.sample.actions.list; + +import java.util.List; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.GET; + +public class ListSampleResourceRestAction extends BaseRestHandler { + + public ListSampleResourceRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(GET, "/_plugins/resource_sharing_example/resource")); + } + + @Override + public String getName() { + return "list_sample_resources"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + final ListSampleResourceRequest listSampleResourceRequest = new ListSampleResourceRequest(); + return channel -> client.executeLocally( + ListSampleResourceAction.INSTANCE, + listSampleResourceRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceTransportAction.java new file mode 100644 index 0000000000..e04435725e --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceTransportAction.java @@ -0,0 +1,52 @@ +/* + * 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. + */ + +package org.opensearch.security.sample.actions.list; + +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.index.query.MatchAllQueryBuilder; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +/** + * Transport action for ListSampleResource. + */ +public class ListSampleResourceTransportAction extends HandledTransportAction { + private final TransportService transportService; + private final Client nodeClient; + + @Inject + public ListSampleResourceTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { + super(ListSampleResourceAction.NAME, transportService, actionFilters, ListSampleResourceRequest::new); + this.transportService = transportService; + this.nodeClient = nodeClient; + } + + @Override + protected void doExecute(Task task, ListSampleResourceRequest request, ActionListener listener) { + try (ThreadContext.StoredContext ignore = transportService.getThreadPool().getThreadContext().stashContext()) { + SearchRequest sr = new SearchRequest(".resource-sharing"); + SearchSourceBuilder matchAllQuery = new SearchSourceBuilder(); + matchAllQuery.query(new MatchAllQueryBuilder()); + sr.source(matchAllQuery); + /* Index already exists, ignore and continue */ + ActionListener searchListener = ActionListener.wrap(response -> { + listener.onResponse(new ListSampleResourceResponse(response.toString())); + }, listener::onFailure); + nodeClient.search(sr, searchListener); + } + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceRequest.java new file mode 100644 index 0000000000..ea1eb57755 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceRequest.java @@ -0,0 +1,50 @@ +/* + * 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. + */ + +package org.opensearch.security.sample.transport; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.security.sample.Resource; + +/** + * Request object for CreateSampleResource transport action + */ +public class CreateResourceRequest extends ActionRequest { + + private final T resource; + + /** + * Default constructor + */ + public CreateResourceRequest(T resource) { + this.resource = resource; + } + + public CreateResourceRequest(StreamInput in, Reader resourceReader) throws IOException { + this.resource = resourceReader.read(in); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + resource.writeTo(out); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public Resource getResource() { + return this.resource; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceResponse.java new file mode 100644 index 0000000000..892cd74108 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceResponse.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package org.opensearch.security.sample.transport; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +/** + * Response to a CreateSampleResourceRequest + */ +public class CreateResourceResponse extends ActionResponse implements ToXContentObject { + private final String message; + + /** + * Default constructor + * + * @param message The message + */ + public CreateResourceResponse(String message) { + this.message = message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(message); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public CreateResourceResponse(final StreamInput in) throws IOException { + message = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("message", message); + builder.endObject(); + return builder; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceTransportAction.java new file mode 100644 index 0000000000..f95e2d5d5a --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceTransportAction.java @@ -0,0 +1,103 @@ +/* + * 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. + */ + +package org.opensearch.security.sample.transport; + +import java.io.IOException; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.accesscontrol.resources.ResourceService; +import org.opensearch.accesscontrol.resources.ResourceSharing; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.index.IndexResponse; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.io.stream.Writeable; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.security.sample.Resource; +import org.opensearch.security.sample.SampleResourcePlugin; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; + +/** + * Transport action for CreateSampleResource. + */ +public class CreateResourceTransportAction extends HandledTransportAction< + CreateResourceRequest, + CreateResourceResponse> { + private static final Logger log = LogManager.getLogger(CreateResourceTransportAction.class); + + private final TransportService transportService; + private final Client nodeClient; + private final String resourceIndex; + + public CreateResourceTransportAction( + TransportService transportService, + ActionFilters actionFilters, + Client nodeClient, + String actionName, + String resourceIndex, + Writeable.Reader resourceReader + ) { + super(actionName, transportService, actionFilters, (in) -> new CreateResourceRequest(in, resourceReader)); + this.transportService = transportService; + this.nodeClient = nodeClient; + this.resourceIndex = resourceIndex; + } + + @Override + protected void doExecute(Task task, CreateResourceRequest request, ActionListener listener) { + try (ThreadContext.StoredContext ignore = transportService.getThreadPool().getThreadContext().stashContext()) { + CreateIndexRequest cir = new CreateIndexRequest(resourceIndex); + ActionListener cirListener = ActionListener.wrap( + response -> { createResource(request, listener); }, + (failResponse) -> { + /* Index already exists, ignore and continue */ + createResource(request, listener); + } + ); + nodeClient.admin().indices().create(cir, cirListener); + } + } + + private void createResource(CreateResourceRequest request, ActionListener listener) { + Resource sample = request.getResource(); + try { + IndexRequest ir = nodeClient.prepareIndex(resourceIndex) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .setSource(sample.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) + .request(); + + log.warn("Index Request: {}", ir.toString()); + + ActionListener irListener = ActionListener.wrap(idxResponse -> { + log.info("Created resource: {}", idxResponse.toString()); + ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); + ResourceSharing sharing = rs.getResourceAccessControlPlugin() + .shareWith(idxResponse.getId(), idxResponse.getIndex(), Map.of()); + log.info("Created resource sharing entry: {}", sharing.toString()); + }, listener::onFailure); + nodeClient.index(ir, irListener); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // TODO add delete implementation as a separate transport action +} diff --git a/sample-resource-plugin/src/main/plugin-metadata/plugin-security.policy b/sample-resource-plugin/src/main/plugin-metadata/plugin-security.policy new file mode 100644 index 0000000000..a5dfc33a87 --- /dev/null +++ b/sample-resource-plugin/src/main/plugin-metadata/plugin-security.policy @@ -0,0 +1,3 @@ +grant { + permission java.lang.RuntimePermission "getClassLoader"; +}; \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/esnode-key.pem b/sample-resource-plugin/src/test/resources/security/esnode-key.pem new file mode 100644 index 0000000000..e90562be43 --- /dev/null +++ b/sample-resource-plugin/src/test/resources/security/esnode-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCm93kXteDQHMAv +bUPNPW5pyRHKDD42XGWSgq0k1D29C/UdyL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0 +o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0HGkn47XVu3EwbfrTENg3jFu+Oem6a/50 +1SzITzJWtS0cn2dIFOBimTVpT/4Zv5qrXA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1 +MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8ndibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b +6l+KLo3IKpfTbAIJXIO+M67FLtWKtttDao94B069skzKk6FPgW/OZh6PRCD0oxOa +vV+ld2SjAgMBAAECggEAQK1+uAOZeaSZggW2jQut+MaN4JHLi61RH2cFgU3COLgo +FIiNjFn8f2KKU3gpkt1It8PjlmprpYut4wHI7r6UQfuv7ZrmncRiPWHm9PB82+ZQ +5MXYqj4YUxoQJ62Cyz4sM6BobZDrjG6HHGTzuwiKvHHkbsEE9jQ4E5m7yfbVvM0O +zvwrSOM1tkZihKSTpR0j2+taji914tjBssbn12TMZQL5ItGnhR3luY8mEwT9MNkZ +xg0VcREoAH+pu9FE0vPUgLVzhJ3be7qZTTSRqv08bmW+y1plu80GbppePcgYhEow +dlW4l6XPJaHVSn1lSFHE6QAx6sqiAnBz0NoTPIaLyQKBgQDZqDOlhCRciMRicSXn +7yid9rhEmdMkySJHTVFOidFWwlBcp0fGxxn8UNSBcXdSy7GLlUtH41W9PWl8tp9U +hQiiXORxOJ7ZcB80uNKXF01hpPj2DpFPWyHFxpDkWiTAYpZl68rOlYujxZUjJIej +VvcykBC2BlEOG9uZv2kxcqLyJwKBgQDEYULTxaTuLIa17wU3nAhaainKB3vHxw9B +Ksy5p3ND43UNEKkQm7K/WENx0q47TA1mKD9i+BhaLod98mu0YZ+BCUNgWKcBHK8c +uXpauvM/pLhFLXZ2jvEJVpFY3J79FSRK8bwE9RgKfVKMMgEk4zOyZowS8WScOqiy +hnQn1vKTJQKBgElhYuAnl9a2qXcC7KOwRsJS3rcKIVxijzL4xzOyVShp5IwIPbOv +hnxBiBOH/JGmaNpFYBcBdvORE9JfA4KMQ2fx53agfzWRjoPI1/7mdUk5RFI4gRb/ +A3jZRBoopgFSe6ArCbnyQxzYzToG48/Wzwp19ZxYrtUR4UyJct6f5n27AoGBAJDh +KIpQQDOvCdtjcbfrF4aM2DPCfaGPzENJriwxy6oEPzDaX8Bu/dqI5Ykt43i/zQrX +GpyLaHvv4+oZVTiI5UIvcVO9U8hQPyiz9f7F+fu0LHZs6f7hyhYXlbe3XFxeop3f +5dTKdWgXuTTRF2L9dABkA2deS9mutRKwezWBMQk5AoGBALPtX0FrT1zIosibmlud +tu49A/0KZu4PBjrFMYTSEWGNJez3Fb2VsJwylVl6HivwbP61FhlYfyksCzQQFU71 ++x7Nmybp7PmpEBECr3deoZKQ/acNHn0iwb0It+YqV5+TquQebqgwK6WCLsMuiYKT +bg/ch9Rhxbq22yrVgWHh6epp +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/esnode.pem b/sample-resource-plugin/src/test/resources/security/esnode.pem new file mode 100644 index 0000000000..44101f0b37 --- /dev/null +++ b/sample-resource-plugin/src/test/resources/security/esnode.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT +AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl +MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud +yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0 +HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr +XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n +dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD +ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R +BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA +AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF +BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo +wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ +KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz +pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi +7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh +hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L +camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg +PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/kirk-key.pem b/sample-resource-plugin/src/test/resources/security/kirk-key.pem new file mode 100644 index 0000000000..1949c26139 --- /dev/null +++ b/sample-resource-plugin/src/test/resources/security/kirk-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCVXDgEJQorgfXp +gpY0TgF55bD2xuzxN5Dc9rDfgWxrsOvOloMpd7k6FR71bKWjJi1KptSmM/cDElky +AWYKSfYWGiGxsQ+EQW+6kwCfEOHXQldn+0+JcWqP+osSPjtJfwRvRN5kRqP69MPo +7U0N2kdqenqMWjmG1chDGLRSOEGU5HIBiDxsZtOcvMaJ8b1eaW0lvS+6gFQ80AvB +GBkDDCOHHLtDXBylrZk2CQP8AzxNicIZ4B8G3CG3OHA8+nBtEtxZoIihrrkqlMt+ +b/5N8u8zB0Encew0kdrc4R/2wS//ahr6U+9Siq8T7WsUtGwKj3BJClg6OyDJRhlu +y2gFnxoPAgMBAAECggEAP5TOycDkx+megAWVoHV2fmgvgZXkBrlzQwUG/VZQi7V4 +ZGzBMBVltdqI38wc5MtbK3TCgHANnnKgor9iq02Z4wXDwytPIiti/ycV9CDRKvv0 +TnD2hllQFjN/IUh5n4thHWbRTxmdM7cfcNgX3aZGkYbLBVVhOMtn4VwyYu/Mxy8j +xClZT2xKOHkxqwmWPmdDTbAeZIbSv7RkIGfrKuQyUGUaWhrPslvYzFkYZ0umaDgQ +OAthZew5Bz3OfUGOMPLH61SVPuJZh9zN1hTWOvT65WFWfsPd2yStI+WD/5PU1Doo +1RyeHJO7s3ug8JPbtNJmaJwHe9nXBb/HXFdqb976yQKBgQDNYhpu+MYSYupaYqjs +9YFmHQNKpNZqgZ4ceRFZ6cMJoqpI5dpEMqToFH7tpor72Lturct2U9nc2WR0HeEs +/6tiptyMPTFEiMFb1opQlXF2ae7LeJllntDGN0Q6vxKnQV+7VMcXA0Y8F7tvGDy3 +qJu5lfvB1mNM2I6y/eMxjBuQhwKBgQC6K41DXMFro0UnoO879pOQYMydCErJRmjG +/tZSy3Wj4KA/QJsDSViwGfvdPuHZRaG9WtxdL6kn0w1exM9Rb0bBKl36lvi7o7xv +M+Lw9eyXMkww8/F5d7YYH77gIhGo+RITkKI3+5BxeBaUnrGvmHrpmpgRXWmINqr0 +0jsnN3u0OQKBgCf45vIgItSjQb8zonLz2SpZjTFy4XQ7I92gxnq8X0Q5z3B+o7tQ +K/4rNwTju/sGFHyXAJlX+nfcK4vZ4OBUJjP+C8CTjEotX4yTNbo3S6zjMyGQqDI5 +9aIOUY4pb+TzeUFJX7If5gR+DfGyQubvvtcg1K3GHu9u2l8FwLj87sRzAoGAflQF +RHuRiG+/AngTPnZAhc0Zq0kwLkpH2Rid6IrFZhGLy8AUL/O6aa0IGoaMDLpSWUJp +nBY2S57MSM11/MVslrEgGmYNnI4r1K25xlaqV6K6ztEJv6n69327MS4NG8L/gCU5 +3pEm38hkUi8pVYU7in7rx4TCkrq94OkzWJYurAkCgYATQCL/rJLQAlJIGulp8s6h +mQGwy8vIqMjAdHGLrCS35sVYBXG13knS52LJHvbVee39AbD5/LlWvjJGlQMzCLrw +F7oILW5kXxhb8S73GWcuMbuQMFVHFONbZAZgn+C9FW4l7XyRdkrbR1MRZ2km8YMs +/AHmo368d4PSNRMMzLHw8Q== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/kirk.pem b/sample-resource-plugin/src/test/resources/security/kirk.pem new file mode 100644 index 0000000000..36b7e19a75 --- /dev/null +++ b/sample-resource-plugin/src/test/resources/security/kirk.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEmDCCA4CgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLcwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzA0MjRaFw0zNDAyMTcxNzA0MjRaME0xCzAJBgNVBAYT +AmRlMQ0wCwYDVQQHDAR0ZXN0MQ8wDQYDVQQKDAZjbGllbnQxDzANBgNVBAsMBmNs +aWVudDENMAsGA1UEAwwEa2lyazCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAJVcOAQlCiuB9emCljROAXnlsPbG7PE3kNz2sN+BbGuw686Wgyl3uToVHvVs +paMmLUqm1KYz9wMSWTIBZgpJ9hYaIbGxD4RBb7qTAJ8Q4ddCV2f7T4lxao/6ixI+ +O0l/BG9E3mRGo/r0w+jtTQ3aR2p6eoxaOYbVyEMYtFI4QZTkcgGIPGxm05y8xonx +vV5pbSW9L7qAVDzQC8EYGQMMI4ccu0NcHKWtmTYJA/wDPE2JwhngHwbcIbc4cDz6 +cG0S3FmgiKGuuSqUy35v/k3y7zMHQSdx7DSR2tzhH/bBL/9qGvpT71KKrxPtaxS0 +bAqPcEkKWDo7IMlGGW7LaAWfGg8CAwEAAaOCASswggEnMAwGA1UdEwEB/wQCMAAw +DgYDVR0PAQH/BAQDAgXgMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMB0GA1UdDgQW +BBSjMS8tgguX/V7KSGLoGg7K6XMzIDCBzwYDVR0jBIHHMIHEgBQXh9+gWutmEqfV +0Pi6EkU8tysAnKGBlaSBkjCBjzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmS +JomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1wbGUgQ29tIEluYy4xITAf +BgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEhMB8GA1UEAwwYRXhhbXBs +ZSBDb20gSW5jLiBSb290IENBghQNZAmZZn3EFOxBR4630XlhI+mo4jANBgkqhkiG +9w0BAQsFAAOCAQEACEUPPE66/Ot3vZqRGpjDjPHAdtOq+ebaglQhvYcnDw8LOZm8 +Gbh9M88CiO6UxC8ipQLTPh2yyeWArkpJzJK/Pi1eoF1XLiAa0sQ/RaJfQWPm9dvl +1ZQeK5vfD4147b3iBobwEV+CR04SKow0YeEEzAJvzr8YdKI6jqr+2GjjVqzxvRBy +KRVHWCFiR7bZhHGLq3br8hSu0hwjb3oGa1ZI8dui6ujyZt6nm6BoEkau3G/6+zq9 +E6vX3+8Fj4HKCAL6i0SwfGmEpTNp5WUhqibK/fMhhmMT4Mx6MxkT+OFnIjdUU0S/ +e3kgnG8qjficUr38CyEli1U0M7koIXUZI7r+LQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/root-ca.pem b/sample-resource-plugin/src/test/resources/security/root-ca.pem new file mode 100644 index 0000000000..d33f5f7216 --- /dev/null +++ b/sample-resource-plugin/src/test/resources/security/root-ca.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIExjCCA66gAwIBAgIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzAwMzZaFw0zNDAyMTcxNzAwMzZaMIGPMRMwEQYKCZIm +iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ +RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290 +IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEPyN7J9VGPyJcQmCBl5TGwfSzvVdWwoQU +j9aEsdfFJ6pBCDQSsj8Lv4RqL0dZra7h7SpZLLX/YZcnjikrYC+rP5OwsI9xEE/4 +U98CsTBPhIMgqFK6SzNE5494BsAk4cL72dOOc8tX19oDS/PvBULbNkthQ0aAF1dg +vbrHvu7hq7LisB5ZRGHVE1k/AbCs2PaaKkn2jCw/b+U0Ml9qPuuEgz2mAqJDGYoA +WSR4YXrOcrmPuRqbws464YZbJW898/0Pn/U300ed+4YHiNYLLJp51AMkR4YEw969 +VRPbWIvLrd0PQBooC/eLrL6rvud/GpYhdQEUx8qcNCKd4bz3OaQ5AgMBAAGjggEW +MIIBEjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQU +F4ffoFrrZhKn1dD4uhJFPLcrAJwwgc8GA1UdIwSBxzCBxIAUF4ffoFrrZhKn1dD4 +uhJFPLcrAJyhgZWkgZIwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJ +k/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYD +VQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUg +Q29tIEluYy4gUm9vdCBDQYIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcN +AQELBQADggEBAL3Q3AHUhMiLUy6OlLSt8wX9I2oNGDKbBu0atpUNDztk/0s3YLQC +YuXgN4KrIcMXQIuAXCx407c+pIlT/T1FNn+VQXwi56PYzxQKtlpoKUL3oPQE1d0V +6EoiNk+6UodvyZqpdQu7fXVentRMk1QX7D9otmiiNuX+GSxJhJC2Lyzw65O9EUgG +1yVJon6RkUGtqBqKIuLksKwEr//ELnjmXit4LQKSnqKr0FTCB7seIrKJNyb35Qnq +qy9a/Unhokrmdda1tr6MbqU8l7HmxLuSd/Ky+L0eDNtYv6YfMewtjg0TtAnFyQov +rdXmeq1dy9HLo3Ds4AFz3Gx9076TxcRS/iI= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/sample.pem b/sample-resource-plugin/src/test/resources/security/sample.pem new file mode 100644 index 0000000000..44101f0b37 --- /dev/null +++ b/sample-resource-plugin/src/test/resources/security/sample.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT +AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl +MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud +yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0 +HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr +XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n +dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD +ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R +BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA +AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF +BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo +wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ +KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz +pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi +7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh +hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L +camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg +PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/test-kirk.jks b/sample-resource-plugin/src/test/resources/security/test-kirk.jks new file mode 100644 index 0000000000000000000000000000000000000000..6c8c5ef77e20980f8c78295b159256b805da6a28 GIT binary patch literal 3766 zcmd^=c{r47AIImJ%`(PV###wuU&o%k$xbMgr4m`Pk2Tv-j4?=zEwY?!X|aVw)I`=A zPAY52Rt6yODkPjhAQ%WsfbL*f;mp!-018Nf*#Q6sf)b!}Nv;s_8gzOC@mTmi+D9F}jyYkhL=#Xk3eYM2csmxKA&W!xAdE{tZ2mEGS z;L%QU`DHcrbdbw$3GsKUvmfQu0Z^?sH7B)!W)eLbG*fXB^G$&6CbCnj4~ z*J>Rkut6vL1EvT!JqAq#X=O~#!JHQ#QVSPuOGlnLrXXB~{{FsGRq?o?I;>^GFEhMB zw;z!v1sXap8nq3zz&+prKs-DRPm*XsS4BaP6Z{8tM~n@m|rxMA=p6*i(w=7 z*2&*Yg-uWU$5|W>>g5h)Fn{3B={`skAJ5_wXB5pDwyj{vG1_{{Y-`wB_i^B!5PA|= zrx=_>rprb&75BQ=J)SKPAJI;?(D#46)o+a?SsR^-&qJjXY2ER8S*1ZvU`t7~M6?NKULuzlAZ8C#X9>8j2;WDY z(TY-^!`&0%67`u|U_-Y(knWVcSlh-kwZQ6KG@S?L`W!iVl>Gyd(LnpMc@C!QeY{(E z)uAwF_CcqH#00}jer2dQk3}R|p^87XCxR8`n4c@g9rASTt9$8}SuGW!!+QQ&w&G!P zvv5Mft<&pzv^&XuuQAj&ieoa*3nI-hx}0`4kym=(cd>?v6yM3v43y@5@;yPeJ_N{@ z622W$@5Z4VqliMF3GAf_RcB;$HX^%cwTCgxg^4)5I0?*&oW|giBB@nUNBO+IX=iON zo~;L}HOwhyeqH4GHvAQ5i=|0c+_5*661aDyT_tr=I#+Zog%!9nRiuBb8m&SS4qp2fv7HJMG zwJFuqV*Hoq3`|Mayml;So|9W4Um6Lu8(k+(Hc2}p@&>?!7!7H~9*O%@BrKNAOa-~e z$e6#G)fJ+wNz5x9zU;#>&V}d z?!F1W_eNN;&LI9$!kWa0Zqa)0CVM4D=x(r>aXgW=XQ)PTRsJJ&MC?WjjoMwLRh`-I z8yD|^&(r#NU|pRpRF%wn&t%X`)8HQe%uxEKnXxIu9yui1s$eH0*YZ^Wvt25yOg6{5 zPefKstjqam-PRDz=&-BVb^xZe>{C{$cza!_sV&3M*l0ocMJVr!l~TlJi4JChDn9Nn zc&la1caY}0P&Ho=r;)l;mKBf$V<6A*R6XC}s98g%I7ZIAFI=e6SqQ4;oevw)nw0%^ zKq9#$;{3R0zJv}#mr7@}e+5-(`{C?^vEE#xb7uBY=X#_1v+@~@l?W@Zaq+Yo9bpu& zR<0us_T`(Q6qp1xYb)Rq;tJ|aTZ&y5xqx<_j-|>1$SEi@3!A|| z9YH<3ub_#ai=2WG_V9iQ!NU8mB|$4ZK3Gr>_s15;6W-XV-*##3TjwoMP&yb zq!L{!sQoUn<_ZWb)BbzloM2Zs1tb=+FBn*$!EQmp3Ml#oe;g0);^XP&_osni`NR1A z0SL>FG{F)8;h%d#4-g0eK+%&0UD-=ghUr~yDQ?!lNE5tKiJ_rjY{@`Q1vjbVAFU;|?Qs;w|1hFx_ z`*jR7rVAU>9*yRSpD1)#aOb!)@ak(5hk;guG$_9)=K8Ie^uOP<63|FjrX2UEcJw07 zD5c?bxHD${?)1+CMgPg@0|kH>4NzJZO*;#rl-xA_8*SHCS}ygKZP7*uHbRtmaTE%n zp7Vt7QIt|IIN?)fyS#8IxKHO$?TeY{DpQl5^kyAd$HH^Aa)SJC+I0!ULR znF7*z6R6~{CCW6M^qKuU!N`I`>YB3i6toA7f7#3%T&$5&wm0nY{&d9(g)LB$%g9dX zf>HfjVn9;)rG-^=)tiGDd<5M4wDHPl@yEGU_whSh78l$%S*WCqjvj^Xt?_VKp0T{pQGU!F;?_^4EMT$__$E zH0hMGQlo@W2p^_tPZsnirl@pGb<#0a^*g5ihYtSzKKx%Wg;i4h8B_c6Z+PPWM!I%g zOr-dLp|0@RV@@&InVrwRJfPT~ZY840gT$Jl4)HP^qcTUWE~1&}C2wS3Sv9pJWiRva zyK}a9ilnrYe7SB$bu~GF&GM`D1h@ukNsJY|Yt>|?q(4gzgSUuGwSIfsmlD)%J2V0@ zTU&-58&x%P)-#Oev2~&}bv^wwRbD$?Enu(jJiuwM3shGOZ{$juY+RGk#m^`!p7+vO zAjWFn1{dq`T?N^TggHmN3~VGf^5?a_)R-cj5yfk-?V<|S)%uKn{YGL)7(~eAhWA56 zj7ZS7amp#qQM;t>%6F)v{1S-Gq>88IPiL?2X9=q_r$vhc4{Pd3$WssBMbZaV2W zu&8||{U99-3!x+JudoA1KSAx^0qg$*YLr)FKtJ($lC@k)W?khPY!~B&3F~Xnxs_WH)b*(MC{~@>r={U4@A6+2p8il>0lojdT`r8~C>rA6;jw^lZK9gk<_y!v za(Rbclc{1;TFBtT`lr|YO0}|UXzh>FLsx6RQUq8=?V4{NR#=oxL2}kHb-ZAfuN Date: Fri, 6 Sep 2024 13:18:33 -0400 Subject: [PATCH 004/201] Removes node_modules entry from gitingore Signed-off-by: Darshit Chanpura --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 6fbfafabac..5eb2da999f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,3 @@ out/ build/ gradle-build/ .gradle/ - -# nodejs -node_modules/ -package-lock.json From 45d4fa580684cec31acb434f9d251d64d88643b1 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 4 Oct 2024 13:47:23 -0400 Subject: [PATCH 005/201] Handles changes related to scope Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 4 ++-- .../security/resources/ResourceAccessHandler.java | 4 ++-- .../resources/ResourceManagementRepository.java | 11 +++++++++++ .../resources/ResourceSharingIndexHandler.java | 14 ++------------ 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 27e89f5c31..62c5cad9fe 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2198,8 +2198,8 @@ public List listAccessibleResourcesForPlugin(String systemIndexName) { } @Override - public boolean hasPermission(String resourceId, String systemIndexName) { - return this.resourceAccessHandler.hasPermission(resourceId, systemIndexName); + public boolean hasPermission(String resourceId, String systemIndexName, String scope) { + return this.resourceAccessHandler.hasPermission(resourceId, systemIndexName, scope); } @Override diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 0861854e13..142c6b67da 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -51,9 +51,9 @@ public List listAccessibleResourcesForPlugin(String systemIndex) { return List.of(); } - public boolean hasPermission(String resourceId, String systemIndexName) { + public boolean hasPermission(String resourceId, String systemIndexName, String scope) { final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - LOGGER.info("Checking if {} has permission to resource {}", user.getName(), resourceId); + LOGGER.info("Checking if {} has {} permission to resource {}", user.getName(), scope, resourceId); // TODO add concrete implementation return false; diff --git a/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java b/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java index df59516a41..7e347a331d 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java +++ b/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java @@ -1,3 +1,14 @@ +/* + * 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.resources; import org.apache.logging.log4j.LogManager; diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 79ef85e7eb..b6f4b02ade 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -71,20 +71,10 @@ public void createIndex(ActionListener listener) { } } - // public void createIndexIfAbsent() { - // try { - // final Map indexSettings = ImmutableMap.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); - // final CreateIndexRequest createIndexRequest = new CreateIndexRequest(resourceSharingIndex).settings(indexSettings); - // final boolean ok = client.admin().indices().create(createIndexRequest).actionGet().isAcknowledged(); - // LOGGER.info("Resource sharing index {} created?: {}", resourceSharingIndex, ok); - // } catch (ResourceAlreadyExistsException resourceAlreadyExistsException) { - // LOGGER.info("Index {} already exists", resourceSharingIndex); - // } - // } - public void createResourceSharingIndexIfAbsent(Callable callable) { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - CreateIndexRequest cir = new CreateIndexRequest(resourceSharingIndex); + CreateIndexRequest cir = new CreateIndexRequest(resourceSharingIndex).settings(INDEX_SETTINGS).waitForActiveShards(1); ActionListener cirListener = ActionListener.wrap(response -> { LOGGER.info("Resource sharing index {} created.", resourceSharingIndex); callable.call(); From ae2464dc83132b21979cae858676fd66861b5c34 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 4 Oct 2024 13:47:47 -0400 Subject: [PATCH 006/201] Updates sample plugin to implement a custom scope Signed-off-by: Darshit Chanpura --- .../security/sample/SampleResourceScope.java | 22 +++++++++++++++++++ .../CreateResourceTransportAction.java | 21 +++++++++++------- 2 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourceScope.java diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourceScope.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourceScope.java new file mode 100644 index 0000000000..797f3e517b --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourceScope.java @@ -0,0 +1,22 @@ +package org.opensearch.security.sample; + +import org.opensearch.accesscontrol.resources.ResourceAccessScope; + +/** + * This class demonstrates a sample implementation of Basic Access Scopes to fit each plugin's use-case. + * The plugin then uses this scope when seeking access evaluation for a user on a particular resource. + */ +enum SampleResourceScope implements ResourceAccessScope { + + SAMPLE_FULL_ACCESS("sample_full_access"); + + private final String name; + + SampleResourceScope(String scopeName) { + this.name = scopeName; + } + + public String getName() { + return name; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceTransportAction.java index f95e2d5d5a..dea075c55e 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceTransportAction.java @@ -9,13 +9,14 @@ package org.opensearch.security.sample.transport; import java.io.IOException; -import java.util.Map; +import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.accesscontrol.resources.ResourceService; import org.opensearch.accesscontrol.resources.ResourceSharing; +import org.opensearch.accesscontrol.resources.ShareWith; import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.action.admin.indices.create.CreateIndexResponse; import org.opensearch.action.index.IndexRequest; @@ -86,18 +87,22 @@ private void createResource(CreateResourceRequest request, ActionListener irListener = ActionListener.wrap(idxResponse -> { - log.info("Created resource: {}", idxResponse.toString()); - ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - ResourceSharing sharing = rs.getResourceAccessControlPlugin() - .shareWith(idxResponse.getId(), idxResponse.getIndex(), Map.of()); - log.info("Created resource sharing entry: {}", sharing.toString()); - }, listener::onFailure); + ActionListener irListener = getIndexResponseActionListener(listener); nodeClient.index(ir, irListener); } catch (IOException e) { throw new RuntimeException(e); } } + private static ActionListener getIndexResponseActionListener(ActionListener listener) { + ShareWith shareWith = new ShareWith(List.of()); + return ActionListener.wrap(idxResponse -> { + log.info("Created resource: {}", idxResponse.toString()); + ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); + ResourceSharing sharing = rs.getResourceAccessControlPlugin().shareWith(idxResponse.getId(), idxResponse.getIndex(), shareWith); + log.info("Created resource sharing entry: {}", sharing.toString()); + }, listener::onFailure); + } + // TODO add delete implementation as a separate transport action } From aea2253a4c19089479394a12fde56ea1e555fae8 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 4 Oct 2024 13:57:17 -0400 Subject: [PATCH 007/201] Fixes Checkstyle and spotless issues Signed-off-by: Darshit Chanpura --- .../java/org/opensearch/security/sample/Resource.java | 11 +++++++++++ .../security/sample/SampleResourceScope.java | 11 +++++++++++ .../sample/actions/create/SampleResource.java | 11 +++++++++++ .../opensearch/security/OpenSearchSecurityPlugin.java | 5 ++++- 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/Resource.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/Resource.java index 6126fdb092..0dd3b856bf 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/Resource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/Resource.java @@ -1,3 +1,14 @@ +/* + * 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.sample; import org.opensearch.core.common.io.stream.NamedWriteable; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourceScope.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourceScope.java index 797f3e517b..7a1b304371 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourceScope.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourceScope.java @@ -1,3 +1,14 @@ +/* + * 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.sample; import org.opensearch.accesscontrol.resources.ResourceAccessScope; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/SampleResource.java index 6bc91c369a..50c013f7dc 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/SampleResource.java @@ -1,3 +1,14 @@ +/* + * 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.sample.actions.create; import java.io.IOException; diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 62c5cad9fe..32a412653f 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -64,7 +64,10 @@ import org.apache.lucene.search.QueryCachingPolicy; import org.apache.lucene.search.Weight; -import org.opensearch.*; +import org.opensearch.OpenSearchException; +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.SpecialPermission; +import org.opensearch.Version; import org.opensearch.accesscontrol.resources.EntityType; import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.accesscontrol.resources.ShareWith; From 1e17dc0b0bd74ec726f68526f3597cbe62550163 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 4 Oct 2024 14:12:38 -0400 Subject: [PATCH 008/201] Fixes initialization error Signed-off-by: Darshit Chanpura --- .../org/opensearch/security/OpenSearchSecurityPlugin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 32a412653f..0a0df9e574 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -493,8 +493,6 @@ public List run() { } } - - this.resourceAccessHandler = new ResourceAccessHandler(threadPool); } private void verifyTLSVersion(final String settings, final List configuredProtocols) { @@ -1211,6 +1209,8 @@ public Collection createComponents( e.subscribeForChanges(dcf); } + resourceAccessHandler = new ResourceAccessHandler(threadPool); + rmr = ResourceManagementRepository.create(settings, threadPool, localClient); components.add(adminDns); From 84746e815b4aff72902bbc6a8e9ad467b594209d Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 4 Oct 2024 14:50:46 -0400 Subject: [PATCH 009/201] Renames sample resource plugin and adds a logger statement Signed-off-by: Darshit Chanpura --- sample-resource-plugin/build.gradle | 5 ++--- .../org/opensearch/security/sample/SampleResourcePlugin.java | 3 ++- settings.gradle | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle index 6d4b084580..dd04d390b0 100644 --- a/sample-resource-plugin/build.gradle +++ b/sample-resource-plugin/build.gradle @@ -11,10 +11,9 @@ import org.opensearch.gradle.test.RestIntegTestTask opensearchplugin { - name 'opensearch-security-sample-resource-plugin' + name 'opensearch-sample-resource-plugin' description 'Sample plugin that extends OpenSearch Resource Plugin' - classname 'org.opensearch.security.sampleresourceplugin.SampleResourcePlugin' - extendedPlugins = ['opensearch-security'] + classname 'org.opensearch.security.sample.SampleResourcePlugin' } ext { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourcePlugin.java index 58e4daa95c..5d598c5650 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourcePlugin.java @@ -81,6 +81,7 @@ public Collection createComponents( Supplier repositoriesServiceSupplier ) { this.client = client; + log.info("Loaded SampleResourcePlugin components."); return Collections.emptyList(); } @@ -118,7 +119,7 @@ public String getResourceType() { @Override public String getResourceIndex() { - return ""; + return RESOURCE_INDEX_NAME; } @Override diff --git a/settings.gradle b/settings.gradle index f2e59414d8..0bb3c5639d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,4 +7,4 @@ rootProject.name = 'opensearch-security' include "sample-resource-plugin" -project(":sample-resource-plugin").name = rootProject.name + "-sample-resource-plugin" +project(":sample-resource-plugin").name = "opensearch-sample-resource-plugin" From 83e4da09ce210143db68b3464dc0dc92e3bbf3cf Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 4 Oct 2024 14:53:10 -0400 Subject: [PATCH 010/201] Changes package name for sample plugin Signed-off-by: Darshit Chanpura --- sample-resource-plugin/build.gradle | 2 +- .../opensearch/{security => }/sample/Resource.java | 2 +- .../sample/SampleResourcePlugin.java | 14 +++++++------- .../{security => }/sample/SampleResourceScope.java | 2 +- .../actions/create/CreateSampleResourceAction.java | 4 ++-- .../create/CreateSampleResourceRequest.java | 4 ++-- .../create/CreateSampleResourceResponse.java | 2 +- .../create/CreateSampleResourceRestAction.java | 4 ++-- .../CreateSampleResourceTransportAction.java | 6 +++--- .../sample/actions/create/SampleResource.java | 6 +++--- .../actions/list/ListSampleResourceAction.java | 2 +- .../actions/list/ListSampleResourceRequest.java | 2 +- .../actions/list/ListSampleResourceResponse.java | 2 +- .../actions/list/ListSampleResourceRestAction.java | 2 +- .../list/ListSampleResourceTransportAction.java | 2 +- .../sample/transport/CreateResourceRequest.java | 4 ++-- .../sample/transport/CreateResourceResponse.java | 2 +- .../transport/CreateResourceTransportAction.java | 6 +++--- 18 files changed, 34 insertions(+), 34 deletions(-) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/Resource.java (93%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/SampleResourcePlugin.java (91%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/SampleResourceScope.java (95%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/actions/create/CreateSampleResourceAction.java (85%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/actions/create/CreateSampleResourceRequest.java (92%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/actions/create/CreateSampleResourceResponse.java (96%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/actions/create/CreateSampleResourceRestAction.java (93%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/actions/create/CreateSampleResourceTransportAction.java (82%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/actions/create/SampleResource.java (87%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/actions/list/ListSampleResourceAction.java (93%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/actions/list/ListSampleResourceRequest.java (95%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/actions/list/ListSampleResourceResponse.java (96%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/actions/list/ListSampleResourceRestAction.java (96%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/actions/list/ListSampleResourceTransportAction.java (97%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/transport/CreateResourceRequest.java (92%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/transport/CreateResourceResponse.java (96%) rename sample-resource-plugin/src/main/java/org/opensearch/{security => }/sample/transport/CreateResourceTransportAction.java (96%) diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle index dd04d390b0..e9822c1f22 100644 --- a/sample-resource-plugin/build.gradle +++ b/sample-resource-plugin/build.gradle @@ -13,7 +13,7 @@ import org.opensearch.gradle.test.RestIntegTestTask opensearchplugin { name 'opensearch-sample-resource-plugin' description 'Sample plugin that extends OpenSearch Resource Plugin' - classname 'org.opensearch.security.sample.SampleResourcePlugin' + classname 'org.opensearch.sample.SampleResourcePlugin' } ext { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/Resource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java similarity index 93% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/Resource.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java index 0dd3b856bf..36e74f1624 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/Resource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java @@ -9,7 +9,7 @@ * GitHub history for details. */ -package org.opensearch.security.sample; +package org.opensearch.sample; import org.opensearch.core.common.io.stream.NamedWriteable; import org.opensearch.core.xcontent.ToXContentFragment; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java similarity index 91% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourcePlugin.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index 5d598c5650..bb272b2201 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -6,7 +6,7 @@ * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ -package org.opensearch.security.sample; +package org.opensearch.sample; import java.util.ArrayList; import java.util.Collection; @@ -44,13 +44,13 @@ import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; +import org.opensearch.sample.actions.create.CreateSampleResourceAction; +import org.opensearch.sample.actions.create.CreateSampleResourceRestAction; +import org.opensearch.sample.actions.create.CreateSampleResourceTransportAction; +import org.opensearch.sample.actions.list.ListSampleResourceAction; +import org.opensearch.sample.actions.list.ListSampleResourceRestAction; +import org.opensearch.sample.actions.list.ListSampleResourceTransportAction; import org.opensearch.script.ScriptService; -import org.opensearch.security.sample.actions.create.CreateSampleResourceAction; -import org.opensearch.security.sample.actions.create.CreateSampleResourceRestAction; -import org.opensearch.security.sample.actions.create.CreateSampleResourceTransportAction; -import org.opensearch.security.sample.actions.list.ListSampleResourceAction; -import org.opensearch.security.sample.actions.list.ListSampleResourceRestAction; -import org.opensearch.security.sample.actions.list.ListSampleResourceTransportAction; import org.opensearch.threadpool.ThreadPool; import org.opensearch.watcher.ResourceWatcherService; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourceScope.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java similarity index 95% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourceScope.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java index 7a1b304371..2784de45b7 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/SampleResourceScope.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java @@ -9,7 +9,7 @@ * GitHub history for details. */ -package org.opensearch.security.sample; +package org.opensearch.sample; import org.opensearch.accesscontrol.resources.ResourceAccessScope; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceAction.java similarity index 85% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceAction.java index 1e106d1a47..fce62be629 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceAction.java @@ -6,10 +6,10 @@ * compatible open source license. */ -package org.opensearch.security.sample.actions.create; +package org.opensearch.sample.actions.create; import org.opensearch.action.ActionType; -import org.opensearch.security.sample.transport.CreateResourceResponse; +import org.opensearch.sample.transport.CreateResourceResponse; /** * Action to create a sample resource diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceRequest.java similarity index 92% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRequest.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceRequest.java index 35815f9a17..a509031b0b 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceRequest.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.sample.actions.create; +package org.opensearch.sample.actions.create; import java.io.IOException; @@ -14,7 +14,7 @@ import org.opensearch.action.ActionRequestValidationException; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.security.sample.Resource; +import org.opensearch.sample.Resource; /** * Request object for CreateSampleResource transport action diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceResponse.java similarity index 96% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceResponse.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceResponse.java index 476d63d5fe..86796bfff5 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceResponse.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.sample.actions.create; +package org.opensearch.sample.actions.create; import java.io.IOException; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceRestAction.java similarity index 93% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRestAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceRestAction.java index 00e41bbdf9..f422835168 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceRestAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.sample.actions.create; +package org.opensearch.sample.actions.create; import java.io.IOException; import java.util.List; @@ -17,7 +17,7 @@ import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.RestRequest; import org.opensearch.rest.action.RestToXContentListener; -import org.opensearch.security.sample.transport.CreateResourceRequest; +import org.opensearch.sample.transport.CreateResourceRequest; import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.POST; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceTransportAction.java similarity index 82% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceTransportAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceTransportAction.java index 23c84aec82..53d9817fbc 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/CreateSampleResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceTransportAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.sample.actions.create; +package org.opensearch.sample.actions.create; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -14,10 +14,10 @@ import org.opensearch.action.support.ActionFilters; import org.opensearch.client.Client; import org.opensearch.common.inject.Inject; -import org.opensearch.security.sample.transport.CreateResourceTransportAction; +import org.opensearch.sample.transport.CreateResourceTransportAction; import org.opensearch.transport.TransportService; -import static org.opensearch.security.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; /** * Transport action for CreateSampleResource. diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java similarity index 87% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/SampleResource.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java index 50c013f7dc..d2528c92be 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/create/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java @@ -9,16 +9,16 @@ * GitHub history for details. */ -package org.opensearch.security.sample.actions.create; +package org.opensearch.sample.actions.create; import java.io.IOException; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.security.sample.Resource; +import org.opensearch.sample.Resource; -import static org.opensearch.security.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; public class SampleResource extends Resource { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceAction.java similarity index 93% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceAction.java index 89bee6c093..17f50cda30 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.sample.actions.list; +package org.opensearch.sample.actions.list; import org.opensearch.action.ActionType; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceRequest.java similarity index 95% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRequest.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceRequest.java index 27d1cd6cfd..ffadf6abbb 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceRequest.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.sample.actions.list; +package org.opensearch.sample.actions.list; import java.io.IOException; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceResponse.java similarity index 96% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceResponse.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceResponse.java index 021d456cab..aaf6bfcd3e 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceResponse.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.sample.actions.list; +package org.opensearch.sample.actions.list; import java.io.IOException; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceRestAction.java similarity index 96% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRestAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceRestAction.java index e56fd08179..3f01bb5e2c 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceRestAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.sample.actions.list; +package org.opensearch.sample.actions.list; import java.util.List; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceTransportAction.java similarity index 97% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceTransportAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceTransportAction.java index e04435725e..ece829fe0d 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/actions/list/ListSampleResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceTransportAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.sample.actions.list; +package org.opensearch.sample.actions.list; import org.opensearch.action.search.SearchRequest; import org.opensearch.action.search.SearchResponse; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceRequest.java similarity index 92% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceRequest.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceRequest.java index ea1eb57755..f23735e7f3 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceRequest.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.sample.transport; +package org.opensearch.sample.transport; import java.io.IOException; @@ -14,7 +14,7 @@ import org.opensearch.action.ActionRequestValidationException; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.security.sample.Resource; +import org.opensearch.sample.Resource; /** * Request object for CreateSampleResource transport action diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceResponse.java similarity index 96% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceResponse.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceResponse.java index 892cd74108..12d7671ac4 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceResponse.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.sample.transport; +package org.opensearch.sample.transport; import java.io.IOException; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java similarity index 96% rename from sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceTransportAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java index dea075c55e..5e2eb6d723 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/security/sample/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.sample.transport; +package org.opensearch.sample.transport; import java.io.IOException; import java.util.List; @@ -29,8 +29,8 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.io.stream.Writeable; import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.security.sample.Resource; -import org.opensearch.security.sample.SampleResourcePlugin; +import org.opensearch.sample.Resource; +import org.opensearch.sample.SampleResourcePlugin; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; From 4b9b9b13bb79029a1fb96e8e8d38525e3aad1ba8 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 4 Oct 2024 18:10:57 -0400 Subject: [PATCH 011/201] Re-organizes and renames sample plugin files Signed-off-by: Darshit Chanpura --- .../sample/SampleResourcePlugin.java | 33 ++++---- ...eAction.java => CreateResourceAction.java} | 7 +- .../create}/CreateResourceRequest.java | 12 +-- .../create}/CreateResourceResponse.java | 2 +- ...ion.java => CreateResourceRestAction.java} | 11 ++- .../create/CreateSampleResourceRequest.java | 55 ------------- .../CreateSampleResourceTransportAction.java | 32 -------- .../sample/actions/create/SampleResource.java | 2 +- ...ava => ListAccessibleResourcesAction.java} | 8 +- ...va => ListAccessibleResourcesRequest.java} | 6 +- .../list/ListAccessibleResourcesResponse.java | 46 +++++++++++ ...=> ListAccessibleResourcesRestAction.java} | 12 +-- .../ListSampleResourceTransportAction.java | 52 ------------- .../actions/share/ShareResourceAction.java | 26 +++++++ .../actions/share/ShareResourceRequest.java | 52 +++++++++++++ .../ShareResourceResponse.java} | 11 +-- .../share/ShareResourceRestAction.java | 51 ++++++++++++ .../verify/VerifyResourceAccessAction.java | 25 ++++++ .../verify/VerifyResourceAccessRequest.java | 69 +++++++++++++++++ .../VerifyResourceAccessResponse.java} | 11 +-- .../VerifyResourceAccessRestAction.java | 52 +++++++++++++ .../CreateResourceTransportAction.java | 32 +++----- ...istAccessibleResourcesTransportAction.java | 56 ++++++++++++++ .../ShareResourceTransportAction.java | 77 +++++++++++++++++++ .../VerifyResourceAccessTransportAction.java | 58 ++++++++++++++ .../resources/ResourceAccessHandler.java | 6 +- 26 files changed, 583 insertions(+), 221 deletions(-) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/{CreateSampleResourceAction.java => CreateResourceAction.java} (67%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/{transport => actions/create}/CreateResourceRequest.java (73%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/{transport => actions/create}/CreateResourceResponse.java (96%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/{CreateSampleResourceRestAction.java => CreateResourceRestAction.java} (75%) delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceRequest.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceTransportAction.java rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/{ListSampleResourceAction.java => ListAccessibleResourcesAction.java} (63%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/{ListSampleResourceRequest.java => ListAccessibleResourcesRequest.java} (81%) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/{ListSampleResourceRestAction.java => ListAccessibleResourcesRestAction.java} (68%) delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{list/ListSampleResourceResponse.java => share/ShareResourceResponse.java} (78%) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{create/CreateSampleResourceResponse.java => verify/VerifyResourceAccessResponse.java} (81%) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index bb272b2201..abc9ed4de7 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -8,10 +8,7 @@ */ package org.opensearch.sample; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; @@ -44,12 +41,16 @@ import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; -import org.opensearch.sample.actions.create.CreateSampleResourceAction; -import org.opensearch.sample.actions.create.CreateSampleResourceRestAction; -import org.opensearch.sample.actions.create.CreateSampleResourceTransportAction; -import org.opensearch.sample.actions.list.ListSampleResourceAction; -import org.opensearch.sample.actions.list.ListSampleResourceRestAction; -import org.opensearch.sample.actions.list.ListSampleResourceTransportAction; +import org.opensearch.sample.actions.create.CreateResourceAction; +import org.opensearch.sample.actions.create.CreateResourceRestAction; +import org.opensearch.sample.actions.list.ListAccessibleResourcesAction; +import org.opensearch.sample.actions.list.ListAccessibleResourcesRestAction; +import org.opensearch.sample.actions.share.ShareResourceAction; +import org.opensearch.sample.actions.verify.VerifyResourceAccessAction; +import org.opensearch.sample.transport.CreateResourceTransportAction; +import org.opensearch.sample.transport.ListAccessibleResourcesTransportAction; +import org.opensearch.sample.transport.ShareResourceTransportAction; +import org.opensearch.sample.transport.VerifyResourceAccessTransportAction; import org.opensearch.script.ScriptService; import org.opensearch.threadpool.ThreadPool; import org.opensearch.watcher.ResourceWatcherService; @@ -62,7 +63,9 @@ public class SampleResourcePlugin extends Plugin implements ActionPlugin, SystemIndexPlugin, ResourcePlugin { private static final Logger log = LogManager.getLogger(SampleResourcePlugin.class); - public static final String RESOURCE_INDEX_NAME = ".sample_resources"; + public static final String RESOURCE_INDEX_NAME = ".sample_resource_sharing_plugin"; + + public final static Map INDEX_SETTINGS = Map.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); private Client client; @@ -95,14 +98,16 @@ public List getRestHandlers( IndexNameExpressionResolver indexNameExpressionResolver, Supplier nodesInCluster ) { - return List.of(new CreateSampleResourceRestAction(), new ListSampleResourceRestAction()); + return List.of(new CreateResourceRestAction(), new ListAccessibleResourcesRestAction()); } @Override public List> getActions() { return List.of( - new ActionHandler<>(CreateSampleResourceAction.INSTANCE, CreateSampleResourceTransportAction.class), - new ActionHandler<>(ListSampleResourceAction.INSTANCE, ListSampleResourceTransportAction.class) + new ActionHandler<>(CreateResourceAction.INSTANCE, CreateResourceTransportAction.class), + new ActionHandler<>(ListAccessibleResourcesAction.INSTANCE, ListAccessibleResourcesTransportAction.class), + new ActionHandler<>(ShareResourceAction.INSTANCE, ShareResourceTransportAction.class), + new ActionHandler<>(VerifyResourceAccessAction.INSTANCE, VerifyResourceAccessTransportAction.class) ); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java similarity index 67% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java index fce62be629..5ddcc79008 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java @@ -9,22 +9,21 @@ package org.opensearch.sample.actions.create; import org.opensearch.action.ActionType; -import org.opensearch.sample.transport.CreateResourceResponse; /** * Action to create a sample resource */ -public class CreateSampleResourceAction extends ActionType { +public class CreateResourceAction extends ActionType { /** * Create sample resource action instance */ - public static final CreateSampleResourceAction INSTANCE = new CreateSampleResourceAction(); + public static final CreateResourceAction INSTANCE = new CreateResourceAction(); /** * Create sample resource action name */ public static final String NAME = "cluster:admin/sampleresource/create"; - private CreateSampleResourceAction() { + private CreateResourceAction() { super(NAME, CreateResourceResponse::new); } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java similarity index 73% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceRequest.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java index f23735e7f3..b31a4b7f2b 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.transport; +package org.opensearch.sample.actions.create; import java.io.IOException; @@ -19,19 +19,19 @@ /** * Request object for CreateSampleResource transport action */ -public class CreateResourceRequest extends ActionRequest { +public class CreateResourceRequest extends ActionRequest { - private final T resource; + private final Resource resource; /** * Default constructor */ - public CreateResourceRequest(T resource) { + public CreateResourceRequest(Resource resource) { this.resource = resource; } - public CreateResourceRequest(StreamInput in, Reader resourceReader) throws IOException { - this.resource = resourceReader.read(in); + public CreateResourceRequest(StreamInput in) throws IOException { + this.resource = in.readNamedWriteable(Resource.class); } @Override diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java similarity index 96% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceResponse.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java index 12d7671ac4..6b966ed08d 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.transport; +package org.opensearch.sample.actions.create; import java.io.IOException; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java similarity index 75% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceRestAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java index f422835168..86346cc279 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java @@ -17,18 +17,17 @@ import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.RestRequest; import org.opensearch.rest.action.RestToXContentListener; -import org.opensearch.sample.transport.CreateResourceRequest; import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.POST; -public class CreateSampleResourceRestAction extends BaseRestHandler { +public class CreateResourceRestAction extends BaseRestHandler { - public CreateSampleResourceRestAction() {} + public CreateResourceRestAction() {} @Override public List routes() { - return singletonList(new Route(POST, "/_plugins/resource_sharing_example/resource")); + return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/resource")); } @Override @@ -46,9 +45,9 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client String name = (String) source.get("name"); SampleResource resource = new SampleResource(); resource.setName(name); - final CreateResourceRequest createSampleResourceRequest = new CreateResourceRequest<>(resource); + final CreateResourceRequest createSampleResourceRequest = new CreateResourceRequest(resource); return channel -> client.executeLocally( - CreateSampleResourceAction.INSTANCE, + CreateResourceAction.INSTANCE, createSampleResourceRequest, new RestToXContentListener<>(channel) ); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceRequest.java deleted file mode 100644 index a509031b0b..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceRequest.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.create; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.sample.Resource; - -/** - * Request object for CreateSampleResource transport action - */ -public class CreateSampleResourceRequest extends ActionRequest { - - private final Resource resource; - - /** - * Default constructor - */ - public CreateSampleResourceRequest(Resource resource) { - this.resource = resource; - } - - /** - * Constructor with stream input - * @param in the stream input - * @throws IOException IOException - */ - public CreateSampleResourceRequest(final StreamInput in) throws IOException { - this.resource = new SampleResource(in); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - resource.writeTo(out); - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - public Resource getResource() { - return this.resource; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceTransportAction.java deleted file mode 100644 index 53d9817fbc..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceTransportAction.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.create; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.action.support.ActionFilters; -import org.opensearch.client.Client; -import org.opensearch.common.inject.Inject; -import org.opensearch.sample.transport.CreateResourceTransportAction; -import org.opensearch.transport.TransportService; - -import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; - -/** - * Transport action for CreateSampleResource. - */ -public class CreateSampleResourceTransportAction extends CreateResourceTransportAction { - private static final Logger log = LogManager.getLogger(CreateSampleResourceTransportAction.class); - - @Inject - public CreateSampleResourceTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { - super(transportService, actionFilters, nodeClient, CreateSampleResourceAction.NAME, RESOURCE_INDEX_NAME, SampleResource::new); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java index d2528c92be..1566abfe69 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java @@ -47,7 +47,7 @@ public void writeTo(StreamOutput streamOutput) throws IOException { @Override public String getWriteableName() { - return "sampled_resource"; + return "sample_resource"; } public void setName(String name) { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java similarity index 63% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java index 17f50cda30..cc7e4769f6 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java @@ -13,17 +13,17 @@ /** * Action to list sample resources */ -public class ListSampleResourceAction extends ActionType { +public class ListAccessibleResourcesAction extends ActionType { /** * List sample resource action instance */ - public static final ListSampleResourceAction INSTANCE = new ListSampleResourceAction(); + public static final ListAccessibleResourcesAction INSTANCE = new ListAccessibleResourcesAction(); /** * List sample resource action name */ public static final String NAME = "cluster:admin/sampleresource/list"; - private ListSampleResourceAction() { - super(NAME, ListSampleResourceResponse::new); + private ListAccessibleResourcesAction() { + super(NAME, ListAccessibleResourcesResponse::new); } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java similarity index 81% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceRequest.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java index ffadf6abbb..b4c0961774 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java @@ -18,16 +18,16 @@ /** * Request object for ListSampleResource transport action */ -public class ListSampleResourceRequest extends ActionRequest { +public class ListAccessibleResourcesRequest extends ActionRequest { - public ListSampleResourceRequest() {} + public ListAccessibleResourcesRequest() {} /** * Constructor with stream input * @param in the stream input * @throws IOException IOException */ - public ListSampleResourceRequest(final StreamInput in) throws IOException {} + public ListAccessibleResourcesRequest(final StreamInput in) throws IOException {} @Override public void writeTo(final StreamOutput out) throws IOException {} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java new file mode 100644 index 0000000000..47a8f88e4e --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java @@ -0,0 +1,46 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.list; + +import java.io.IOException; +import java.util.List; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +/** + * Response to a ListAccessibleResourcesRequest + */ +public class ListAccessibleResourcesResponse extends ActionResponse implements ToXContentObject { + private final List resourceIds; + + public ListAccessibleResourcesResponse(List resourceIds) { + this.resourceIds = resourceIds; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeStringArray(resourceIds.toArray(new String[0])); + } + + public ListAccessibleResourcesResponse(final StreamInput in) throws IOException { + resourceIds = in.readStringList(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("resource-ids", resourceIds); + builder.endObject(); + return builder; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java similarity index 68% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceRestAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java index 3f01bb5e2c..bb921fce00 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java @@ -18,13 +18,13 @@ import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.GET; -public class ListSampleResourceRestAction extends BaseRestHandler { +public class ListAccessibleResourcesRestAction extends BaseRestHandler { - public ListSampleResourceRestAction() {} + public ListAccessibleResourcesRestAction() {} @Override public List routes() { - return singletonList(new Route(GET, "/_plugins/resource_sharing_example/resource")); + return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/resource")); } @Override @@ -34,10 +34,10 @@ public String getName() { @Override public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { - final ListSampleResourceRequest listSampleResourceRequest = new ListSampleResourceRequest(); + final ListAccessibleResourcesRequest listAccessibleResourcesRequest = new ListAccessibleResourcesRequest(); return channel -> client.executeLocally( - ListSampleResourceAction.INSTANCE, - listSampleResourceRequest, + ListAccessibleResourcesAction.INSTANCE, + listAccessibleResourcesRequest, new RestToXContentListener<>(channel) ); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceTransportAction.java deleted file mode 100644 index ece829fe0d..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceTransportAction.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.list; - -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.client.Client; -import org.opensearch.common.inject.Inject; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.core.action.ActionListener; -import org.opensearch.index.query.MatchAllQueryBuilder; -import org.opensearch.search.builder.SearchSourceBuilder; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -/** - * Transport action for ListSampleResource. - */ -public class ListSampleResourceTransportAction extends HandledTransportAction { - private final TransportService transportService; - private final Client nodeClient; - - @Inject - public ListSampleResourceTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { - super(ListSampleResourceAction.NAME, transportService, actionFilters, ListSampleResourceRequest::new); - this.transportService = transportService; - this.nodeClient = nodeClient; - } - - @Override - protected void doExecute(Task task, ListSampleResourceRequest request, ActionListener listener) { - try (ThreadContext.StoredContext ignore = transportService.getThreadPool().getThreadContext().stashContext()) { - SearchRequest sr = new SearchRequest(".resource-sharing"); - SearchSourceBuilder matchAllQuery = new SearchSourceBuilder(); - matchAllQuery.query(new MatchAllQueryBuilder()); - sr.source(matchAllQuery); - /* Index already exists, ignore and continue */ - ActionListener searchListener = ActionListener.wrap(response -> { - listener.onResponse(new ListSampleResourceResponse(response.toString())); - }, listener::onFailure); - nodeClient.search(sr, searchListener); - } - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.java new file mode 100644 index 0000000000..152caf8c8c --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.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. + */ + +package org.opensearch.sample.actions.share; + +import org.opensearch.action.ActionType; + +public class ShareResourceAction extends ActionType { + /** + * List sample resource action instance + */ + public static final ShareResourceAction INSTANCE = new ShareResourceAction(); + /** + * List sample resource action name + */ + public static final String NAME = "cluster:admin/sampleresource/share"; + + private ShareResourceAction() { + super(NAME, ShareResourceResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java new file mode 100644 index 0000000000..01866fd516 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java @@ -0,0 +1,52 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.share; + +import java.io.IOException; + +import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +public class ShareResourceRequest extends ActionRequest { + + private final String resourceId; + private final ShareWith shareWith; + + public ShareResourceRequest(String resourceId, ShareWith shareWith) { + this.resourceId = resourceId; + this.shareWith = shareWith; + } + + public ShareResourceRequest(StreamInput in) throws IOException { + this.resourceId = in.readString(); + this.shareWith = in.readNamedWriteable(ShareWith.class); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(resourceId); + out.writeNamedWriteable(shareWith); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceId() { + return resourceId; + } + + public ShareWith getShareWith() { + return shareWith; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java similarity index 78% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceResponse.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java index aaf6bfcd3e..a6a85d206d 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListSampleResourceResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.list; +package org.opensearch.sample.actions.share; import java.io.IOException; @@ -16,10 +16,7 @@ import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; -/** - * Response to a ListSampleResourceRequest - */ -public class ListSampleResourceResponse extends ActionResponse implements ToXContentObject { +public class ShareResourceResponse extends ActionResponse implements ToXContentObject { private final String message; /** @@ -27,7 +24,7 @@ public class ListSampleResourceResponse extends ActionResponse implements ToXCon * * @param message The message */ - public ListSampleResourceResponse(String message) { + public ShareResourceResponse(String message) { this.message = message; } @@ -41,7 +38,7 @@ public void writeTo(StreamOutput out) throws IOException { * * @param in the stream input */ - public ListSampleResourceResponse(final StreamInput in) throws IOException { + public ShareResourceResponse(final StreamInput in) throws IOException { message = in.readString(); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java new file mode 100644 index 0000000000..87bc083f2e --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java @@ -0,0 +1,51 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.share; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.GET; + +public class ShareResourceRestAction extends BaseRestHandler { + + public ShareResourceRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/share/{resource_id}")); + } + + @Override + public String getName() { + return "list_sample_resources"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String resourceId = (String) source.get("resource_id"); + ShareWith shareWith = (ShareWith) source.get("share_with"); + final ShareResourceRequest shareResourceRequest = new ShareResourceRequest(resourceId, shareWith); + return channel -> client.executeLocally(ShareResourceAction.INSTANCE, shareResourceRequest, new RestToXContentListener<>(channel)); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java new file mode 100644 index 0000000000..2e57786a13 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.verify; + +import org.opensearch.action.ActionType; + +/** + * Action to verify resource access for current user + */ +public class VerifyResourceAccessAction extends ActionType { + + public static final VerifyResourceAccessAction INSTANCE = new VerifyResourceAccessAction(); + + public static final String NAME = "cluster:admin/sampleresource/verify/resource_access"; + + private VerifyResourceAccessAction() { + super(NAME, VerifyResourceAccessResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java new file mode 100644 index 0000000000..e9b20118db --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java @@ -0,0 +1,69 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.verify; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +public class VerifyResourceAccessRequest extends ActionRequest { + + private final String resourceId; + + private final String sourceIdx; + + private final String scope; + + /** + * Default constructor + */ + public VerifyResourceAccessRequest(String resourceId, String sourceIdx, String scope) { + this.resourceId = resourceId; + this.sourceIdx = sourceIdx; + this.scope = scope; + } + + /** + * Constructor with stream input + * @param in the stream input + * @throws IOException IOException + */ + public VerifyResourceAccessRequest(final StreamInput in) throws IOException { + this.resourceId = in.readString(); + this.sourceIdx = in.readString(); + this.scope = in.readString(); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(resourceId); + out.writeString(sourceIdx); + out.writeString(scope); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceId() { + return resourceId; + } + + public String getSourceIdx() { + return sourceIdx; + } + + public String getScope() { + return scope; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java similarity index 81% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceResponse.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java index 86796bfff5..660ac03f71 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateSampleResourceResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.create; +package org.opensearch.sample.actions.verify; import java.io.IOException; @@ -16,10 +16,7 @@ import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; -/** - * Response to a CreateSampleResourceRequest - */ -public class CreateSampleResourceResponse extends ActionResponse implements ToXContentObject { +public class VerifyResourceAccessResponse extends ActionResponse implements ToXContentObject { private final String message; /** @@ -27,7 +24,7 @@ public class CreateSampleResourceResponse extends ActionResponse implements ToXC * * @param message The message */ - public CreateSampleResourceResponse(String message) { + public VerifyResourceAccessResponse(String message) { this.message = message; } @@ -41,7 +38,7 @@ public void writeTo(StreamOutput out) throws IOException { * * @param in the stream input */ - public CreateSampleResourceResponse(final StreamInput in) throws IOException { + public VerifyResourceAccessResponse(final StreamInput in) throws IOException { message = in.readString(); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java new file mode 100644 index 0000000000..34bfed4e9f --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java @@ -0,0 +1,52 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.verify; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.POST; + +public class VerifyResourceAccessRestAction extends BaseRestHandler { + + public VerifyResourceAccessRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/verify_resource_access")); + } + + @Override + public String getName() { + return "verify_resource_access"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String resourceIdx = (String) source.get("resource_idx"); + String sourceIdx = (String) source.get("source_idx"); + String scope = (String) source.get("scope"); + + // final CreateResourceRequest createSampleResourceRequest = new CreateResourceRequest<>(resource); + return channel -> client.executeLocally(VerifyResourceAccessAction.INSTANCE, null, new RestToXContentListener<>(channel)); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java index 5e2eb6d723..d3bb8f19b2 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java @@ -17,8 +17,6 @@ import org.opensearch.accesscontrol.resources.ResourceService; import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.accesscontrol.resources.ShareWith; -import org.opensearch.action.admin.indices.create.CreateIndexRequest; -import org.opensearch.action.admin.indices.create.CreateIndexResponse; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.index.IndexResponse; import org.opensearch.action.support.ActionFilters; @@ -27,10 +25,11 @@ import org.opensearch.client.Client; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; -import org.opensearch.core.common.io.stream.Writeable; import org.opensearch.core.xcontent.ToXContent; import org.opensearch.sample.Resource; import org.opensearch.sample.SampleResourcePlugin; +import org.opensearch.sample.actions.create.CreateResourceRequest; +import org.opensearch.sample.actions.create.CreateResourceResponse; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; @@ -39,9 +38,7 @@ /** * Transport action for CreateSampleResource. */ -public class CreateResourceTransportAction extends HandledTransportAction< - CreateResourceRequest, - CreateResourceResponse> { +public class CreateResourceTransportAction extends HandledTransportAction { private static final Logger log = LogManager.getLogger(CreateResourceTransportAction.class); private final TransportService transportService; @@ -53,31 +50,25 @@ public CreateResourceTransportAction( ActionFilters actionFilters, Client nodeClient, String actionName, - String resourceIndex, - Writeable.Reader resourceReader + String resourceIndex ) { - super(actionName, transportService, actionFilters, (in) -> new CreateResourceRequest(in, resourceReader)); + super(actionName, transportService, actionFilters, (in) -> new CreateResourceRequest(in)); this.transportService = transportService; this.nodeClient = nodeClient; this.resourceIndex = resourceIndex; } @Override - protected void doExecute(Task task, CreateResourceRequest request, ActionListener listener) { + protected void doExecute(Task task, CreateResourceRequest request, ActionListener listener) { try (ThreadContext.StoredContext ignore = transportService.getThreadPool().getThreadContext().stashContext()) { - CreateIndexRequest cir = new CreateIndexRequest(resourceIndex); - ActionListener cirListener = ActionListener.wrap( - response -> { createResource(request, listener); }, - (failResponse) -> { - /* Index already exists, ignore and continue */ - createResource(request, listener); - } - ); - nodeClient.admin().indices().create(cir, cirListener); + createResource(request, listener); + listener.onResponse(new CreateResourceResponse("Resource " + request.getResource() + " created successfully.")); + } catch (Exception e) { + listener.onFailure(e); } } - private void createResource(CreateResourceRequest request, ActionListener listener) { + private void createResource(CreateResourceRequest request, ActionListener listener) { Resource sample = request.getResource(); try { IndexRequest ir = nodeClient.prepareIndex(resourceIndex) @@ -104,5 +95,4 @@ private static ActionListener getIndexResponseActionListener(Acti }, listener::onFailure); } - // TODO add delete implementation as a separate transport action } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java new file mode 100644 index 0000000000..c4734ad928 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package org.opensearch.sample.transport; + +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.accesscontrol.resources.ResourceService; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.SampleResourcePlugin; +import org.opensearch.sample.actions.list.ListAccessibleResourcesAction; +import org.opensearch.sample.actions.list.ListAccessibleResourcesRequest; +import org.opensearch.sample.actions.list.ListAccessibleResourcesResponse; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; + +/** + * Transport action for ListSampleResource. + */ +public class ListAccessibleResourcesTransportAction extends HandledTransportAction< + ListAccessibleResourcesRequest, + ListAccessibleResourcesResponse> { + private static final Logger log = LogManager.getLogger(ListAccessibleResourcesTransportAction.class); + + @Inject + public ListAccessibleResourcesTransportAction(TransportService transportService, ActionFilters actionFilters) { + super(ListAccessibleResourcesAction.NAME, transportService, actionFilters, ListAccessibleResourcesRequest::new); + } + + @Override + protected void doExecute(Task task, ListAccessibleResourcesRequest request, ActionListener listener) { + try { + ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); + List resourceIds = rs.getResourceAccessControlPlugin().listAccessibleResourcesForPlugin(RESOURCE_INDEX_NAME); + log.info("Successfully fetched accessible resources for current user"); + listener.onResponse(new ListAccessibleResourcesResponse(resourceIds)); + } catch (Exception e) { + log.info("Failed to list accessible resources for current user: ", e); + listener.onFailure(e); + } + + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java new file mode 100644 index 0000000000..0dfab3fade --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java @@ -0,0 +1,77 @@ +/* + * 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. + */ + +package org.opensearch.sample.transport; + +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.accesscontrol.resources.ResourceService; +import org.opensearch.accesscontrol.resources.ResourceSharing; +import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.client.Client; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.SampleResourcePlugin; +import org.opensearch.sample.actions.share.ShareResourceRequest; +import org.opensearch.sample.actions.share.ShareResourceResponse; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; + +/** + * Transport action for CreateSampleResource. + */ +public class ShareResourceTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(ShareResourceTransportAction.class); + + private final TransportService transportService; + private final Client nodeClient; + private final String resourceIndex; + + public ShareResourceTransportAction( + TransportService transportService, + ActionFilters actionFilters, + Client nodeClient, + String actionName, + String resourceIndex + ) { + super(actionName, transportService, actionFilters, ShareResourceRequest::new); + this.transportService = transportService; + this.nodeClient = nodeClient; + this.resourceIndex = resourceIndex; + } + + @Override + protected void doExecute(Task task, ShareResourceRequest request, ActionListener listener) { + try (ThreadContext.StoredContext ignore = transportService.getThreadPool().getThreadContext().stashContext()) { + shareResource(request); + listener.onResponse(new ShareResourceResponse("Resource " + request.getResourceId() + " shared successfully.")); + } catch (Exception e) { + listener.onFailure(e); + } + } + + private void shareResource(ShareResourceRequest request) { + try { + ShareWith shareWith = new ShareWith(List.of()); + ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); + ResourceSharing sharing = rs.getResourceAccessControlPlugin() + .shareWith(request.getResourceId(), RESOURCE_INDEX_NAME, shareWith); + log.info("Shared resource : {} with {}", request.getResourceId(), sharing.toString()); + } catch (Exception e) { + log.info("Failed to share resource {}", request.getResourceId(), e); + throw e; + } + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java new file mode 100644 index 0000000000..947dcec59e --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java @@ -0,0 +1,58 @@ +/* + * 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. + */ + +package org.opensearch.sample.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.accesscontrol.resources.ResourceService; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.SampleResourcePlugin; +import org.opensearch.sample.actions.verify.VerifyResourceAccessAction; +import org.opensearch.sample.actions.verify.VerifyResourceAccessRequest; +import org.opensearch.sample.actions.verify.VerifyResourceAccessResponse; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +public class VerifyResourceAccessTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(VerifyResourceAccessTransportAction.class); + + @Inject + public VerifyResourceAccessTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { + super(VerifyResourceAccessAction.NAME, transportService, actionFilters, VerifyResourceAccessRequest::new); + } + + @Override + protected void doExecute(Task task, VerifyResourceAccessRequest request, ActionListener listener) { + try { + ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); + boolean hasRequestedScopeAccess = rs.getResourceAccessControlPlugin() + .hasPermission(request.getResourceId(), request.getSourceIdx(), request.getScope()); + + StringBuilder sb = new StringBuilder(); + sb.append("User does"); + sb.append(hasRequestedScopeAccess ? " " : " not "); + sb.append("have requested scope "); + sb.append(request.getScope()); + sb.append(" access to "); + sb.append(request.getResourceId()); + + log.info(sb.toString()); + listener.onResponse(new VerifyResourceAccessResponse(sb.toString())); + } catch (Exception e) { + log.info("Failed to check user permissions for resource {}", request.getResourceId(), e); + listener.onFailure(e); + } + } + +} diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 142c6b67da..9c26811dc9 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -17,6 +17,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.accesscontrol.resources.CreatedBy; import org.opensearch.accesscontrol.resources.EntityType; import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.accesscontrol.resources.ShareWith; @@ -61,10 +62,11 @@ public boolean hasPermission(String resourceId, String systemIndexName, String s public ResourceSharing shareWith(String resourceId, String systemIndexName, ShareWith shareWith) { final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith); + LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user, shareWith); // TODO add concrete implementation - return null; + CreatedBy c = new CreatedBy("", null); + return new ResourceSharing(systemIndexName, resourceId, c, shareWith); } public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> revokeAccess) { From 81216f17d6b61ef11b4c393362b92ecd2b861477 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 4 Oct 2024 18:24:05 -0400 Subject: [PATCH 012/201] Updates method references to conform to core Signed-off-by: Darshit Chanpura --- .../sample/transport/CreateResourceTransportAction.java | 2 ++ .../ListAccessibleResourcesTransportAction.java | 2 +- .../sample/transport/ShareResourceTransportAction.java | 2 ++ .../opensearch/security/OpenSearchSecurityPlugin.java | 9 ++------- .../security/resources/ResourceAccessHandler.java | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java index d3bb8f19b2..44d18ef846 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java @@ -23,6 +23,7 @@ import org.opensearch.action.support.HandledTransportAction; import org.opensearch.action.support.WriteRequest; import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; import org.opensearch.core.xcontent.ToXContent; @@ -45,6 +46,7 @@ public class CreateResourceTransportAction extends HandledTransportAction listener) { try { ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - List resourceIds = rs.getResourceAccessControlPlugin().listAccessibleResourcesForPlugin(RESOURCE_INDEX_NAME); + List resourceIds = rs.getResourceAccessControlPlugin().listAccessibleResourcesInPlugin(RESOURCE_INDEX_NAME); log.info("Successfully fetched accessible resources for current user"); listener.onResponse(new ListAccessibleResourcesResponse(resourceIds)); } catch (Exception e) { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java index 0dfab3fade..ff1541773e 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java @@ -19,6 +19,7 @@ import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; import org.opensearch.sample.SampleResourcePlugin; @@ -39,6 +40,7 @@ public class ShareResourceTransportAction extends HandledTransportAction> listAccessibleResources() { - return this.resourceAccessHandler.listAccessibleResources(); - } - - @Override - public List listAccessibleResourcesForPlugin(String systemIndexName) { - return this.resourceAccessHandler.listAccessibleResourcesForPlugin(systemIndexName); + public List listAccessibleResourcesInPlugin(String systemIndexName) { + return this.resourceAccessHandler.listAccessibleResourcesInPlugin(systemIndexName); } @Override diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 9c26811dc9..838785ee7f 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -44,7 +44,7 @@ public Map> listAccessibleResources() { return Map.of(); } - public List listAccessibleResourcesForPlugin(String systemIndex) { + public List listAccessibleResourcesInPlugin(String systemIndex) { final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Listing accessible resource within a system index {} for : {}", systemIndex, user.getName()); From 1e33dad85da54f7074a2f50715006fd7e30a5e57 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 4 Oct 2024 18:58:31 -0400 Subject: [PATCH 013/201] Fixes compile errors Signed-off-by: Darshit Chanpura --- .../actions/create/CreateResourceAction.java | 2 +- .../list/ListAccessibleResourcesAction.java | 2 +- .../actions/share/ShareResourceAction.java | 2 +- .../verify/VerifyResourceAccessAction.java | 2 +- .../CreateResourceTransportAction.java | 16 +++++--------- .../ShareResourceTransportAction.java | 22 ++++--------------- 6 files changed, 13 insertions(+), 33 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java index 5ddcc79008..e7c02278ab 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java @@ -21,7 +21,7 @@ public class CreateResourceAction extends ActionType { /** * Create sample resource action name */ - public static final String NAME = "cluster:admin/sampleresource/create"; + public static final String NAME = "cluster:admin/sample-resource-plugin/create"; private CreateResourceAction() { super(NAME, CreateResourceResponse::new); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java index cc7e4769f6..b4e9e29e22 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java @@ -21,7 +21,7 @@ public class ListAccessibleResourcesAction extends ActionType { /** * List sample resource action name */ - public static final String NAME = "cluster:admin/sampleresource/share"; + public static final String NAME = "cluster:admin/sample-resource-plugin/share"; private ShareResourceAction() { super(NAME, ShareResourceResponse::new); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java index 2e57786a13..1378d561f5 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java @@ -17,7 +17,7 @@ public class VerifyResourceAccessAction extends ActionType new CreateResourceRequest(in)); + public CreateResourceTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { + super(CreateResourceAction.NAME, transportService, actionFilters, CreateResourceRequest::new); this.transportService = transportService; this.nodeClient = nodeClient; - this.resourceIndex = resourceIndex; } @Override @@ -73,7 +67,7 @@ protected void doExecute(Task task, CreateResourceRequest request, ActionListene private void createResource(CreateResourceRequest request, ActionListener listener) { Resource sample = request.getResource(); try { - IndexRequest ir = nodeClient.prepareIndex(resourceIndex) + IndexRequest ir = nodeClient.prepareIndex(RESOURCE_INDEX_NAME) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) .setSource(sample.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) .request(); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java index ff1541773e..ccbfc31b78 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java @@ -18,11 +18,10 @@ import org.opensearch.accesscontrol.resources.ShareWith; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.client.Client; import org.opensearch.common.inject.Inject; -import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; import org.opensearch.sample.SampleResourcePlugin; +import org.opensearch.sample.actions.share.ShareResourceAction; import org.opensearch.sample.actions.share.ShareResourceRequest; import org.opensearch.sample.actions.share.ShareResourceResponse; import org.opensearch.tasks.Task; @@ -36,27 +35,14 @@ public class ShareResourceTransportAction extends HandledTransportAction { private static final Logger log = LogManager.getLogger(ShareResourceTransportAction.class); - private final TransportService transportService; - private final Client nodeClient; - private final String resourceIndex; - @Inject - public ShareResourceTransportAction( - TransportService transportService, - ActionFilters actionFilters, - Client nodeClient, - String actionName, - String resourceIndex - ) { - super(actionName, transportService, actionFilters, ShareResourceRequest::new); - this.transportService = transportService; - this.nodeClient = nodeClient; - this.resourceIndex = resourceIndex; + public ShareResourceTransportAction(TransportService transportService, ActionFilters actionFilters) { + super(ShareResourceAction.NAME, transportService, actionFilters, ShareResourceRequest::new); } @Override protected void doExecute(Task task, ShareResourceRequest request, ActionListener listener) { - try (ThreadContext.StoredContext ignore = transportService.getThreadPool().getThreadContext().stashContext()) { + try { shareResource(request); listener.onResponse(new ShareResourceResponse("Resource " + request.getResourceId() + " shared successfully.")); } catch (Exception e) { From a671cc16a7ff47b351509c2cd31c86ac386cb264 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 14 Oct 2024 17:12:55 -0400 Subject: [PATCH 014/201] Fixes some names and method implementations Signed-off-by: Darshit Chanpura --- .../org/opensearch/sample/SampleResourcePlugin.java | 9 ++++++++- .../org/opensearch/sample/SampleResourceScope.java | 2 +- .../sample/actions/share/ShareResourceRestAction.java | 2 +- .../transport/CreateResourceTransportAction.java | 11 ++++++++++- .../services/org.opensearch.plugins.ResourcePlugin | 1 + 5 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index abc9ed4de7..a96a3d52ff 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -46,7 +46,9 @@ import org.opensearch.sample.actions.list.ListAccessibleResourcesAction; import org.opensearch.sample.actions.list.ListAccessibleResourcesRestAction; import org.opensearch.sample.actions.share.ShareResourceAction; +import org.opensearch.sample.actions.share.ShareResourceRestAction; import org.opensearch.sample.actions.verify.VerifyResourceAccessAction; +import org.opensearch.sample.actions.verify.VerifyResourceAccessRestAction; import org.opensearch.sample.transport.CreateResourceTransportAction; import org.opensearch.sample.transport.ListAccessibleResourcesTransportAction; import org.opensearch.sample.transport.ShareResourceTransportAction; @@ -98,7 +100,12 @@ public List getRestHandlers( IndexNameExpressionResolver indexNameExpressionResolver, Supplier nodesInCluster ) { - return List.of(new CreateResourceRestAction(), new ListAccessibleResourcesRestAction()); + return List.of( + new CreateResourceRestAction(), + new ListAccessibleResourcesRestAction(), + new VerifyResourceAccessRestAction(), + new ShareResourceRestAction() + ); } @Override diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java index 2784de45b7..90df0d3764 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java @@ -17,7 +17,7 @@ * This class demonstrates a sample implementation of Basic Access Scopes to fit each plugin's use-case. * The plugin then uses this scope when seeking access evaluation for a user on a particular resource. */ -enum SampleResourceScope implements ResourceAccessScope { +public enum SampleResourceScope implements ResourceAccessScope { SAMPLE_FULL_ACCESS("sample_full_access"); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java index 87bc083f2e..347fb49e68 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java @@ -33,7 +33,7 @@ public List routes() { @Override public String getName() { - return "list_sample_resources"; + return "share_sample_resources"; } @Override diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java index 985d80b919..2de452a5de 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java @@ -17,6 +17,7 @@ import org.opensearch.accesscontrol.resources.ResourceService; import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.accesscontrol.resources.SharedWithScope; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.index.IndexResponse; import org.opensearch.action.support.ActionFilters; @@ -29,6 +30,7 @@ import org.opensearch.core.xcontent.ToXContent; import org.opensearch.sample.Resource; import org.opensearch.sample.SampleResourcePlugin; +import org.opensearch.sample.SampleResourceScope; import org.opensearch.sample.actions.create.CreateResourceAction; import org.opensearch.sample.actions.create.CreateResourceRequest; import org.opensearch.sample.actions.create.CreateResourceResponse; @@ -60,6 +62,7 @@ protected void doExecute(Task task, CreateResourceRequest request, ActionListene createResource(request, listener); listener.onResponse(new CreateResourceResponse("Resource " + request.getResource() + " created successfully.")); } catch (Exception e) { + log.info("Failed to create resource", e); listener.onFailure(e); } } @@ -82,7 +85,13 @@ private void createResource(CreateResourceRequest request, ActionListener getIndexResponseActionListener(ActionListener listener) { - ShareWith shareWith = new ShareWith(List.of()); + SharedWithScope.SharedWithPerScope sharedWithPerScope = new SharedWithScope.SharedWithPerScope( + List.of(), + List.of(), + List.of() + ); + SharedWithScope sharedWithScope = new SharedWithScope(SampleResourceScope.SAMPLE_FULL_ACCESS.getName(), sharedWithPerScope); + ShareWith shareWith = new ShareWith(List.of(sharedWithScope)); return ActionListener.wrap(idxResponse -> { log.info("Created resource: {}", idxResponse.toString()); ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); diff --git a/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin b/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin new file mode 100644 index 0000000000..1ca89eaf74 --- /dev/null +++ b/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin @@ -0,0 +1 @@ +org.opensearch.sample.SampleResourcePlugin \ No newline at end of file From 47b73da2bc522c462d9db6c3ad7acbeff0c1caf9 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 14 Oct 2024 17:15:13 -0400 Subject: [PATCH 015/201] Adds few concrete method implementations in security plugin Signed-off-by: Darshit Chanpura --- .../CreateResourceTransportAction.java | 6 +- .../security/OpenSearchSecurityPlugin.java | 31 +++- .../resources/ResourceAccessHandler.java | 159 +++++++++++++++--- .../ResourceManagementRepository.java | 16 +- .../ResourceSharingIndexHandler.java | 97 ++++++----- .../ResourceSharingIndexListener.java | 23 ++- 6 files changed, 252 insertions(+), 80 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java index 2de452a5de..8bff7b44a3 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java @@ -85,11 +85,7 @@ private void createResource(CreateResourceRequest request, ActionListener getIndexResponseActionListener(ActionListener listener) { - SharedWithScope.SharedWithPerScope sharedWithPerScope = new SharedWithScope.SharedWithPerScope( - List.of(), - List.of(), - List.of() - ); + SharedWithScope.SharedWithPerScope sharedWithPerScope = new SharedWithScope.SharedWithPerScope(List.of(), List.of(), List.of()); SharedWithScope sharedWithScope = new SharedWithScope(SampleResourceScope.SAMPLE_FULL_ACCESS.getName(), sharedWithPerScope); ShareWith shareWith = new ShareWith(List.of(sharedWithScope)); return ActionListener.wrap(idxResponse -> { diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index e7f013d936..e8b2e45cd4 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -69,6 +69,7 @@ import org.opensearch.SpecialPermission; import org.opensearch.Version; import org.opensearch.accesscontrol.resources.EntityType; +import org.opensearch.accesscontrol.resources.ResourceService; import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.accesscontrol.resources.ShareWith; import org.opensearch.action.ActionRequest; @@ -119,11 +120,13 @@ import org.opensearch.indices.IndicesService; import org.opensearch.indices.SystemIndexDescriptor; import org.opensearch.plugins.ClusterPlugin; +import org.opensearch.plugins.ExtensiblePlugin; import org.opensearch.plugins.ExtensionAwarePlugin; import org.opensearch.plugins.IdentityPlugin; import org.opensearch.plugins.MapperPlugin; import org.opensearch.plugins.Plugin; import org.opensearch.plugins.ResourceAccessControlPlugin; +import org.opensearch.plugins.ResourcePlugin; import org.opensearch.plugins.SecureHttpTransportSettingsProvider; import org.opensearch.plugins.SecureSettingsFactory; import org.opensearch.plugins.SecureTransportSettingsProvider; @@ -179,6 +182,7 @@ import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resources.ResourceAccessHandler; import org.opensearch.security.resources.ResourceManagementRepository; +import org.opensearch.security.resources.ResourceSharingIndexHandler; import org.opensearch.security.resources.ResourceSharingIndexListener; import org.opensearch.security.rest.DashboardsInfoAction; import org.opensearch.security.rest.SecurityConfigUpdateAction; @@ -237,10 +241,11 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin implements ClusterPlugin, MapperPlugin, + IdentityPlugin, + ResourceAccessControlPlugin, // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings ExtensionAwarePlugin, - IdentityPlugin, - ResourceAccessControlPlugin + ExtensiblePlugin // CS-ENFORCE-SINGLE { @@ -845,6 +850,20 @@ public void onQueryPhase(SearchContext searchContext, long tookInNanos) { } } + // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions manager used to allow/disallow TLS connections to extensions + @Override + public void loadExtensions(ExtensionLoader loader) { + + log.info("Loading resource plugins"); + for (ResourcePlugin resourcePlugin : loader.loadExtensions(ResourcePlugin.class)) { + String resourceIndex = resourcePlugin.getResourceIndex(); + + this.indicesToListen.add(resourceIndex); + log.info("Loaded resource plugin: {}, index: {}", resourcePlugin, resourceIndex); + } + } + // CS-ENFORCE-SINGLE + @Override public List getActionFilters() { List filters = new ArrayList<>(1); @@ -1209,9 +1228,11 @@ public Collection createComponents( e.subscribeForChanges(dcf); } - resourceAccessHandler = new ResourceAccessHandler(threadPool); + final var resourceSharingIndex = ConfigConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; + ResourceSharingIndexHandler rsIndexHandler = new ResourceSharingIndexHandler(resourceSharingIndex, localClient, threadPool); + resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns); - rmr = ResourceManagementRepository.create(settings, threadPool, localClient); + rmr = ResourceManagementRepository.create(settings, threadPool, localClient, rsIndexHandler); components.add(adminDns); components.add(cr); @@ -2087,6 +2108,7 @@ public void onNodeStarted(DiscoveryNode localNode) { if (!SSLConfig.isSslOnlyMode() && !client && !disabled && !useClusterStateToInitSecurityConfig(settings)) { cr.initOnNodeStart(); } + // create resource sharing index if absent rmr.createResourceSharingIndexIfAbsent(); final Set securityModules = ReflectionHelper.getModulesLoaded(); @@ -2226,6 +2248,7 @@ public static class GuiceHolder implements LifecycleComponent { private static RemoteClusterService remoteClusterService; private static IndicesService indicesService; private static PitService pitService; + private static ResourceService resourceService; // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions manager used to allow/disallow TLS connections to extensions private static ExtensionsManager extensionsManager; diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 838785ee7f..32fa077e71 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -11,8 +11,11 @@ package org.opensearch.security.resources; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -21,7 +24,9 @@ import org.opensearch.accesscontrol.resources.EntityType; import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.accesscontrol.resources.SharedWithScope; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; @@ -30,67 +35,177 @@ public class ResourceAccessHandler { private static final Logger LOGGER = LogManager.getLogger(ResourceAccessHandler.class); private final ThreadContext threadContext; - - public ResourceAccessHandler(final ThreadPool threadPool) { + private final ResourceSharingIndexHandler resourceSharingIndexHandler; + private final AdminDNs adminDNs; + + public ResourceAccessHandler( + final ThreadPool threadPool, + final ResourceSharingIndexHandler resourceSharingIndexHandler, + AdminDNs adminDns + ) { super(); this.threadContext = threadPool.getThreadContext(); - } - - public Map> listAccessibleResources() { - final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - LOGGER.info("Listing accessible resource for: {}", user.getName()); - - // TODO add concrete implementation - return Map.of(); + this.resourceSharingIndexHandler = resourceSharingIndexHandler; + this.adminDNs = adminDns; } public List listAccessibleResourcesInPlugin(String systemIndex) { final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + if (user == null) { + LOGGER.info("Unable to fetch user details "); + return Collections.emptyList(); + } + LOGGER.info("Listing accessible resource within a system index {} for : {}", systemIndex, user.getName()); - // TODO add concrete implementation - return List.of(); + // TODO check if user is admin, if yes all resources should be accessible + if (adminDNs.isAdmin(user)) { + return loadAllResources(systemIndex); + } + + Set result = new HashSet<>(); + + // 0. Own resources + result.addAll(loadOwnResources(systemIndex, user.getName())); + + // 1. By username + result.addAll(loadSharedWithResources(systemIndex, Set.of(user.getName()), "users")); + + // 2. By roles + Set roles = user.getSecurityRoles(); + result.addAll(loadSharedWithResources(systemIndex, roles, "roles")); + + // 3. By backend_roles + Set backendRoles = user.getRoles(); + result.addAll(loadSharedWithResources(systemIndex, backendRoles, "backend_roles")); + + return result.stream().toList(); } public boolean hasPermission(String resourceId, String systemIndexName, String scope) { final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Checking if {} has {} permission to resource {}", user.getName(), scope, resourceId); - // TODO add concrete implementation + Set userRoles = user.getSecurityRoles(); + Set userBackendRoles = user.getRoles(); + + ResourceSharing document = this.resourceSharingIndexHandler.fetchDocumentById(systemIndexName, resourceId); + if (document == null) { + LOGGER.warn("Resource {} not found in index {}", resourceId, systemIndexName); + return false; // If the document doesn't exist, no permissions can be granted + } + + if (isSharedWithEveryone(document) + || isOwnerOfResource(document, user.getName()) + || isSharedWithUser(document, user.getName(), scope) + || isSharedWithGroup(document, userRoles, scope) + || isSharedWithGroup(document, userBackendRoles, scope)) { + LOGGER.info("User {} has {} access to {}", user.getName(), scope, resourceId); + return true; + } + + LOGGER.info("User {} does not have {} access to {} ", user.getName(), scope, resourceId); return false; } public ResourceSharing shareWith(String resourceId, String systemIndexName, ShareWith shareWith) { final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user, shareWith); + LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user, shareWith.toString()); - // TODO add concrete implementation - CreatedBy c = new CreatedBy("", null); - return new ResourceSharing(systemIndexName, resourceId, c, shareWith); + // TODO fix this to fetch user-name correctly, need to hydrate user context since context might have been stashed. + // (persistentHeader?) + CreatedBy createdBy = new CreatedBy("", ""); + return this.resourceSharingIndexHandler.updateResourceSharingInfo(resourceId, systemIndexName, createdBy, shareWith); } public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> revokeAccess) { final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Revoking access to resource {} created by {} for {}", resourceId, user.getName(), revokeAccess); - // TODO add concrete implementation - return null; + return this.resourceSharingIndexHandler.revokeAccess(resourceId, systemIndexName, revokeAccess); } public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Deleting resource sharing record for resource {} in {} created by {}", resourceId, systemIndexName, user.getName()); - // TODO add concrete implementation - return false; + ResourceSharing document = this.resourceSharingIndexHandler.fetchDocumentById(systemIndexName, resourceId); + if (document == null) { + LOGGER.info("Document {} does not exist in index {}", resourceId, systemIndexName); + return false; + } + if (!(adminDNs.isAdmin(user) || isOwnerOfResource(document, user.getName()))) { + LOGGER.info("User {} does not have access to delete the record {} ", user.getName(), resourceId); + return false; + } + return this.resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, systemIndexName); } public boolean deleteAllResourceSharingRecordsForCurrentUser() { final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Deleting all resource sharing records for resource {}", user.getName()); - // TODO add concrete implementation + return this.resourceSharingIndexHandler.deleteAllRecordsForUser(user.getName()); + } + + // Helper methods + + private List loadAllResources(String systemIndex) { + return this.resourceSharingIndexHandler.fetchAllDocuments(systemIndex); + } + + private List loadOwnResources(String systemIndex, String username) { + // TODO check if this magic variable can be replaced + return this.resourceSharingIndexHandler.fetchDocumentsByField(systemIndex, "created_by.user", username); + } + + private List loadSharedWithResources(String systemIndex, Set accessWays, String shareWithType) { + return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(systemIndex, accessWays, shareWithType); + } + + private boolean isOwnerOfResource(ResourceSharing document, String userName) { + return document.getCreatedBy() != null && document.getCreatedBy().getUser().equals(userName); + } + + private boolean isSharedWithUser(ResourceSharing document, String userName, String scope) { + return checkSharing(document, "users", userName, scope); + } + + private boolean isSharedWithGroup(ResourceSharing document, Set roles, String scope) { + for (String role : roles) { + if (checkSharing(document, "roles", role, scope)) { + return true; + } + } return false; } + private boolean isSharedWithEveryone(ResourceSharing document) { + return document.getShareWith() != null + && document.getShareWith().getSharedWithScopes().stream().anyMatch(sharedWithScope -> sharedWithScope.getScope().equals("*")); + } + + private boolean checkSharing(ResourceSharing document, String sharingType, String identifier, String scope) { + if (document.getShareWith() == null) { + return false; + } + + return document.getShareWith() + .getSharedWithScopes() + .stream() + .filter(sharedWithScope -> sharedWithScope.getScope().equals(scope)) + .findFirst() + .map(sharedWithScope -> { + SharedWithScope.SharedWithPerScope scopePermissions = sharedWithScope.getSharedWithPerScope(); + + return switch (sharingType) { + case "users" -> scopePermissions.getUsers().contains(identifier); + case "roles" -> scopePermissions.getRoles().contains(identifier); + case "backend_roles" -> scopePermissions.getBackendRoles().contains(identifier); + default -> false; + }; + }) + .orElse(false); // Return false if no matching scope is found + } + } diff --git a/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java b/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java index 7e347a331d..da3678728d 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java +++ b/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java @@ -17,7 +17,6 @@ import org.opensearch.client.Client; import org.opensearch.common.settings.Settings; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; public class ResourceManagementRepository { @@ -40,13 +39,14 @@ protected ResourceManagementRepository( this.resourceSharingIndexHandler = resourceSharingIndexHandler; } - public static ResourceManagementRepository create(Settings settings, final ThreadPool threadPool, Client client) { - final var resourceSharingIndex = ConfigConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; - return new ResourceManagementRepository( - threadPool, - client, - new ResourceSharingIndexHandler(resourceSharingIndex, settings, client, threadPool) - ); + public static ResourceManagementRepository create( + Settings settings, + final ThreadPool threadPool, + Client client, + ResourceSharingIndexHandler resourceSharingIndexHandler + ) { + + return new ResourceManagementRepository(threadPool, client, resourceSharingIndexHandler); } public void createResourceSharingIndexIfAbsent() { diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index b6f4b02ade..b175ad53d0 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -10,13 +10,16 @@ package org.opensearch.security.resources; import java.io.IOException; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.accesscontrol.resources.CreatedBy; +import org.opensearch.accesscontrol.resources.EntityType; import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.accesscontrol.resources.ShareWith; import org.opensearch.action.admin.indices.create.CreateIndexRequest; @@ -25,7 +28,6 @@ import org.opensearch.action.index.IndexResponse; import org.opensearch.action.support.WriteRequest; import org.opensearch.client.Client; -import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; import org.opensearch.core.xcontent.ToXContent; @@ -39,38 +41,20 @@ public class ResourceSharingIndexHandler { private static final Logger LOGGER = LogManager.getLogger(ResourceSharingIndexHandler.class); - private final Settings settings; - private final Client client; private final String resourceSharingIndex; private final ThreadPool threadPool; - public ResourceSharingIndexHandler(final String indexName, final Settings settings, final Client client, ThreadPool threadPool) { + public ResourceSharingIndexHandler(final String indexName, final Client client, ThreadPool threadPool) { this.resourceSharingIndex = indexName; - this.settings = settings; this.client = client; this.threadPool = threadPool; } public final static Map INDEX_SETTINGS = Map.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); - public void createIndex(ActionListener listener) { - try (final ThreadContext.StoredContext threadContext = client.threadPool().getThreadContext().stashContext()) { - client.admin() - .indices() - .create( - new CreateIndexRequest(resourceSharingIndex).settings(INDEX_SETTINGS).waitForActiveShards(1), - ActionListener.runBefore(ActionListener.wrap(r -> { - if (r.isAcknowledged()) { - listener.onResponse(true); - } else listener.onFailure(new SecurityException("Couldn't create resource sharing index " + resourceSharingIndex)); - }, listener::onFailure), threadContext::restore) - ); - } - } - public void createResourceSharingIndexIfAbsent(Callable callable) { // TODO: Once stashContext is replaced with switchContext this call will have to be modified try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { @@ -91,14 +75,10 @@ public void createResourceSharingIndexIfAbsent(Callable callable) { } } - public boolean indexResourceSharing( - String resourceId, - String resourceIndex, - CreatedBy createdBy, - ShareWith shareWith, - ActionListener listener - ) throws IOException { - createResourceSharingIndexIfAbsent(() -> { + public boolean indexResourceSharing(String resourceId, String resourceIndex, CreatedBy createdBy, ShareWith shareWith) + throws IOException { + + try { ResourceSharing entry = new ResourceSharing(resourceIndex, resourceId, createdBy, shareWith); IndexRequest ir = client.prepareIndex(resourceSharingIndex) @@ -108,17 +88,58 @@ public boolean indexResourceSharing( LOGGER.info("Index Request: {}", ir.toString()); - ActionListener irListener = ActionListener.wrap(idxResponse -> { - LOGGER.info("Created {} entry.", resourceSharingIndex); - listener.onResponse(idxResponse); - }, (failResponse) -> { - LOGGER.error(failResponse.getMessage()); - LOGGER.info("Failed to create {} entry.", resourceSharingIndex); - listener.onFailure(failResponse); - }); + ActionListener irListener = ActionListener.wrap( + idxResponse -> { LOGGER.info("Created {} entry.", resourceSharingIndex); }, + (failResponse) -> { + LOGGER.error(failResponse.getMessage()); + LOGGER.info("Failed to create {} entry.", resourceSharingIndex); + } + ); client.index(ir, irListener); - return null; - }); + } catch (Exception e) { + LOGGER.info("Failed to create {} entry.", resourceSharingIndex, e); + return false; + } return true; } + + public List fetchDocumentsByField(String systemIndex, String field, String value) { + LOGGER.info("Fetching documents from index: {}, where {} = {}", systemIndex, field, value); + + return List.of(); + } + + public List fetchAllDocuments(String systemIndex) { + LOGGER.info("Fetching all documents from index: {}", systemIndex); + return List.of(); + } + + public List fetchDocumentsForAllScopes(String systemIndex, Set accessWays, String shareWithType) { + return List.of(); + } + + public ResourceSharing fetchDocumentById(String systemIndexName, String resourceId) { + return null; + } + + public ResourceSharing updateResourceSharingInfo(String resourceId, String systemIndexName, CreatedBy createdBy, ShareWith shareWith) { + try { + boolean success = indexResourceSharing(resourceId, systemIndexName, createdBy, shareWith); + return success ? new ResourceSharing(resourceId, systemIndexName, createdBy, shareWith) : null; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> revokeAccess) { + return null; + } + + public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { + return false; + } + + public boolean deleteAllRecordsForUser(String name) { + return false; + } } diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java index 7a2af9f3bd..d6b1180d46 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -8,13 +8,17 @@ package org.opensearch.security.resources; +import java.io.IOException; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.accesscontrol.resources.CreatedBy; import org.opensearch.client.Client; import org.opensearch.core.index.shard.ShardId; import org.opensearch.index.engine.Engine; import org.opensearch.index.shard.IndexingOperationListener; +import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; /** @@ -26,6 +30,7 @@ public class ResourceSharingIndexListener implements IndexingOperationListener { private final static Logger log = LogManager.getLogger(ResourceSharingIndexListener.class); private static final ResourceSharingIndexListener INSTANCE = new ResourceSharingIndexListener(); + private ResourceSharingIndexHandler resourceSharingIndexHandler; private boolean initialized; @@ -52,6 +57,12 @@ public void initialize(ThreadPool threadPool, Client client) { this.threadPool = threadPool; this.client = client; + this.resourceSharingIndexHandler = new ResourceSharingIndexHandler( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_INDEX, + client, + threadPool + ); + ; } @@ -60,19 +71,25 @@ public boolean isInitialized() { } @Override - public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { // implement a check to see if a resource was updated - log.warn("postIndex called on " + shardId.getIndexName()); + log.info("postIndex called on {}", shardId.getIndexName()); String resourceId = index.id(); String resourceIndex = shardId.getIndexName(); + + try { + this.resourceSharingIndexHandler.indexResourceSharing(resourceId, resourceIndex, new CreatedBy("bleh", ""), null); + log.info("successfully indexed resource {}", resourceId); + } catch (IOException e) { + log.info("failed to index resource {}", resourceId); + throw new RuntimeException(e); + } } @Override - public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { // implement a check to see if a resource was deleted From 8942a800623449ebb626cda5a58e5e9bd1791c21 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 15 Oct 2024 01:09:57 -0400 Subject: [PATCH 016/201] Adds capability to introduce index listeners for all resource plugins Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 37 ++++++++++--------- .../transport/SecurityInterceptorTests.java | 4 +- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index e8b2e45cd4..96aa7c2bf6 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -120,7 +120,6 @@ import org.opensearch.indices.IndicesService; import org.opensearch.indices.SystemIndexDescriptor; import org.opensearch.plugins.ClusterPlugin; -import org.opensearch.plugins.ExtensiblePlugin; import org.opensearch.plugins.ExtensionAwarePlugin; import org.opensearch.plugins.IdentityPlugin; import org.opensearch.plugins.MapperPlugin; @@ -244,8 +243,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin IdentityPlugin, ResourceAccessControlPlugin, // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings - ExtensionAwarePlugin, - ExtensiblePlugin + ExtensionAwarePlugin // CS-ENFORCE-SINGLE { @@ -726,6 +724,7 @@ public void onIndexModule(IndexModule indexModule) { ) ); + log.info("Indices to listen to: {}", this.indicesToListen); if (this.indicesToListen.contains(indexModule.getIndex().getName())) { indexModule.addIndexOperationListener(ResourceSharingIndexListener.getInstance()); log.warn("Security plugin started listening to operations on index {}", indexModule.getIndex().getName()); @@ -850,20 +849,6 @@ public void onQueryPhase(SearchContext searchContext, long tookInNanos) { } } - // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions manager used to allow/disallow TLS connections to extensions - @Override - public void loadExtensions(ExtensionLoader loader) { - - log.info("Loading resource plugins"); - for (ResourcePlugin resourcePlugin : loader.loadExtensions(ResourcePlugin.class)) { - String resourceIndex = resourcePlugin.getResourceIndex(); - - this.indicesToListen.add(resourceIndex); - log.info("Loaded resource plugin: {}, index: {}", resourcePlugin, resourceIndex); - } - } - // CS-ENFORCE-SINGLE - @Override public List getActionFilters() { List filters = new ArrayList<>(1); @@ -2111,6 +2096,15 @@ public void onNodeStarted(DiscoveryNode localNode) { // create resource sharing index if absent rmr.createResourceSharingIndexIfAbsent(); + + log.info("Loading resource plugins"); + for (ResourcePlugin resourcePlugin : OpenSearchSecurityPlugin.GuiceHolder.getResourceService().listResourcePlugins()) { + String resourceIndex = resourcePlugin.getResourceIndex(); + + this.indicesToListen.add(resourceIndex); + log.info("Loaded resource plugin: {}, index: {}", resourcePlugin, resourceIndex); + } + final Set securityModules = ReflectionHelper.getModulesLoaded(); log.info("{} OpenSearch Security modules loaded so far: {}", securityModules.size(), securityModules); } @@ -2128,6 +2122,7 @@ public Collection> getGuiceServiceClasses() final List> services = new ArrayList<>(1); services.add(GuiceHolder.class); + log.info("Guice service classes loaded"); return services; } @@ -2259,13 +2254,15 @@ public GuiceHolder( final TransportService remoteClusterService, IndicesService indicesService, PitService pitService, - ExtensionsManager extensionsManager + ExtensionsManager extensionsManager, + ResourceService resourceService ) { GuiceHolder.repositoriesService = repositoriesService; GuiceHolder.remoteClusterService = remoteClusterService.getRemoteClusterService(); GuiceHolder.indicesService = indicesService; GuiceHolder.pitService = pitService; GuiceHolder.extensionsManager = extensionsManager; + GuiceHolder.resourceService = resourceService; } // CS-ENFORCE-SINGLE @@ -2291,6 +2288,10 @@ public static ExtensionsManager getExtensionsManager() { } // CS-ENFORCE-SINGLE + public static ResourceService getResourceService() { + return resourceService; + } + @Override public void close() {} diff --git a/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java b/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java index d12fafb247..0f7d5c59c5 100644 --- a/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java +++ b/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.opensearch.Version; +import org.opensearch.accesscontrol.resources.ResourceService; import org.opensearch.action.search.PitService; import org.opensearch.cluster.ClusterName; import org.opensearch.cluster.node.DiscoveryNode; @@ -171,7 +172,8 @@ public void setup() { transportService, mock(IndicesService.class), mock(PitService.class), - mock(ExtensionsManager.class) + mock(ExtensionsManager.class), + mock(ResourceService.class) ); // CS-ENFORCE-SINGLE From 2b06603da9d2742efa2c8d984fac044984a246ba Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 11 Nov 2024 13:08:18 -0500 Subject: [PATCH 017/201] Removes sampleplugin to be added in a separate PR Signed-off-by: Darshit Chanpura --- sample-resource-plugin/build.gradle | 166 ---------------- .../java/org/opensearch/sample/Resource.java | 19 -- .../sample/SampleResourcePlugin.java | 178 ------------------ .../sample/SampleResourceScope.java | 33 ---- .../actions/create/CreateResourceAction.java | 29 --- .../actions/create/CreateResourceRequest.java | 50 ----- .../create/CreateResourceResponse.java | 55 ------ .../create/CreateResourceRestAction.java | 55 ------ .../sample/actions/create/SampleResource.java | 56 ------ .../list/ListAccessibleResourcesAction.java | 29 --- .../list/ListAccessibleResourcesRequest.java | 39 ---- .../list/ListAccessibleResourcesResponse.java | 46 ----- .../ListAccessibleResourcesRestAction.java | 44 ----- .../actions/share/ShareResourceAction.java | 26 --- .../actions/share/ShareResourceRequest.java | 52 ----- .../actions/share/ShareResourceResponse.java | 52 ----- .../share/ShareResourceRestAction.java | 51 ----- .../verify/VerifyResourceAccessAction.java | 25 --- .../verify/VerifyResourceAccessRequest.java | 69 ------- .../verify/VerifyResourceAccessResponse.java | 52 ----- .../VerifyResourceAccessRestAction.java | 52 ----- .../CreateResourceTransportAction.java | 99 ---------- ...istAccessibleResourcesTransportAction.java | 56 ------ .../ShareResourceTransportAction.java | 65 ------- .../VerifyResourceAccessTransportAction.java | 58 ------ .../plugin-metadata/plugin-security.policy | 3 - .../org.opensearch.plugins.ResourcePlugin | 1 - .../test/resources/security/esnode-key.pem | 28 --- .../src/test/resources/security/esnode.pem | 25 --- .../src/test/resources/security/kirk-key.pem | 28 --- .../src/test/resources/security/kirk.pem | 27 --- .../src/test/resources/security/root-ca.pem | 28 --- .../src/test/resources/security/sample.pem | 25 --- .../src/test/resources/security/test-kirk.jks | Bin 3766 -> 0 bytes 34 files changed, 1621 deletions(-) delete mode 100644 sample-resource-plugin/build.gradle delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java delete mode 100644 sample-resource-plugin/src/main/plugin-metadata/plugin-security.policy delete mode 100644 sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin delete mode 100644 sample-resource-plugin/src/test/resources/security/esnode-key.pem delete mode 100644 sample-resource-plugin/src/test/resources/security/esnode.pem delete mode 100644 sample-resource-plugin/src/test/resources/security/kirk-key.pem delete mode 100644 sample-resource-plugin/src/test/resources/security/kirk.pem delete mode 100644 sample-resource-plugin/src/test/resources/security/root-ca.pem delete mode 100644 sample-resource-plugin/src/test/resources/security/sample.pem delete mode 100644 sample-resource-plugin/src/test/resources/security/test-kirk.jks diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle deleted file mode 100644 index e9822c1f22..0000000000 --- a/sample-resource-plugin/build.gradle +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -apply plugin: 'opensearch.opensearchplugin' -apply plugin: 'opensearch.testclusters' -apply plugin: 'opensearch.java-rest-test' - -import org.opensearch.gradle.test.RestIntegTestTask - - -opensearchplugin { - name 'opensearch-sample-resource-plugin' - description 'Sample plugin that extends OpenSearch Resource Plugin' - classname 'org.opensearch.sample.SampleResourcePlugin' -} - -ext { - projectSubstitutions = [:] - licenseFile = rootProject.file('LICENSE.txt') - noticeFile = rootProject.file('NOTICE.txt') -} - -repositories { - mavenLocal() - mavenCentral() - maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } -} - -dependencies { -} - -def es_tmp_dir = rootProject.file('build/private/es_tmp').absoluteFile -es_tmp_dir.mkdirs() - -File repo = file("$buildDir/testclusters/repo") -def _numNodes = findProperty('numNodes') as Integer ?: 1 - -licenseHeaders.enabled = true -validateNebulaPom.enabled = false -testingConventions.enabled = false -loggerUsageCheck.enabled = false - -javaRestTest.dependsOn(rootProject.assemble) -javaRestTest { - systemProperty 'tests.security.manager', 'false' -} -testClusters.javaRestTest { - testDistribution = 'INTEG_TEST' -} - -task integTest(type: RestIntegTestTask) { - description = "Run tests against a cluster" - testClassesDirs = sourceSets.test.output.classesDirs - classpath = sourceSets.test.runtimeClasspath -} -tasks.named("check").configure { dependsOn(integTest) } - -integTest { - if (project.hasProperty('excludeTests')) { - project.properties['excludeTests']?.replaceAll('\\s', '')?.split('[,;]')?.each { - exclude "${it}" - } - } - systemProperty 'tests.security.manager', 'false' - systemProperty 'java.io.tmpdir', es_tmp_dir.absolutePath - - systemProperty "https", System.getProperty("https") - systemProperty "user", System.getProperty("user") - systemProperty "password", System.getProperty("password") - // Tell the test JVM if the cluster JVM is running under a debugger so that tests can use longer timeouts for - // requests. The 'doFirst' delays reading the debug setting on the cluster till execution time. - doFirst { - // Tell the test JVM if the cluster JVM is running under a debugger so that tests can - // use longer timeouts for requests. - def isDebuggingCluster = getDebug() || System.getProperty("test.debug") != null - systemProperty 'cluster.debug', isDebuggingCluster - // Set number of nodes system property to be used in tests - systemProperty 'cluster.number_of_nodes', "${_numNodes}" - // There seems to be an issue when running multi node run or integ tasks with unicast_hosts - // not being written, the waitForAllConditions ensures it's written - getClusters().forEach { cluster -> - cluster.waitForAllConditions() - } - } - - // The -Dcluster.debug option makes the cluster debuggable; this makes the tests debuggable - if (System.getProperty("test.debug") != null) { - jvmArgs '-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=8000' - } - if (System.getProperty("tests.rest.bwcsuite") == null) { - filter { - excludeTestsMatching "org.opensearch.security.sampleextension.bwc.*IT" - } - } -} -project.getTasks().getByName('bundlePlugin').dependsOn(rootProject.tasks.getByName('build')) -Zip bundle = (Zip) project.getTasks().getByName("bundlePlugin"); -Zip rootBundle = (Zip) rootProject.getTasks().getByName("bundlePlugin"); -integTest.dependsOn(bundle) -integTest.getClusters().forEach{c -> { - c.plugin(rootProject.getObjects().fileProperty().value(rootBundle.getArchiveFile())) - c.plugin(project.getObjects().fileProperty().value(bundle.getArchiveFile())) -}} - -testClusters.integTest { - testDistribution = 'INTEG_TEST' - - // Cluster shrink exception thrown if we try to set numberOfNodes to 1, so only apply if > 1 - if (_numNodes > 1) numberOfNodes = _numNodes - // When running integration tests it doesn't forward the --debug-jvm to the cluster anymore - // i.e. we have to use a custom property to flag when we want to debug OpenSearch JVM - // since we also support multi node integration tests we increase debugPort per node - if (System.getProperty("cluster.debug") != null) { - def debugPort = 5005 - nodes.forEach { node -> - node.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=*:${debugPort}") - debugPort += 1 - } - } - setting 'path.repo', repo.absolutePath -} - -afterEvaluate { - testClusters.integTest.nodes.each { node -> - def plugins = node.plugins - def firstPlugin = plugins.get(0) - if (firstPlugin.provider == project.bundlePlugin.archiveFile) { - plugins.remove(0) - plugins.add(firstPlugin) - } - - node.extraConfigFile("kirk.pem", file("src/test/resources/security/kirk.pem")) - node.extraConfigFile("kirk-key.pem", file("src/test/resources/security/kirk-key.pem")) - node.extraConfigFile("esnode.pem", file("src/test/resources/security/esnode.pem")) - node.extraConfigFile("esnode-key.pem", file("src/test/resources/security/esnode-key.pem")) - node.extraConfigFile("root-ca.pem", file("src/test/resources/security/root-ca.pem")) - node.setting("plugins.security.ssl.transport.pemcert_filepath", "esnode.pem") - node.setting("plugins.security.ssl.transport.pemkey_filepath", "esnode-key.pem") - node.setting("plugins.security.ssl.transport.pemtrustedcas_filepath", "root-ca.pem") - node.setting("plugins.security.ssl.transport.enforce_hostname_verification", "false") - node.setting("plugins.security.ssl.http.enabled", "true") - node.setting("plugins.security.ssl.http.pemcert_filepath", "esnode.pem") - node.setting("plugins.security.ssl.http.pemkey_filepath", "esnode-key.pem") - node.setting("plugins.security.ssl.http.pemtrustedcas_filepath", "root-ca.pem") - node.setting("plugins.security.allow_unsafe_democertificates", "true") - node.setting("plugins.security.allow_default_init_securityindex", "true") - node.setting("plugins.security.authcz.admin_dn", "\n - CN=kirk,OU=client,O=client,L=test,C=de") - node.setting("plugins.security.audit.type", "internal_opensearch") - node.setting("plugins.security.enable_snapshot_restore_privilege", "true") - node.setting("plugins.security.check_snapshot_restore_write_privileges", "true") - node.setting("plugins.security.restapi.roles_enabled", "[\"all_access\", \"security_rest_api_access\"]") - } -} - -run { - doFirst { - // There seems to be an issue when running multi node run or integ tasks with unicast_hosts - // not being written, the waitForAllConditions ensures it's written - getClusters().forEach { cluster -> - cluster.waitForAllConditions() - } - } - useCluster testClusters.integTest -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java deleted file mode 100644 index 36e74f1624..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.sample; - -import org.opensearch.core.common.io.stream.NamedWriteable; -import org.opensearch.core.xcontent.ToXContentFragment; - -public abstract class Resource implements NamedWriteable, ToXContentFragment { - protected abstract String getResourceIndex(); -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java deleted file mode 100644 index a96a3d52ff..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * 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. - */ -package org.opensearch.sample; - -import java.util.*; -import java.util.function.Supplier; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.accesscontrol.resources.ResourceService; -import org.opensearch.action.ActionRequest; -import org.opensearch.client.Client; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.node.DiscoveryNodes; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.inject.Inject; -import org.opensearch.common.lifecycle.Lifecycle; -import org.opensearch.common.lifecycle.LifecycleComponent; -import org.opensearch.common.lifecycle.LifecycleListener; -import org.opensearch.common.settings.ClusterSettings; -import org.opensearch.common.settings.IndexScopedSettings; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.settings.SettingsFilter; -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.NamedWriteableRegistry; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.env.Environment; -import org.opensearch.env.NodeEnvironment; -import org.opensearch.indices.SystemIndexDescriptor; -import org.opensearch.plugins.ActionPlugin; -import org.opensearch.plugins.Plugin; -import org.opensearch.plugins.ResourcePlugin; -import org.opensearch.plugins.SystemIndexPlugin; -import org.opensearch.repositories.RepositoriesService; -import org.opensearch.rest.RestController; -import org.opensearch.rest.RestHandler; -import org.opensearch.sample.actions.create.CreateResourceAction; -import org.opensearch.sample.actions.create.CreateResourceRestAction; -import org.opensearch.sample.actions.list.ListAccessibleResourcesAction; -import org.opensearch.sample.actions.list.ListAccessibleResourcesRestAction; -import org.opensearch.sample.actions.share.ShareResourceAction; -import org.opensearch.sample.actions.share.ShareResourceRestAction; -import org.opensearch.sample.actions.verify.VerifyResourceAccessAction; -import org.opensearch.sample.actions.verify.VerifyResourceAccessRestAction; -import org.opensearch.sample.transport.CreateResourceTransportAction; -import org.opensearch.sample.transport.ListAccessibleResourcesTransportAction; -import org.opensearch.sample.transport.ShareResourceTransportAction; -import org.opensearch.sample.transport.VerifyResourceAccessTransportAction; -import org.opensearch.script.ScriptService; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.watcher.ResourceWatcherService; - -/** - * Sample Resource plugin. - * It uses ".sample_resources" index to manage its resources, and exposes a REST API - * - */ -public class SampleResourcePlugin extends Plugin implements ActionPlugin, SystemIndexPlugin, ResourcePlugin { - private static final Logger log = LogManager.getLogger(SampleResourcePlugin.class); - - public static final String RESOURCE_INDEX_NAME = ".sample_resource_sharing_plugin"; - - public final static Map INDEX_SETTINGS = Map.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); - - private Client client; - - @Override - public Collection createComponents( - Client client, - ClusterService clusterService, - ThreadPool threadPool, - ResourceWatcherService resourceWatcherService, - ScriptService scriptService, - NamedXContentRegistry xContentRegistry, - Environment environment, - NodeEnvironment nodeEnvironment, - NamedWriteableRegistry namedWriteableRegistry, - IndexNameExpressionResolver indexNameExpressionResolver, - Supplier repositoriesServiceSupplier - ) { - this.client = client; - log.info("Loaded SampleResourcePlugin components."); - return Collections.emptyList(); - } - - @Override - public List getRestHandlers( - Settings settings, - RestController restController, - ClusterSettings clusterSettings, - IndexScopedSettings indexScopedSettings, - SettingsFilter settingsFilter, - IndexNameExpressionResolver indexNameExpressionResolver, - Supplier nodesInCluster - ) { - return List.of( - new CreateResourceRestAction(), - new ListAccessibleResourcesRestAction(), - new VerifyResourceAccessRestAction(), - new ShareResourceRestAction() - ); - } - - @Override - public List> getActions() { - return List.of( - new ActionHandler<>(CreateResourceAction.INSTANCE, CreateResourceTransportAction.class), - new ActionHandler<>(ListAccessibleResourcesAction.INSTANCE, ListAccessibleResourcesTransportAction.class), - new ActionHandler<>(ShareResourceAction.INSTANCE, ShareResourceTransportAction.class), - new ActionHandler<>(VerifyResourceAccessAction.INSTANCE, VerifyResourceAccessTransportAction.class) - ); - } - - @Override - public Collection getSystemIndexDescriptors(Settings settings) { - final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(RESOURCE_INDEX_NAME, "Example index with resources"); - return Collections.singletonList(systemIndexDescriptor); - } - - @Override - public String getResourceType() { - return ""; - } - - @Override - public String getResourceIndex() { - return RESOURCE_INDEX_NAME; - } - - @Override - public Collection> getGuiceServiceClasses() { - final List> services = new ArrayList<>(1); - services.add(GuiceHolder.class); - return services; - } - - public static class GuiceHolder implements LifecycleComponent { - - private static ResourceService resourceService; - - @Inject - public GuiceHolder(final ResourceService resourceService) { - GuiceHolder.resourceService = resourceService; - } - - public static ResourceService getResourceService() { - return resourceService; - } - - @Override - public void close() {} - - @Override - public Lifecycle.State lifecycleState() { - return null; - } - - @Override - public void addLifecycleListener(LifecycleListener listener) {} - - @Override - public void removeLifecycleListener(LifecycleListener listener) {} - - @Override - public void start() {} - - @Override - public void stop() {} - - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java deleted file mode 100644 index 90df0d3764..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.sample; - -import org.opensearch.accesscontrol.resources.ResourceAccessScope; - -/** - * This class demonstrates a sample implementation of Basic Access Scopes to fit each plugin's use-case. - * The plugin then uses this scope when seeking access evaluation for a user on a particular resource. - */ -public enum SampleResourceScope implements ResourceAccessScope { - - SAMPLE_FULL_ACCESS("sample_full_access"); - - private final String name; - - SampleResourceScope(String scopeName) { - this.name = scopeName; - } - - public String getName() { - return name; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java deleted file mode 100644 index e7c02278ab..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.create; - -import org.opensearch.action.ActionType; - -/** - * Action to create a sample resource - */ -public class CreateResourceAction extends ActionType { - /** - * Create sample resource action instance - */ - public static final CreateResourceAction INSTANCE = new CreateResourceAction(); - /** - * Create sample resource action name - */ - public static final String NAME = "cluster:admin/sample-resource-plugin/create"; - - private CreateResourceAction() { - super(NAME, CreateResourceResponse::new); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java deleted file mode 100644 index b31a4b7f2b..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.create; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.sample.Resource; - -/** - * Request object for CreateSampleResource transport action - */ -public class CreateResourceRequest extends ActionRequest { - - private final Resource resource; - - /** - * Default constructor - */ - public CreateResourceRequest(Resource resource) { - this.resource = resource; - } - - public CreateResourceRequest(StreamInput in) throws IOException { - this.resource = in.readNamedWriteable(Resource.class); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - resource.writeTo(out); - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - public Resource getResource() { - return this.resource; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java deleted file mode 100644 index 6b966ed08d..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.create; - -import java.io.IOException; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; - -/** - * Response to a CreateSampleResourceRequest - */ -public class CreateResourceResponse extends ActionResponse implements ToXContentObject { - private final String message; - - /** - * Default constructor - * - * @param message The message - */ - public CreateResourceResponse(String message) { - this.message = message; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(message); - } - - /** - * Constructor with StreamInput - * - * @param in the stream input - */ - public CreateResourceResponse(final StreamInput in) throws IOException { - message = in.readString(); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("message", message); - builder.endObject(); - return builder; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java deleted file mode 100644 index 86346cc279..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.create; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import org.opensearch.client.node.NodeClient; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; - -import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.POST; - -public class CreateResourceRestAction extends BaseRestHandler { - - public CreateResourceRestAction() {} - - @Override - public List routes() { - return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/resource")); - } - - @Override - public String getName() { - return "create_sample_resource"; - } - - @Override - public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - - String name = (String) source.get("name"); - SampleResource resource = new SampleResource(); - resource.setName(name); - final CreateResourceRequest createSampleResourceRequest = new CreateResourceRequest(resource); - return channel -> client.executeLocally( - CreateResourceAction.INSTANCE, - createSampleResourceRequest, - new RestToXContentListener<>(channel) - ); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java deleted file mode 100644 index 1566abfe69..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.sample.actions.create; - -import java.io.IOException; - -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.sample.Resource; - -import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; - -public class SampleResource extends Resource { - - private String name; - - public SampleResource() {} - - SampleResource(StreamInput in) throws IOException { - this.name = in.readString(); - } - - @Override - public String getResourceIndex() { - return RESOURCE_INDEX_NAME; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return builder.startObject().field("name", name).endObject(); - } - - @Override - public void writeTo(StreamOutput streamOutput) throws IOException { - streamOutput.writeString(name); - } - - @Override - public String getWriteableName() { - return "sample_resource"; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java deleted file mode 100644 index b4e9e29e22..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.list; - -import org.opensearch.action.ActionType; - -/** - * Action to list sample resources - */ -public class ListAccessibleResourcesAction extends ActionType { - /** - * List sample resource action instance - */ - public static final ListAccessibleResourcesAction INSTANCE = new ListAccessibleResourcesAction(); - /** - * List sample resource action name - */ - public static final String NAME = "cluster:admin/sample-resource-plugin/list"; - - private ListAccessibleResourcesAction() { - super(NAME, ListAccessibleResourcesResponse::new); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java deleted file mode 100644 index b4c0961774..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.list; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; - -/** - * Request object for ListSampleResource transport action - */ -public class ListAccessibleResourcesRequest extends ActionRequest { - - public ListAccessibleResourcesRequest() {} - - /** - * Constructor with stream input - * @param in the stream input - * @throws IOException IOException - */ - public ListAccessibleResourcesRequest(final StreamInput in) throws IOException {} - - @Override - public void writeTo(final StreamOutput out) throws IOException {} - - @Override - public ActionRequestValidationException validate() { - return null; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java deleted file mode 100644 index 47a8f88e4e..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.list; - -import java.io.IOException; -import java.util.List; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; - -/** - * Response to a ListAccessibleResourcesRequest - */ -public class ListAccessibleResourcesResponse extends ActionResponse implements ToXContentObject { - private final List resourceIds; - - public ListAccessibleResourcesResponse(List resourceIds) { - this.resourceIds = resourceIds; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeStringArray(resourceIds.toArray(new String[0])); - } - - public ListAccessibleResourcesResponse(final StreamInput in) throws IOException { - resourceIds = in.readStringList(); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("resource-ids", resourceIds); - builder.endObject(); - return builder; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java deleted file mode 100644 index bb921fce00..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.list; - -import java.util.List; - -import org.opensearch.client.node.NodeClient; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; - -import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.GET; - -public class ListAccessibleResourcesRestAction extends BaseRestHandler { - - public ListAccessibleResourcesRestAction() {} - - @Override - public List routes() { - return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/resource")); - } - - @Override - public String getName() { - return "list_sample_resources"; - } - - @Override - public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { - final ListAccessibleResourcesRequest listAccessibleResourcesRequest = new ListAccessibleResourcesRequest(); - return channel -> client.executeLocally( - ListAccessibleResourcesAction.INSTANCE, - listAccessibleResourcesRequest, - new RestToXContentListener<>(channel) - ); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.java deleted file mode 100644 index d362b1927c..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.share; - -import org.opensearch.action.ActionType; - -public class ShareResourceAction extends ActionType { - /** - * List sample resource action instance - */ - public static final ShareResourceAction INSTANCE = new ShareResourceAction(); - /** - * List sample resource action name - */ - public static final String NAME = "cluster:admin/sample-resource-plugin/share"; - - private ShareResourceAction() { - super(NAME, ShareResourceResponse::new); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java deleted file mode 100644 index 01866fd516..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.share; - -import java.io.IOException; - -import org.opensearch.accesscontrol.resources.ShareWith; -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; - -public class ShareResourceRequest extends ActionRequest { - - private final String resourceId; - private final ShareWith shareWith; - - public ShareResourceRequest(String resourceId, ShareWith shareWith) { - this.resourceId = resourceId; - this.shareWith = shareWith; - } - - public ShareResourceRequest(StreamInput in) throws IOException { - this.resourceId = in.readString(); - this.shareWith = in.readNamedWriteable(ShareWith.class); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeString(resourceId); - out.writeNamedWriteable(shareWith); - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - public String getResourceId() { - return resourceId; - } - - public ShareWith getShareWith() { - return shareWith; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java deleted file mode 100644 index a6a85d206d..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.share; - -import java.io.IOException; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; - -public class ShareResourceResponse extends ActionResponse implements ToXContentObject { - private final String message; - - /** - * Default constructor - * - * @param message The message - */ - public ShareResourceResponse(String message) { - this.message = message; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(message); - } - - /** - * Constructor with StreamInput - * - * @param in the stream input - */ - public ShareResourceResponse(final StreamInput in) throws IOException { - message = in.readString(); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("message", message); - builder.endObject(); - return builder; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java deleted file mode 100644 index 347fb49e68..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.share; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import org.opensearch.accesscontrol.resources.ShareWith; -import org.opensearch.client.node.NodeClient; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; - -import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.GET; - -public class ShareResourceRestAction extends BaseRestHandler { - - public ShareResourceRestAction() {} - - @Override - public List routes() { - return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/share/{resource_id}")); - } - - @Override - public String getName() { - return "share_sample_resources"; - } - - @Override - public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - - String resourceId = (String) source.get("resource_id"); - ShareWith shareWith = (ShareWith) source.get("share_with"); - final ShareResourceRequest shareResourceRequest = new ShareResourceRequest(resourceId, shareWith); - return channel -> client.executeLocally(ShareResourceAction.INSTANCE, shareResourceRequest, new RestToXContentListener<>(channel)); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java deleted file mode 100644 index 1378d561f5..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.verify; - -import org.opensearch.action.ActionType; - -/** - * Action to verify resource access for current user - */ -public class VerifyResourceAccessAction extends ActionType { - - public static final VerifyResourceAccessAction INSTANCE = new VerifyResourceAccessAction(); - - public static final String NAME = "cluster:admin/sample-resource-plugin/verify/resource_access"; - - private VerifyResourceAccessAction() { - super(NAME, VerifyResourceAccessResponse::new); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java deleted file mode 100644 index e9b20118db..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.verify; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; - -public class VerifyResourceAccessRequest extends ActionRequest { - - private final String resourceId; - - private final String sourceIdx; - - private final String scope; - - /** - * Default constructor - */ - public VerifyResourceAccessRequest(String resourceId, String sourceIdx, String scope) { - this.resourceId = resourceId; - this.sourceIdx = sourceIdx; - this.scope = scope; - } - - /** - * Constructor with stream input - * @param in the stream input - * @throws IOException IOException - */ - public VerifyResourceAccessRequest(final StreamInput in) throws IOException { - this.resourceId = in.readString(); - this.sourceIdx = in.readString(); - this.scope = in.readString(); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeString(resourceId); - out.writeString(sourceIdx); - out.writeString(scope); - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - public String getResourceId() { - return resourceId; - } - - public String getSourceIdx() { - return sourceIdx; - } - - public String getScope() { - return scope; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java deleted file mode 100644 index 660ac03f71..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.verify; - -import java.io.IOException; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; - -public class VerifyResourceAccessResponse extends ActionResponse implements ToXContentObject { - private final String message; - - /** - * Default constructor - * - * @param message The message - */ - public VerifyResourceAccessResponse(String message) { - this.message = message; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(message); - } - - /** - * Constructor with StreamInput - * - * @param in the stream input - */ - public VerifyResourceAccessResponse(final StreamInput in) throws IOException { - message = in.readString(); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("message", message); - builder.endObject(); - return builder; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java deleted file mode 100644 index 34bfed4e9f..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.verify; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import org.opensearch.client.node.NodeClient; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; - -import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.POST; - -public class VerifyResourceAccessRestAction extends BaseRestHandler { - - public VerifyResourceAccessRestAction() {} - - @Override - public List routes() { - return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/verify_resource_access")); - } - - @Override - public String getName() { - return "verify_resource_access"; - } - - @Override - public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - - String resourceIdx = (String) source.get("resource_idx"); - String sourceIdx = (String) source.get("source_idx"); - String scope = (String) source.get("scope"); - - // final CreateResourceRequest createSampleResourceRequest = new CreateResourceRequest<>(resource); - return channel -> client.executeLocally(VerifyResourceAccessAction.INSTANCE, null, new RestToXContentListener<>(channel)); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java deleted file mode 100644 index 8bff7b44a3..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.transport; - -import java.io.IOException; -import java.util.List; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.accesscontrol.resources.ResourceService; -import org.opensearch.accesscontrol.resources.ResourceSharing; -import org.opensearch.accesscontrol.resources.ShareWith; -import org.opensearch.accesscontrol.resources.SharedWithScope; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.index.IndexResponse; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.action.support.WriteRequest; -import org.opensearch.client.Client; -import org.opensearch.common.inject.Inject; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.core.action.ActionListener; -import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.sample.Resource; -import org.opensearch.sample.SampleResourcePlugin; -import org.opensearch.sample.SampleResourceScope; -import org.opensearch.sample.actions.create.CreateResourceAction; -import org.opensearch.sample.actions.create.CreateResourceRequest; -import org.opensearch.sample.actions.create.CreateResourceResponse; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; - -/** - * Transport action for CreateSampleResource. - */ -public class CreateResourceTransportAction extends HandledTransportAction { - private static final Logger log = LogManager.getLogger(CreateResourceTransportAction.class); - - private final TransportService transportService; - private final Client nodeClient; - - @Inject - public CreateResourceTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { - super(CreateResourceAction.NAME, transportService, actionFilters, CreateResourceRequest::new); - this.transportService = transportService; - this.nodeClient = nodeClient; - } - - @Override - protected void doExecute(Task task, CreateResourceRequest request, ActionListener listener) { - try (ThreadContext.StoredContext ignore = transportService.getThreadPool().getThreadContext().stashContext()) { - createResource(request, listener); - listener.onResponse(new CreateResourceResponse("Resource " + request.getResource() + " created successfully.")); - } catch (Exception e) { - log.info("Failed to create resource", e); - listener.onFailure(e); - } - } - - private void createResource(CreateResourceRequest request, ActionListener listener) { - Resource sample = request.getResource(); - try { - IndexRequest ir = nodeClient.prepareIndex(RESOURCE_INDEX_NAME) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .setSource(sample.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) - .request(); - - log.warn("Index Request: {}", ir.toString()); - - ActionListener irListener = getIndexResponseActionListener(listener); - nodeClient.index(ir, irListener); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static ActionListener getIndexResponseActionListener(ActionListener listener) { - SharedWithScope.SharedWithPerScope sharedWithPerScope = new SharedWithScope.SharedWithPerScope(List.of(), List.of(), List.of()); - SharedWithScope sharedWithScope = new SharedWithScope(SampleResourceScope.SAMPLE_FULL_ACCESS.getName(), sharedWithPerScope); - ShareWith shareWith = new ShareWith(List.of(sharedWithScope)); - return ActionListener.wrap(idxResponse -> { - log.info("Created resource: {}", idxResponse.toString()); - ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - ResourceSharing sharing = rs.getResourceAccessControlPlugin().shareWith(idxResponse.getId(), idxResponse.getIndex(), shareWith); - log.info("Created resource sharing entry: {}", sharing.toString()); - }, listener::onFailure); - } - -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java deleted file mode 100644 index d56eb6d291..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.transport; - -import java.util.List; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.accesscontrol.resources.ResourceService; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.common.inject.Inject; -import org.opensearch.core.action.ActionListener; -import org.opensearch.sample.SampleResourcePlugin; -import org.opensearch.sample.actions.list.ListAccessibleResourcesAction; -import org.opensearch.sample.actions.list.ListAccessibleResourcesRequest; -import org.opensearch.sample.actions.list.ListAccessibleResourcesResponse; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; - -/** - * Transport action for ListSampleResource. - */ -public class ListAccessibleResourcesTransportAction extends HandledTransportAction< - ListAccessibleResourcesRequest, - ListAccessibleResourcesResponse> { - private static final Logger log = LogManager.getLogger(ListAccessibleResourcesTransportAction.class); - - @Inject - public ListAccessibleResourcesTransportAction(TransportService transportService, ActionFilters actionFilters) { - super(ListAccessibleResourcesAction.NAME, transportService, actionFilters, ListAccessibleResourcesRequest::new); - } - - @Override - protected void doExecute(Task task, ListAccessibleResourcesRequest request, ActionListener listener) { - try { - ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - List resourceIds = rs.getResourceAccessControlPlugin().listAccessibleResourcesInPlugin(RESOURCE_INDEX_NAME); - log.info("Successfully fetched accessible resources for current user"); - listener.onResponse(new ListAccessibleResourcesResponse(resourceIds)); - } catch (Exception e) { - log.info("Failed to list accessible resources for current user: ", e); - listener.onFailure(e); - } - - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java deleted file mode 100644 index ccbfc31b78..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.transport; - -import java.util.List; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.accesscontrol.resources.ResourceService; -import org.opensearch.accesscontrol.resources.ResourceSharing; -import org.opensearch.accesscontrol.resources.ShareWith; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.common.inject.Inject; -import org.opensearch.core.action.ActionListener; -import org.opensearch.sample.SampleResourcePlugin; -import org.opensearch.sample.actions.share.ShareResourceAction; -import org.opensearch.sample.actions.share.ShareResourceRequest; -import org.opensearch.sample.actions.share.ShareResourceResponse; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; - -/** - * Transport action for CreateSampleResource. - */ -public class ShareResourceTransportAction extends HandledTransportAction { - private static final Logger log = LogManager.getLogger(ShareResourceTransportAction.class); - - @Inject - public ShareResourceTransportAction(TransportService transportService, ActionFilters actionFilters) { - super(ShareResourceAction.NAME, transportService, actionFilters, ShareResourceRequest::new); - } - - @Override - protected void doExecute(Task task, ShareResourceRequest request, ActionListener listener) { - try { - shareResource(request); - listener.onResponse(new ShareResourceResponse("Resource " + request.getResourceId() + " shared successfully.")); - } catch (Exception e) { - listener.onFailure(e); - } - } - - private void shareResource(ShareResourceRequest request) { - try { - ShareWith shareWith = new ShareWith(List.of()); - ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - ResourceSharing sharing = rs.getResourceAccessControlPlugin() - .shareWith(request.getResourceId(), RESOURCE_INDEX_NAME, shareWith); - log.info("Shared resource : {} with {}", request.getResourceId(), sharing.toString()); - } catch (Exception e) { - log.info("Failed to share resource {}", request.getResourceId(), e); - throw e; - } - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java deleted file mode 100644 index 947dcec59e..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.transport; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.accesscontrol.resources.ResourceService; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.client.Client; -import org.opensearch.common.inject.Inject; -import org.opensearch.core.action.ActionListener; -import org.opensearch.sample.SampleResourcePlugin; -import org.opensearch.sample.actions.verify.VerifyResourceAccessAction; -import org.opensearch.sample.actions.verify.VerifyResourceAccessRequest; -import org.opensearch.sample.actions.verify.VerifyResourceAccessResponse; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -public class VerifyResourceAccessTransportAction extends HandledTransportAction { - private static final Logger log = LogManager.getLogger(VerifyResourceAccessTransportAction.class); - - @Inject - public VerifyResourceAccessTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { - super(VerifyResourceAccessAction.NAME, transportService, actionFilters, VerifyResourceAccessRequest::new); - } - - @Override - protected void doExecute(Task task, VerifyResourceAccessRequest request, ActionListener listener) { - try { - ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - boolean hasRequestedScopeAccess = rs.getResourceAccessControlPlugin() - .hasPermission(request.getResourceId(), request.getSourceIdx(), request.getScope()); - - StringBuilder sb = new StringBuilder(); - sb.append("User does"); - sb.append(hasRequestedScopeAccess ? " " : " not "); - sb.append("have requested scope "); - sb.append(request.getScope()); - sb.append(" access to "); - sb.append(request.getResourceId()); - - log.info(sb.toString()); - listener.onResponse(new VerifyResourceAccessResponse(sb.toString())); - } catch (Exception e) { - log.info("Failed to check user permissions for resource {}", request.getResourceId(), e); - listener.onFailure(e); - } - } - -} diff --git a/sample-resource-plugin/src/main/plugin-metadata/plugin-security.policy b/sample-resource-plugin/src/main/plugin-metadata/plugin-security.policy deleted file mode 100644 index a5dfc33a87..0000000000 --- a/sample-resource-plugin/src/main/plugin-metadata/plugin-security.policy +++ /dev/null @@ -1,3 +0,0 @@ -grant { - permission java.lang.RuntimePermission "getClassLoader"; -}; \ No newline at end of file diff --git a/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin b/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin deleted file mode 100644 index 1ca89eaf74..0000000000 --- a/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin +++ /dev/null @@ -1 +0,0 @@ -org.opensearch.sample.SampleResourcePlugin \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/esnode-key.pem b/sample-resource-plugin/src/test/resources/security/esnode-key.pem deleted file mode 100644 index e90562be43..0000000000 --- a/sample-resource-plugin/src/test/resources/security/esnode-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCm93kXteDQHMAv -bUPNPW5pyRHKDD42XGWSgq0k1D29C/UdyL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0 -o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0HGkn47XVu3EwbfrTENg3jFu+Oem6a/50 -1SzITzJWtS0cn2dIFOBimTVpT/4Zv5qrXA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1 -MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8ndibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b -6l+KLo3IKpfTbAIJXIO+M67FLtWKtttDao94B069skzKk6FPgW/OZh6PRCD0oxOa -vV+ld2SjAgMBAAECggEAQK1+uAOZeaSZggW2jQut+MaN4JHLi61RH2cFgU3COLgo -FIiNjFn8f2KKU3gpkt1It8PjlmprpYut4wHI7r6UQfuv7ZrmncRiPWHm9PB82+ZQ -5MXYqj4YUxoQJ62Cyz4sM6BobZDrjG6HHGTzuwiKvHHkbsEE9jQ4E5m7yfbVvM0O -zvwrSOM1tkZihKSTpR0j2+taji914tjBssbn12TMZQL5ItGnhR3luY8mEwT9MNkZ -xg0VcREoAH+pu9FE0vPUgLVzhJ3be7qZTTSRqv08bmW+y1plu80GbppePcgYhEow -dlW4l6XPJaHVSn1lSFHE6QAx6sqiAnBz0NoTPIaLyQKBgQDZqDOlhCRciMRicSXn -7yid9rhEmdMkySJHTVFOidFWwlBcp0fGxxn8UNSBcXdSy7GLlUtH41W9PWl8tp9U -hQiiXORxOJ7ZcB80uNKXF01hpPj2DpFPWyHFxpDkWiTAYpZl68rOlYujxZUjJIej -VvcykBC2BlEOG9uZv2kxcqLyJwKBgQDEYULTxaTuLIa17wU3nAhaainKB3vHxw9B -Ksy5p3ND43UNEKkQm7K/WENx0q47TA1mKD9i+BhaLod98mu0YZ+BCUNgWKcBHK8c -uXpauvM/pLhFLXZ2jvEJVpFY3J79FSRK8bwE9RgKfVKMMgEk4zOyZowS8WScOqiy -hnQn1vKTJQKBgElhYuAnl9a2qXcC7KOwRsJS3rcKIVxijzL4xzOyVShp5IwIPbOv -hnxBiBOH/JGmaNpFYBcBdvORE9JfA4KMQ2fx53agfzWRjoPI1/7mdUk5RFI4gRb/ -A3jZRBoopgFSe6ArCbnyQxzYzToG48/Wzwp19ZxYrtUR4UyJct6f5n27AoGBAJDh -KIpQQDOvCdtjcbfrF4aM2DPCfaGPzENJriwxy6oEPzDaX8Bu/dqI5Ykt43i/zQrX -GpyLaHvv4+oZVTiI5UIvcVO9U8hQPyiz9f7F+fu0LHZs6f7hyhYXlbe3XFxeop3f -5dTKdWgXuTTRF2L9dABkA2deS9mutRKwezWBMQk5AoGBALPtX0FrT1zIosibmlud -tu49A/0KZu4PBjrFMYTSEWGNJez3Fb2VsJwylVl6HivwbP61FhlYfyksCzQQFU71 -+x7Nmybp7PmpEBECr3deoZKQ/acNHn0iwb0It+YqV5+TquQebqgwK6WCLsMuiYKT -bg/ch9Rhxbq22yrVgWHh6epp ------END PRIVATE KEY----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/esnode.pem b/sample-resource-plugin/src/test/resources/security/esnode.pem deleted file mode 100644 index 44101f0b37..0000000000 --- a/sample-resource-plugin/src/test/resources/security/esnode.pem +++ /dev/null @@ -1,25 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v -dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT -AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl -MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA -A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud -yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0 -HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr -XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n -dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD -ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R -BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA -AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF -BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo -wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ -KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz -pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi -7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh -hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L -camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg -PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg= ------END CERTIFICATE----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/kirk-key.pem b/sample-resource-plugin/src/test/resources/security/kirk-key.pem deleted file mode 100644 index 1949c26139..0000000000 --- a/sample-resource-plugin/src/test/resources/security/kirk-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCVXDgEJQorgfXp -gpY0TgF55bD2xuzxN5Dc9rDfgWxrsOvOloMpd7k6FR71bKWjJi1KptSmM/cDElky -AWYKSfYWGiGxsQ+EQW+6kwCfEOHXQldn+0+JcWqP+osSPjtJfwRvRN5kRqP69MPo -7U0N2kdqenqMWjmG1chDGLRSOEGU5HIBiDxsZtOcvMaJ8b1eaW0lvS+6gFQ80AvB -GBkDDCOHHLtDXBylrZk2CQP8AzxNicIZ4B8G3CG3OHA8+nBtEtxZoIihrrkqlMt+ -b/5N8u8zB0Encew0kdrc4R/2wS//ahr6U+9Siq8T7WsUtGwKj3BJClg6OyDJRhlu -y2gFnxoPAgMBAAECggEAP5TOycDkx+megAWVoHV2fmgvgZXkBrlzQwUG/VZQi7V4 -ZGzBMBVltdqI38wc5MtbK3TCgHANnnKgor9iq02Z4wXDwytPIiti/ycV9CDRKvv0 -TnD2hllQFjN/IUh5n4thHWbRTxmdM7cfcNgX3aZGkYbLBVVhOMtn4VwyYu/Mxy8j -xClZT2xKOHkxqwmWPmdDTbAeZIbSv7RkIGfrKuQyUGUaWhrPslvYzFkYZ0umaDgQ -OAthZew5Bz3OfUGOMPLH61SVPuJZh9zN1hTWOvT65WFWfsPd2yStI+WD/5PU1Doo -1RyeHJO7s3ug8JPbtNJmaJwHe9nXBb/HXFdqb976yQKBgQDNYhpu+MYSYupaYqjs -9YFmHQNKpNZqgZ4ceRFZ6cMJoqpI5dpEMqToFH7tpor72Lturct2U9nc2WR0HeEs -/6tiptyMPTFEiMFb1opQlXF2ae7LeJllntDGN0Q6vxKnQV+7VMcXA0Y8F7tvGDy3 -qJu5lfvB1mNM2I6y/eMxjBuQhwKBgQC6K41DXMFro0UnoO879pOQYMydCErJRmjG -/tZSy3Wj4KA/QJsDSViwGfvdPuHZRaG9WtxdL6kn0w1exM9Rb0bBKl36lvi7o7xv -M+Lw9eyXMkww8/F5d7YYH77gIhGo+RITkKI3+5BxeBaUnrGvmHrpmpgRXWmINqr0 -0jsnN3u0OQKBgCf45vIgItSjQb8zonLz2SpZjTFy4XQ7I92gxnq8X0Q5z3B+o7tQ -K/4rNwTju/sGFHyXAJlX+nfcK4vZ4OBUJjP+C8CTjEotX4yTNbo3S6zjMyGQqDI5 -9aIOUY4pb+TzeUFJX7If5gR+DfGyQubvvtcg1K3GHu9u2l8FwLj87sRzAoGAflQF -RHuRiG+/AngTPnZAhc0Zq0kwLkpH2Rid6IrFZhGLy8AUL/O6aa0IGoaMDLpSWUJp -nBY2S57MSM11/MVslrEgGmYNnI4r1K25xlaqV6K6ztEJv6n69327MS4NG8L/gCU5 -3pEm38hkUi8pVYU7in7rx4TCkrq94OkzWJYurAkCgYATQCL/rJLQAlJIGulp8s6h -mQGwy8vIqMjAdHGLrCS35sVYBXG13knS52LJHvbVee39AbD5/LlWvjJGlQMzCLrw -F7oILW5kXxhb8S73GWcuMbuQMFVHFONbZAZgn+C9FW4l7XyRdkrbR1MRZ2km8YMs -/AHmo368d4PSNRMMzLHw8Q== ------END PRIVATE KEY----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/kirk.pem b/sample-resource-plugin/src/test/resources/security/kirk.pem deleted file mode 100644 index 36b7e19a75..0000000000 --- a/sample-resource-plugin/src/test/resources/security/kirk.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEmDCCA4CgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLcwDQYJKoZIhvcNAQEL -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v -dCBDQTAeFw0yNDAyMjAxNzA0MjRaFw0zNDAyMTcxNzA0MjRaME0xCzAJBgNVBAYT -AmRlMQ0wCwYDVQQHDAR0ZXN0MQ8wDQYDVQQKDAZjbGllbnQxDzANBgNVBAsMBmNs -aWVudDENMAsGA1UEAwwEa2lyazCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAJVcOAQlCiuB9emCljROAXnlsPbG7PE3kNz2sN+BbGuw686Wgyl3uToVHvVs -paMmLUqm1KYz9wMSWTIBZgpJ9hYaIbGxD4RBb7qTAJ8Q4ddCV2f7T4lxao/6ixI+ -O0l/BG9E3mRGo/r0w+jtTQ3aR2p6eoxaOYbVyEMYtFI4QZTkcgGIPGxm05y8xonx -vV5pbSW9L7qAVDzQC8EYGQMMI4ccu0NcHKWtmTYJA/wDPE2JwhngHwbcIbc4cDz6 -cG0S3FmgiKGuuSqUy35v/k3y7zMHQSdx7DSR2tzhH/bBL/9qGvpT71KKrxPtaxS0 -bAqPcEkKWDo7IMlGGW7LaAWfGg8CAwEAAaOCASswggEnMAwGA1UdEwEB/wQCMAAw -DgYDVR0PAQH/BAQDAgXgMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMB0GA1UdDgQW -BBSjMS8tgguX/V7KSGLoGg7K6XMzIDCBzwYDVR0jBIHHMIHEgBQXh9+gWutmEqfV -0Pi6EkU8tysAnKGBlaSBkjCBjzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmS -JomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1wbGUgQ29tIEluYy4xITAf -BgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEhMB8GA1UEAwwYRXhhbXBs -ZSBDb20gSW5jLiBSb290IENBghQNZAmZZn3EFOxBR4630XlhI+mo4jANBgkqhkiG -9w0BAQsFAAOCAQEACEUPPE66/Ot3vZqRGpjDjPHAdtOq+ebaglQhvYcnDw8LOZm8 -Gbh9M88CiO6UxC8ipQLTPh2yyeWArkpJzJK/Pi1eoF1XLiAa0sQ/RaJfQWPm9dvl -1ZQeK5vfD4147b3iBobwEV+CR04SKow0YeEEzAJvzr8YdKI6jqr+2GjjVqzxvRBy -KRVHWCFiR7bZhHGLq3br8hSu0hwjb3oGa1ZI8dui6ujyZt6nm6BoEkau3G/6+zq9 -E6vX3+8Fj4HKCAL6i0SwfGmEpTNp5WUhqibK/fMhhmMT4Mx6MxkT+OFnIjdUU0S/ -e3kgnG8qjficUr38CyEli1U0M7koIXUZI7r+LQ== ------END CERTIFICATE----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/root-ca.pem b/sample-resource-plugin/src/test/resources/security/root-ca.pem deleted file mode 100644 index d33f5f7216..0000000000 --- a/sample-resource-plugin/src/test/resources/security/root-ca.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIExjCCA66gAwIBAgIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcNAQEL -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v -dCBDQTAeFw0yNDAyMjAxNzAwMzZaFw0zNDAyMTcxNzAwMzZaMIGPMRMwEQYKCZIm -iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ -RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290 -IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEPyN7J9VGPyJcQmCBl5TGwfSzvVdWwoQU -j9aEsdfFJ6pBCDQSsj8Lv4RqL0dZra7h7SpZLLX/YZcnjikrYC+rP5OwsI9xEE/4 -U98CsTBPhIMgqFK6SzNE5494BsAk4cL72dOOc8tX19oDS/PvBULbNkthQ0aAF1dg -vbrHvu7hq7LisB5ZRGHVE1k/AbCs2PaaKkn2jCw/b+U0Ml9qPuuEgz2mAqJDGYoA -WSR4YXrOcrmPuRqbws464YZbJW898/0Pn/U300ed+4YHiNYLLJp51AMkR4YEw969 -VRPbWIvLrd0PQBooC/eLrL6rvud/GpYhdQEUx8qcNCKd4bz3OaQ5AgMBAAGjggEW -MIIBEjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQU -F4ffoFrrZhKn1dD4uhJFPLcrAJwwgc8GA1UdIwSBxzCBxIAUF4ffoFrrZhKn1dD4 -uhJFPLcrAJyhgZWkgZIwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJ -k/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYD -VQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUg -Q29tIEluYy4gUm9vdCBDQYIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcN -AQELBQADggEBAL3Q3AHUhMiLUy6OlLSt8wX9I2oNGDKbBu0atpUNDztk/0s3YLQC -YuXgN4KrIcMXQIuAXCx407c+pIlT/T1FNn+VQXwi56PYzxQKtlpoKUL3oPQE1d0V -6EoiNk+6UodvyZqpdQu7fXVentRMk1QX7D9otmiiNuX+GSxJhJC2Lyzw65O9EUgG -1yVJon6RkUGtqBqKIuLksKwEr//ELnjmXit4LQKSnqKr0FTCB7seIrKJNyb35Qnq -qy9a/Unhokrmdda1tr6MbqU8l7HmxLuSd/Ky+L0eDNtYv6YfMewtjg0TtAnFyQov -rdXmeq1dy9HLo3Ds4AFz3Gx9076TxcRS/iI= ------END CERTIFICATE----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/sample.pem b/sample-resource-plugin/src/test/resources/security/sample.pem deleted file mode 100644 index 44101f0b37..0000000000 --- a/sample-resource-plugin/src/test/resources/security/sample.pem +++ /dev/null @@ -1,25 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v -dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT -AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl -MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA -A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud -yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0 -HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr -XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n -dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD -ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R -BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA -AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF -BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo -wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ -KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz -pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi -7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh -hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L -camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg -PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg= ------END CERTIFICATE----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/test-kirk.jks b/sample-resource-plugin/src/test/resources/security/test-kirk.jks deleted file mode 100644 index 6c8c5ef77e20980f8c78295b159256b805da6a28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3766 zcmd^=c{r47AIImJ%`(PV###wuU&o%k$xbMgr4m`Pk2Tv-j4?=zEwY?!X|aVw)I`=A zPAY52Rt6yODkPjhAQ%WsfbL*f;mp!-018Nf*#Q6sf)b!}Nv;s_8gzOC@mTmi+D9F}jyYkhL=#Xk3eYM2csmxKA&W!xAdE{tZ2mEGS z;L%QU`DHcrbdbw$3GsKUvmfQu0Z^?sH7B)!W)eLbG*fXB^G$&6CbCnj4~ z*J>Rkut6vL1EvT!JqAq#X=O~#!JHQ#QVSPuOGlnLrXXB~{{FsGRq?o?I;>^GFEhMB zw;z!v1sXap8nq3zz&+prKs-DRPm*XsS4BaP6Z{8tM~n@m|rxMA=p6*i(w=7 z*2&*Yg-uWU$5|W>>g5h)Fn{3B={`skAJ5_wXB5pDwyj{vG1_{{Y-`wB_i^B!5PA|= zrx=_>rprb&75BQ=J)SKPAJI;?(D#46)o+a?SsR^-&qJjXY2ER8S*1ZvU`t7~M6?NKULuzlAZ8C#X9>8j2;WDY z(TY-^!`&0%67`u|U_-Y(knWVcSlh-kwZQ6KG@S?L`W!iVl>Gyd(LnpMc@C!QeY{(E z)uAwF_CcqH#00}jer2dQk3}R|p^87XCxR8`n4c@g9rASTt9$8}SuGW!!+QQ&w&G!P zvv5Mft<&pzv^&XuuQAj&ieoa*3nI-hx}0`4kym=(cd>?v6yM3v43y@5@;yPeJ_N{@ z622W$@5Z4VqliMF3GAf_RcB;$HX^%cwTCgxg^4)5I0?*&oW|giBB@nUNBO+IX=iON zo~;L}HOwhyeqH4GHvAQ5i=|0c+_5*661aDyT_tr=I#+Zog%!9nRiuBb8m&SS4qp2fv7HJMG zwJFuqV*Hoq3`|Mayml;So|9W4Um6Lu8(k+(Hc2}p@&>?!7!7H~9*O%@BrKNAOa-~e z$e6#G)fJ+wNz5x9zU;#>&V}d z?!F1W_eNN;&LI9$!kWa0Zqa)0CVM4D=x(r>aXgW=XQ)PTRsJJ&MC?WjjoMwLRh`-I z8yD|^&(r#NU|pRpRF%wn&t%X`)8HQe%uxEKnXxIu9yui1s$eH0*YZ^Wvt25yOg6{5 zPefKstjqam-PRDz=&-BVb^xZe>{C{$cza!_sV&3M*l0ocMJVr!l~TlJi4JChDn9Nn zc&la1caY}0P&Ho=r;)l;mKBf$V<6A*R6XC}s98g%I7ZIAFI=e6SqQ4;oevw)nw0%^ zKq9#$;{3R0zJv}#mr7@}e+5-(`{C?^vEE#xb7uBY=X#_1v+@~@l?W@Zaq+Yo9bpu& zR<0us_T`(Q6qp1xYb)Rq;tJ|aTZ&y5xqx<_j-|>1$SEi@3!A|| z9YH<3ub_#ai=2WG_V9iQ!NU8mB|$4ZK3Gr>_s15;6W-XV-*##3TjwoMP&yb zq!L{!sQoUn<_ZWb)BbzloM2Zs1tb=+FBn*$!EQmp3Ml#oe;g0);^XP&_osni`NR1A z0SL>FG{F)8;h%d#4-g0eK+%&0UD-=ghUr~yDQ?!lNE5tKiJ_rjY{@`Q1vjbVAFU;|?Qs;w|1hFx_ z`*jR7rVAU>9*yRSpD1)#aOb!)@ak(5hk;guG$_9)=K8Ie^uOP<63|FjrX2UEcJw07 zD5c?bxHD${?)1+CMgPg@0|kH>4NzJZO*;#rl-xA_8*SHCS}ygKZP7*uHbRtmaTE%n zp7Vt7QIt|IIN?)fyS#8IxKHO$?TeY{DpQl5^kyAd$HH^Aa)SJC+I0!ULR znF7*z6R6~{CCW6M^qKuU!N`I`>YB3i6toA7f7#3%T&$5&wm0nY{&d9(g)LB$%g9dX zf>HfjVn9;)rG-^=)tiGDd<5M4wDHPl@yEGU_whSh78l$%S*WCqjvj^Xt?_VKp0T{pQGU!F;?_^4EMT$__$E zH0hMGQlo@W2p^_tPZsnirl@pGb<#0a^*g5ihYtSzKKx%Wg;i4h8B_c6Z+PPWM!I%g zOr-dLp|0@RV@@&InVrwRJfPT~ZY840gT$Jl4)HP^qcTUWE~1&}C2wS3Sv9pJWiRva zyK}a9ilnrYe7SB$bu~GF&GM`D1h@ukNsJY|Yt>|?q(4gzgSUuGwSIfsmlD)%J2V0@ zTU&-58&x%P)-#Oev2~&}bv^wwRbD$?Enu(jJiuwM3shGOZ{$juY+RGk#m^`!p7+vO zAjWFn1{dq`T?N^TggHmN3~VGf^5?a_)R-cj5yfk-?V<|S)%uKn{YGL)7(~eAhWA56 zj7ZS7amp#qQM;t>%6F)v{1S-Gq>88IPiL?2X9=q_r$vhc4{Pd3$WssBMbZaV2W zu&8||{U99-3!x+JudoA1KSAx^0qg$*YLr)FKtJ($lC@k)W?khPY!~B&3F~Xnxs_WH)b*(MC{~@>r={U4@A6+2p8il>0lojdT`r8~C>rA6;jw^lZK9gk<_y!v za(Rbclc{1;TFBtT`lr|YO0}|UXzh>FLsx6RQUq8=?V4{NR#=oxL2}kHb-ZAfuN Date: Mon, 11 Nov 2024 13:20:06 -0500 Subject: [PATCH 018/201] Updates settings.gradle Signed-off-by: Darshit Chanpura --- settings.gradle | 3 --- 1 file changed, 3 deletions(-) diff --git a/settings.gradle b/settings.gradle index 0bb3c5639d..1c3e7ff5aa 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,6 +5,3 @@ */ rootProject.name = 'opensearch-security' - -include "sample-resource-plugin" -project(":sample-resource-plugin").name = "opensearch-sample-resource-plugin" From 561e294d87e57fb6fef680eb5482e9c17ca0bff4 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 11 Nov 2024 13:23:30 -0500 Subject: [PATCH 019/201] Adds a sample resource plugin to demonstrate resource access control in action Signed-off-by: Darshit Chanpura --- sample-resource-plugin/build.gradle | 166 ++++++++++++++++ .../java/org/opensearch/sample/Resource.java | 19 ++ .../sample/SampleResourcePlugin.java | 182 ++++++++++++++++++ .../sample/SampleResourceScope.java | 33 ++++ .../actions/create/CreateResourceAction.java | 29 +++ .../actions/create/CreateResourceRequest.java | 50 +++++ .../create/CreateResourceResponse.java | 55 ++++++ .../create/CreateResourceRestAction.java | 55 ++++++ .../sample/actions/create/SampleResource.java | 56 ++++++ .../list/ListAccessibleResourcesAction.java | 29 +++ .../list/ListAccessibleResourcesRequest.java | 39 ++++ .../list/ListAccessibleResourcesResponse.java | 46 +++++ .../ListAccessibleResourcesRestAction.java | 44 +++++ .../actions/share/ShareResourceAction.java | 26 +++ .../actions/share/ShareResourceRequest.java | 52 +++++ .../actions/share/ShareResourceResponse.java | 52 +++++ .../share/ShareResourceRestAction.java | 51 +++++ .../verify/VerifyResourceAccessAction.java | 25 +++ .../verify/VerifyResourceAccessRequest.java | 69 +++++++ .../verify/VerifyResourceAccessResponse.java | 52 +++++ .../VerifyResourceAccessRestAction.java | 52 +++++ .../CreateResourceTransportAction.java | 99 ++++++++++ ...istAccessibleResourcesTransportAction.java | 56 ++++++ .../ShareResourceTransportAction.java | 65 +++++++ .../VerifyResourceAccessTransportAction.java | 58 ++++++ .../plugin-metadata/plugin-security.policy | 3 + .../org.opensearch.plugins.ResourcePlugin | 1 + .../test/resources/security/esnode-key.pem | 28 +++ .../src/test/resources/security/esnode.pem | 25 +++ .../src/test/resources/security/kirk-key.pem | 28 +++ .../src/test/resources/security/kirk.pem | 27 +++ .../src/test/resources/security/root-ca.pem | 28 +++ .../src/test/resources/security/sample.pem | 25 +++ .../src/test/resources/security/test-kirk.jks | Bin 0 -> 3766 bytes settings.gradle | 3 + 35 files changed, 1628 insertions(+) create mode 100644 sample-resource-plugin/build.gradle create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java create mode 100644 sample-resource-plugin/src/main/plugin-metadata/plugin-security.policy create mode 100644 sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin create mode 100644 sample-resource-plugin/src/test/resources/security/esnode-key.pem create mode 100644 sample-resource-plugin/src/test/resources/security/esnode.pem create mode 100644 sample-resource-plugin/src/test/resources/security/kirk-key.pem create mode 100644 sample-resource-plugin/src/test/resources/security/kirk.pem create mode 100644 sample-resource-plugin/src/test/resources/security/root-ca.pem create mode 100644 sample-resource-plugin/src/test/resources/security/sample.pem create mode 100644 sample-resource-plugin/src/test/resources/security/test-kirk.jks diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle new file mode 100644 index 0000000000..e9822c1f22 --- /dev/null +++ b/sample-resource-plugin/build.gradle @@ -0,0 +1,166 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'opensearch.opensearchplugin' +apply plugin: 'opensearch.testclusters' +apply plugin: 'opensearch.java-rest-test' + +import org.opensearch.gradle.test.RestIntegTestTask + + +opensearchplugin { + name 'opensearch-sample-resource-plugin' + description 'Sample plugin that extends OpenSearch Resource Plugin' + classname 'org.opensearch.sample.SampleResourcePlugin' +} + +ext { + projectSubstitutions = [:] + licenseFile = rootProject.file('LICENSE.txt') + noticeFile = rootProject.file('NOTICE.txt') +} + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } +} + +dependencies { +} + +def es_tmp_dir = rootProject.file('build/private/es_tmp').absoluteFile +es_tmp_dir.mkdirs() + +File repo = file("$buildDir/testclusters/repo") +def _numNodes = findProperty('numNodes') as Integer ?: 1 + +licenseHeaders.enabled = true +validateNebulaPom.enabled = false +testingConventions.enabled = false +loggerUsageCheck.enabled = false + +javaRestTest.dependsOn(rootProject.assemble) +javaRestTest { + systemProperty 'tests.security.manager', 'false' +} +testClusters.javaRestTest { + testDistribution = 'INTEG_TEST' +} + +task integTest(type: RestIntegTestTask) { + description = "Run tests against a cluster" + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath +} +tasks.named("check").configure { dependsOn(integTest) } + +integTest { + if (project.hasProperty('excludeTests')) { + project.properties['excludeTests']?.replaceAll('\\s', '')?.split('[,;]')?.each { + exclude "${it}" + } + } + systemProperty 'tests.security.manager', 'false' + systemProperty 'java.io.tmpdir', es_tmp_dir.absolutePath + + systemProperty "https", System.getProperty("https") + systemProperty "user", System.getProperty("user") + systemProperty "password", System.getProperty("password") + // Tell the test JVM if the cluster JVM is running under a debugger so that tests can use longer timeouts for + // requests. The 'doFirst' delays reading the debug setting on the cluster till execution time. + doFirst { + // Tell the test JVM if the cluster JVM is running under a debugger so that tests can + // use longer timeouts for requests. + def isDebuggingCluster = getDebug() || System.getProperty("test.debug") != null + systemProperty 'cluster.debug', isDebuggingCluster + // Set number of nodes system property to be used in tests + systemProperty 'cluster.number_of_nodes', "${_numNodes}" + // There seems to be an issue when running multi node run or integ tasks with unicast_hosts + // not being written, the waitForAllConditions ensures it's written + getClusters().forEach { cluster -> + cluster.waitForAllConditions() + } + } + + // The -Dcluster.debug option makes the cluster debuggable; this makes the tests debuggable + if (System.getProperty("test.debug") != null) { + jvmArgs '-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=8000' + } + if (System.getProperty("tests.rest.bwcsuite") == null) { + filter { + excludeTestsMatching "org.opensearch.security.sampleextension.bwc.*IT" + } + } +} +project.getTasks().getByName('bundlePlugin').dependsOn(rootProject.tasks.getByName('build')) +Zip bundle = (Zip) project.getTasks().getByName("bundlePlugin"); +Zip rootBundle = (Zip) rootProject.getTasks().getByName("bundlePlugin"); +integTest.dependsOn(bundle) +integTest.getClusters().forEach{c -> { + c.plugin(rootProject.getObjects().fileProperty().value(rootBundle.getArchiveFile())) + c.plugin(project.getObjects().fileProperty().value(bundle.getArchiveFile())) +}} + +testClusters.integTest { + testDistribution = 'INTEG_TEST' + + // Cluster shrink exception thrown if we try to set numberOfNodes to 1, so only apply if > 1 + if (_numNodes > 1) numberOfNodes = _numNodes + // When running integration tests it doesn't forward the --debug-jvm to the cluster anymore + // i.e. we have to use a custom property to flag when we want to debug OpenSearch JVM + // since we also support multi node integration tests we increase debugPort per node + if (System.getProperty("cluster.debug") != null) { + def debugPort = 5005 + nodes.forEach { node -> + node.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=*:${debugPort}") + debugPort += 1 + } + } + setting 'path.repo', repo.absolutePath +} + +afterEvaluate { + testClusters.integTest.nodes.each { node -> + def plugins = node.plugins + def firstPlugin = plugins.get(0) + if (firstPlugin.provider == project.bundlePlugin.archiveFile) { + plugins.remove(0) + plugins.add(firstPlugin) + } + + node.extraConfigFile("kirk.pem", file("src/test/resources/security/kirk.pem")) + node.extraConfigFile("kirk-key.pem", file("src/test/resources/security/kirk-key.pem")) + node.extraConfigFile("esnode.pem", file("src/test/resources/security/esnode.pem")) + node.extraConfigFile("esnode-key.pem", file("src/test/resources/security/esnode-key.pem")) + node.extraConfigFile("root-ca.pem", file("src/test/resources/security/root-ca.pem")) + node.setting("plugins.security.ssl.transport.pemcert_filepath", "esnode.pem") + node.setting("plugins.security.ssl.transport.pemkey_filepath", "esnode-key.pem") + node.setting("plugins.security.ssl.transport.pemtrustedcas_filepath", "root-ca.pem") + node.setting("plugins.security.ssl.transport.enforce_hostname_verification", "false") + node.setting("plugins.security.ssl.http.enabled", "true") + node.setting("plugins.security.ssl.http.pemcert_filepath", "esnode.pem") + node.setting("plugins.security.ssl.http.pemkey_filepath", "esnode-key.pem") + node.setting("plugins.security.ssl.http.pemtrustedcas_filepath", "root-ca.pem") + node.setting("plugins.security.allow_unsafe_democertificates", "true") + node.setting("plugins.security.allow_default_init_securityindex", "true") + node.setting("plugins.security.authcz.admin_dn", "\n - CN=kirk,OU=client,O=client,L=test,C=de") + node.setting("plugins.security.audit.type", "internal_opensearch") + node.setting("plugins.security.enable_snapshot_restore_privilege", "true") + node.setting("plugins.security.check_snapshot_restore_write_privileges", "true") + node.setting("plugins.security.restapi.roles_enabled", "[\"all_access\", \"security_rest_api_access\"]") + } +} + +run { + doFirst { + // There seems to be an issue when running multi node run or integ tasks with unicast_hosts + // not being written, the waitForAllConditions ensures it's written + getClusters().forEach { cluster -> + cluster.waitForAllConditions() + } + } + useCluster testClusters.integTest +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java new file mode 100644 index 0000000000..36e74f1624 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java @@ -0,0 +1,19 @@ +/* + * 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.sample; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.xcontent.ToXContentFragment; + +public abstract class Resource implements NamedWriteable, ToXContentFragment { + protected abstract String getResourceIndex(); +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java new file mode 100644 index 0000000000..74a8378887 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -0,0 +1,182 @@ +/* + * Copyright OpenSearch Contributors + * 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. + */ +package org.opensearch.sample; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.accesscontrol.resources.ResourceService; +import org.opensearch.action.ActionRequest; +import org.opensearch.client.Client; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.node.DiscoveryNodes; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.lifecycle.Lifecycle; +import org.opensearch.common.lifecycle.LifecycleComponent; +import org.opensearch.common.lifecycle.LifecycleListener; +import org.opensearch.common.settings.ClusterSettings; +import org.opensearch.common.settings.IndexScopedSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.settings.SettingsFilter; +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.NamedWriteableRegistry; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.env.Environment; +import org.opensearch.env.NodeEnvironment; +import org.opensearch.indices.SystemIndexDescriptor; +import org.opensearch.plugins.ActionPlugin; +import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.ResourcePlugin; +import org.opensearch.plugins.SystemIndexPlugin; +import org.opensearch.repositories.RepositoriesService; +import org.opensearch.rest.RestController; +import org.opensearch.rest.RestHandler; +import org.opensearch.sample.actions.create.CreateResourceAction; +import org.opensearch.sample.actions.create.CreateResourceRestAction; +import org.opensearch.sample.actions.list.ListAccessibleResourcesAction; +import org.opensearch.sample.actions.list.ListAccessibleResourcesRestAction; +import org.opensearch.sample.actions.share.ShareResourceAction; +import org.opensearch.sample.actions.share.ShareResourceRestAction; +import org.opensearch.sample.actions.verify.VerifyResourceAccessAction; +import org.opensearch.sample.actions.verify.VerifyResourceAccessRestAction; +import org.opensearch.sample.transport.CreateResourceTransportAction; +import org.opensearch.sample.transport.ListAccessibleResourcesTransportAction; +import org.opensearch.sample.transport.ShareResourceTransportAction; +import org.opensearch.sample.transport.VerifyResourceAccessTransportAction; +import org.opensearch.script.ScriptService; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.watcher.ResourceWatcherService; + +/** + * Sample Resource plugin. + * It uses ".sample_resources" index to manage its resources, and exposes a REST API + * + */ +public class SampleResourcePlugin extends Plugin implements ActionPlugin, SystemIndexPlugin, ResourcePlugin { + private static final Logger log = LogManager.getLogger(SampleResourcePlugin.class); + + public static final String RESOURCE_INDEX_NAME = ".sample_resource_sharing_plugin"; + + public final static Map INDEX_SETTINGS = Map.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); + + private Client client; + + @Override + public Collection createComponents( + Client client, + ClusterService clusterService, + ThreadPool threadPool, + ResourceWatcherService resourceWatcherService, + ScriptService scriptService, + NamedXContentRegistry xContentRegistry, + Environment environment, + NodeEnvironment nodeEnvironment, + NamedWriteableRegistry namedWriteableRegistry, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier repositoriesServiceSupplier + ) { + this.client = client; + log.info("Loaded SampleResourcePlugin components."); + return Collections.emptyList(); + } + + @Override + public List getRestHandlers( + Settings settings, + RestController restController, + ClusterSettings clusterSettings, + IndexScopedSettings indexScopedSettings, + SettingsFilter settingsFilter, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier nodesInCluster + ) { + return List.of( + new CreateResourceRestAction(), + new ListAccessibleResourcesRestAction(), + new VerifyResourceAccessRestAction(), + new ShareResourceRestAction() + ); + } + + @Override + public List> getActions() { + return List.of( + new ActionHandler<>(CreateResourceAction.INSTANCE, CreateResourceTransportAction.class), + new ActionHandler<>(ListAccessibleResourcesAction.INSTANCE, ListAccessibleResourcesTransportAction.class), + new ActionHandler<>(ShareResourceAction.INSTANCE, ShareResourceTransportAction.class), + new ActionHandler<>(VerifyResourceAccessAction.INSTANCE, VerifyResourceAccessTransportAction.class) + ); + } + + @Override + public Collection getSystemIndexDescriptors(Settings settings) { + final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(RESOURCE_INDEX_NAME, "Example index with resources"); + return Collections.singletonList(systemIndexDescriptor); + } + + @Override + public String getResourceType() { + return ""; + } + + @Override + public String getResourceIndex() { + return RESOURCE_INDEX_NAME; + } + + @Override + public Collection> getGuiceServiceClasses() { + final List> services = new ArrayList<>(1); + services.add(GuiceHolder.class); + return services; + } + + public static class GuiceHolder implements LifecycleComponent { + + private static ResourceService resourceService; + + @Inject + public GuiceHolder(final ResourceService resourceService) { + GuiceHolder.resourceService = resourceService; + } + + public static ResourceService getResourceService() { + return resourceService; + } + + @Override + public void close() {} + + @Override + public Lifecycle.State lifecycleState() { + return null; + } + + @Override + public void addLifecycleListener(LifecycleListener listener) {} + + @Override + public void removeLifecycleListener(LifecycleListener listener) {} + + @Override + public void start() {} + + @Override + public void stop() {} + + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java new file mode 100644 index 0000000000..90df0d3764 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java @@ -0,0 +1,33 @@ +/* + * 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.sample; + +import org.opensearch.accesscontrol.resources.ResourceAccessScope; + +/** + * This class demonstrates a sample implementation of Basic Access Scopes to fit each plugin's use-case. + * The plugin then uses this scope when seeking access evaluation for a user on a particular resource. + */ +public enum SampleResourceScope implements ResourceAccessScope { + + SAMPLE_FULL_ACCESS("sample_full_access"); + + private final String name; + + SampleResourceScope(String scopeName) { + this.name = scopeName; + } + + public String getName() { + return name; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java new file mode 100644 index 0000000000..e7c02278ab --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.create; + +import org.opensearch.action.ActionType; + +/** + * Action to create a sample resource + */ +public class CreateResourceAction extends ActionType { + /** + * Create sample resource action instance + */ + public static final CreateResourceAction INSTANCE = new CreateResourceAction(); + /** + * Create sample resource action name + */ + public static final String NAME = "cluster:admin/sample-resource-plugin/create"; + + private CreateResourceAction() { + super(NAME, CreateResourceResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java new file mode 100644 index 0000000000..b31a4b7f2b --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java @@ -0,0 +1,50 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.create; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.sample.Resource; + +/** + * Request object for CreateSampleResource transport action + */ +public class CreateResourceRequest extends ActionRequest { + + private final Resource resource; + + /** + * Default constructor + */ + public CreateResourceRequest(Resource resource) { + this.resource = resource; + } + + public CreateResourceRequest(StreamInput in) throws IOException { + this.resource = in.readNamedWriteable(Resource.class); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + resource.writeTo(out); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public Resource getResource() { + return this.resource; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java new file mode 100644 index 0000000000..6b966ed08d --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.create; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +/** + * Response to a CreateSampleResourceRequest + */ +public class CreateResourceResponse extends ActionResponse implements ToXContentObject { + private final String message; + + /** + * Default constructor + * + * @param message The message + */ + public CreateResourceResponse(String message) { + this.message = message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(message); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public CreateResourceResponse(final StreamInput in) throws IOException { + message = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("message", message); + builder.endObject(); + return builder; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java new file mode 100644 index 0000000000..86346cc279 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.create; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.POST; + +public class CreateResourceRestAction extends BaseRestHandler { + + public CreateResourceRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/resource")); + } + + @Override + public String getName() { + return "create_sample_resource"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String name = (String) source.get("name"); + SampleResource resource = new SampleResource(); + resource.setName(name); + final CreateResourceRequest createSampleResourceRequest = new CreateResourceRequest(resource); + return channel -> client.executeLocally( + CreateResourceAction.INSTANCE, + createSampleResourceRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java new file mode 100644 index 0000000000..1566abfe69 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java @@ -0,0 +1,56 @@ +/* + * 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.sample.actions.create; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.sample.Resource; + +import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; + +public class SampleResource extends Resource { + + private String name; + + public SampleResource() {} + + SampleResource(StreamInput in) throws IOException { + this.name = in.readString(); + } + + @Override + public String getResourceIndex() { + return RESOURCE_INDEX_NAME; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().field("name", name).endObject(); + } + + @Override + public void writeTo(StreamOutput streamOutput) throws IOException { + streamOutput.writeString(name); + } + + @Override + public String getWriteableName() { + return "sample_resource"; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java new file mode 100644 index 0000000000..b4e9e29e22 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.list; + +import org.opensearch.action.ActionType; + +/** + * Action to list sample resources + */ +public class ListAccessibleResourcesAction extends ActionType { + /** + * List sample resource action instance + */ + public static final ListAccessibleResourcesAction INSTANCE = new ListAccessibleResourcesAction(); + /** + * List sample resource action name + */ + public static final String NAME = "cluster:admin/sample-resource-plugin/list"; + + private ListAccessibleResourcesAction() { + super(NAME, ListAccessibleResourcesResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java new file mode 100644 index 0000000000..b4c0961774 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java @@ -0,0 +1,39 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.list; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +/** + * Request object for ListSampleResource transport action + */ +public class ListAccessibleResourcesRequest extends ActionRequest { + + public ListAccessibleResourcesRequest() {} + + /** + * Constructor with stream input + * @param in the stream input + * @throws IOException IOException + */ + public ListAccessibleResourcesRequest(final StreamInput in) throws IOException {} + + @Override + public void writeTo(final StreamOutput out) throws IOException {} + + @Override + public ActionRequestValidationException validate() { + return null; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java new file mode 100644 index 0000000000..47a8f88e4e --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java @@ -0,0 +1,46 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.list; + +import java.io.IOException; +import java.util.List; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +/** + * Response to a ListAccessibleResourcesRequest + */ +public class ListAccessibleResourcesResponse extends ActionResponse implements ToXContentObject { + private final List resourceIds; + + public ListAccessibleResourcesResponse(List resourceIds) { + this.resourceIds = resourceIds; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeStringArray(resourceIds.toArray(new String[0])); + } + + public ListAccessibleResourcesResponse(final StreamInput in) throws IOException { + resourceIds = in.readStringList(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("resource-ids", resourceIds); + builder.endObject(); + return builder; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java new file mode 100644 index 0000000000..bb921fce00 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java @@ -0,0 +1,44 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.list; + +import java.util.List; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.GET; + +public class ListAccessibleResourcesRestAction extends BaseRestHandler { + + public ListAccessibleResourcesRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/resource")); + } + + @Override + public String getName() { + return "list_sample_resources"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + final ListAccessibleResourcesRequest listAccessibleResourcesRequest = new ListAccessibleResourcesRequest(); + return channel -> client.executeLocally( + ListAccessibleResourcesAction.INSTANCE, + listAccessibleResourcesRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.java new file mode 100644 index 0000000000..d362b1927c --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.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. + */ + +package org.opensearch.sample.actions.share; + +import org.opensearch.action.ActionType; + +public class ShareResourceAction extends ActionType { + /** + * List sample resource action instance + */ + public static final ShareResourceAction INSTANCE = new ShareResourceAction(); + /** + * List sample resource action name + */ + public static final String NAME = "cluster:admin/sample-resource-plugin/share"; + + private ShareResourceAction() { + super(NAME, ShareResourceResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java new file mode 100644 index 0000000000..01866fd516 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java @@ -0,0 +1,52 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.share; + +import java.io.IOException; + +import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +public class ShareResourceRequest extends ActionRequest { + + private final String resourceId; + private final ShareWith shareWith; + + public ShareResourceRequest(String resourceId, ShareWith shareWith) { + this.resourceId = resourceId; + this.shareWith = shareWith; + } + + public ShareResourceRequest(StreamInput in) throws IOException { + this.resourceId = in.readString(); + this.shareWith = in.readNamedWriteable(ShareWith.class); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(resourceId); + out.writeNamedWriteable(shareWith); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceId() { + return resourceId; + } + + public ShareWith getShareWith() { + return shareWith; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java new file mode 100644 index 0000000000..a6a85d206d --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java @@ -0,0 +1,52 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.share; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +public class ShareResourceResponse extends ActionResponse implements ToXContentObject { + private final String message; + + /** + * Default constructor + * + * @param message The message + */ + public ShareResourceResponse(String message) { + this.message = message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(message); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public ShareResourceResponse(final StreamInput in) throws IOException { + message = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("message", message); + builder.endObject(); + return builder; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java new file mode 100644 index 0000000000..347fb49e68 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java @@ -0,0 +1,51 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.share; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.GET; + +public class ShareResourceRestAction extends BaseRestHandler { + + public ShareResourceRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/share/{resource_id}")); + } + + @Override + public String getName() { + return "share_sample_resources"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String resourceId = (String) source.get("resource_id"); + ShareWith shareWith = (ShareWith) source.get("share_with"); + final ShareResourceRequest shareResourceRequest = new ShareResourceRequest(resourceId, shareWith); + return channel -> client.executeLocally(ShareResourceAction.INSTANCE, shareResourceRequest, new RestToXContentListener<>(channel)); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java new file mode 100644 index 0000000000..1378d561f5 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.verify; + +import org.opensearch.action.ActionType; + +/** + * Action to verify resource access for current user + */ +public class VerifyResourceAccessAction extends ActionType { + + public static final VerifyResourceAccessAction INSTANCE = new VerifyResourceAccessAction(); + + public static final String NAME = "cluster:admin/sample-resource-plugin/verify/resource_access"; + + private VerifyResourceAccessAction() { + super(NAME, VerifyResourceAccessResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java new file mode 100644 index 0000000000..e9b20118db --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java @@ -0,0 +1,69 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.verify; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +public class VerifyResourceAccessRequest extends ActionRequest { + + private final String resourceId; + + private final String sourceIdx; + + private final String scope; + + /** + * Default constructor + */ + public VerifyResourceAccessRequest(String resourceId, String sourceIdx, String scope) { + this.resourceId = resourceId; + this.sourceIdx = sourceIdx; + this.scope = scope; + } + + /** + * Constructor with stream input + * @param in the stream input + * @throws IOException IOException + */ + public VerifyResourceAccessRequest(final StreamInput in) throws IOException { + this.resourceId = in.readString(); + this.sourceIdx = in.readString(); + this.scope = in.readString(); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(resourceId); + out.writeString(sourceIdx); + out.writeString(scope); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceId() { + return resourceId; + } + + public String getSourceIdx() { + return sourceIdx; + } + + public String getScope() { + return scope; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java new file mode 100644 index 0000000000..660ac03f71 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java @@ -0,0 +1,52 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.verify; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +public class VerifyResourceAccessResponse extends ActionResponse implements ToXContentObject { + private final String message; + + /** + * Default constructor + * + * @param message The message + */ + public VerifyResourceAccessResponse(String message) { + this.message = message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(message); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public VerifyResourceAccessResponse(final StreamInput in) throws IOException { + message = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("message", message); + builder.endObject(); + return builder; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java new file mode 100644 index 0000000000..34bfed4e9f --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java @@ -0,0 +1,52 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.verify; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.POST; + +public class VerifyResourceAccessRestAction extends BaseRestHandler { + + public VerifyResourceAccessRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/verify_resource_access")); + } + + @Override + public String getName() { + return "verify_resource_access"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String resourceIdx = (String) source.get("resource_idx"); + String sourceIdx = (String) source.get("source_idx"); + String scope = (String) source.get("scope"); + + // final CreateResourceRequest createSampleResourceRequest = new CreateResourceRequest<>(resource); + return channel -> client.executeLocally(VerifyResourceAccessAction.INSTANCE, null, new RestToXContentListener<>(channel)); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java new file mode 100644 index 0000000000..8bff7b44a3 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java @@ -0,0 +1,99 @@ +/* + * 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. + */ + +package org.opensearch.sample.transport; + +import java.io.IOException; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.accesscontrol.resources.ResourceService; +import org.opensearch.accesscontrol.resources.ResourceSharing; +import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.accesscontrol.resources.SharedWithScope; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.index.IndexResponse; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.sample.Resource; +import org.opensearch.sample.SampleResourcePlugin; +import org.opensearch.sample.SampleResourceScope; +import org.opensearch.sample.actions.create.CreateResourceAction; +import org.opensearch.sample.actions.create.CreateResourceRequest; +import org.opensearch.sample.actions.create.CreateResourceResponse; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; + +/** + * Transport action for CreateSampleResource. + */ +public class CreateResourceTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(CreateResourceTransportAction.class); + + private final TransportService transportService; + private final Client nodeClient; + + @Inject + public CreateResourceTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { + super(CreateResourceAction.NAME, transportService, actionFilters, CreateResourceRequest::new); + this.transportService = transportService; + this.nodeClient = nodeClient; + } + + @Override + protected void doExecute(Task task, CreateResourceRequest request, ActionListener listener) { + try (ThreadContext.StoredContext ignore = transportService.getThreadPool().getThreadContext().stashContext()) { + createResource(request, listener); + listener.onResponse(new CreateResourceResponse("Resource " + request.getResource() + " created successfully.")); + } catch (Exception e) { + log.info("Failed to create resource", e); + listener.onFailure(e); + } + } + + private void createResource(CreateResourceRequest request, ActionListener listener) { + Resource sample = request.getResource(); + try { + IndexRequest ir = nodeClient.prepareIndex(RESOURCE_INDEX_NAME) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .setSource(sample.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) + .request(); + + log.warn("Index Request: {}", ir.toString()); + + ActionListener irListener = getIndexResponseActionListener(listener); + nodeClient.index(ir, irListener); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static ActionListener getIndexResponseActionListener(ActionListener listener) { + SharedWithScope.SharedWithPerScope sharedWithPerScope = new SharedWithScope.SharedWithPerScope(List.of(), List.of(), List.of()); + SharedWithScope sharedWithScope = new SharedWithScope(SampleResourceScope.SAMPLE_FULL_ACCESS.getName(), sharedWithPerScope); + ShareWith shareWith = new ShareWith(List.of(sharedWithScope)); + return ActionListener.wrap(idxResponse -> { + log.info("Created resource: {}", idxResponse.toString()); + ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); + ResourceSharing sharing = rs.getResourceAccessControlPlugin().shareWith(idxResponse.getId(), idxResponse.getIndex(), shareWith); + log.info("Created resource sharing entry: {}", sharing.toString()); + }, listener::onFailure); + } + +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java new file mode 100644 index 0000000000..d56eb6d291 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package org.opensearch.sample.transport; + +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.accesscontrol.resources.ResourceService; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.SampleResourcePlugin; +import org.opensearch.sample.actions.list.ListAccessibleResourcesAction; +import org.opensearch.sample.actions.list.ListAccessibleResourcesRequest; +import org.opensearch.sample.actions.list.ListAccessibleResourcesResponse; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; + +/** + * Transport action for ListSampleResource. + */ +public class ListAccessibleResourcesTransportAction extends HandledTransportAction< + ListAccessibleResourcesRequest, + ListAccessibleResourcesResponse> { + private static final Logger log = LogManager.getLogger(ListAccessibleResourcesTransportAction.class); + + @Inject + public ListAccessibleResourcesTransportAction(TransportService transportService, ActionFilters actionFilters) { + super(ListAccessibleResourcesAction.NAME, transportService, actionFilters, ListAccessibleResourcesRequest::new); + } + + @Override + protected void doExecute(Task task, ListAccessibleResourcesRequest request, ActionListener listener) { + try { + ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); + List resourceIds = rs.getResourceAccessControlPlugin().listAccessibleResourcesInPlugin(RESOURCE_INDEX_NAME); + log.info("Successfully fetched accessible resources for current user"); + listener.onResponse(new ListAccessibleResourcesResponse(resourceIds)); + } catch (Exception e) { + log.info("Failed to list accessible resources for current user: ", e); + listener.onFailure(e); + } + + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java new file mode 100644 index 0000000000..ccbfc31b78 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java @@ -0,0 +1,65 @@ +/* + * 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. + */ + +package org.opensearch.sample.transport; + +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.accesscontrol.resources.ResourceService; +import org.opensearch.accesscontrol.resources.ResourceSharing; +import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.SampleResourcePlugin; +import org.opensearch.sample.actions.share.ShareResourceAction; +import org.opensearch.sample.actions.share.ShareResourceRequest; +import org.opensearch.sample.actions.share.ShareResourceResponse; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; + +/** + * Transport action for CreateSampleResource. + */ +public class ShareResourceTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(ShareResourceTransportAction.class); + + @Inject + public ShareResourceTransportAction(TransportService transportService, ActionFilters actionFilters) { + super(ShareResourceAction.NAME, transportService, actionFilters, ShareResourceRequest::new); + } + + @Override + protected void doExecute(Task task, ShareResourceRequest request, ActionListener listener) { + try { + shareResource(request); + listener.onResponse(new ShareResourceResponse("Resource " + request.getResourceId() + " shared successfully.")); + } catch (Exception e) { + listener.onFailure(e); + } + } + + private void shareResource(ShareResourceRequest request) { + try { + ShareWith shareWith = new ShareWith(List.of()); + ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); + ResourceSharing sharing = rs.getResourceAccessControlPlugin() + .shareWith(request.getResourceId(), RESOURCE_INDEX_NAME, shareWith); + log.info("Shared resource : {} with {}", request.getResourceId(), sharing.toString()); + } catch (Exception e) { + log.info("Failed to share resource {}", request.getResourceId(), e); + throw e; + } + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java new file mode 100644 index 0000000000..947dcec59e --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java @@ -0,0 +1,58 @@ +/* + * 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. + */ + +package org.opensearch.sample.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.accesscontrol.resources.ResourceService; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.SampleResourcePlugin; +import org.opensearch.sample.actions.verify.VerifyResourceAccessAction; +import org.opensearch.sample.actions.verify.VerifyResourceAccessRequest; +import org.opensearch.sample.actions.verify.VerifyResourceAccessResponse; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +public class VerifyResourceAccessTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(VerifyResourceAccessTransportAction.class); + + @Inject + public VerifyResourceAccessTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { + super(VerifyResourceAccessAction.NAME, transportService, actionFilters, VerifyResourceAccessRequest::new); + } + + @Override + protected void doExecute(Task task, VerifyResourceAccessRequest request, ActionListener listener) { + try { + ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); + boolean hasRequestedScopeAccess = rs.getResourceAccessControlPlugin() + .hasPermission(request.getResourceId(), request.getSourceIdx(), request.getScope()); + + StringBuilder sb = new StringBuilder(); + sb.append("User does"); + sb.append(hasRequestedScopeAccess ? " " : " not "); + sb.append("have requested scope "); + sb.append(request.getScope()); + sb.append(" access to "); + sb.append(request.getResourceId()); + + log.info(sb.toString()); + listener.onResponse(new VerifyResourceAccessResponse(sb.toString())); + } catch (Exception e) { + log.info("Failed to check user permissions for resource {}", request.getResourceId(), e); + listener.onFailure(e); + } + } + +} diff --git a/sample-resource-plugin/src/main/plugin-metadata/plugin-security.policy b/sample-resource-plugin/src/main/plugin-metadata/plugin-security.policy new file mode 100644 index 0000000000..a5dfc33a87 --- /dev/null +++ b/sample-resource-plugin/src/main/plugin-metadata/plugin-security.policy @@ -0,0 +1,3 @@ +grant { + permission java.lang.RuntimePermission "getClassLoader"; +}; \ No newline at end of file diff --git a/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin b/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin new file mode 100644 index 0000000000..1ca89eaf74 --- /dev/null +++ b/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin @@ -0,0 +1 @@ +org.opensearch.sample.SampleResourcePlugin \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/esnode-key.pem b/sample-resource-plugin/src/test/resources/security/esnode-key.pem new file mode 100644 index 0000000000..e90562be43 --- /dev/null +++ b/sample-resource-plugin/src/test/resources/security/esnode-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCm93kXteDQHMAv +bUPNPW5pyRHKDD42XGWSgq0k1D29C/UdyL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0 +o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0HGkn47XVu3EwbfrTENg3jFu+Oem6a/50 +1SzITzJWtS0cn2dIFOBimTVpT/4Zv5qrXA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1 +MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8ndibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b +6l+KLo3IKpfTbAIJXIO+M67FLtWKtttDao94B069skzKk6FPgW/OZh6PRCD0oxOa +vV+ld2SjAgMBAAECggEAQK1+uAOZeaSZggW2jQut+MaN4JHLi61RH2cFgU3COLgo +FIiNjFn8f2KKU3gpkt1It8PjlmprpYut4wHI7r6UQfuv7ZrmncRiPWHm9PB82+ZQ +5MXYqj4YUxoQJ62Cyz4sM6BobZDrjG6HHGTzuwiKvHHkbsEE9jQ4E5m7yfbVvM0O +zvwrSOM1tkZihKSTpR0j2+taji914tjBssbn12TMZQL5ItGnhR3luY8mEwT9MNkZ +xg0VcREoAH+pu9FE0vPUgLVzhJ3be7qZTTSRqv08bmW+y1plu80GbppePcgYhEow +dlW4l6XPJaHVSn1lSFHE6QAx6sqiAnBz0NoTPIaLyQKBgQDZqDOlhCRciMRicSXn +7yid9rhEmdMkySJHTVFOidFWwlBcp0fGxxn8UNSBcXdSy7GLlUtH41W9PWl8tp9U +hQiiXORxOJ7ZcB80uNKXF01hpPj2DpFPWyHFxpDkWiTAYpZl68rOlYujxZUjJIej +VvcykBC2BlEOG9uZv2kxcqLyJwKBgQDEYULTxaTuLIa17wU3nAhaainKB3vHxw9B +Ksy5p3ND43UNEKkQm7K/WENx0q47TA1mKD9i+BhaLod98mu0YZ+BCUNgWKcBHK8c +uXpauvM/pLhFLXZ2jvEJVpFY3J79FSRK8bwE9RgKfVKMMgEk4zOyZowS8WScOqiy +hnQn1vKTJQKBgElhYuAnl9a2qXcC7KOwRsJS3rcKIVxijzL4xzOyVShp5IwIPbOv +hnxBiBOH/JGmaNpFYBcBdvORE9JfA4KMQ2fx53agfzWRjoPI1/7mdUk5RFI4gRb/ +A3jZRBoopgFSe6ArCbnyQxzYzToG48/Wzwp19ZxYrtUR4UyJct6f5n27AoGBAJDh +KIpQQDOvCdtjcbfrF4aM2DPCfaGPzENJriwxy6oEPzDaX8Bu/dqI5Ykt43i/zQrX +GpyLaHvv4+oZVTiI5UIvcVO9U8hQPyiz9f7F+fu0LHZs6f7hyhYXlbe3XFxeop3f +5dTKdWgXuTTRF2L9dABkA2deS9mutRKwezWBMQk5AoGBALPtX0FrT1zIosibmlud +tu49A/0KZu4PBjrFMYTSEWGNJez3Fb2VsJwylVl6HivwbP61FhlYfyksCzQQFU71 ++x7Nmybp7PmpEBECr3deoZKQ/acNHn0iwb0It+YqV5+TquQebqgwK6WCLsMuiYKT +bg/ch9Rhxbq22yrVgWHh6epp +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/esnode.pem b/sample-resource-plugin/src/test/resources/security/esnode.pem new file mode 100644 index 0000000000..44101f0b37 --- /dev/null +++ b/sample-resource-plugin/src/test/resources/security/esnode.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT +AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl +MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud +yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0 +HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr +XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n +dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD +ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R +BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA +AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF +BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo +wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ +KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz +pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi +7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh +hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L +camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg +PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/kirk-key.pem b/sample-resource-plugin/src/test/resources/security/kirk-key.pem new file mode 100644 index 0000000000..1949c26139 --- /dev/null +++ b/sample-resource-plugin/src/test/resources/security/kirk-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCVXDgEJQorgfXp +gpY0TgF55bD2xuzxN5Dc9rDfgWxrsOvOloMpd7k6FR71bKWjJi1KptSmM/cDElky +AWYKSfYWGiGxsQ+EQW+6kwCfEOHXQldn+0+JcWqP+osSPjtJfwRvRN5kRqP69MPo +7U0N2kdqenqMWjmG1chDGLRSOEGU5HIBiDxsZtOcvMaJ8b1eaW0lvS+6gFQ80AvB +GBkDDCOHHLtDXBylrZk2CQP8AzxNicIZ4B8G3CG3OHA8+nBtEtxZoIihrrkqlMt+ +b/5N8u8zB0Encew0kdrc4R/2wS//ahr6U+9Siq8T7WsUtGwKj3BJClg6OyDJRhlu +y2gFnxoPAgMBAAECggEAP5TOycDkx+megAWVoHV2fmgvgZXkBrlzQwUG/VZQi7V4 +ZGzBMBVltdqI38wc5MtbK3TCgHANnnKgor9iq02Z4wXDwytPIiti/ycV9CDRKvv0 +TnD2hllQFjN/IUh5n4thHWbRTxmdM7cfcNgX3aZGkYbLBVVhOMtn4VwyYu/Mxy8j +xClZT2xKOHkxqwmWPmdDTbAeZIbSv7RkIGfrKuQyUGUaWhrPslvYzFkYZ0umaDgQ +OAthZew5Bz3OfUGOMPLH61SVPuJZh9zN1hTWOvT65WFWfsPd2yStI+WD/5PU1Doo +1RyeHJO7s3ug8JPbtNJmaJwHe9nXBb/HXFdqb976yQKBgQDNYhpu+MYSYupaYqjs +9YFmHQNKpNZqgZ4ceRFZ6cMJoqpI5dpEMqToFH7tpor72Lturct2U9nc2WR0HeEs +/6tiptyMPTFEiMFb1opQlXF2ae7LeJllntDGN0Q6vxKnQV+7VMcXA0Y8F7tvGDy3 +qJu5lfvB1mNM2I6y/eMxjBuQhwKBgQC6K41DXMFro0UnoO879pOQYMydCErJRmjG +/tZSy3Wj4KA/QJsDSViwGfvdPuHZRaG9WtxdL6kn0w1exM9Rb0bBKl36lvi7o7xv +M+Lw9eyXMkww8/F5d7YYH77gIhGo+RITkKI3+5BxeBaUnrGvmHrpmpgRXWmINqr0 +0jsnN3u0OQKBgCf45vIgItSjQb8zonLz2SpZjTFy4XQ7I92gxnq8X0Q5z3B+o7tQ +K/4rNwTju/sGFHyXAJlX+nfcK4vZ4OBUJjP+C8CTjEotX4yTNbo3S6zjMyGQqDI5 +9aIOUY4pb+TzeUFJX7If5gR+DfGyQubvvtcg1K3GHu9u2l8FwLj87sRzAoGAflQF +RHuRiG+/AngTPnZAhc0Zq0kwLkpH2Rid6IrFZhGLy8AUL/O6aa0IGoaMDLpSWUJp +nBY2S57MSM11/MVslrEgGmYNnI4r1K25xlaqV6K6ztEJv6n69327MS4NG8L/gCU5 +3pEm38hkUi8pVYU7in7rx4TCkrq94OkzWJYurAkCgYATQCL/rJLQAlJIGulp8s6h +mQGwy8vIqMjAdHGLrCS35sVYBXG13knS52LJHvbVee39AbD5/LlWvjJGlQMzCLrw +F7oILW5kXxhb8S73GWcuMbuQMFVHFONbZAZgn+C9FW4l7XyRdkrbR1MRZ2km8YMs +/AHmo368d4PSNRMMzLHw8Q== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/kirk.pem b/sample-resource-plugin/src/test/resources/security/kirk.pem new file mode 100644 index 0000000000..36b7e19a75 --- /dev/null +++ b/sample-resource-plugin/src/test/resources/security/kirk.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEmDCCA4CgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLcwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzA0MjRaFw0zNDAyMTcxNzA0MjRaME0xCzAJBgNVBAYT +AmRlMQ0wCwYDVQQHDAR0ZXN0MQ8wDQYDVQQKDAZjbGllbnQxDzANBgNVBAsMBmNs +aWVudDENMAsGA1UEAwwEa2lyazCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAJVcOAQlCiuB9emCljROAXnlsPbG7PE3kNz2sN+BbGuw686Wgyl3uToVHvVs +paMmLUqm1KYz9wMSWTIBZgpJ9hYaIbGxD4RBb7qTAJ8Q4ddCV2f7T4lxao/6ixI+ +O0l/BG9E3mRGo/r0w+jtTQ3aR2p6eoxaOYbVyEMYtFI4QZTkcgGIPGxm05y8xonx +vV5pbSW9L7qAVDzQC8EYGQMMI4ccu0NcHKWtmTYJA/wDPE2JwhngHwbcIbc4cDz6 +cG0S3FmgiKGuuSqUy35v/k3y7zMHQSdx7DSR2tzhH/bBL/9qGvpT71KKrxPtaxS0 +bAqPcEkKWDo7IMlGGW7LaAWfGg8CAwEAAaOCASswggEnMAwGA1UdEwEB/wQCMAAw +DgYDVR0PAQH/BAQDAgXgMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMB0GA1UdDgQW +BBSjMS8tgguX/V7KSGLoGg7K6XMzIDCBzwYDVR0jBIHHMIHEgBQXh9+gWutmEqfV +0Pi6EkU8tysAnKGBlaSBkjCBjzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmS +JomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1wbGUgQ29tIEluYy4xITAf +BgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEhMB8GA1UEAwwYRXhhbXBs +ZSBDb20gSW5jLiBSb290IENBghQNZAmZZn3EFOxBR4630XlhI+mo4jANBgkqhkiG +9w0BAQsFAAOCAQEACEUPPE66/Ot3vZqRGpjDjPHAdtOq+ebaglQhvYcnDw8LOZm8 +Gbh9M88CiO6UxC8ipQLTPh2yyeWArkpJzJK/Pi1eoF1XLiAa0sQ/RaJfQWPm9dvl +1ZQeK5vfD4147b3iBobwEV+CR04SKow0YeEEzAJvzr8YdKI6jqr+2GjjVqzxvRBy +KRVHWCFiR7bZhHGLq3br8hSu0hwjb3oGa1ZI8dui6ujyZt6nm6BoEkau3G/6+zq9 +E6vX3+8Fj4HKCAL6i0SwfGmEpTNp5WUhqibK/fMhhmMT4Mx6MxkT+OFnIjdUU0S/ +e3kgnG8qjficUr38CyEli1U0M7koIXUZI7r+LQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/root-ca.pem b/sample-resource-plugin/src/test/resources/security/root-ca.pem new file mode 100644 index 0000000000..d33f5f7216 --- /dev/null +++ b/sample-resource-plugin/src/test/resources/security/root-ca.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIExjCCA66gAwIBAgIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzAwMzZaFw0zNDAyMTcxNzAwMzZaMIGPMRMwEQYKCZIm +iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ +RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290 +IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEPyN7J9VGPyJcQmCBl5TGwfSzvVdWwoQU +j9aEsdfFJ6pBCDQSsj8Lv4RqL0dZra7h7SpZLLX/YZcnjikrYC+rP5OwsI9xEE/4 +U98CsTBPhIMgqFK6SzNE5494BsAk4cL72dOOc8tX19oDS/PvBULbNkthQ0aAF1dg +vbrHvu7hq7LisB5ZRGHVE1k/AbCs2PaaKkn2jCw/b+U0Ml9qPuuEgz2mAqJDGYoA +WSR4YXrOcrmPuRqbws464YZbJW898/0Pn/U300ed+4YHiNYLLJp51AMkR4YEw969 +VRPbWIvLrd0PQBooC/eLrL6rvud/GpYhdQEUx8qcNCKd4bz3OaQ5AgMBAAGjggEW +MIIBEjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQU +F4ffoFrrZhKn1dD4uhJFPLcrAJwwgc8GA1UdIwSBxzCBxIAUF4ffoFrrZhKn1dD4 +uhJFPLcrAJyhgZWkgZIwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJ +k/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYD +VQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUg +Q29tIEluYy4gUm9vdCBDQYIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcN +AQELBQADggEBAL3Q3AHUhMiLUy6OlLSt8wX9I2oNGDKbBu0atpUNDztk/0s3YLQC +YuXgN4KrIcMXQIuAXCx407c+pIlT/T1FNn+VQXwi56PYzxQKtlpoKUL3oPQE1d0V +6EoiNk+6UodvyZqpdQu7fXVentRMk1QX7D9otmiiNuX+GSxJhJC2Lyzw65O9EUgG +1yVJon6RkUGtqBqKIuLksKwEr//ELnjmXit4LQKSnqKr0FTCB7seIrKJNyb35Qnq +qy9a/Unhokrmdda1tr6MbqU8l7HmxLuSd/Ky+L0eDNtYv6YfMewtjg0TtAnFyQov +rdXmeq1dy9HLo3Ds4AFz3Gx9076TxcRS/iI= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/sample.pem b/sample-resource-plugin/src/test/resources/security/sample.pem new file mode 100644 index 0000000000..44101f0b37 --- /dev/null +++ b/sample-resource-plugin/src/test/resources/security/sample.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT +AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl +MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud +yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0 +HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr +XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n +dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD +ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R +BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA +AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF +BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo +wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ +KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz +pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi +7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh +hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L +camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg +PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/sample-resource-plugin/src/test/resources/security/test-kirk.jks b/sample-resource-plugin/src/test/resources/security/test-kirk.jks new file mode 100644 index 0000000000000000000000000000000000000000..6c8c5ef77e20980f8c78295b159256b805da6a28 GIT binary patch literal 3766 zcmd^=c{r47AIImJ%`(PV###wuU&o%k$xbMgr4m`Pk2Tv-j4?=zEwY?!X|aVw)I`=A zPAY52Rt6yODkPjhAQ%WsfbL*f;mp!-018Nf*#Q6sf)b!}Nv;s_8gzOC@mTmi+D9F}jyYkhL=#Xk3eYM2csmxKA&W!xAdE{tZ2mEGS z;L%QU`DHcrbdbw$3GsKUvmfQu0Z^?sH7B)!W)eLbG*fXB^G$&6CbCnj4~ z*J>Rkut6vL1EvT!JqAq#X=O~#!JHQ#QVSPuOGlnLrXXB~{{FsGRq?o?I;>^GFEhMB zw;z!v1sXap8nq3zz&+prKs-DRPm*XsS4BaP6Z{8tM~n@m|rxMA=p6*i(w=7 z*2&*Yg-uWU$5|W>>g5h)Fn{3B={`skAJ5_wXB5pDwyj{vG1_{{Y-`wB_i^B!5PA|= zrx=_>rprb&75BQ=J)SKPAJI;?(D#46)o+a?SsR^-&qJjXY2ER8S*1ZvU`t7~M6?NKULuzlAZ8C#X9>8j2;WDY z(TY-^!`&0%67`u|U_-Y(knWVcSlh-kwZQ6KG@S?L`W!iVl>Gyd(LnpMc@C!QeY{(E z)uAwF_CcqH#00}jer2dQk3}R|p^87XCxR8`n4c@g9rASTt9$8}SuGW!!+QQ&w&G!P zvv5Mft<&pzv^&XuuQAj&ieoa*3nI-hx}0`4kym=(cd>?v6yM3v43y@5@;yPeJ_N{@ z622W$@5Z4VqliMF3GAf_RcB;$HX^%cwTCgxg^4)5I0?*&oW|giBB@nUNBO+IX=iON zo~;L}HOwhyeqH4GHvAQ5i=|0c+_5*661aDyT_tr=I#+Zog%!9nRiuBb8m&SS4qp2fv7HJMG zwJFuqV*Hoq3`|Mayml;So|9W4Um6Lu8(k+(Hc2}p@&>?!7!7H~9*O%@BrKNAOa-~e z$e6#G)fJ+wNz5x9zU;#>&V}d z?!F1W_eNN;&LI9$!kWa0Zqa)0CVM4D=x(r>aXgW=XQ)PTRsJJ&MC?WjjoMwLRh`-I z8yD|^&(r#NU|pRpRF%wn&t%X`)8HQe%uxEKnXxIu9yui1s$eH0*YZ^Wvt25yOg6{5 zPefKstjqam-PRDz=&-BVb^xZe>{C{$cza!_sV&3M*l0ocMJVr!l~TlJi4JChDn9Nn zc&la1caY}0P&Ho=r;)l;mKBf$V<6A*R6XC}s98g%I7ZIAFI=e6SqQ4;oevw)nw0%^ zKq9#$;{3R0zJv}#mr7@}e+5-(`{C?^vEE#xb7uBY=X#_1v+@~@l?W@Zaq+Yo9bpu& zR<0us_T`(Q6qp1xYb)Rq;tJ|aTZ&y5xqx<_j-|>1$SEi@3!A|| z9YH<3ub_#ai=2WG_V9iQ!NU8mB|$4ZK3Gr>_s15;6W-XV-*##3TjwoMP&yb zq!L{!sQoUn<_ZWb)BbzloM2Zs1tb=+FBn*$!EQmp3Ml#oe;g0);^XP&_osni`NR1A z0SL>FG{F)8;h%d#4-g0eK+%&0UD-=ghUr~yDQ?!lNE5tKiJ_rjY{@`Q1vjbVAFU;|?Qs;w|1hFx_ z`*jR7rVAU>9*yRSpD1)#aOb!)@ak(5hk;guG$_9)=K8Ie^uOP<63|FjrX2UEcJw07 zD5c?bxHD${?)1+CMgPg@0|kH>4NzJZO*;#rl-xA_8*SHCS}ygKZP7*uHbRtmaTE%n zp7Vt7QIt|IIN?)fyS#8IxKHO$?TeY{DpQl5^kyAd$HH^Aa)SJC+I0!ULR znF7*z6R6~{CCW6M^qKuU!N`I`>YB3i6toA7f7#3%T&$5&wm0nY{&d9(g)LB$%g9dX zf>HfjVn9;)rG-^=)tiGDd<5M4wDHPl@yEGU_whSh78l$%S*WCqjvj^Xt?_VKp0T{pQGU!F;?_^4EMT$__$E zH0hMGQlo@W2p^_tPZsnirl@pGb<#0a^*g5ihYtSzKKx%Wg;i4h8B_c6Z+PPWM!I%g zOr-dLp|0@RV@@&InVrwRJfPT~ZY840gT$Jl4)HP^qcTUWE~1&}C2wS3Sv9pJWiRva zyK}a9ilnrYe7SB$bu~GF&GM`D1h@ukNsJY|Yt>|?q(4gzgSUuGwSIfsmlD)%J2V0@ zTU&-58&x%P)-#Oev2~&}bv^wwRbD$?Enu(jJiuwM3shGOZ{$juY+RGk#m^`!p7+vO zAjWFn1{dq`T?N^TggHmN3~VGf^5?a_)R-cj5yfk-?V<|S)%uKn{YGL)7(~eAhWA56 zj7ZS7amp#qQM;t>%6F)v{1S-Gq>88IPiL?2X9=q_r$vhc4{Pd3$WssBMbZaV2W zu&8||{U99-3!x+JudoA1KSAx^0qg$*YLr)FKtJ($lC@k)W?khPY!~B&3F~Xnxs_WH)b*(MC{~@>r={U4@A6+2p8il>0lojdT`r8~C>rA6;jw^lZK9gk<_y!v za(Rbclc{1;TFBtT`lr|YO0}|UXzh>FLsx6RQUq8=?V4{NR#=oxL2}kHb-ZAfuN Date: Wed, 20 Nov 2024 17:41:36 -0500 Subject: [PATCH 020/201] Fixes imports Signed-off-by: Darshit Chanpura --- .../java/org/opensearch/security/OpenSearchSecurityPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 96aa7c2bf6..1931486eb8 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -43,6 +43,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -279,7 +280,6 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile OpensearchDynamicSetting transportPassiveAuthSetting; private volatile PasswordHasher passwordHasher; private volatile DlsFlsBaseContext dlsFlsBaseContext; - private ResourceAccessEvaluator resourceAccessEvaluator; private ResourceManagementRepository rmr; private ResourceAccessHandler resourceAccessHandler; private final Set indicesToListen = new HashSet<>(); From 57661e7d00a1b249526d47d38809cb08ac3dd757 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 27 Nov 2024 11:16:19 -0500 Subject: [PATCH 021/201] Cleans up create action Signed-off-by: Darshit Chanpura --- .../sample/SampleResourcePlugin.java | 4 --- .../CreateResourceTransportAction.java | 32 ++++++------------- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index 74a8378887..6ba4b82b4a 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -12,7 +12,6 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; @@ -70,9 +69,6 @@ public class SampleResourcePlugin extends Plugin implements ActionPlugin, System private static final Logger log = LogManager.getLogger(SampleResourcePlugin.class); public static final String RESOURCE_INDEX_NAME = ".sample_resource_sharing_plugin"; - - public final static Map INDEX_SETTINGS = Map.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); - private Client client; @Override diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java index 8bff7b44a3..53e251c5b6 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java @@ -9,15 +9,10 @@ package org.opensearch.sample.transport; import java.io.IOException; -import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.accesscontrol.resources.ResourceService; -import org.opensearch.accesscontrol.resources.ResourceSharing; -import org.opensearch.accesscontrol.resources.ShareWith; -import org.opensearch.accesscontrol.resources.SharedWithScope; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.index.IndexResponse; import org.opensearch.action.support.ActionFilters; @@ -28,9 +23,8 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.sample.Resource; -import org.opensearch.sample.SampleResourcePlugin; -import org.opensearch.sample.SampleResourceScope; import org.opensearch.sample.actions.create.CreateResourceAction; import org.opensearch.sample.actions.create.CreateResourceRequest; import org.opensearch.sample.actions.create.CreateResourceResponse; @@ -58,7 +52,8 @@ public CreateResourceTransportAction(TransportService transportService, ActionFi @Override protected void doExecute(Task task, CreateResourceRequest request, ActionListener listener) { - try (ThreadContext.StoredContext ignore = transportService.getThreadPool().getThreadContext().stashContext()) { + ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { createResource(request, listener); listener.onResponse(new CreateResourceResponse("Resource " + request.getResource() + " created successfully.")); } catch (Exception e) { @@ -69,31 +64,22 @@ protected void doExecute(Task task, CreateResourceRequest request, ActionListene private void createResource(CreateResourceRequest request, ActionListener listener) { Resource sample = request.getResource(); - try { + try (XContentBuilder builder = jsonBuilder()) { IndexRequest ir = nodeClient.prepareIndex(RESOURCE_INDEX_NAME) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .setSource(sample.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) + .setSource(sample.toXContent(builder, ToXContent.EMPTY_PARAMS)) .request(); - log.warn("Index Request: {}", ir.toString()); + log.info("Index Request: {}", ir.toString()); - ActionListener irListener = getIndexResponseActionListener(listener); - nodeClient.index(ir, irListener); + nodeClient.index(ir, getIndexResponseActionListener(listener)); } catch (IOException e) { - throw new RuntimeException(e); + listener.onFailure(new RuntimeException(e)); } } private static ActionListener getIndexResponseActionListener(ActionListener listener) { - SharedWithScope.SharedWithPerScope sharedWithPerScope = new SharedWithScope.SharedWithPerScope(List.of(), List.of(), List.of()); - SharedWithScope sharedWithScope = new SharedWithScope(SampleResourceScope.SAMPLE_FULL_ACCESS.getName(), sharedWithPerScope); - ShareWith shareWith = new ShareWith(List.of(sharedWithScope)); - return ActionListener.wrap(idxResponse -> { - log.info("Created resource: {}", idxResponse.toString()); - ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - ResourceSharing sharing = rs.getResourceAccessControlPlugin().shareWith(idxResponse.getId(), idxResponse.getIndex(), shareWith); - log.info("Created resource sharing entry: {}", sharing.toString()); - }, listener::onFailure); + return ActionListener.wrap(idxResponse -> { log.info("Created resource: {}", idxResponse.toString()); }, listener::onFailure); } } From a30be5779b0c285a7b1cc6e654b77893c2c19896 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 27 Nov 2024 14:18:16 -0500 Subject: [PATCH 022/201] Adds concrete implementations of remainder methods Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 8 +- .../security/auth/BackendRegistry.java | 4 + .../security/filter/SecurityFilter.java | 1 + .../resources/ResourceAccessHandler.java | 36 +- .../ResourceManagementRepository.java | 37 +- .../ResourceSharingIndexHandler.java | 907 +++++++++++++++++- .../ResourceSharingIndexListener.java | 40 +- 7 files changed, 930 insertions(+), 103 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 1931486eb8..ccee464e01 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -726,7 +726,9 @@ public void onIndexModule(IndexModule indexModule) { log.info("Indices to listen to: {}", this.indicesToListen); if (this.indicesToListen.contains(indexModule.getIndex().getName())) { - indexModule.addIndexOperationListener(ResourceSharingIndexListener.getInstance()); + ResourceSharingIndexListener resourceSharingIndexListener = ResourceSharingIndexListener.getInstance(); + resourceSharingIndexListener.initialize(threadPool, localClient); + indexModule.addIndexOperationListener(resourceSharingIndexListener); log.warn("Security plugin started listening to operations on index {}", indexModule.getIndex().getName()); } @@ -1205,7 +1207,7 @@ public Collection createComponents( // NOTE: We need to create DefaultInterClusterRequestEvaluator before creating ConfigurationRepository since the latter requires // security index to be accessible which means - // communciation with other nodes is already up. However for the communication to be up, there needs to be trusted nodes_dn. Hence + // communication with other nodes is already up. However for the communication to be up, there needs to be trusted nodes_dn. Hence // the base values from opensearch.yml // is used to first establish trust between same cluster nodes and there after dynamic config is loaded if enabled. if (DEFAULT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS.equals(className)) { @@ -1217,7 +1219,7 @@ public Collection createComponents( ResourceSharingIndexHandler rsIndexHandler = new ResourceSharingIndexHandler(resourceSharingIndex, localClient, threadPool); resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns); - rmr = ResourceManagementRepository.create(settings, threadPool, localClient, rsIndexHandler); + rmr = ResourceManagementRepository.create(rsIndexHandler); components.add(adminDns); components.add(cr); diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 0b00bcf943..eb9bb504fd 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -224,6 +224,7 @@ public boolean authenticate(final SecurityRequestChannel request) { if (adminDns.isAdminDN(sslPrincipal)) { // PKI authenticated REST call threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User(sslPrincipal)); + threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER, new User(sslPrincipal)); auditLog.logSucceededLogin(sslPrincipal, true, null, request); return true; } @@ -389,6 +390,8 @@ public boolean authenticate(final SecurityRequestChannel request) { final User impersonatedUser = impersonate(request, authenticatedUser); threadPool.getThreadContext() .putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, impersonatedUser == null ? authenticatedUser : impersonatedUser); + threadPool.getThreadContext() + .putPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER, impersonatedUser == null ? authenticatedUser : impersonatedUser); auditLog.logSucceededLogin( (impersonatedUser == null ? authenticatedUser : impersonatedUser).getName(), false, @@ -422,6 +425,7 @@ public boolean authenticate(final SecurityRequestChannel request) { anonymousUser.setRequestedTenant(tenant); threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, anonymousUser); + threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER, anonymousUser); auditLog.logSucceededLogin(anonymousUser.getName(), false, null, request); if (isDebugEnabled) { log.debug("Anonymous User is authenticated"); diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index 3323c9e38a..b2ede030a7 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -345,6 +345,7 @@ private void ap log.info("Transport auth in passive mode and no user found. Injecting default user"); user = User.DEFAULT_TRANSPORT_USER; threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); + threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER, user); } else { log.error( "No user found for " diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 32fa077e71..d5e79a1fdf 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -49,41 +49,41 @@ public ResourceAccessHandler( this.adminDNs = adminDns; } - public List listAccessibleResourcesInPlugin(String systemIndex) { - final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + public List listAccessibleResourcesInPlugin(String pluginIndex) { + final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); if (user == null) { LOGGER.info("Unable to fetch user details "); return Collections.emptyList(); } - LOGGER.info("Listing accessible resource within a system index {} for : {}", systemIndex, user.getName()); + LOGGER.info("Listing accessible resource within a system index {} for : {}", pluginIndex, user.getName()); - // TODO check if user is admin, if yes all resources should be accessible + // check if user is admin, if yes all resources should be accessible if (adminDNs.isAdmin(user)) { - return loadAllResources(systemIndex); + return loadAllResources(pluginIndex); } Set result = new HashSet<>(); // 0. Own resources - result.addAll(loadOwnResources(systemIndex, user.getName())); + result.addAll(loadOwnResources(pluginIndex, user.getName())); // 1. By username - result.addAll(loadSharedWithResources(systemIndex, Set.of(user.getName()), "users")); + result.addAll(loadSharedWithResources(pluginIndex, Set.of(user.getName()), EntityType.USERS.toString())); // 2. By roles Set roles = user.getSecurityRoles(); - result.addAll(loadSharedWithResources(systemIndex, roles, "roles")); + result.addAll(loadSharedWithResources(pluginIndex, roles, EntityType.ROLES.toString())); // 3. By backend_roles Set backendRoles = user.getRoles(); - result.addAll(loadSharedWithResources(systemIndex, backendRoles, "backend_roles")); + result.addAll(loadSharedWithResources(pluginIndex, backendRoles, EntityType.BACKEND_ROLES.toString())); return result.stream().toList(); } public boolean hasPermission(String resourceId, String systemIndexName, String scope) { - final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Checking if {} has {} permission to resource {}", user.getName(), scope, resourceId); Set userRoles = user.getSecurityRoles(); @@ -109,24 +109,22 @@ public boolean hasPermission(String resourceId, String systemIndexName, String s } public ResourceSharing shareWith(String resourceId, String systemIndexName, ShareWith shareWith) { - final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user, shareWith.toString()); - // TODO fix this to fetch user-name correctly, need to hydrate user context since context might have been stashed. - // (persistentHeader?) - CreatedBy createdBy = new CreatedBy("", ""); + CreatedBy createdBy = new CreatedBy(user.getName()); return this.resourceSharingIndexHandler.updateResourceSharingInfo(resourceId, systemIndexName, createdBy, shareWith); } public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> revokeAccess) { - final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Revoking access to resource {} created by {} for {}", resourceId, user.getName(), revokeAccess); return this.resourceSharingIndexHandler.revokeAccess(resourceId, systemIndexName, revokeAccess); } public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { - final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Deleting resource sharing record for resource {} in {} created by {}", resourceId, systemIndexName, user.getName()); ResourceSharing document = this.resourceSharingIndexHandler.fetchDocumentById(systemIndexName, resourceId); @@ -142,7 +140,7 @@ public boolean deleteResourceSharingRecord(String resourceId, String systemIndex } public boolean deleteAllResourceSharingRecordsForCurrentUser() { - final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Deleting all resource sharing records for resource {}", user.getName()); return this.resourceSharingIndexHandler.deleteAllRecordsForUser(user.getName()); @@ -159,8 +157,8 @@ private List loadOwnResources(String systemIndex, String username) { return this.resourceSharingIndexHandler.fetchDocumentsByField(systemIndex, "created_by.user", username); } - private List loadSharedWithResources(String systemIndex, Set accessWays, String shareWithType) { - return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(systemIndex, accessWays, shareWithType); + private List loadSharedWithResources(String systemIndex, Set entities, String shareWithType) { + return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(systemIndex, entities, shareWithType); } private boolean isOwnerOfResource(ResourceSharing document, String userName) { diff --git a/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java b/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java index da3678728d..84749153f5 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java +++ b/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java @@ -11,44 +11,25 @@ package org.opensearch.security.resources; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.client.Client; -import org.opensearch.common.settings.Settings; -import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.threadpool.ThreadPool; - public class ResourceManagementRepository { - private static final Logger LOGGER = LogManager.getLogger(ConfigurationRepository.class); - - private final Client client; - - private final ThreadPool threadPool; - private final ResourceSharingIndexHandler resourceSharingIndexHandler; - protected ResourceManagementRepository( - final ThreadPool threadPool, - final Client client, - final ResourceSharingIndexHandler resourceSharingIndexHandler - ) { - this.client = client; - this.threadPool = threadPool; + protected ResourceManagementRepository(final ResourceSharingIndexHandler resourceSharingIndexHandler) { this.resourceSharingIndexHandler = resourceSharingIndexHandler; } - public static ResourceManagementRepository create( - Settings settings, - final ThreadPool threadPool, - Client client, - ResourceSharingIndexHandler resourceSharingIndexHandler - ) { + public static ResourceManagementRepository create(ResourceSharingIndexHandler resourceSharingIndexHandler) { - return new ResourceManagementRepository(threadPool, client, resourceSharingIndexHandler); + return new ResourceManagementRepository(resourceSharingIndexHandler); } + /** + * Creates the resource sharing index if it doesn't already exist. + * This method is called during the initialization phase of the repository. + * It ensures that the index is set up with the necessary mappings and settings + * before any operations are performed on the index. + */ public void createResourceSharingIndexIfAbsent() { // TODO check if this should be wrapped in an atomic completable future diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index b175ad53d0..5568ee06d6 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -10,35 +10,44 @@ package org.opensearch.security.resources; import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.Callable; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.lucene.search.join.ScoreMode; -import org.opensearch.accesscontrol.resources.CreatedBy; -import org.opensearch.accesscontrol.resources.EntityType; -import org.opensearch.accesscontrol.resources.ResourceSharing; -import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.accesscontrol.resources.*; import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.action.admin.indices.create.CreateIndexResponse; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.index.IndexResponse; +import org.opensearch.action.search.*; import org.opensearch.action.support.WriteRequest; import org.opensearch.client.Client; +import org.opensearch.common.unit.TimeValue; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.index.reindex.*; +import org.opensearch.script.Script; +import org.opensearch.script.ScriptType; +import org.opensearch.search.Scroll; +import org.opensearch.search.SearchHit; +import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.threadpool.ThreadPool; import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; public class ResourceSharingIndexHandler { - private final static int MINIMUM_HASH_BITS = 128; - private static final Logger LOGGER = LogManager.getLogger(ResourceSharingIndexHandler.class); private final Client client; @@ -55,6 +64,25 @@ public ResourceSharingIndexHandler(final String indexName, final Client client, public final static Map INDEX_SETTINGS = Map.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); + /** + * Creates the resource sharing index if it doesn't already exist. + * This method initializes the index with predefined mappings and settings + * for storing resource sharing information. + * The index will be created with the following structure: + * - source_idx (keyword): The source index containing the original document + * - resource_id (keyword): The ID of the shared resource + * - created_by (object): Information about the user who created the sharing + * - user (keyword): Username of the creator + * - share_with (object): Access control configuration for shared resources + * - [group_name] (object): Name of the access group + * - users (array): List of users with access + * - roles (array): List of roles with access + * - backend_roles (array): List of backend roles with access + * + * @throws RuntimeException if there are issues reading/writing index settings + * or communicating with the cluster + */ + public void createResourceSharingIndexIfAbsent(Callable callable) { // TODO: Once stashContext is replaced with switchContext this call will have to be modified try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { @@ -75,7 +103,29 @@ public void createResourceSharingIndexIfAbsent(Callable callable) { } } - public boolean indexResourceSharing(String resourceId, String resourceIndex, CreatedBy createdBy, ShareWith shareWith) + /** + * Creates or updates a resource sharing record in the dedicated resource sharing index. + * This method handles the persistence of sharing metadata for resources, including + * the creator information and sharing permissions. + * + * @param resourceId The unique identifier of the resource being shared + * @param resourceIndex The source index where the original resource is stored + * @param createdBy Object containing information about the user creating/updating the sharing + * @param shareWith Object containing the sharing permissions' configuration. Can be null for initial creation. + * When provided, it should contain the access control settings for different groups: + * { + * "group_name": { + * "users": ["user1", "user2"], + * "roles": ["role1", "role2"], + * "backend_roles": ["backend_role1"] + * } + * } + * + * @return ResourceSharing Returns resourceSharing object if the operation was successful, null otherwise + * @throws IOException if there are issues with index operations or JSON processing + */ + + public ResourceSharing indexResourceSharing(String resourceId, String resourceIndex, CreatedBy createdBy, ShareWith shareWith) throws IOException { try { @@ -88,58 +138,839 @@ public boolean indexResourceSharing(String resourceId, String resourceIndex, Cre LOGGER.info("Index Request: {}", ir.toString()); - ActionListener irListener = ActionListener.wrap( - idxResponse -> { LOGGER.info("Created {} entry.", resourceSharingIndex); }, - (failResponse) -> { - LOGGER.error(failResponse.getMessage()); - LOGGER.info("Failed to create {} entry.", resourceSharingIndex); - } - ); + ActionListener irListener = ActionListener.wrap(idxResponse -> { + LOGGER.info("Successfully created {} entry.", resourceSharingIndex); + }, (failResponse) -> { + LOGGER.error(failResponse.getMessage()); + LOGGER.info("Failed to create {} entry.", resourceSharingIndex); + }); client.index(ir, irListener); + return entry; } catch (Exception e) { LOGGER.info("Failed to create {} entry.", resourceSharingIndex, e); - return false; + return null; } - return true; } - public List fetchDocumentsByField(String systemIndex, String field, String value) { - LOGGER.info("Fetching documents from index: {}, where {} = {}", systemIndex, field, value); + /** + * Fetches all resource sharing records that match the specified system index. This method retrieves + * a list of resource IDs associated with the given system index from the resource sharing index. + * + *

The method executes the following steps: + *

    + *
  1. Creates a search request with term query matching the system index
  2. + *
  3. Applies source filtering to only fetch resource_id field
  4. + *
  5. Executes the search with a limit of 10000 documents
  6. + *
  7. Processes the results to extract resource IDs
  8. + *
+ * + *

Example query structure: + *

+        * {
+        *   "query": {
+        *     "term": {
+        *       "source_idx": "system_index_name"
+        *     }
+        *   },
+        *   "_source": ["resource_id"],
+        *   "size": 10000
+        * }
+        * 
+ * + * @param pluginIndex The source index to match against the source_idx field + * @return List containing resource IDs that belong to the specified system index. + * Returns an empty list if: + *
    + *
  • No matching documents are found
  • + *
  • An error occurs during the search operation
  • + *
  • The system index parameter is invalid
  • + *
+ * + * @apiNote This method: + *
    + *
  • Uses source filtering for optimal performance
  • + *
  • Performs exact matching on the source_idx field
  • + *
  • Returns an empty list instead of throwing exceptions
  • + *
+ */ + public List fetchAllDocuments(String pluginIndex) { + LOGGER.debug("Fetching all documents from {} where source_idx = {}", resourceSharingIndex, pluginIndex); + + try { + SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); + + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.query(QueryBuilders.termQuery("source_idx", pluginIndex)); + searchSourceBuilder.size(10000); // TODO check what size should be set here. + + searchSourceBuilder.fetchSource(new String[] { "resource_id" }, null); + + searchRequest.source(searchSourceBuilder); + + SearchResponse searchResponse = client.search(searchRequest).actionGet(); + + List resourceIds = new ArrayList<>(); - return List.of(); + SearchHit[] hits = searchResponse.getHits().getHits(); + for (SearchHit hit : hits) { + Map sourceAsMap = hit.getSourceAsMap(); + if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { + resourceIds.add(sourceAsMap.get("resource_id").toString()); + } + } + + LOGGER.debug("Found {} documents in {} for source_idx: {}", resourceIds.size(), resourceSharingIndex, pluginIndex); + + return resourceIds; + + } catch (Exception e) { + LOGGER.error("Failed to fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); + return List.of(); + } } - public List fetchAllDocuments(String systemIndex) { - LOGGER.info("Fetching all documents from index: {}", systemIndex); - return List.of(); + /** + * Fetches documents that match the specified system index and have specific access type values. + * This method uses scroll API to handle large result sets efficiently. + * + *

The method executes the following steps: + *

    + *
  1. Validates the entityType parameter
  2. + *
  3. Creates a scrolling search request with a compound query
  4. + *
  5. Processes results in batches using scroll API
  6. + *
  7. Collects all matching resource IDs
  8. + *
  9. Cleans up scroll context
  10. + *
+ * + *

Example query structure: + *

+    * {
+    *   "query": {
+    *     "bool": {
+    *       "must": [
+    *         { "term": { "source_idx": "system_index_name" } },
+    *         {
+    *           "bool": {
+    *             "should": [
+    *               {
+    *                 "nested": {
+    *                   "path": "share_with.*.entityType",
+    *                   "query": {
+    *                     "term": { "share_with.*.entityType": "entity_value" }
+    *                   }
+    *                 }
+    *               }
+    *             ],
+    *             "minimum_should_match": 1
+    *           }
+    *         }
+    *       ]
+    *     }
+    *   },
+    *   "_source": ["resource_id"],
+    *   "size": 1000
+    * }
+    * 
+ * + * @param pluginIndex The source index to match against the source_idx field + * @param entities Set of values to match in the specified entityType field + * @param entityType The type of association with the resource. Must be one of: + *
    + *
  • "users" - for user-based access
  • + *
  • "roles" - for role-based access
  • + *
  • "backend_roles" - for backend role-based access
  • + *
+ * @return List List of resource IDs that match the criteria. The list may be empty + * if no matches are found + * + * @throws RuntimeException if the search operation fails + * + * @apiNote This method: + *
    + *
  • Uses scroll API with 1-minute timeout
  • + *
  • Processes results in batches of 1000 documents
  • + *
  • Performs source filtering for optimization
  • + *
  • Uses nested queries for accessing array elements
  • + *
  • Properly cleans up scroll context after use
  • + *
+ */ + + public List fetchDocumentsForAllScopes(String pluginIndex, Set entities, String entityType) { + LOGGER.debug("Fetching documents from index: {}, where share_with.*.{} contains any of {}", pluginIndex, entityType, entities); + + List resourceIds = new ArrayList<>(); + final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); + + try { + SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); + searchRequest.scroll(scroll); + + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery().must(QueryBuilders.termQuery("source_idx", pluginIndex)); + + BoolQueryBuilder shouldQuery = QueryBuilders.boolQuery(); + for (String entity : entities) { + shouldQuery.should( + QueryBuilders.nestedQuery( + "share_with.*." + entityType, + QueryBuilders.termQuery("share_with.*." + entityType, entity), + ScoreMode.None + ) + ); + } + shouldQuery.minimumShouldMatch(1); + boolQuery.must(shouldQuery); + + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery) + .size(1000) + .fetchSource(new String[] { "resource_id" }, null); + + searchRequest.source(searchSourceBuilder); + + SearchResponse searchResponse = client.search(searchRequest).actionGet(); + String scrollId = searchResponse.getScrollId(); + SearchHit[] hits = searchResponse.getHits().getHits(); + + while (hits != null && hits.length > 0) { + for (SearchHit hit : hits) { + Map sourceAsMap = hit.getSourceAsMap(); + if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { + resourceIds.add(sourceAsMap.get("resource_id").toString()); + } + } + + SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); + scrollRequest.scroll(scroll); + searchResponse = client.execute(SearchScrollAction.INSTANCE, scrollRequest).actionGet(); + scrollId = searchResponse.getScrollId(); + hits = searchResponse.getHits().getHits(); + } + + ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); + clearScrollRequest.addScrollId(scrollId); + client.clearScroll(clearScrollRequest).actionGet(); + + LOGGER.debug("Found {} documents matching the criteria in {}", resourceIds.size(), resourceSharingIndex); + + return resourceIds; + + } catch (Exception e) { + LOGGER.error( + "Failed to fetch documents from {} for criteria - systemIndex: {}, shareWithType: {}, accessWays: {}", + resourceSharingIndex, + pluginIndex, + entityType, + entities, + e + ); + throw new RuntimeException("Failed to fetch documents: " + e.getMessage(), e); + } } - public List fetchDocumentsForAllScopes(String systemIndex, Set accessWays, String shareWithType) { - return List.of(); + /** + * Fetches documents from the resource sharing index that match a specific field value. + * This method uses scroll API to efficiently handle large result sets and performs exact + * matching on both system index and the specified field. + * + *

The method executes the following steps: + *

    + *
  1. Validates input parameters for null/empty values
  2. + *
  3. Creates a scrolling search request with a bool query
  4. + *
  5. Processes results in batches using scroll API
  6. + *
  7. Extracts resource IDs from matching documents
  8. + *
  9. Cleans up scroll context after completion
  10. + *
+ * + *

Example query structure: + *

+     * {
+     *   "query": {
+     *     "bool": {
+     *       "must": [
+     *         { "term": { "source_idx": "system_index_value" } },
+     *         { "term": { "field_name": "field_value" } }
+     *       ]
+     *     }
+     *   },
+     *   "_source": ["resource_id"],
+     *   "size": 1000
+     * }
+     * 
+ * + * @param systemIndex The source index to match against the source_idx field + * @param field The field name to search in. Must be a valid field in the index mapping + * @param value The value to match for the specified field. Performs exact term matching + * @return List List of resource IDs that match the criteria. Returns an empty list + * if no matches are found + * + * @throws IllegalArgumentException if any parameter is null or empty + * @throws RuntimeException if the search operation fails, wrapping the underlying exception + * + * @apiNote This method: + *
    + *
  • Uses scroll API with 1-minute timeout for handling large result sets
  • + *
  • Performs exact term matching (not analyzed) on field values
  • + *
  • Processes results in batches of 1000 documents
  • + *
  • Uses source filtering to only fetch resource_id field
  • + *
  • Automatically cleans up scroll context after use
  • + *
+ * + * Example usage: + *
+     * List resources = fetchDocumentsByField("myIndex", "status", "active");
+     * 
+ */ + + public List fetchDocumentsByField(String systemIndex, String field, String value) { + if (StringUtils.isBlank(systemIndex) || StringUtils.isBlank(field) || StringUtils.isBlank(value)) { + throw new IllegalArgumentException("systemIndex, field, and value must not be null or empty"); + } + + LOGGER.debug("Fetching documents from index: {}, where {} = {}", systemIndex, field, value); + + List resourceIds = new ArrayList<>(); + final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); + + try { + // Create initial search request + SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); + searchRequest.scroll(scroll); + + // Build the query + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("source_idx", systemIndex)) + .must(QueryBuilders.termQuery(field, value)); + + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery) + .size(1000) + .fetchSource(new String[] { "resource_id" }, null); + + searchRequest.source(searchSourceBuilder); + + // Execute initial search + SearchResponse searchResponse = client.search(searchRequest).actionGet(); + String scrollId = searchResponse.getScrollId(); + SearchHit[] hits = searchResponse.getHits().getHits(); + + // Process results in batches + while (hits != null && hits.length > 0) { + for (SearchHit hit : hits) { + Map sourceAsMap = hit.getSourceAsMap(); + if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { + resourceIds.add(sourceAsMap.get("resource_id").toString()); + } + } + + SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); + scrollRequest.scroll(scroll); + searchResponse = client.execute(SearchScrollAction.INSTANCE, scrollRequest).actionGet(); + scrollId = searchResponse.getScrollId(); + hits = searchResponse.getHits().getHits(); + } + + // Clear scroll + ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); + clearScrollRequest.addScrollId(scrollId); + client.clearScroll(clearScrollRequest).actionGet(); + + LOGGER.debug("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); + + return resourceIds; + + } catch (Exception e) { + LOGGER.error("Failed to fetch documents from {} where {} = {}", resourceSharingIndex, field, value, e); + throw new RuntimeException("Failed to fetch documents: " + e.getMessage(), e); + } } - public ResourceSharing fetchDocumentById(String systemIndexName, String resourceId) { - return null; + /** + * Fetches a specific resource sharing document by its resource ID and system index. + * This method performs an exact match search and parses the result into a ResourceSharing object. + * + *

The method executes the following steps: + *

    + *
  1. Validates input parameters for null/empty values
  2. + *
  3. Creates a search request with a bool query for exact matching
  4. + *
  5. Executes the search with a limit of 1 document
  6. + *
  7. Parses the result using XContent parser if found
  8. + *
  9. Returns null if no matching document exists
  10. + *
+ * + *

Example query structure: + *

+    * {
+    *   "query": {
+    *     "bool": {
+    *       "must": [
+    *         { "term": { "source_idx": "system_index_name" } },
+    *         { "term": { "resource_id": "resource_id_value" } }
+    *       ]
+    *     }
+    *   },
+    *   "size": 1
+    * }
+    * 
+ * + * @param pluginIndex The source index to match against the source_idx field + * @param resourceId The resource ID to fetch. Must exactly match the resource_id field + * @return ResourceSharing object if a matching document is found, null if no document + * matches the criteria + * + * @throws IllegalArgumentException if systemIndexName or resourceId is null or empty + * @throws RuntimeException if the search operation fails or parsing errors occur, + * wrapping the underlying exception + * + * @apiNote This method: + *
    + *
  • Uses term queries for exact matching
  • + *
  • Expects only one matching document per resource ID
  • + *
  • Uses XContent parsing for consistent object creation
  • + *
  • Returns null instead of throwing exceptions for non-existent documents
  • + *
  • Provides detailed logging for troubleshooting
  • + *
+ * + * Example usage: + *
+    * ResourceSharing sharing = fetchDocumentById("myIndex", "resource123");
+    * if (sharing != null) {
+    *     // Process the resource sharing object
+    * }
+    * 
+ */ + + public ResourceSharing fetchDocumentById(String pluginIndex, String resourceId) { + // Input validation + if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(resourceId)) { + throw new IllegalArgumentException("systemIndexName and resourceId must not be null or empty"); + } + + LOGGER.debug("Fetching document from index: {}, with resourceId: {}", pluginIndex, resourceId); + + try { + SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); + + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("source_idx", pluginIndex)) + .must(QueryBuilders.termQuery("resource_id", resourceId)); + + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery).size(1); // We only need one document since + // a resource must have only one + // sharing entry + + searchRequest.source(searchSourceBuilder); + + SearchResponse searchResponse = client.search(searchRequest).actionGet(); + + SearchHit[] hits = searchResponse.getHits().getHits(); + if (hits.length == 0) { + LOGGER.debug("No document found for resourceId: {} in index: {}", resourceId, pluginIndex); + return null; + } + + SearchHit hit = hits[0]; + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, hit.getSourceAsString()) + ) { + + parser.nextToken(); + + ResourceSharing resourceSharing = ResourceSharing.fromXContent(parser); + + LOGGER.debug("Successfully fetched document for resourceId: {} from index: {}", resourceId, pluginIndex); + + return resourceSharing; + } + + } catch (Exception e) { + LOGGER.error("Failed to fetch document for resourceId: {} from index: {}", resourceId, pluginIndex, e); + throw new RuntimeException("Failed to fetch document: " + e.getMessage(), e); + } } - public ResourceSharing updateResourceSharingInfo(String resourceId, String systemIndexName, CreatedBy createdBy, ShareWith shareWith) { + /** + * Updates resource sharing entries that match the specified source index and resource ID + * using the provided update script. This method performs an update-by-query operation + * in the resource sharing index. + * + *

The method executes the following steps: + *

    + *
  1. Creates a bool query to match exact source index and resource ID
  2. + *
  3. Constructs an update-by-query request with the query and update script
  4. + *
  5. Executes the update operation
  6. + *
  7. Returns success/failure status based on update results
  8. + *
+ * + *

Example document matching structure: + *

+     * {
+     *   "source_idx": "source_index_name",
+     *   "resource_id": "resource_id_value",
+     *   "share_with": {
+     *     // sharing configuration to be updated
+     *   }
+     * }
+     * 
+ * + * @param sourceIdx The source index to match in the query (exact match) + * @param resourceId The resource ID to match in the query (exact match) + * @param updateScript The script containing the update operations to be performed. + * This script defines how the matching documents should be modified + * @return boolean true if at least one document was updated, false if no documents + * were found or update failed + * + * @apiNote This method: + *
    + *
  • Uses term queries for exact matching of source_idx and resource_id
  • + *
  • Returns false for both "no matching documents" and "operation failure" cases
  • + *
  • Logs the complete update request for debugging purposes
  • + *
  • Provides detailed logging for success and failure scenarios
  • + *
+ * + * @implNote The update operation uses a bool query with two must clauses: + *
+     * {
+     *   "query": {
+     *     "bool": {
+     *       "must": [
+     *         { "term": { "source_idx": sourceIdx } },
+     *         { "term": { "resource_id": resourceId } }
+     *       ]
+     *     }
+     *   }
+     * }
+     * 
+ */ + private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId, Script updateScript) { try { - boolean success = indexResourceSharing(resourceId, systemIndexName, createdBy, shareWith); - return success ? new ResourceSharing(resourceId, systemIndexName, createdBy, shareWith) : null; - } catch (IOException e) { - throw new RuntimeException(e); + // Create a bool query to match both fields + BoolQueryBuilder query = QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("source_idx", sourceIdx)) + .must(QueryBuilders.termQuery("resource_id", resourceId)); + + UpdateByQueryRequest ubq = new UpdateByQueryRequest(resourceSharingIndex).setQuery(query).setScript(updateScript); + + LOGGER.info("Update By Query Request: {}", ubq.toString()); + + BulkByScrollResponse response = client.execute(UpdateByQueryAction.INSTANCE, ubq).actionGet(); + + if (response.getUpdated() > 0) { + LOGGER.info("Successfully updated {} documents in {}.", response.getUpdated(), resourceSharingIndex); + return true; + } else { + LOGGER.info( + "No documents found to update in {} for source_idx: {} and resource_id: {}", + resourceSharingIndex, + sourceIdx, + resourceId + ); + return false; + } + + } catch (Exception e) { + LOGGER.error("Failed to update documents in {}.", resourceSharingIndex, e); + return false; } } + /** + * Updates the sharing configuration for an existing resource in the resource sharing index. + * This method modifies the sharing permissions for a specific resource identified by its + * resource ID and source index. + * + * @param resourceId The unique identifier of the resource whose sharing configuration needs to be updated + * @param sourceIdx The source index where the original resource is stored + * @param shareWith Updated sharing configuration object containing access control settings: + * { + * "scope": { + * "users": ["user1", "user2"], + * "roles": ["role1", "role2"], + * "backend_roles": ["backend_role1"] + * } + * } + * @return ResourceSharing Returns resourceSharing object if the update was successful, null otherwise + * @throws RuntimeException if there's an error during the update operation + */ + public ResourceSharing updateResourceSharingInfo(String resourceId, String sourceIdx, CreatedBy createdBy, ShareWith shareWith) { + Script updateScript = new Script( + ScriptType.INLINE, + "painless", + "ctx._source.shareWith = params.newShareWith", + Collections.singletonMap("newShareWith", shareWith) + ); + + boolean success = updateByQueryResourceSharing(sourceIdx, resourceId, updateScript); + return success ? new ResourceSharing(resourceId, sourceIdx, createdBy, shareWith) : null; + } + + /** + * Revokes access for specified entities from a resource sharing document. This method removes the specified + * entities (users, roles, or backend roles) from the existing sharing configuration while preserving other + * sharing settings. + * + *

The method performs the following steps: + *

    + *
  1. Fetches the existing document
  2. + *
  3. Removes specified entities from their respective lists in all sharing groups
  4. + *
  5. Updates the document if modifications were made
  6. + *
  7. Returns the updated resource sharing configuration
  8. + *
+ * + *

Example document structure: + *

+     * {
+     *   "source_idx": "system_index_name",
+     *   "resource_id": "resource_id",
+     *   "share_with": {
+     *     "group_name": {
+     *       "users": ["user1", "user2"],
+     *       "roles": ["role1", "role2"],
+     *       "backend_roles": ["backend_role1"]
+     *     }
+     *   }
+     * }
+     * 
+ * + * @param resourceId The ID of the resource from which to revoke access + * @param systemIndexName The name of the system index where the resource exists + * @param revokeAccess A map containing entity types (USER, ROLE, BACKEND_ROLE) and their corresponding + * values to be removed from the sharing configuration + * @return The updated ResourceSharing object after revoking access, or null if the document doesn't exist + * @throws IllegalArgumentException if resourceId, systemIndexName is null/empty, or if revokeAccess is null/empty + * @throws RuntimeException if the update operation fails or encounters an error + * + * @see EntityType + * @see ResourceSharing + * + * @apiNote This method modifies the existing document. If no modifications are needed (i.e., specified + * entities don't exist in the current configuration), the original document is returned unchanged. + * @example + *
+     * Map> revokeAccess = new HashMap<>();
+     * revokeAccess.put(EntityType.USER, Arrays.asList("user1", "user2"));
+     * revokeAccess.put(EntityType.ROLE, Arrays.asList("role1"));
+     * ResourceSharing updated = revokeAccess("resourceId", "systemIndex", revokeAccess);
+     * 
+ */ + public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> revokeAccess) { - return null; + // TODO; check if this needs to be done per scope rather than for all scopes + + // Input validation + if (StringUtils.isBlank(resourceId) || StringUtils.isBlank(systemIndexName) || revokeAccess == null || revokeAccess.isEmpty()) { + throw new IllegalArgumentException("resourceId, systemIndexName, and revokeAccess must not be null or empty"); + } + + LOGGER.debug("Revoking access for resource {} in {} for entities: {}", resourceId, systemIndexName, revokeAccess); + + try { + // First fetch the existing document + ResourceSharing existingResource = fetchDocumentById(systemIndexName, resourceId); + if (existingResource == null) { + LOGGER.warn("No document found for resourceId: {} in index: {}", resourceId, systemIndexName); + return null; + } + + ShareWith shareWith = existingResource.getShareWith(); + boolean modified = false; + + if (shareWith != null) { + for (SharedWithScope sharedWithScope : shareWith.getSharedWithScopes()) { + SharedWithScope.SharedWithPerScope sharedWithPerScope = sharedWithScope.getSharedWithPerScope(); + + for (Map.Entry> entry : revokeAccess.entrySet()) { + EntityType entityType = entry.getKey(); + List entities = entry.getValue(); + + // Check if the entity type exists in the share_with configuration + switch (entityType) { + case USERS: + if (sharedWithPerScope.getUsers() != null) { + modified = sharedWithPerScope.getUsers().removeAll(entities) || modified; + } + break; + case ROLES: + if (sharedWithPerScope.getRoles() != null) { + modified = sharedWithPerScope.getRoles().removeAll(entities) || modified; + } + break; + case BACKEND_ROLES: + if (sharedWithPerScope.getBackendRoles() != null) { + modified = sharedWithPerScope.getBackendRoles().removeAll(entities) || modified; + } + break; + } + } + } + } + + if (!modified) { + LOGGER.debug("No modifications needed for resource: {}", resourceId); + return existingResource; + } + + // Update resource sharing info + return updateResourceSharingInfo(resourceId, systemIndexName, existingResource.getCreatedBy(), shareWith); + + } catch (Exception e) { + LOGGER.error("Failed to revoke access for resource: {} in index: {}", resourceId, systemIndexName, e); + throw new RuntimeException("Failed to revoke access: " + e.getMessage(), e); + } } - public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { - return false; + /** + * Deletes resource sharing records that match the specified source index and resource ID. + * This method performs a delete-by-query operation in the resource sharing index. + * + *

The method executes the following steps: + *

    + *
  1. Creates a delete-by-query request with a bool query
  2. + *
  3. Matches documents based on exact source index and resource ID
  4. + *
  5. Executes the delete operation with immediate refresh
  6. + *
  7. Returns the success/failure status based on deletion results
  8. + *
+ * + *

Example document structure that will be deleted: + *

+     * {
+     *   "source_idx": "source_index_name",
+     *   "resource_id": "resource_id_value",
+     *   "share_with": {
+     *     // sharing configuration
+     *   }
+     * }
+     * 
+ * + * @param sourceIdx The source index to match in the query (exact match) + * @param resourceId The resource ID to match in the query (exact match) + * @return boolean true if at least one document was deleted, false if no documents were found or deletion failed + * + * @implNote The delete operation uses a bool query with two must clauses to ensure exact matching: + *
+     * {
+     *   "query": {
+     *     "bool": {
+     *       "must": [
+     *         { "term": { "source_idx": sourceIdx } },
+     *         { "term": { "resource_id": resourceId } }
+     *       ]
+     *     }
+     *   }
+     * }
+     * 
+ */ + public boolean deleteResourceSharingRecord(String resourceId, String sourceIdx) { + LOGGER.info("Deleting documents from {} where source_idx = {} and resource_id = {}", resourceSharingIndex, sourceIdx, resourceId); + + try { + DeleteByQueryRequest dbq = new DeleteByQueryRequest(resourceSharingIndex).setQuery( + QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("source_idx", sourceIdx)) + .must(QueryBuilders.termQuery("resource_id", resourceId)) + ).setRefresh(true); + + BulkByScrollResponse response = client.execute(DeleteByQueryAction.INSTANCE, dbq).actionGet(); + + if (response.getDeleted() > 0) { + LOGGER.info("Successfully deleted {} documents from {}", response.getDeleted(), resourceSharingIndex); + return true; + } else { + LOGGER.info( + "No documents found to delete in {} for source_idx: {} and resource_id: {}", + resourceSharingIndex, + sourceIdx, + resourceId + ); + return false; + } + + } catch (Exception e) { + LOGGER.error("Failed to delete documents from {}", resourceSharingIndex, e); + return false; + } } + /** + * Deletes all resource sharing records that were created by a specific user. + * This method performs a delete-by-query operation to remove all documents where + * the created_by.user field matches the specified username. + * + *

The method executes the following steps: + *

    + *
  1. Validates the input username parameter
  2. + *
  3. Creates a delete-by-query request with term query matching
  4. + *
  5. Executes the delete operation with immediate refresh
  6. + *
  7. Returns the operation status based on number of deleted documents
  8. + *
+ * + *

Example query structure: + *

+        * {
+        *   "query": {
+        *     "term": {
+        *       "created_by.user": "username"
+        *     }
+        *   }
+        * }
+        * 
+ * + * @param name The username to match against the created_by.user field + * @return boolean indicating whether the deletion was successful: + *
    + *
  • true - if one or more documents were deleted
  • + *
  • false - if no documents were found
  • + *
  • false - if the operation failed due to an error
  • + *
+ * + * @throws IllegalArgumentException if name is null or empty + * + * + * @implNote Implementation details: + *
    + *
  • Uses DeleteByQueryRequest for efficient bulk deletion
  • + *
  • Sets refresh=true for immediate consistency
  • + *
  • Uses term query for exact username matching
  • + *
  • Implements comprehensive error handling and logging
  • + *
+ * + * Example usage: + *
+        * boolean success = deleteAllRecordsForUser("john.doe");
+        * if (success) {
+        *     // Records were successfully deleted
+        * } else {
+        *     // No matching records found or operation failed
+        * }
+        * 
+ */ public boolean deleteAllRecordsForUser(String name) { - return false; + // Input validation + if (StringUtils.isBlank(name)) { + throw new IllegalArgumentException("Username must not be null or empty"); + } + + LOGGER.info("Deleting all records for user {}", name); + + try { + DeleteByQueryRequest deleteRequest = new DeleteByQueryRequest(resourceSharingIndex).setQuery( + QueryBuilders.termQuery("created_by.user", name) + ).setRefresh(true); + + BulkByScrollResponse response = client.execute(DeleteByQueryAction.INSTANCE, deleteRequest).actionGet(); + + long deletedDocs = response.getDeleted(); + + if (deletedDocs > 0) { + LOGGER.info("Successfully deleted {} documents created by user {}", deletedDocs, name); + return true; + } else { + LOGGER.info("No documents found for user {}", name); + return false; + } + + } catch (Exception e) { + LOGGER.error("Failed to delete documents for user {}", name, e); + return false; + } } + } diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java index d6b1180d46..d7b149a2fb 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -14,11 +14,13 @@ import org.apache.logging.log4j.Logger; import org.opensearch.accesscontrol.resources.CreatedBy; +import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.client.Client; import org.opensearch.core.index.shard.ShardId; import org.opensearch.index.engine.Engine; import org.opensearch.index.shard.IndexingOperationListener; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; /** @@ -36,8 +38,6 @@ public class ResourceSharingIndexListener implements IndexingOperationListener { private ThreadPool threadPool; - private Client client; - private ResourceSharingIndexListener() {} public static ResourceSharingIndexListener getInstance() { @@ -53,16 +53,12 @@ public void initialize(ThreadPool threadPool, Client client) { } initialized = true; - this.threadPool = threadPool; - - this.client = client; this.resourceSharingIndexHandler = new ResourceSharingIndexHandler( ConfigConstants.OPENSEARCH_RESOURCE_SHARING_INDEX, client, threadPool ); - ; } @@ -73,27 +69,41 @@ public boolean isInitialized() { @Override public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { - // implement a check to see if a resource was updated - log.info("postIndex called on {}", shardId.getIndexName()); + String resourceIndex = shardId.getIndexName(); + log.info("postIndex called on {}", resourceIndex); String resourceId = index.id(); - String resourceIndex = shardId.getIndexName(); + User user = threadPool.getThreadContext().getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); try { - this.resourceSharingIndexHandler.indexResourceSharing(resourceId, resourceIndex, new CreatedBy("bleh", ""), null); - log.info("successfully indexed resource {}", resourceId); + ResourceSharing sharing = this.resourceSharingIndexHandler.indexResourceSharing( + resourceId, + resourceIndex, + new CreatedBy(user.getName()), + null + ); + log.info("Successfully created a resource sharing entry {}", sharing); } catch (IOException e) { - log.info("failed to index resource {}", resourceId); - throw new RuntimeException(e); + log.info("Failed to create a resource sharing entry for resource: {}", resourceId); } } @Override public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { - // implement a check to see if a resource was deleted - log.warn("postDelete called on " + shardId.getIndexName()); + String resourceIndex = shardId.getIndexName(); + log.info("postDelete called on {}", resourceIndex); + + String resourceId = delete.id(); + + boolean success = this.resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex); + if (success) { + log.info("Successfully deleted resource sharing entries for resource {}", resourceId); + } else { + log.info("Failed to delete resource sharing entry for resource {}", resourceId); + } + } } From d68f349d43bc7a1eb3f496d75817be8f67880aa0 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 27 Nov 2024 14:26:21 -0500 Subject: [PATCH 023/201] Fixes create API Signed-off-by: Darshit Chanpura --- .../transport/CreateResourceTransportAction.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java index 53e251c5b6..f5deeb961d 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java @@ -14,7 +14,6 @@ import org.apache.logging.log4j.Logger; import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.index.IndexResponse; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.action.support.WriteRequest; @@ -72,14 +71,12 @@ private void createResource(CreateResourceRequest request, ActionListener { log.info("Created resource: {}", idxResponse.toString()); }, listener::onFailure) + ); } catch (IOException e) { listener.onFailure(new RuntimeException(e)); } } - - private static ActionListener getIndexResponseActionListener(ActionListener listener) { - return ActionListener.wrap(idxResponse -> { log.info("Created resource: {}", idxResponse.toString()); }, listener::onFailure); - } - } From 58003f6881d0ede85eb624617dfb48f7e3c13daa Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 27 Nov 2024 14:50:18 -0500 Subject: [PATCH 024/201] Fixes spotless errors Signed-off-by: Darshit Chanpura --- .../ResourceSharingIndexHandler.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 5568ee06d6..f4e2c134c1 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -10,7 +10,11 @@ package org.opensearch.security.resources; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; import org.apache.commons.lang3.StringUtils; @@ -18,12 +22,20 @@ import org.apache.logging.log4j.Logger; import org.apache.lucene.search.join.ScoreMode; -import org.opensearch.accesscontrol.resources.*; +import org.opensearch.accesscontrol.resources.CreatedBy; +import org.opensearch.accesscontrol.resources.EntityType; +import org.opensearch.accesscontrol.resources.ResourceSharing; +import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.accesscontrol.resources.SharedWithScope; import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.action.admin.indices.create.CreateIndexResponse; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.index.IndexResponse; -import org.opensearch.action.search.*; +import org.opensearch.action.search.ClearScrollRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.search.SearchScrollAction; +import org.opensearch.action.search.SearchScrollRequest; import org.opensearch.action.support.WriteRequest; import org.opensearch.client.Client; import org.opensearch.common.unit.TimeValue; @@ -36,7 +48,11 @@ import org.opensearch.core.xcontent.XContentParser; import org.opensearch.index.query.BoolQueryBuilder; import org.opensearch.index.query.QueryBuilders; -import org.opensearch.index.reindex.*; +import org.opensearch.index.reindex.BulkByScrollResponse; +import org.opensearch.index.reindex.DeleteByQueryAction; +import org.opensearch.index.reindex.DeleteByQueryRequest; +import org.opensearch.index.reindex.UpdateByQueryAction; +import org.opensearch.index.reindex.UpdateByQueryRequest; import org.opensearch.script.Script; import org.opensearch.script.ScriptType; import org.opensearch.search.Scroll; From 078a976edcdca9095b52d88fa6aadb9d95fd8f46 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 27 Nov 2024 14:53:13 -0500 Subject: [PATCH 025/201] Fixes log statement Signed-off-by: Darshit Chanpura --- .../org/opensearch/security/OpenSearchSecurityPlugin.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index ccee464e01..24f146a033 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -724,7 +724,6 @@ public void onIndexModule(IndexModule indexModule) { ) ); - log.info("Indices to listen to: {}", this.indicesToListen); if (this.indicesToListen.contains(indexModule.getIndex().getName())) { ResourceSharingIndexListener resourceSharingIndexListener = ResourceSharingIndexListener.getInstance(); resourceSharingIndexListener.initialize(threadPool, localClient); @@ -2099,12 +2098,11 @@ public void onNodeStarted(DiscoveryNode localNode) { // create resource sharing index if absent rmr.createResourceSharingIndexIfAbsent(); - log.info("Loading resource plugins"); for (ResourcePlugin resourcePlugin : OpenSearchSecurityPlugin.GuiceHolder.getResourceService().listResourcePlugins()) { String resourceIndex = resourcePlugin.getResourceIndex(); this.indicesToListen.add(resourceIndex); - log.info("Loaded resource plugin: {}, index: {}", resourcePlugin, resourceIndex); + log.info("Preparing to listen to index: {} of plugin: {}", resourceIndex, resourcePlugin); } final Set securityModules = ReflectionHelper.getModulesLoaded(); From 04605491b15ec19c598639e21c29818e124d99ea Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 3 Dec 2024 17:37:12 -0500 Subject: [PATCH 026/201] Adds Revoke API and cleans up existing APIs Signed-off-by: Darshit Chanpura --- .../java/org/opensearch/sample/Resource.java | 6 +- .../sample/SampleResourcePlugin.java | 12 ++-- .../create/CreateResourceRestAction.java | 2 +- .../sample/actions/create/SampleResource.java | 9 ++- .../revoke/RevokeResourceAccessAction.java | 21 +++++++ .../revoke/RevokeResourceAccessRequest.java | 58 +++++++++++++++++++ .../revoke/RevokeResourceAccessResponse.java | 42 ++++++++++++++ .../RevokeResourceAccessRestAction.java | 55 ++++++++++++++++++ .../actions/share/ShareResourceRequest.java | 16 +++++ .../share/ShareResourceRestAction.java | 30 +++++++++- .../verify/VerifyResourceAccessRequest.java | 22 +++---- .../VerifyResourceAccessRestAction.java | 15 +++-- .../CreateResourceTransportAction.java | 10 ++-- ...istAccessibleResourcesTransportAction.java | 7 +-- .../RevokeResourceAccessTransportAction.java | 58 +++++++++++++++++++ .../ShareResourceTransportAction.java | 11 +--- .../VerifyResourceAccessTransportAction.java | 10 ++-- .../opensearch/sample/utils/Constants.java | 13 +++++ 18 files changed, 345 insertions(+), 52 deletions(-) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/RevokeResourceAccessTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Constants.java diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java index 36e74f1624..4ddb56f395 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java @@ -14,6 +14,8 @@ import org.opensearch.core.common.io.stream.NamedWriteable; import org.opensearch.core.xcontent.ToXContentFragment; -public abstract class Resource implements NamedWriteable, ToXContentFragment { - protected abstract String getResourceIndex(); +public interface Resource extends NamedWriteable, ToXContentFragment { + String getResourceIndex(); + + String getResourceName(); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index 6ba4b82b4a..753803ddaf 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -48,18 +48,19 @@ import org.opensearch.sample.actions.create.CreateResourceRestAction; import org.opensearch.sample.actions.list.ListAccessibleResourcesAction; import org.opensearch.sample.actions.list.ListAccessibleResourcesRestAction; +import org.opensearch.sample.actions.revoke.RevokeResourceAccessAction; +import org.opensearch.sample.actions.revoke.RevokeResourceAccessRestAction; import org.opensearch.sample.actions.share.ShareResourceAction; import org.opensearch.sample.actions.share.ShareResourceRestAction; import org.opensearch.sample.actions.verify.VerifyResourceAccessAction; import org.opensearch.sample.actions.verify.VerifyResourceAccessRestAction; -import org.opensearch.sample.transport.CreateResourceTransportAction; -import org.opensearch.sample.transport.ListAccessibleResourcesTransportAction; -import org.opensearch.sample.transport.ShareResourceTransportAction; -import org.opensearch.sample.transport.VerifyResourceAccessTransportAction; +import org.opensearch.sample.transport.*; import org.opensearch.script.ScriptService; import org.opensearch.threadpool.ThreadPool; import org.opensearch.watcher.ResourceWatcherService; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + /** * Sample Resource plugin. * It uses ".sample_resources" index to manage its resources, and exposes a REST API @@ -68,7 +69,6 @@ public class SampleResourcePlugin extends Plugin implements ActionPlugin, SystemIndexPlugin, ResourcePlugin { private static final Logger log = LogManager.getLogger(SampleResourcePlugin.class); - public static final String RESOURCE_INDEX_NAME = ".sample_resource_sharing_plugin"; private Client client; @Override @@ -104,6 +104,7 @@ public List getRestHandlers( new CreateResourceRestAction(), new ListAccessibleResourcesRestAction(), new VerifyResourceAccessRestAction(), + new RevokeResourceAccessRestAction(), new ShareResourceRestAction() ); } @@ -114,6 +115,7 @@ public List getRestHandlers( new ActionHandler<>(CreateResourceAction.INSTANCE, CreateResourceTransportAction.class), new ActionHandler<>(ListAccessibleResourcesAction.INSTANCE, ListAccessibleResourcesTransportAction.class), new ActionHandler<>(ShareResourceAction.INSTANCE, ShareResourceTransportAction.class), + new ActionHandler<>(RevokeResourceAccessAction.INSTANCE, RevokeResourceAccessTransportAction.class), new ActionHandler<>(VerifyResourceAccessAction.INSTANCE, VerifyResourceAccessTransportAction.class) ); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java index 86346cc279..7a9265a6b5 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java @@ -27,7 +27,7 @@ public CreateResourceRestAction() {} @Override public List routes() { - return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/resource")); + return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/create")); } @Override diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java index 1566abfe69..af3388ca14 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java @@ -18,9 +18,9 @@ import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.sample.Resource; -import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; -public class SampleResource extends Resource { +public class SampleResource implements Resource { private String name; @@ -35,6 +35,11 @@ public String getResourceIndex() { return RESOURCE_INDEX_NAME; } + @Override + public String getResourceName() { + return this.name; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { return builder.startObject().field("name", name).endObject(); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessAction.java new file mode 100644 index 0000000000..9261d5ad83 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessAction.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.revoke; + +import org.opensearch.action.ActionType; + +public class RevokeResourceAccessAction extends ActionType { + public static final RevokeResourceAccessAction INSTANCE = new RevokeResourceAccessAction(); + + public static final String NAME = "cluster:admin/sample-resource-plugin/revoke"; + + private RevokeResourceAccessAction() { + super(NAME, RevokeResourceAccessResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRequest.java new file mode 100644 index 0000000000..504b651f8b --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRequest.java @@ -0,0 +1,58 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.revoke; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.accesscontrol.resources.EntityType; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +public class RevokeResourceAccessRequest extends ActionRequest { + + private final String resourceId; + private final Map> revokeAccess; + + public RevokeResourceAccessRequest(String resourceId, Map> revokeAccess) { + this.resourceId = resourceId; + this.revokeAccess = revokeAccess; + } + + public RevokeResourceAccessRequest(StreamInput in) throws IOException { + this.resourceId = in.readString(); + this.revokeAccess = in.readMap(input -> EntityType.valueOf(input.readString()), StreamInput::readStringList); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(resourceId); + out.writeMap( + revokeAccess, + (streamOutput, entityType) -> streamOutput.writeString(entityType.name()), + StreamOutput::writeStringCollection + ); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceId() { + return resourceId; + } + + public Map> getRevokeAccess() { + return revokeAccess; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessResponse.java new file mode 100644 index 0000000000..1236be267e --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessResponse.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.revoke; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +public class RevokeResourceAccessResponse extends ActionResponse implements ToXContentObject { + private final String message; + + public RevokeResourceAccessResponse(String message) { + this.message = message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(message); + } + + public RevokeResourceAccessResponse(final StreamInput in) throws IOException { + message = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("message", message); + builder.endObject(); + return builder; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRestAction.java new file mode 100644 index 0000000000..b5fb28ab30 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRestAction.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.revoke; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.accesscontrol.resources.EntityType; +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.GET; + +public class RevokeResourceAccessRestAction extends BaseRestHandler { + + public RevokeResourceAccessRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/revoke")); + } + + @Override + public String getName() { + return "revoke_sample_resources_access"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String resourceId = (String) source.get("resource_id"); + Map> revoke = (Map>) source.get("revoke"); + final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest(resourceId, revoke); + return channel -> client.executeLocally( + RevokeResourceAccessAction.INSTANCE, + revokeResourceAccessRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java index 01866fd516..3c9b2cd77a 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java @@ -9,12 +9,15 @@ package org.opensearch.sample.actions.share; import java.io.IOException; +import java.util.Arrays; import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.accesscontrol.resources.SharedWithScope; import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionRequestValidationException; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.sample.SampleResourceScope; public class ShareResourceRequest extends ActionRequest { @@ -39,6 +42,19 @@ public void writeTo(final StreamOutput out) throws IOException { @Override public ActionRequestValidationException validate() { + + for (SharedWithScope s : shareWith.getSharedWithScopes()) { + try { + SampleResourceScope.valueOf(s.getScope()); + } catch (IllegalArgumentException | NullPointerException e) { + ActionRequestValidationException exception = new ActionRequestValidationException(); + exception.addValidationError( + "Invalid scope: " + s.getScope() + ". Scope must be one of: " + Arrays.toString(SampleResourceScope.values()) + ); + return exception; + } + return null; + } return null; } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java index 347fb49e68..d15901c96a 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java @@ -14,13 +14,17 @@ import org.opensearch.accesscontrol.resources.ShareWith; import org.opensearch.client.node.NodeClient; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.RestRequest; import org.opensearch.rest.action.RestToXContentListener; import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.POST; public class ShareResourceRestAction extends BaseRestHandler { @@ -28,7 +32,7 @@ public ShareResourceRestAction() {} @Override public List routes() { - return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/share/{resource_id}")); + return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/share")); } @Override @@ -44,8 +48,28 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client } String resourceId = (String) source.get("resource_id"); - ShareWith shareWith = (ShareWith) source.get("share_with"); + + ShareWith shareWith = parseShareWith(source); final ShareResourceRequest shareResourceRequest = new ShareResourceRequest(resourceId, shareWith); return channel -> client.executeLocally(ShareResourceAction.INSTANCE, shareResourceRequest, new RestToXContentListener<>(channel)); } + + private ShareWith parseShareWith(Map source) throws IOException { + @SuppressWarnings("unchecked") + Map shareWithMap = (Map) source.get("share_with"); + if (shareWithMap == null || shareWithMap.isEmpty()) { + throw new IllegalArgumentException("share_with is required and cannot be empty"); + } + + String jsonString = XContentFactory.jsonBuilder().map(shareWithMap).toString(); + + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, jsonString) + ) { + return ShareWith.fromXContent(parser); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); + } + } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java index e9b20118db..f46ebf2ce6 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java @@ -9,26 +9,25 @@ package org.opensearch.sample.actions.verify; import java.io.IOException; +import java.util.Arrays; import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionRequestValidationException; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.sample.SampleResourceScope; public class VerifyResourceAccessRequest extends ActionRequest { private final String resourceId; - private final String sourceIdx; - private final String scope; /** * Default constructor */ - public VerifyResourceAccessRequest(String resourceId, String sourceIdx, String scope) { + public VerifyResourceAccessRequest(String resourceId, String scope) { this.resourceId = resourceId; - this.sourceIdx = sourceIdx; this.scope = scope; } @@ -39,19 +38,26 @@ public VerifyResourceAccessRequest(String resourceId, String sourceIdx, String s */ public VerifyResourceAccessRequest(final StreamInput in) throws IOException { this.resourceId = in.readString(); - this.sourceIdx = in.readString(); this.scope = in.readString(); } @Override public void writeTo(final StreamOutput out) throws IOException { out.writeString(resourceId); - out.writeString(sourceIdx); out.writeString(scope); } @Override public ActionRequestValidationException validate() { + try { + SampleResourceScope.valueOf(scope); + } catch (IllegalArgumentException | NullPointerException e) { + ActionRequestValidationException exception = new ActionRequestValidationException(); + exception.addValidationError( + "Invalid scope: " + scope + ". Scope must be one of: " + Arrays.toString(SampleResourceScope.values()) + ); + return exception; + } return null; } @@ -59,10 +65,6 @@ public String getResourceId() { return resourceId; } - public String getSourceIdx() { - return sourceIdx; - } - public String getScope() { return scope; } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java index 34bfed4e9f..0d48137369 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java @@ -19,7 +19,7 @@ import org.opensearch.rest.action.RestToXContentListener; import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.rest.RestRequest.Method.GET; public class VerifyResourceAccessRestAction extends BaseRestHandler { @@ -27,7 +27,7 @@ public VerifyResourceAccessRestAction() {} @Override public List routes() { - return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/verify_resource_access")); + return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/verify_resource_access")); } @Override @@ -42,11 +42,14 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client source = parser.map(); } - String resourceIdx = (String) source.get("resource_idx"); - String sourceIdx = (String) source.get("source_idx"); + String resourceId = (String) source.get("resource_id"); String scope = (String) source.get("scope"); - // final CreateResourceRequest createSampleResourceRequest = new CreateResourceRequest<>(resource); - return channel -> client.executeLocally(VerifyResourceAccessAction.INSTANCE, null, new RestToXContentListener<>(channel)); + final VerifyResourceAccessRequest verifyResourceAccessRequest = new VerifyResourceAccessRequest(resourceId, scope); + return channel -> client.executeLocally( + VerifyResourceAccessAction.INSTANCE, + verifyResourceAccessRequest, + new RestToXContentListener<>(channel) + ); } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java index f5deeb961d..4b5889153e 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java @@ -31,11 +31,8 @@ import org.opensearch.transport.TransportService; import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; -/** - * Transport action for CreateSampleResource. - */ public class CreateResourceTransportAction extends HandledTransportAction { private static final Logger log = LogManager.getLogger(CreateResourceTransportAction.class); @@ -54,7 +51,9 @@ protected void doExecute(Task task, CreateResourceRequest request, ActionListene ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { createResource(request, listener); - listener.onResponse(new CreateResourceResponse("Resource " + request.getResource() + " created successfully.")); + listener.onResponse( + new CreateResourceResponse("Resource " + request.getResource().getResourceName() + " created successfully.") + ); } catch (Exception e) { log.info("Failed to create resource", e); listener.onFailure(e); @@ -65,6 +64,7 @@ private void createResource(CreateResourceRequest request, ActionListener { @@ -45,7 +42,7 @@ protected void doExecute(Task task, ListAccessibleResourcesRequest request, Acti try { ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); List resourceIds = rs.getResourceAccessControlPlugin().listAccessibleResourcesInPlugin(RESOURCE_INDEX_NAME); - log.info("Successfully fetched accessible resources for current user"); + log.info("Successfully fetched accessible resources for current user : {}", resourceIds); listener.onResponse(new ListAccessibleResourcesResponse(resourceIds)); } catch (Exception e) { log.info("Failed to list accessible resources for current user: ", e); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/RevokeResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/RevokeResourceAccessTransportAction.java new file mode 100644 index 0000000000..fb73bccc8b --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/RevokeResourceAccessTransportAction.java @@ -0,0 +1,58 @@ +/* + * 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. + */ + +package org.opensearch.sample.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.accesscontrol.resources.ResourceService; +import org.opensearch.accesscontrol.resources.ResourceSharing; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.SampleResourcePlugin; +import org.opensearch.sample.actions.revoke.RevokeResourceAccessAction; +import org.opensearch.sample.actions.revoke.RevokeResourceAccessRequest; +import org.opensearch.sample.actions.revoke.RevokeResourceAccessResponse; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +public class RevokeResourceAccessTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(RevokeResourceAccessTransportAction.class); + + @Inject + public RevokeResourceAccessTransportAction(TransportService transportService, ActionFilters actionFilters) { + super(RevokeResourceAccessAction.NAME, transportService, actionFilters, RevokeResourceAccessRequest::new); + } + + @Override + protected void doExecute(Task task, RevokeResourceAccessRequest request, ActionListener listener) { + try { + revokeAccess(request); + listener.onResponse(new RevokeResourceAccessResponse("Resource " + request.getResourceId() + " access revoked successfully.")); + } catch (Exception e) { + listener.onFailure(e); + } + } + + private void revokeAccess(RevokeResourceAccessRequest request) { + try { + ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); + ResourceSharing revoke = rs.getResourceAccessControlPlugin() + .revokeAccess(request.getResourceId(), RESOURCE_INDEX_NAME, request.getRevokeAccess()); + log.info("Revoked resource access for resource: {} with {}", request.getResourceId(), revoke.toString()); + } catch (Exception e) { + log.info("Failed to revoke access for resource {}", request.getResourceId(), e); + throw e; + } + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java index ccbfc31b78..5bd681e510 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java @@ -8,14 +8,11 @@ package org.opensearch.sample.transport; -import java.util.List; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.accesscontrol.resources.ResourceService; import org.opensearch.accesscontrol.resources.ResourceSharing; -import org.opensearch.accesscontrol.resources.ShareWith; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.common.inject.Inject; @@ -27,11 +24,8 @@ import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; -import static org.opensearch.sample.SampleResourcePlugin.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; -/** - * Transport action for CreateSampleResource. - */ public class ShareResourceTransportAction extends HandledTransportAction { private static final Logger log = LogManager.getLogger(ShareResourceTransportAction.class); @@ -52,10 +46,9 @@ protected void doExecute(Task task, ShareResourceRequest request, ActionListener private void shareResource(ShareResourceRequest request) { try { - ShareWith shareWith = new ShareWith(List.of()); ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); ResourceSharing sharing = rs.getResourceAccessControlPlugin() - .shareWith(request.getResourceId(), RESOURCE_INDEX_NAME, shareWith); + .shareWith(request.getResourceId(), RESOURCE_INDEX_NAME, request.getShareWith()); log.info("Shared resource : {} with {}", request.getResourceId(), sharing.toString()); } catch (Exception e) { log.info("Failed to share resource {}", request.getResourceId(), e); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java index 947dcec59e..9ec528d205 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java @@ -24,6 +24,8 @@ import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + public class VerifyResourceAccessTransportAction extends HandledTransportAction { private static final Logger log = LogManager.getLogger(VerifyResourceAccessTransportAction.class); @@ -37,12 +39,12 @@ protected void doExecute(Task task, VerifyResourceAccessRequest request, ActionL try { ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); boolean hasRequestedScopeAccess = rs.getResourceAccessControlPlugin() - .hasPermission(request.getResourceId(), request.getSourceIdx(), request.getScope()); + .hasPermission(request.getResourceId(), RESOURCE_INDEX_NAME, request.getScope()); StringBuilder sb = new StringBuilder(); - sb.append("User does"); - sb.append(hasRequestedScopeAccess ? " " : " not "); - sb.append("have requested scope "); + sb.append("User "); + sb.append(hasRequestedScopeAccess ? "has" : "does not have"); + sb.append(" requested scope "); sb.append(request.getScope()); sb.append(" access to "); sb.append(request.getResourceId()); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Constants.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Constants.java new file mode 100644 index 0000000000..ff7404d2cd --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Constants.java @@ -0,0 +1,13 @@ +/* + * 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. + */ + +package org.opensearch.sample.utils; + +public class Constants { + public static final String RESOURCE_INDEX_NAME = ".sample_resource_sharing_plugin"; +} From 8e44cf333e67f07acf07141210e869262ab1dedf Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 4 Dec 2024 14:34:35 -0500 Subject: [PATCH 027/201] Renames ResourceManagement repository and add keyword to search query term Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 6 +- .../ResourceSharingIndexHandler.java | 217 +++++++++++------- ...urceSharingIndexManagementRepository.java} | 8 +- 3 files changed, 142 insertions(+), 89 deletions(-) rename src/main/java/org/opensearch/security/resources/{ResourceManagementRepository.java => ResourceSharingIndexManagementRepository.java} (72%) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 24f146a033..4297a95083 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -181,9 +181,9 @@ import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resources.ResourceAccessHandler; -import org.opensearch.security.resources.ResourceManagementRepository; import org.opensearch.security.resources.ResourceSharingIndexHandler; import org.opensearch.security.resources.ResourceSharingIndexListener; +import org.opensearch.security.resources.ResourceSharingIndexManagementRepository; import org.opensearch.security.rest.DashboardsInfoAction; import org.opensearch.security.rest.SecurityConfigUpdateAction; import org.opensearch.security.rest.SecurityHealthAction; @@ -280,7 +280,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile OpensearchDynamicSetting transportPassiveAuthSetting; private volatile PasswordHasher passwordHasher; private volatile DlsFlsBaseContext dlsFlsBaseContext; - private ResourceManagementRepository rmr; + private ResourceSharingIndexManagementRepository rmr; private ResourceAccessHandler resourceAccessHandler; private final Set indicesToListen = new HashSet<>(); @@ -1218,7 +1218,7 @@ public Collection createComponents( ResourceSharingIndexHandler rsIndexHandler = new ResourceSharingIndexHandler(resourceSharingIndex, localClient, threadPool); resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns); - rmr = ResourceManagementRepository.create(rsIndexHandler); + rmr = ResourceSharingIndexManagementRepository.create(rsIndexHandler); components.add(adminDns); components.add(cr); diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index f4e2c134c1..592162f206 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -216,7 +216,7 @@ public List fetchAllDocuments(String pluginIndex) { SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(QueryBuilders.termQuery("source_idx", pluginIndex)); + searchSourceBuilder.query(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)); searchSourceBuilder.size(10000); // TODO check what size should be set here. searchSourceBuilder.fetchSource(new String[] { "resource_id" }, null); @@ -312,7 +312,84 @@ public List fetchAllDocuments(String pluginIndex) { */ public List fetchDocumentsForAllScopes(String pluginIndex, Set entities, String entityType) { - LOGGER.debug("Fetching documents from index: {}, where share_with.*.{} contains any of {}", pluginIndex, entityType, entities); + // "*" must match all scopes + return fetchDocumentsForAGivenScope(pluginIndex, entities, entityType, "*"); + } + + /** + * Fetches documents that match the specified system index and have specific access type values for a given scope. + * This method uses scroll API to handle large result sets efficiently. + * + *

The method executes the following steps: + *

    + *
  1. Validates the entityType parameter
  2. + *
  3. Creates a scrolling search request with a compound query
  4. + *
  5. Processes results in batches using scroll API
  6. + *
  7. Collects all matching resource IDs
  8. + *
  9. Cleans up scroll context
  10. + *
+ * + *

Example query structure: + *

+     * {
+     *   "query": {
+     *     "bool": {
+     *       "must": [
+     *         { "term": { "source_idx": "system_index_name" } },
+     *         {
+     *           "bool": {
+     *             "should": [
+     *               {
+     *                 "nested": {
+     *                   "path": "share_with.scope.entityType",
+     *                   "query": {
+     *                     "term": { "share_with.scope.entityType": "entity_value" }
+     *                   }
+     *                 }
+     *               }
+     *             ],
+     *             "minimum_should_match": 1
+     *           }
+     *         }
+     *       ]
+     *     }
+     *   },
+     *   "_source": ["resource_id"],
+     *   "size": 1000
+     * }
+     * 
+ * + * @param pluginIndex The source index to match against the source_idx field + * @param entities Set of values to match in the specified entityType field + * @param entityType The type of association with the resource. Must be one of: + *
    + *
  • "users" - for user-based access
  • + *
  • "roles" - for role-based access
  • + *
  • "backend_roles" - for backend role-based access
  • + *
+ * @param scope The scope of the access. Should be implementation of {@link org.opensearch.accesscontrol.resources.ResourceAccessScope} + * @return List List of resource IDs that match the criteria. The list may be empty + * if no matches are found + * + * @throws RuntimeException if the search operation fails + * + * @apiNote This method: + *
    + *
  • Uses scroll API with 1-minute timeout
  • + *
  • Processes results in batches of 1000 documents
  • + *
  • Performs source filtering for optimization
  • + *
  • Uses nested queries for accessing array elements
  • + *
  • Properly cleans up scroll context after use
  • + *
+ */ + public List fetchDocumentsForAGivenScope(String pluginIndex, Set entities, String entityType, String scope) { + LOGGER.debug( + "Fetching documents from index: {}, where share_with.{}.{} contains any of {}", + pluginIndex, + scope, + entityType, + entities + ); List resourceIds = new ArrayList<>(); final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); @@ -321,49 +398,23 @@ public List fetchDocumentsForAllScopes(String pluginIndex, Set e SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); searchRequest.scroll(scroll); - BoolQueryBuilder boolQuery = QueryBuilders.boolQuery().must(QueryBuilders.termQuery("source_idx", pluginIndex)); + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery().must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)); BoolQueryBuilder shouldQuery = QueryBuilders.boolQuery(); for (String entity : entities) { shouldQuery.should( QueryBuilders.nestedQuery( - "share_with.*." + entityType, - QueryBuilders.termQuery("share_with.*." + entityType, entity), + "share_with." + scope + "." + entityType, + QueryBuilders.termQuery("share_with." + scope + "." + entityType, entity), ScoreMode.None ) ); } shouldQuery.minimumShouldMatch(1); - boolQuery.must(shouldQuery); - - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery) - .size(1000) - .fetchSource(new String[] { "resource_id" }, null); - - searchRequest.source(searchSourceBuilder); - - SearchResponse searchResponse = client.search(searchRequest).actionGet(); - String scrollId = searchResponse.getScrollId(); - SearchHit[] hits = searchResponse.getHits().getHits(); - - while (hits != null && hits.length > 0) { - for (SearchHit hit : hits) { - Map sourceAsMap = hit.getSourceAsMap(); - if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { - resourceIds.add(sourceAsMap.get("resource_id").toString()); - } - } - SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); - scrollRequest.scroll(scroll); - searchResponse = client.execute(SearchScrollAction.INSTANCE, scrollRequest).actionGet(); - scrollId = searchResponse.getScrollId(); - hits = searchResponse.getHits().getHits(); - } + boolQuery.must(QueryBuilders.existsQuery("share_with")).must(shouldQuery); - ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); - clearScrollRequest.addScrollId(scrollId); - client.clearScroll(clearScrollRequest).actionGet(); + executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery); LOGGER.debug("Found {} documents matching the criteria in {}", resourceIds.size(), resourceSharingIndex); @@ -371,9 +422,10 @@ public List fetchDocumentsForAllScopes(String pluginIndex, Set e } catch (Exception e) { LOGGER.error( - "Failed to fetch documents from {} for criteria - systemIndex: {}, shareWithType: {}, accessWays: {}", + "Failed to fetch documents from {} for criteria - systemIndex: {}, scope: {}, entityType: {}, entities: {}", resourceSharingIndex, pluginIndex, + scope, entityType, entities, e @@ -435,7 +487,6 @@ public List fetchDocumentsForAllScopes(String pluginIndex, Set e * List resources = fetchDocumentsByField("myIndex", "status", "active"); * */ - public List fetchDocumentsByField(String systemIndex, String field, String value) { if (StringUtils.isBlank(systemIndex) || StringUtils.isBlank(field) || StringUtils.isBlank(value)) { throw new IllegalArgumentException("systemIndex, field, and value must not be null or empty"); @@ -447,48 +498,16 @@ public List fetchDocumentsByField(String systemIndex, String field, Stri final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); try { - // Create initial search request SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); searchRequest.scroll(scroll); - // Build the query BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery("source_idx", systemIndex)) - .must(QueryBuilders.termQuery(field, value)); + .must(QueryBuilders.termQuery("source_idx.keyword", systemIndex)) + .must(QueryBuilders.termQuery(field + ".keyword", value)); - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery) - .size(1000) - .fetchSource(new String[] { "resource_id" }, null); + executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery); - searchRequest.source(searchSourceBuilder); - - // Execute initial search - SearchResponse searchResponse = client.search(searchRequest).actionGet(); - String scrollId = searchResponse.getScrollId(); - SearchHit[] hits = searchResponse.getHits().getHits(); - - // Process results in batches - while (hits != null && hits.length > 0) { - for (SearchHit hit : hits) { - Map sourceAsMap = hit.getSourceAsMap(); - if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { - resourceIds.add(sourceAsMap.get("resource_id").toString()); - } - } - - SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); - scrollRequest.scroll(scroll); - searchResponse = client.execute(SearchScrollAction.INSTANCE, scrollRequest).actionGet(); - scrollId = searchResponse.getScrollId(); - hits = searchResponse.getHits().getHits(); - } - - // Clear scroll - ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); - clearScrollRequest.addScrollId(scrollId); - client.clearScroll(clearScrollRequest).actionGet(); - - LOGGER.debug("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); + LOGGER.info("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); return resourceIds; @@ -565,8 +584,8 @@ public ResourceSharing fetchDocumentById(String pluginIndex, String resourceId) SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery("source_idx", pluginIndex)) - .must(QueryBuilders.termQuery("resource_id", resourceId)); + .must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)) + .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery).size(1); // We only need one document since // a resource must have only one @@ -603,6 +622,44 @@ public ResourceSharing fetchDocumentById(String pluginIndex, String resourceId) } } + /** + * Helper method to execute a search request and collect resource IDs from the results. + * @param resourceIds List to collect resource IDs + * @param scroll Search Scroll + * @param searchRequest Request to execute + * @param boolQuery Query to execute with the request + */ + private void executeSearchRequest(List resourceIds, Scroll scroll, SearchRequest searchRequest, BoolQueryBuilder boolQuery) { + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery) + .size(1000) + .fetchSource(new String[] { "resource_id" }, null); + + searchRequest.source(searchSourceBuilder); + + SearchResponse searchResponse = client.search(searchRequest).actionGet(); + String scrollId = searchResponse.getScrollId(); + SearchHit[] hits = searchResponse.getHits().getHits(); + + while (hits != null && hits.length > 0) { + for (SearchHit hit : hits) { + Map sourceAsMap = hit.getSourceAsMap(); + if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { + resourceIds.add(sourceAsMap.get("resource_id").toString()); + } + } + + SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); + scrollRequest.scroll(scroll); + searchResponse = client.execute(SearchScrollAction.INSTANCE, scrollRequest).actionGet(); + scrollId = searchResponse.getScrollId(); + hits = searchResponse.getHits().getHits(); + } + + ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); + clearScrollRequest.addScrollId(scrollId); + client.clearScroll(clearScrollRequest).actionGet(); + } + /** * Updates resource sharing entries that match the specified source index and resource ID * using the provided update script. This method performs an update-by-query operation @@ -660,8 +717,8 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId try { // Create a bool query to match both fields BoolQueryBuilder query = QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery("source_idx", sourceIdx)) - .must(QueryBuilders.termQuery("resource_id", resourceId)); + .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) + .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); UpdateByQueryRequest ubq = new UpdateByQueryRequest(resourceSharingIndex).setQuery(query).setScript(updateScript); @@ -710,7 +767,7 @@ public ResourceSharing updateResourceSharingInfo(String resourceId, String sourc Script updateScript = new Script( ScriptType.INLINE, "painless", - "ctx._source.shareWith = params.newShareWith", + "ctx._source.share_with = params.newShareWith", Collections.singletonMap("newShareWith", shareWith) ); @@ -737,7 +794,7 @@ public ResourceSharing updateResourceSharingInfo(String resourceId, String sourc * "source_idx": "system_index_name", * "resource_id": "resource_id", * "share_with": { - * "group_name": { + * "scope": { * "users": ["user1", "user2"], * "roles": ["role1", "role2"], * "backend_roles": ["backend_role1"] @@ -767,11 +824,9 @@ public ResourceSharing updateResourceSharingInfo(String resourceId, String sourc * ResourceSharing updated = revokeAccess("resourceId", "systemIndex", revokeAccess); * */ - public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> revokeAccess) { // TODO; check if this needs to be done per scope rather than for all scopes - // Input validation if (StringUtils.isBlank(resourceId) || StringUtils.isBlank(systemIndexName) || revokeAccess == null || revokeAccess.isEmpty()) { throw new IllegalArgumentException("resourceId, systemIndexName, and revokeAccess must not be null or empty"); } @@ -779,7 +834,6 @@ public ResourceSharing revokeAccess(String resourceId, String systemIndexName, M LOGGER.debug("Revoking access for resource {} in {} for entities: {}", resourceId, systemIndexName, revokeAccess); try { - // First fetch the existing document ResourceSharing existingResource = fetchDocumentById(systemIndexName, resourceId); if (existingResource == null) { LOGGER.warn("No document found for resourceId: {} in index: {}", resourceId, systemIndexName); @@ -880,7 +934,7 @@ public boolean deleteResourceSharingRecord(String resourceId, String sourceIdx) try { DeleteByQueryRequest dbq = new DeleteByQueryRequest(resourceSharingIndex).setQuery( QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery("source_idx", sourceIdx)) + .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) .must(QueryBuilders.termQuery("resource_id", resourceId)) ).setRefresh(true); @@ -959,7 +1013,6 @@ public boolean deleteResourceSharingRecord(String resourceId, String sourceIdx) * */ public boolean deleteAllRecordsForUser(String name) { - // Input validation if (StringUtils.isBlank(name)) { throw new IllegalArgumentException("Username must not be null or empty"); } diff --git a/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java similarity index 72% rename from src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java rename to src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java index 84749153f5..60cb48145f 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceManagementRepository.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java @@ -11,17 +11,17 @@ package org.opensearch.security.resources; -public class ResourceManagementRepository { +public class ResourceSharingIndexManagementRepository { private final ResourceSharingIndexHandler resourceSharingIndexHandler; - protected ResourceManagementRepository(final ResourceSharingIndexHandler resourceSharingIndexHandler) { + protected ResourceSharingIndexManagementRepository(final ResourceSharingIndexHandler resourceSharingIndexHandler) { this.resourceSharingIndexHandler = resourceSharingIndexHandler; } - public static ResourceManagementRepository create(ResourceSharingIndexHandler resourceSharingIndexHandler) { + public static ResourceSharingIndexManagementRepository create(ResourceSharingIndexHandler resourceSharingIndexHandler) { - return new ResourceManagementRepository(resourceSharingIndexHandler); + return new ResourceSharingIndexManagementRepository(resourceSharingIndexHandler); } /** From 16a0ba69d222a1bb7b1c344edb958dd50f6aa0e1 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 4 Dec 2024 17:37:41 -0500 Subject: [PATCH 028/201] Fixes delete method Signed-off-by: Darshit Chanpura --- .../security/resources/ResourceSharingIndexHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 592162f206..7270117a1a 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -935,7 +935,7 @@ public boolean deleteResourceSharingRecord(String resourceId, String sourceIdx) DeleteByQueryRequest dbq = new DeleteByQueryRequest(resourceSharingIndex).setQuery( QueryBuilders.boolQuery() .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) - .must(QueryBuilders.termQuery("resource_id", resourceId)) + .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)) ).setRefresh(true); BulkByScrollResponse response = client.execute(DeleteByQueryAction.INSTANCE, dbq).actionGet(); From 46960ea341e602c575037bc36e40131d63b181f4 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 5 Dec 2024 08:58:44 -0500 Subject: [PATCH 029/201] Adds delete API and refactors package structure Signed-off-by: Darshit Chanpura --- .../sample/SampleResourcePlugin.java | 35 +++++---- .../list/ListAccessibleResourcesAction.java | 2 +- .../list/ListAccessibleResourcesRequest.java | 2 +- .../list/ListAccessibleResourcesResponse.java | 2 +- .../ListAccessibleResourcesRestAction.java | 4 +- .../revoke/RevokeResourceAccessAction.java | 2 +- .../revoke/RevokeResourceAccessRequest.java | 2 +- .../revoke/RevokeResourceAccessResponse.java | 2 +- .../RevokeResourceAccessRestAction.java | 23 ++++-- .../share/ShareResourceAction.java | 2 +- .../share/ShareResourceRequest.java | 2 +- .../share/ShareResourceResponse.java | 2 +- .../share/ShareResourceRestAction.java | 4 +- .../verify/VerifyResourceAccessAction.java | 2 +- .../verify/VerifyResourceAccessRequest.java | 2 +- .../verify/VerifyResourceAccessResponse.java | 2 +- .../VerifyResourceAccessRestAction.java | 4 +- .../create/CreateResourceAction.java | 2 +- .../create/CreateResourceRequest.java | 2 +- .../create/CreateResourceResponse.java | 2 +- .../create/CreateResourceRestAction.java | 4 +- .../{ => resource}/create/SampleResource.java | 2 +- .../resource/delete/DeleteResourceAction.java | 29 +++++++ .../delete/DeleteResourceRequest.java | 49 ++++++++++++ .../delete/DeleteResourceResponse.java | 52 +++++++++++++ .../delete/DeleteResourceRestAction.java | 49 ++++++++++++ ...istAccessibleResourcesTransportAction.java | 8 +- .../RevokeResourceAccessTransportAction.java | 8 +- .../ShareResourceTransportAction.java | 13 ++-- .../VerifyResourceAccessTransportAction.java | 8 +- .../CreateResourceTransportAction.java | 8 +- .../DeleteResourceTransportAction.java | 76 +++++++++++++++++++ 32 files changed, 343 insertions(+), 63 deletions(-) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/list/ListAccessibleResourcesAction.java (94%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/list/ListAccessibleResourcesRequest.java (95%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/list/ListAccessibleResourcesResponse.java (96%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/list/ListAccessibleResourcesRestAction.java (89%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/revoke/RevokeResourceAccessAction.java (92%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/revoke/RevokeResourceAccessRequest.java (97%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/revoke/RevokeResourceAccessResponse.java (95%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/revoke/RevokeResourceAccessRestAction.java (59%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/share/ShareResourceAction.java (93%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/share/ShareResourceRequest.java (97%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/share/ShareResourceResponse.java (96%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/share/ShareResourceRestAction.java (94%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/verify/VerifyResourceAccessAction.java (93%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/verify/VerifyResourceAccessRequest.java (97%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/verify/VerifyResourceAccessResponse.java (96%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => access}/verify/VerifyResourceAccessRestAction.java (90%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => resource}/create/CreateResourceAction.java (93%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => resource}/create/CreateResourceRequest.java (95%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => resource}/create/CreateResourceResponse.java (96%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => resource}/create/CreateResourceRestAction.java (90%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/actions/{ => resource}/create/SampleResource.java (96%) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRestAction.java rename sample-resource-plugin/src/main/java/org/opensearch/sample/transport/{ => access}/ListAccessibleResourcesTransportAction.java (87%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/transport/{ => access}/RevokeResourceAccessTransportAction.java (89%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/transport/{ => access}/ShareResourceTransportAction.java (81%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/transport/{ => access}/VerifyResourceAccessTransportAction.java (89%) rename sample-resource-plugin/src/main/java/org/opensearch/sample/transport/{ => resource}/CreateResourceTransportAction.java (92%) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/DeleteResourceTransportAction.java diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index 753803ddaf..90a62f7286 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -44,17 +44,24 @@ import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; -import org.opensearch.sample.actions.create.CreateResourceAction; -import org.opensearch.sample.actions.create.CreateResourceRestAction; -import org.opensearch.sample.actions.list.ListAccessibleResourcesAction; -import org.opensearch.sample.actions.list.ListAccessibleResourcesRestAction; -import org.opensearch.sample.actions.revoke.RevokeResourceAccessAction; -import org.opensearch.sample.actions.revoke.RevokeResourceAccessRestAction; -import org.opensearch.sample.actions.share.ShareResourceAction; -import org.opensearch.sample.actions.share.ShareResourceRestAction; -import org.opensearch.sample.actions.verify.VerifyResourceAccessAction; -import org.opensearch.sample.actions.verify.VerifyResourceAccessRestAction; -import org.opensearch.sample.transport.*; +import org.opensearch.sample.actions.access.list.ListAccessibleResourcesAction; +import org.opensearch.sample.actions.access.list.ListAccessibleResourcesRestAction; +import org.opensearch.sample.actions.access.revoke.RevokeResourceAccessAction; +import org.opensearch.sample.actions.access.revoke.RevokeResourceAccessRestAction; +import org.opensearch.sample.actions.access.share.ShareResourceAction; +import org.opensearch.sample.actions.access.share.ShareResourceRestAction; +import org.opensearch.sample.actions.access.verify.VerifyResourceAccessAction; +import org.opensearch.sample.actions.access.verify.VerifyResourceAccessRestAction; +import org.opensearch.sample.actions.resource.create.CreateResourceAction; +import org.opensearch.sample.actions.resource.create.CreateResourceRestAction; +import org.opensearch.sample.actions.resource.delete.DeleteResourceAction; +import org.opensearch.sample.actions.resource.delete.DeleteResourceRestAction; +import org.opensearch.sample.transport.access.ListAccessibleResourcesTransportAction; +import org.opensearch.sample.transport.access.RevokeResourceAccessTransportAction; +import org.opensearch.sample.transport.access.ShareResourceTransportAction; +import org.opensearch.sample.transport.access.VerifyResourceAccessTransportAction; +import org.opensearch.sample.transport.resource.CreateResourceTransportAction; +import org.opensearch.sample.transport.resource.DeleteResourceTransportAction; import org.opensearch.script.ScriptService; import org.opensearch.threadpool.ThreadPool; import org.opensearch.watcher.ResourceWatcherService; @@ -105,7 +112,8 @@ public List getRestHandlers( new ListAccessibleResourcesRestAction(), new VerifyResourceAccessRestAction(), new RevokeResourceAccessRestAction(), - new ShareResourceRestAction() + new ShareResourceRestAction(), + new DeleteResourceRestAction() ); } @@ -116,7 +124,8 @@ public List getRestHandlers( new ActionHandler<>(ListAccessibleResourcesAction.INSTANCE, ListAccessibleResourcesTransportAction.class), new ActionHandler<>(ShareResourceAction.INSTANCE, ShareResourceTransportAction.class), new ActionHandler<>(RevokeResourceAccessAction.INSTANCE, RevokeResourceAccessTransportAction.class), - new ActionHandler<>(VerifyResourceAccessAction.INSTANCE, VerifyResourceAccessTransportAction.class) + new ActionHandler<>(VerifyResourceAccessAction.INSTANCE, VerifyResourceAccessTransportAction.class), + new ActionHandler<>(DeleteResourceAction.INSTANCE, DeleteResourceTransportAction.class) ); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesAction.java similarity index 94% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesAction.java index b4e9e29e22..3bea515a19 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.list; +package org.opensearch.sample.actions.access.list; import org.opensearch.action.ActionType; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRequest.java similarity index 95% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRequest.java index b4c0961774..4a9315bfd9 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRequest.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.list; +package org.opensearch.sample.actions.access.list; import java.io.IOException; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java similarity index 96% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java index 47a8f88e4e..5c3715d143 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.list; +package org.opensearch.sample.actions.access.list; import java.io.IOException; import java.util.List; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRestAction.java similarity index 89% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRestAction.java index bb921fce00..2eee67e0f1 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/list/ListAccessibleResourcesRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRestAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.list; +package org.opensearch.sample.actions.access.list; import java.util.List; @@ -33,7 +33,7 @@ public String getName() { } @Override - public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { final ListAccessibleResourcesRequest listAccessibleResourcesRequest = new ListAccessibleResourcesRequest(); return channel -> client.executeLocally( ListAccessibleResourcesAction.INSTANCE, diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessAction.java similarity index 92% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessAction.java index 9261d5ad83..a040cb0732 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.revoke; +package org.opensearch.sample.actions.access.revoke; import org.opensearch.action.ActionType; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java similarity index 97% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRequest.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java index 504b651f8b..c59fc721f2 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.revoke; +package org.opensearch.sample.actions.access.revoke; import java.io.IOException; import java.util.List; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessResponse.java similarity index 95% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessResponse.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessResponse.java index 1236be267e..4cfd3d74e5 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessResponse.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.revoke; +package org.opensearch.sample.actions.access.revoke; import java.io.IOException; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java similarity index 59% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRestAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java index b5fb28ab30..01e1b7591c 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/revoke/RevokeResourceAccessRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java @@ -6,11 +6,13 @@ * compatible open source license. */ -package org.opensearch.sample.actions.revoke; +package org.opensearch.sample.actions.access.revoke; import java.io.IOException; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.opensearch.accesscontrol.resources.EntityType; import org.opensearch.client.node.NodeClient; @@ -20,7 +22,7 @@ import org.opensearch.rest.action.RestToXContentListener; import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.POST; public class RevokeResourceAccessRestAction extends BaseRestHandler { @@ -28,7 +30,7 @@ public RevokeResourceAccessRestAction() {} @Override public List routes() { - return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/revoke")); + return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/revoke")); } @Override @@ -37,14 +39,25 @@ public String getName() { } @Override - public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { Map source; try (XContentParser parser = request.contentParser()) { source = parser.map(); } String resourceId = (String) source.get("resource_id"); - Map> revoke = (Map>) source.get("revoke"); + @SuppressWarnings("unchecked") + Map> revokeSource = (Map>) source.get("revoke"); + Map> revoke = revokeSource.entrySet().stream().collect(Collectors.toMap(entry -> { + try { + return EntityType.fromValue(entry.getKey()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Invalid entity type: " + entry.getKey() + ". Valid values are: " + Arrays.toString(EntityType.values()) + ); + } + }, Map.Entry::getValue)); + final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest(resourceId, revoke); return channel -> client.executeLocally( RevokeResourceAccessAction.INSTANCE, diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceAction.java similarity index 93% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceAction.java index d362b1927c..768a811e27 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.share; +package org.opensearch.sample.actions.access.share; import org.opensearch.action.ActionType; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRequest.java similarity index 97% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRequest.java index 3c9b2cd77a..b222364c0c 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRequest.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.share; +package org.opensearch.sample.actions.access.share; import java.io.IOException; import java.util.Arrays; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceResponse.java similarity index 96% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceResponse.java index a6a85d206d..035a9a245e 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceResponse.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.share; +package org.opensearch.sample.actions.access.share; import java.io.IOException; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRestAction.java similarity index 94% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRestAction.java index d15901c96a..0db4208c05 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/share/ShareResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRestAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.share; +package org.opensearch.sample.actions.access.share; import java.io.IOException; import java.util.List; @@ -41,7 +41,7 @@ public String getName() { } @Override - public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { Map source; try (XContentParser parser = request.contentParser()) { source = parser.map(); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessAction.java similarity index 93% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessAction.java index 1378d561f5..466cc901c6 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.verify; +package org.opensearch.sample.actions.access.verify; import org.opensearch.action.ActionType; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRequest.java similarity index 97% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRequest.java index f46ebf2ce6..87c5b5a7f0 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRequest.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.verify; +package org.opensearch.sample.actions.access.verify; import java.io.IOException; import java.util.Arrays; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessResponse.java similarity index 96% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessResponse.java index 660ac03f71..f7c419b9d1 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessResponse.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.verify; +package org.opensearch.sample.actions.access.verify; import java.io.IOException; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRestAction.java similarity index 90% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRestAction.java index 0d48137369..3118fd54e6 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/verify/VerifyResourceAccessRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRestAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.verify; +package org.opensearch.sample.actions.access.verify; import java.io.IOException; import java.util.List; @@ -36,7 +36,7 @@ public String getName() { } @Override - public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { Map source; try (XContentParser parser = request.contentParser()) { source = parser.map(); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceAction.java similarity index 93% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceAction.java index e7c02278ab..a2b91185e1 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.create; +package org.opensearch.sample.actions.resource.create; import org.opensearch.action.ActionType; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java similarity index 95% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java index b31a4b7f2b..3f330d9719 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.create; +package org.opensearch.sample.actions.resource.create; import java.io.IOException; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceResponse.java similarity index 96% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceResponse.java index 6b966ed08d..6b980c9912 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceResponse.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.create; +package org.opensearch.sample.actions.resource.create; import java.io.IOException; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java similarity index 90% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java index 7a9265a6b5..171c539a7c 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/CreateResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.actions.create; +package org.opensearch.sample.actions.resource.create; import java.io.IOException; import java.util.List; @@ -36,7 +36,7 @@ public String getName() { } @Override - public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { Map source; try (XContentParser parser = request.contentParser()) { source = parser.map(); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/SampleResource.java similarity index 96% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/SampleResource.java index af3388ca14..db475b7018 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/create/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/SampleResource.java @@ -9,7 +9,7 @@ * GitHub history for details. */ -package org.opensearch.sample.actions.create; +package org.opensearch.sample.actions.resource.create; import java.io.IOException; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceAction.java new file mode 100644 index 0000000000..ccb31f7ab2 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceAction.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.resource.delete; + +import org.opensearch.action.ActionType; + +/** + * Action to create a sample resource + */ +public class DeleteResourceAction extends ActionType { + /** + * Create sample resource action instance + */ + public static final DeleteResourceAction INSTANCE = new DeleteResourceAction(); + /** + * Create sample resource action name + */ + public static final String NAME = "cluster:admin/sample-resource-plugin/delete"; + + private DeleteResourceAction() { + super(NAME, DeleteResourceResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRequest.java new file mode 100644 index 0000000000..1cb58989d3 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRequest.java @@ -0,0 +1,49 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.resource.delete; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +/** + * Request object for CreateSampleResource transport action + */ +public class DeleteResourceRequest extends ActionRequest { + + private final String resourceId; + + /** + * Default constructor + */ + public DeleteResourceRequest(String resourceId) { + this.resourceId = resourceId; + } + + public DeleteResourceRequest(StreamInput in) throws IOException { + this.resourceId = in.readString(); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(this.resourceId); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceId() { + return this.resourceId; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceResponse.java new file mode 100644 index 0000000000..ba3cddc04b --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceResponse.java @@ -0,0 +1,52 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.resource.delete; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +public class DeleteResourceResponse extends ActionResponse implements ToXContentObject { + private final String message; + + /** + * Default constructor + * + * @param message The message + */ + public DeleteResourceResponse(String message) { + this.message = message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(message); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public DeleteResourceResponse(final StreamInput in) throws IOException { + message = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("message", message); + builder.endObject(); + return builder; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRestAction.java new file mode 100644 index 0000000000..9a10ca2a62 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRestAction.java @@ -0,0 +1,49 @@ +/* + * 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. + */ + +package org.opensearch.sample.actions.resource.delete; + +import java.util.List; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.common.Strings; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.DELETE; + +public class DeleteResourceRestAction extends BaseRestHandler { + + public DeleteResourceRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(DELETE, "/_plugins/sample_resource_sharing/delete/{resource_id}")); + } + + @Override + public String getName() { + return "delete_sample_resource"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + String resourceId = request.param("resource_id"); + if (Strings.isNullOrEmpty(resourceId)) { + throw new IllegalArgumentException("resource_id parameter is required"); + } + final DeleteResourceRequest createSampleResourceRequest = new DeleteResourceRequest(resourceId); + return channel -> client.executeLocally( + DeleteResourceAction.INSTANCE, + createSampleResourceRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java similarity index 87% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java index 7ef71e4e42..794675d3f3 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ListAccessibleResourcesTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.transport; +package org.opensearch.sample.transport.access; import java.util.List; @@ -19,9 +19,9 @@ import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; import org.opensearch.sample.SampleResourcePlugin; -import org.opensearch.sample.actions.list.ListAccessibleResourcesAction; -import org.opensearch.sample.actions.list.ListAccessibleResourcesRequest; -import org.opensearch.sample.actions.list.ListAccessibleResourcesResponse; +import org.opensearch.sample.actions.access.list.ListAccessibleResourcesAction; +import org.opensearch.sample.actions.access.list.ListAccessibleResourcesRequest; +import org.opensearch.sample.actions.access.list.ListAccessibleResourcesResponse; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/RevokeResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java similarity index 89% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/transport/RevokeResourceAccessTransportAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java index fb73bccc8b..14fa982e52 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/RevokeResourceAccessTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.transport; +package org.opensearch.sample.transport.access; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -18,9 +18,9 @@ import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; import org.opensearch.sample.SampleResourcePlugin; -import org.opensearch.sample.actions.revoke.RevokeResourceAccessAction; -import org.opensearch.sample.actions.revoke.RevokeResourceAccessRequest; -import org.opensearch.sample.actions.revoke.RevokeResourceAccessResponse; +import org.opensearch.sample.actions.access.revoke.RevokeResourceAccessAction; +import org.opensearch.sample.actions.access.revoke.RevokeResourceAccessRequest; +import org.opensearch.sample.actions.access.revoke.RevokeResourceAccessResponse; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ShareResourceTransportAction.java similarity index 81% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ShareResourceTransportAction.java index 5bd681e510..e99a9abf24 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/ShareResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ShareResourceTransportAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.transport; +package org.opensearch.sample.transport.access; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -18,9 +18,9 @@ import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; import org.opensearch.sample.SampleResourcePlugin; -import org.opensearch.sample.actions.share.ShareResourceAction; -import org.opensearch.sample.actions.share.ShareResourceRequest; -import org.opensearch.sample.actions.share.ShareResourceResponse; +import org.opensearch.sample.actions.access.share.ShareResourceAction; +import org.opensearch.sample.actions.access.share.ShareResourceRequest; +import org.opensearch.sample.actions.access.share.ShareResourceResponse; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; @@ -44,11 +44,14 @@ protected void doExecute(Task task, ShareResourceRequest request, ActionListener } } - private void shareResource(ShareResourceRequest request) { + private void shareResource(ShareResourceRequest request) throws Exception { try { ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); ResourceSharing sharing = rs.getResourceAccessControlPlugin() .shareWith(request.getResourceId(), RESOURCE_INDEX_NAME, request.getShareWith()); + if (sharing == null) { + throw new Exception("Failed to share resource " + request.getResourceId()); + } log.info("Shared resource : {} with {}", request.getResourceId(), sharing.toString()); } catch (Exception e) { log.info("Failed to share resource {}", request.getResourceId(), e); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/VerifyResourceAccessTransportAction.java similarity index 89% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/VerifyResourceAccessTransportAction.java index 9ec528d205..681e4546cc 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/VerifyResourceAccessTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/VerifyResourceAccessTransportAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.transport; +package org.opensearch.sample.transport.access; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -18,9 +18,9 @@ import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; import org.opensearch.sample.SampleResourcePlugin; -import org.opensearch.sample.actions.verify.VerifyResourceAccessAction; -import org.opensearch.sample.actions.verify.VerifyResourceAccessRequest; -import org.opensearch.sample.actions.verify.VerifyResourceAccessResponse; +import org.opensearch.sample.actions.access.verify.VerifyResourceAccessAction; +import org.opensearch.sample.actions.access.verify.VerifyResourceAccessRequest; +import org.opensearch.sample.actions.access.verify.VerifyResourceAccessResponse; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java similarity index 92% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java index 4b5889153e..9a764b61de 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.sample.transport; +package org.opensearch.sample.transport.resource; import java.io.IOException; @@ -24,9 +24,9 @@ import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.sample.Resource; -import org.opensearch.sample.actions.create.CreateResourceAction; -import org.opensearch.sample.actions.create.CreateResourceRequest; -import org.opensearch.sample.actions.create.CreateResourceResponse; +import org.opensearch.sample.actions.resource.create.CreateResourceAction; +import org.opensearch.sample.actions.resource.create.CreateResourceRequest; +import org.opensearch.sample.actions.resource.create.CreateResourceResponse; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/DeleteResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/DeleteResourceTransportAction.java new file mode 100644 index 0000000000..bdc19ab8b3 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/DeleteResourceTransportAction.java @@ -0,0 +1,76 @@ +/* + * 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. + */ + +package org.opensearch.sample.transport.resource; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.ResourceNotFoundException; +import org.opensearch.action.DocWriteResponse; +import org.opensearch.action.delete.DeleteRequest; +import org.opensearch.action.delete.DeleteResponse; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.actions.resource.delete.DeleteResourceAction; +import org.opensearch.sample.actions.resource.delete.DeleteResourceRequest; +import org.opensearch.sample.actions.resource.delete.DeleteResourceResponse; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +public class DeleteResourceTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(DeleteResourceTransportAction.class); + + private final TransportService transportService; + private final Client nodeClient; + + @Inject + public DeleteResourceTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { + super(DeleteResourceAction.NAME, transportService, actionFilters, DeleteResourceRequest::new); + this.transportService = transportService; + this.nodeClient = nodeClient; + } + + @Override + protected void doExecute(Task task, DeleteResourceRequest request, ActionListener listener) { + if (request.getResourceId() == null || request.getResourceId().isEmpty()) { + listener.onFailure(new IllegalArgumentException("Resource ID cannot be null or empty")); + return; + } + + ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + deleteResource(request, ActionListener.wrap(deleteResponse -> { + if (deleteResponse.getResult() == DocWriteResponse.Result.NOT_FOUND) { + listener.onFailure(new ResourceNotFoundException("Resource " + request.getResourceId() + " not found")); + } else { + listener.onResponse(new DeleteResourceResponse("Resource " + request.getResourceId() + " deleted successfully")); + } + }, exception -> { + log.error("Failed to delete resource: " + request.getResourceId(), exception); + listener.onFailure(exception); + })); + } + } + + private void deleteResource(DeleteResourceRequest request, ActionListener listener) { + DeleteRequest deleteRequest = new DeleteRequest(RESOURCE_INDEX_NAME, request.getResourceId()).setRefreshPolicy( + WriteRequest.RefreshPolicy.IMMEDIATE + ); + + nodeClient.delete(deleteRequest, listener); + } + +} From ac53c8feb37871ceedc276786bcaab6ec8170892 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 5 Dec 2024 12:32:01 -0500 Subject: [PATCH 030/201] Fixes updateByQuery painless script Signed-off-by: Darshit Chanpura --- .../ResourceSharingIndexHandler.java | 123 +++++++++++++----- 1 file changed, 90 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 7270117a1a..46e61f372a 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -17,6 +17,7 @@ import java.util.Set; import java.util.concurrent.Callable; +import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -41,10 +42,12 @@ import org.opensearch.common.unit.TimeValue; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentFactory; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.action.ActionListener; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.index.query.BoolQueryBuilder; import org.opensearch.index.query.QueryBuilders; @@ -58,6 +61,7 @@ import org.opensearch.search.Scroll; import org.opensearch.search.SearchHit; import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.security.DefaultObjectMapper; import org.opensearch.threadpool.ThreadPool; import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; @@ -660,6 +664,88 @@ private void executeSearchRequest(List resourceIds, Scroll scroll, Searc client.clearScroll(clearScrollRequest).actionGet(); } + /** + * Updates the sharing configuration for an existing resource in the resource sharing index. + * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map)} + * This method modifies the sharing permissions for a specific resource identified by its + * resource ID and source index. + * + * @param resourceId The unique identifier of the resource whose sharing configuration needs to be updated + * @param sourceIdx The source index where the original resource is stored + * @param shareWith Updated sharing configuration object containing access control settings: + * { + * "scope": { + * "users": ["user1", "user2"], + * "roles": ["role1", "role2"], + * "backend_roles": ["backend_role1"] + * } + * } + * @return ResourceSharing Returns resourceSharing object if the update was successful, null otherwise + * @throws RuntimeException if there's an error during the update operation + */ + public ResourceSharing updateResourceSharingInfo(String resourceId, String sourceIdx, CreatedBy createdBy, ShareWith shareWith) { + XContentBuilder builder; + Map shareWithMap; + try { + builder = XContentFactory.jsonBuilder(); + shareWith.toXContent(builder, ToXContent.EMPTY_PARAMS); + String json = builder.toString(); + shareWithMap = DefaultObjectMapper.readValue(json, new TypeReference<>() { + }); + + } catch (IOException e) { + LOGGER.error("Failed to build json content", e); + return null; + } + + // Atomic operation + Script updateScript = new Script(ScriptType.INLINE, "painless", """ + if (ctx._source.share_with == null) { + ctx._source.share_with = [:]; + } + for (def entry : params.shareWith.entrySet()) { + def scopeName = entry.getKey(); + def newScope = entry.getValue(); + if (ctx._source.share_with.containsKey(scopeName)) { + def existingScope = ctx._source.share_with.get(scopeName); + if (newScope.users != null) { + if (existingScope.users == null) { + existingScope.users = new HashSet(); + } + existingScope.users.addAll(newScope.users); + } + if (newScope.roles != null) { + if (existingScope.roles == null) { + existingScope.roles = new HashSet(); + } + existingScope.roles.addAll(newScope.roles); + } + if (newScope.backend_roles != null) { + if (existingScope.backend_roles == null) { + existingScope.backend_roles = new HashSet(); + } + existingScope.backend_roles.addAll(newScope.backend_roles); + } + } else { + def newScopeEntry = [:]; + if (newScope.users != null) { + newScopeEntry.users = new HashSet(newScope.users); + } + if (newScope.roles != null) { + newScopeEntry.roles = new HashSet(newScope.roles); + } + if (newScope.backend_roles != null) { + newScopeEntry.backend_roles = new HashSet(newScope.backend_roles); + } + ctx._source.share_with.put(scopeName, newScopeEntry); + } + } + """, Collections.singletonMap("shareWith", shareWithMap)); + + boolean success = updateByQueryResourceSharing(sourceIdx, resourceId, updateScript); + return success ? new ResourceSharing(resourceId, sourceIdx, createdBy, shareWith) : null; + } + /** * Updates resource sharing entries that match the specified source index and resource ID * using the provided update script. This method performs an update-by-query operation @@ -715,14 +801,15 @@ private void executeSearchRequest(List resourceIds, Scroll scroll, Searc */ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId, Script updateScript) { try { - // Create a bool query to match both fields BoolQueryBuilder query = QueryBuilders.boolQuery() .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); - UpdateByQueryRequest ubq = new UpdateByQueryRequest(resourceSharingIndex).setQuery(query).setScript(updateScript); + UpdateByQueryRequest ubq = new UpdateByQueryRequest(resourceSharingIndex).setQuery(query) + .setScript(updateScript) + .setRefresh(true); - LOGGER.info("Update By Query Request: {}", ubq.toString()); + LOGGER.debug("Update By Query Request: {}", ubq.toString()); BulkByScrollResponse response = client.execute(UpdateByQueryAction.INSTANCE, ubq).actionGet(); @@ -745,36 +832,6 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId } } - /** - * Updates the sharing configuration for an existing resource in the resource sharing index. - * This method modifies the sharing permissions for a specific resource identified by its - * resource ID and source index. - * - * @param resourceId The unique identifier of the resource whose sharing configuration needs to be updated - * @param sourceIdx The source index where the original resource is stored - * @param shareWith Updated sharing configuration object containing access control settings: - * { - * "scope": { - * "users": ["user1", "user2"], - * "roles": ["role1", "role2"], - * "backend_roles": ["backend_role1"] - * } - * } - * @return ResourceSharing Returns resourceSharing object if the update was successful, null otherwise - * @throws RuntimeException if there's an error during the update operation - */ - public ResourceSharing updateResourceSharingInfo(String resourceId, String sourceIdx, CreatedBy createdBy, ShareWith shareWith) { - Script updateScript = new Script( - ScriptType.INLINE, - "painless", - "ctx._source.share_with = params.newShareWith", - Collections.singletonMap("newShareWith", shareWith) - ); - - boolean success = updateByQueryResourceSharing(sourceIdx, resourceId, updateScript); - return success ? new ResourceSharing(resourceId, sourceIdx, createdBy, shareWith) : null; - } - /** * Revokes access for specified entities from a resource sharing document. This method removes the specified * entities (users, roles, or backend roles) from the existing sharing configuration while preserving other From bc67926c53ab7092b3de64302649668ae3a98a1d Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 5 Dec 2024 16:56:12 -0500 Subject: [PATCH 031/201] Updates Revoke request Signed-off-by: Darshit Chanpura --- .../access/revoke/RevokeResourceAccessRequest.java | 10 +++++++++- .../access/revoke/RevokeResourceAccessRestAction.java | 7 ++++--- .../access/RevokeResourceAccessTransportAction.java | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java index c59fc721f2..3b7b10f19a 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java @@ -22,15 +22,18 @@ public class RevokeResourceAccessRequest extends ActionRequest { private final String resourceId; private final Map> revokeAccess; + private final List scopes; - public RevokeResourceAccessRequest(String resourceId, Map> revokeAccess) { + public RevokeResourceAccessRequest(String resourceId, Map> revokeAccess, List scopes) { this.resourceId = resourceId; this.revokeAccess = revokeAccess; + this.scopes = scopes; } public RevokeResourceAccessRequest(StreamInput in) throws IOException { this.resourceId = in.readString(); this.revokeAccess = in.readMap(input -> EntityType.valueOf(input.readString()), StreamInput::readStringList); + this.scopes = in.readStringList(); } @Override @@ -41,6 +44,7 @@ public void writeTo(final StreamOutput out) throws IOException { (streamOutput, entityType) -> streamOutput.writeString(entityType.name()), StreamOutput::writeStringCollection ); + out.writeStringCollection(scopes); } @Override @@ -55,4 +59,8 @@ public String getResourceId() { public Map> getRevokeAccess() { return revokeAccess; } + + public List getScopes() { + return scopes; + } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java index 01e1b7591c..85a01d2234 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java @@ -47,7 +47,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli String resourceId = (String) source.get("resource_id"); @SuppressWarnings("unchecked") - Map> revokeSource = (Map>) source.get("revoke"); + Map> revokeSource = (Map>) source.get("entities"); Map> revoke = revokeSource.entrySet().stream().collect(Collectors.toMap(entry -> { try { return EntityType.fromValue(entry.getKey()); @@ -57,8 +57,9 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli ); } }, Map.Entry::getValue)); - - final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest(resourceId, revoke); + @SuppressWarnings("unchecked") + List scopes = source.containsKey("scopes") ? (List) source.get("scopes") : List.of(); + final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest(resourceId, revoke, scopes); return channel -> client.executeLocally( RevokeResourceAccessAction.INSTANCE, revokeResourceAccessRequest, diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java index 14fa982e52..dd7757e4f2 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java @@ -48,7 +48,7 @@ private void revokeAccess(RevokeResourceAccessRequest request) { try { ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); ResourceSharing revoke = rs.getResourceAccessControlPlugin() - .revokeAccess(request.getResourceId(), RESOURCE_INDEX_NAME, request.getRevokeAccess()); + .revokeAccess(request.getResourceId(), RESOURCE_INDEX_NAME, request.getRevokeAccess(), request.getScopes()); log.info("Revoked resource access for resource: {} with {}", request.getResourceId(), revoke.toString()); } catch (Exception e) { log.info("Failed to revoke access for resource {}", request.getResourceId(), e); From 9e6ae850428211dd4d79608f9897a7cfeb0283cf Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 5 Dec 2024 16:57:16 -0500 Subject: [PATCH 032/201] Updates revoke handler to use painless script Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 9 +- .../resources/ResourceAccessHandler.java | 9 +- .../ResourceSharingIndexHandler.java | 130 +++++++++--------- 3 files changed, 77 insertions(+), 71 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 4297a95083..8b70509390 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2223,8 +2223,13 @@ public ResourceSharing shareWith(String resourceId, String systemIndexName, Shar } @Override - public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> entities) { - return this.resourceAccessHandler.revokeAccess(resourceId, systemIndexName, entities); + public ResourceSharing revokeAccess( + String resourceId, + String systemIndexName, + Map> entities, + List scopes + ) { + return this.resourceAccessHandler.revokeAccess(resourceId, systemIndexName, entities, scopes); } @Override diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index d5e79a1fdf..143dda52a3 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -116,11 +116,16 @@ public ResourceSharing shareWith(String resourceId, String systemIndexName, Shar return this.resourceSharingIndexHandler.updateResourceSharingInfo(resourceId, systemIndexName, createdBy, shareWith); } - public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> revokeAccess) { + public ResourceSharing revokeAccess( + String resourceId, + String systemIndexName, + Map> revokeAccess, + List scopes + ) { final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Revoking access to resource {} created by {} for {}", resourceId, user.getName(), revokeAccess); - return this.resourceSharingIndexHandler.revokeAccess(resourceId, systemIndexName, revokeAccess); + return this.resourceSharingIndexHandler.revokeAccess(resourceId, systemIndexName, revokeAccess, scopes); } public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 46e61f372a..503701127a 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -21,13 +21,11 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.lucene.search.join.ScoreMode; import org.opensearch.accesscontrol.resources.CreatedBy; import org.opensearch.accesscontrol.resources.EntityType; import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.accesscontrol.resources.ShareWith; -import org.opensearch.accesscontrol.resources.SharedWithScope; import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.action.admin.indices.create.CreateIndexResponse; import org.opensearch.action.index.IndexRequest; @@ -50,6 +48,7 @@ import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.MultiMatchQueryBuilder; import org.opensearch.index.query.QueryBuilders; import org.opensearch.index.reindex.BulkByScrollResponse; import org.opensearch.index.reindex.DeleteByQueryAction; @@ -156,8 +155,6 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn .setSource(entry.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) .request(); - LOGGER.info("Index Request: {}", ir.toString()); - ActionListener irListener = ActionListener.wrap(idxResponse -> { LOGGER.info("Successfully created {} entry.", resourceSharingIndex); }, (failResponse) -> { @@ -405,14 +402,19 @@ public List fetchDocumentsForAGivenScope(String pluginIndex, Set BoolQueryBuilder boolQuery = QueryBuilders.boolQuery().must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)); BoolQueryBuilder shouldQuery = QueryBuilders.boolQuery(); - for (String entity : entities) { - shouldQuery.should( - QueryBuilders.nestedQuery( - "share_with." + scope + "." + entityType, - QueryBuilders.termQuery("share_with." + scope + "." + entityType, entity), - ScoreMode.None - ) - ); + if ("*".equals(scope)) { + // Wildcard behavior: Match any scope dynamically + for (String entity : entities) { + shouldQuery.should( + QueryBuilders.multiMatchQuery(entity, "share_with.*." + entityType + ".keyword") + .type(MultiMatchQueryBuilder.Type.BEST_FIELDS) + ); + } + } else { + // Match the specific scope + for (String entity : entities) { + shouldQuery.should(QueryBuilders.termQuery("share_with." + scope + "." + entityType + ".keyword", entity)); + } } shouldQuery.minimumShouldMatch(1); @@ -666,7 +668,7 @@ private void executeSearchRequest(List resourceIds, Scroll scroll, Searc /** * Updates the sharing configuration for an existing resource in the resource sharing index. - * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map)} + * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map, List)} * This method modifies the sharing permissions for a specific resource identified by its * resource ID and source index. * @@ -861,11 +863,12 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId * * * @param resourceId The ID of the resource from which to revoke access - * @param systemIndexName The name of the system index where the resource exists + * @param sourceIdx The name of the system index where the resource exists * @param revokeAccess A map containing entity types (USER, ROLE, BACKEND_ROLE) and their corresponding * values to be removed from the sharing configuration + * @param scopes A list of scopes to revoke access from. If null or empty, access is revoked from all scopes * @return The updated ResourceSharing object after revoking access, or null if the document doesn't exist - * @throws IllegalArgumentException if resourceId, systemIndexName is null/empty, or if revokeAccess is null/empty + * @throws IllegalArgumentException if resourceId, sourceIdx is null/empty, or if revokeAccess is null/empty * @throws RuntimeException if the update operation fails or encounters an error * * @see EntityType @@ -881,65 +884,58 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId * ResourceSharing updated = revokeAccess("resourceId", "systemIndex", revokeAccess); * */ - public ResourceSharing revokeAccess(String resourceId, String systemIndexName, Map> revokeAccess) { - // TODO; check if this needs to be done per scope rather than for all scopes - - if (StringUtils.isBlank(resourceId) || StringUtils.isBlank(systemIndexName) || revokeAccess == null || revokeAccess.isEmpty()) { - throw new IllegalArgumentException("resourceId, systemIndexName, and revokeAccess must not be null or empty"); + public ResourceSharing revokeAccess( + String resourceId, + String sourceIdx, + Map> revokeAccess, + List scopes + ) { + if (StringUtils.isBlank(resourceId) || StringUtils.isBlank(sourceIdx) || revokeAccess == null || revokeAccess.isEmpty()) { + throw new IllegalArgumentException("resourceId, sourceIdx, and revokeAccess must not be null or empty"); } - LOGGER.debug("Revoking access for resource {} in {} for entities: {}", resourceId, systemIndexName, revokeAccess); + LOGGER.debug("Revoking access for resource {} in {} for entities: {} and scopes: {}", resourceId, sourceIdx, revokeAccess, scopes); try { - ResourceSharing existingResource = fetchDocumentById(systemIndexName, resourceId); - if (existingResource == null) { - LOGGER.warn("No document found for resourceId: {} in index: {}", resourceId, systemIndexName); - return null; - } - - ShareWith shareWith = existingResource.getShareWith(); - boolean modified = false; - - if (shareWith != null) { - for (SharedWithScope sharedWithScope : shareWith.getSharedWithScopes()) { - SharedWithScope.SharedWithPerScope sharedWithPerScope = sharedWithScope.getSharedWithPerScope(); - - for (Map.Entry> entry : revokeAccess.entrySet()) { - EntityType entityType = entry.getKey(); - List entities = entry.getValue(); - - // Check if the entity type exists in the share_with configuration - switch (entityType) { - case USERS: - if (sharedWithPerScope.getUsers() != null) { - modified = sharedWithPerScope.getUsers().removeAll(entities) || modified; - } - break; - case ROLES: - if (sharedWithPerScope.getRoles() != null) { - modified = sharedWithPerScope.getRoles().removeAll(entities) || modified; + // Revoke resource access + Script revokeScript = new Script( + ScriptType.INLINE, + "painless", + """ + if (ctx._source.share_with != null) { + List scopesToProcess = params.scopes == null || params.scopes.isEmpty() ? new ArrayList(ctx._source.share_with.keySet()) : params.scopes; + + for (def scopeName : scopesToProcess) { + if (ctx._source.share_with.containsKey(scopeName)) { + def existingScope = ctx._source.share_with.get(scopeName); + + for (def entry : params.revokeAccess.entrySet()) { + def entityType = entry.getKey(); + def entitiesToRemove = entry.getValue(); + + if (existingScope.containsKey(entityType) && existingScope[entityType] != null) { + existingScope[entityType].removeAll(entitiesToRemove); + if (existingScope[entityType].isEmpty()) { + existingScope.remove(entityType); + } + } + } + + if (existingScope.isEmpty()) { + ctx._source.share_with.remove(scopeName); + } } - break; - case BACKEND_ROLES: - if (sharedWithPerScope.getBackendRoles() != null) { - modified = sharedWithPerScope.getBackendRoles().removeAll(entities) || modified; - } - break; + } } - } - } - } - - if (!modified) { - LOGGER.debug("No modifications needed for resource: {}", resourceId); - return existingResource; - } + """, + Map.of("revokeAccess", revokeAccess, "scopes", scopes) + ); - // Update resource sharing info - return updateResourceSharingInfo(resourceId, systemIndexName, existingResource.getCreatedBy(), shareWith); + boolean success = updateByQueryResourceSharing(sourceIdx, resourceId, revokeScript); + return success ? fetchDocumentById(sourceIdx, resourceId) : null; } catch (Exception e) { - LOGGER.error("Failed to revoke access for resource: {} in index: {}", resourceId, systemIndexName, e); + LOGGER.error("Failed to revoke access for resource: {} in index: {}", resourceId, sourceIdx, e); throw new RuntimeException("Failed to revoke access: " + e.getMessage(), e); } } @@ -986,7 +982,7 @@ public ResourceSharing revokeAccess(String resourceId, String systemIndexName, M * */ public boolean deleteResourceSharingRecord(String resourceId, String sourceIdx) { - LOGGER.info("Deleting documents from {} where source_idx = {} and resource_id = {}", resourceSharingIndex, sourceIdx, resourceId); + LOGGER.debug("Deleting documents from {} where source_idx = {} and resource_id = {}", resourceSharingIndex, sourceIdx, resourceId); try { DeleteByQueryRequest dbq = new DeleteByQueryRequest(resourceSharingIndex).setQuery( @@ -1074,7 +1070,7 @@ public boolean deleteAllRecordsForUser(String name) { throw new IllegalArgumentException("Username must not be null or empty"); } - LOGGER.info("Deleting all records for user {}", name); + LOGGER.debug("Deleting all records for user {}", name); try { DeleteByQueryRequest deleteRequest = new DeleteByQueryRequest(resourceSharingIndex).setQuery( From 0fe9779de06e7b57ecb9358d3c517d9cf64f9add Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 5 Dec 2024 17:58:37 -0500 Subject: [PATCH 033/201] Convert sets to lists Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 6 +-- .../resources/ResourceAccessHandler.java | 17 ++++--- .../ResourceSharingIndexHandler.java | 48 +++++++++---------- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 8b70509390..0e3e612e20 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2208,7 +2208,7 @@ private void tryAddSecurityProvider() { } @Override - public List listAccessibleResourcesInPlugin(String systemIndexName) { + public Set listAccessibleResourcesInPlugin(String systemIndexName) { return this.resourceAccessHandler.listAccessibleResourcesInPlugin(systemIndexName); } @@ -2226,8 +2226,8 @@ public ResourceSharing shareWith(String resourceId, String systemIndexName, Shar public ResourceSharing revokeAccess( String resourceId, String systemIndexName, - Map> entities, - List scopes + Map> entities, + Set scopes ) { return this.resourceAccessHandler.revokeAccess(resourceId, systemIndexName, entities, scopes); } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 143dda52a3..7812778981 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -13,7 +13,6 @@ import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; @@ -49,11 +48,11 @@ public ResourceAccessHandler( this.adminDNs = adminDns; } - public List listAccessibleResourcesInPlugin(String pluginIndex) { + public Set listAccessibleResourcesInPlugin(String pluginIndex) { final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); if (user == null) { LOGGER.info("Unable to fetch user details "); - return Collections.emptyList(); + return Collections.emptySet(); } LOGGER.info("Listing accessible resource within a system index {} for : {}", pluginIndex, user.getName()); @@ -79,7 +78,7 @@ public List listAccessibleResourcesInPlugin(String pluginIndex) { Set backendRoles = user.getRoles(); result.addAll(loadSharedWithResources(pluginIndex, backendRoles, EntityType.BACKEND_ROLES.toString())); - return result.stream().toList(); + return result; } public boolean hasPermission(String resourceId, String systemIndexName, String scope) { @@ -119,8 +118,8 @@ public ResourceSharing shareWith(String resourceId, String systemIndexName, Shar public ResourceSharing revokeAccess( String resourceId, String systemIndexName, - Map> revokeAccess, - List scopes + Map> revokeAccess, + Set scopes ) { final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Revoking access to resource {} created by {} for {}", resourceId, user.getName(), revokeAccess); @@ -153,16 +152,16 @@ public boolean deleteAllResourceSharingRecordsForCurrentUser() { // Helper methods - private List loadAllResources(String systemIndex) { + private Set loadAllResources(String systemIndex) { return this.resourceSharingIndexHandler.fetchAllDocuments(systemIndex); } - private List loadOwnResources(String systemIndex, String username) { + private Set loadOwnResources(String systemIndex, String username) { // TODO check if this magic variable can be replaced return this.resourceSharingIndexHandler.fetchDocumentsByField(systemIndex, "created_by.user", username); } - private List loadSharedWithResources(String systemIndex, Set entities, String shareWithType) { + private Set loadSharedWithResources(String systemIndex, Set entities, String shareWithType) { return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(systemIndex, entities, shareWithType); } diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 503701127a..309daa93d6 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -10,11 +10,7 @@ package org.opensearch.security.resources; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.Callable; import com.fasterxml.jackson.core.type.TypeReference; @@ -195,7 +191,7 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn * * * @param pluginIndex The source index to match against the source_idx field - * @return List containing resource IDs that belong to the specified system index. + * @return Set containing resource IDs that belong to the specified system index. * Returns an empty list if: *
    *
  • No matching documents are found
  • @@ -210,7 +206,7 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn *
  • Returns an empty list instead of throwing exceptions
  • *
*/ - public List fetchAllDocuments(String pluginIndex) { + public Set fetchAllDocuments(String pluginIndex) { LOGGER.debug("Fetching all documents from {} where source_idx = {}", resourceSharingIndex, pluginIndex); try { @@ -226,7 +222,7 @@ public List fetchAllDocuments(String pluginIndex) { SearchResponse searchResponse = client.search(searchRequest).actionGet(); - List resourceIds = new ArrayList<>(); + Set resourceIds = new HashSet<>(); SearchHit[] hits = searchResponse.getHits().getHits(); for (SearchHit hit : hits) { @@ -242,7 +238,7 @@ public List fetchAllDocuments(String pluginIndex) { } catch (Exception e) { LOGGER.error("Failed to fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); - return List.of(); + return Set.of(); } } @@ -297,7 +293,7 @@ public List fetchAllDocuments(String pluginIndex) { *
  • "roles" - for role-based access
  • *
  • "backend_roles" - for backend role-based access
  • * - * @return List List of resource IDs that match the criteria. The list may be empty + * @return Set List of resource IDs that match the criteria. The list may be empty * if no matches are found * * @throws RuntimeException if the search operation fails @@ -312,7 +308,7 @@ public List fetchAllDocuments(String pluginIndex) { * */ - public List fetchDocumentsForAllScopes(String pluginIndex, Set entities, String entityType) { + public Set fetchDocumentsForAllScopes(String pluginIndex, Set entities, String entityType) { // "*" must match all scopes return fetchDocumentsForAGivenScope(pluginIndex, entities, entityType, "*"); } @@ -369,7 +365,7 @@ public List fetchDocumentsForAllScopes(String pluginIndex, Set e *
  • "backend_roles" - for backend role-based access
  • * * @param scope The scope of the access. Should be implementation of {@link org.opensearch.accesscontrol.resources.ResourceAccessScope} - * @return List List of resource IDs that match the criteria. The list may be empty + * @return Set List of resource IDs that match the criteria. The list may be empty * if no matches are found * * @throws RuntimeException if the search operation fails @@ -383,7 +379,7 @@ public List fetchDocumentsForAllScopes(String pluginIndex, Set e *
  • Properly cleans up scroll context after use
  • * */ - public List fetchDocumentsForAGivenScope(String pluginIndex, Set entities, String entityType, String scope) { + public Set fetchDocumentsForAGivenScope(String pluginIndex, Set entities, String entityType, String scope) { LOGGER.debug( "Fetching documents from index: {}, where share_with.{}.{} contains any of {}", pluginIndex, @@ -392,7 +388,7 @@ public List fetchDocumentsForAGivenScope(String pluginIndex, Set entities ); - List resourceIds = new ArrayList<>(); + Set resourceIds = new HashSet<>(); final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); try { @@ -473,7 +469,7 @@ public List fetchDocumentsForAGivenScope(String pluginIndex, Set * @param systemIndex The source index to match against the source_idx field * @param field The field name to search in. Must be a valid field in the index mapping * @param value The value to match for the specified field. Performs exact term matching - * @return List List of resource IDs that match the criteria. Returns an empty list + * @return Set List of resource IDs that match the criteria. Returns an empty list * if no matches are found * * @throws IllegalArgumentException if any parameter is null or empty @@ -490,17 +486,17 @@ public List fetchDocumentsForAGivenScope(String pluginIndex, Set * * Example usage: *
    -     * List resources = fetchDocumentsByField("myIndex", "status", "active");
    +     * Set resources = fetchDocumentsByField("myIndex", "status", "active");
          * 
    */ - public List fetchDocumentsByField(String systemIndex, String field, String value) { + public Set fetchDocumentsByField(String systemIndex, String field, String value) { if (StringUtils.isBlank(systemIndex) || StringUtils.isBlank(field) || StringUtils.isBlank(value)) { throw new IllegalArgumentException("systemIndex, field, and value must not be null or empty"); } LOGGER.debug("Fetching documents from index: {}, where {} = {}", systemIndex, field, value); - List resourceIds = new ArrayList<>(); + Set resourceIds = new HashSet<>(); final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); try { @@ -635,7 +631,7 @@ public ResourceSharing fetchDocumentById(String pluginIndex, String resourceId) * @param searchRequest Request to execute * @param boolQuery Query to execute with the request */ - private void executeSearchRequest(List resourceIds, Scroll scroll, SearchRequest searchRequest, BoolQueryBuilder boolQuery) { + private void executeSearchRequest(Set resourceIds, Scroll scroll, SearchRequest searchRequest, BoolQueryBuilder boolQuery) { SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery) .size(1000) .fetchSource(new String[] { "resource_id" }, null); @@ -668,7 +664,7 @@ private void executeSearchRequest(List resourceIds, Scroll scroll, Searc /** * Updates the sharing configuration for an existing resource in the resource sharing index. - * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map, List)} + * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map, Set)} * This method modifies the sharing permissions for a specific resource identified by its * resource ID and source index. * @@ -878,17 +874,17 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId * entities don't exist in the current configuration), the original document is returned unchanged. * @example *
    -     * Map> revokeAccess = new HashMap<>();
    -     * revokeAccess.put(EntityType.USER, Arrays.asList("user1", "user2"));
    -     * revokeAccess.put(EntityType.ROLE, Arrays.asList("role1"));
    +     * Map> revokeAccess = new HashMap<>();
    +     * revokeAccess.put(EntityType.USER, Set.of("user1", "user2"));
    +     * revokeAccess.put(EntityType.ROLE, Set.of("role1"));
          * ResourceSharing updated = revokeAccess("resourceId", "systemIndex", revokeAccess);
          * 
    */ public ResourceSharing revokeAccess( String resourceId, String sourceIdx, - Map> revokeAccess, - List scopes + Map> revokeAccess, + Set scopes ) { if (StringUtils.isBlank(resourceId) || StringUtils.isBlank(sourceIdx) || revokeAccess == null || revokeAccess.isEmpty()) { throw new IllegalArgumentException("resourceId, sourceIdx, and revokeAccess must not be null or empty"); @@ -903,7 +899,7 @@ public ResourceSharing revokeAccess( "painless", """ if (ctx._source.share_with != null) { - List scopesToProcess = params.scopes == null || params.scopes.isEmpty() ? new ArrayList(ctx._source.share_with.keySet()) : params.scopes; + Set scopesToProcess = params.scopes == null || params.scopes.isEmpty() ? ctx._source.share_with.keySet() : params.scopes; for (def scopeName : scopesToProcess) { if (ctx._source.share_with.containsKey(scopeName)) { From ccd5d07fb98c8ec586f87a513085cf67385a8d9e Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 5 Dec 2024 18:15:35 -0500 Subject: [PATCH 034/201] Convert sets to lists Signed-off-by: Darshit Chanpura --- .../list/ListAccessibleResourcesResponse.java | 8 ++--- .../revoke/RevokeResourceAccessRequest.java | 22 ++++++++----- .../RevokeResourceAccessRestAction.java | 7 ++-- .../access/share/ShareResourceRequest.java | 20 +++--------- .../verify/VerifyResourceAccessRequest.java | 15 ++------- ...istAccessibleResourcesTransportAction.java | 4 +-- .../opensearch/sample/utils/Validation.java | 32 +++++++++++++++++++ 7 files changed, 64 insertions(+), 44 deletions(-) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java index 5c3715d143..fb1112bc1d 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java @@ -9,7 +9,7 @@ package org.opensearch.sample.actions.access.list; import java.io.IOException; -import java.util.List; +import java.util.Set; import org.opensearch.core.action.ActionResponse; import org.opensearch.core.common.io.stream.StreamInput; @@ -21,9 +21,9 @@ * Response to a ListAccessibleResourcesRequest */ public class ListAccessibleResourcesResponse extends ActionResponse implements ToXContentObject { - private final List resourceIds; + private final Set resourceIds; - public ListAccessibleResourcesResponse(List resourceIds) { + public ListAccessibleResourcesResponse(Set resourceIds) { this.resourceIds = resourceIds; } @@ -33,7 +33,7 @@ public void writeTo(StreamOutput out) throws IOException { } public ListAccessibleResourcesResponse(final StreamInput in) throws IOException { - resourceIds = in.readStringList(); + resourceIds = in.readSet(StreamInput::readString); } @Override diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java index 3b7b10f19a..e97a2d1244 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java @@ -9,22 +9,23 @@ package org.opensearch.sample.actions.access.revoke; import java.io.IOException; -import java.util.List; import java.util.Map; +import java.util.Set; import org.opensearch.accesscontrol.resources.EntityType; import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionRequestValidationException; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.sample.utils.Validation; public class RevokeResourceAccessRequest extends ActionRequest { private final String resourceId; - private final Map> revokeAccess; - private final List scopes; + private final Map> revokeAccess; + private final Set scopes; - public RevokeResourceAccessRequest(String resourceId, Map> revokeAccess, List scopes) { + public RevokeResourceAccessRequest(String resourceId, Map> revokeAccess, Set scopes) { this.resourceId = resourceId; this.revokeAccess = revokeAccess; this.scopes = scopes; @@ -32,8 +33,8 @@ public RevokeResourceAccessRequest(String resourceId, Map EntityType.valueOf(input.readString()), StreamInput::readStringList); - this.scopes = in.readStringList(); + this.revokeAccess = in.readMap(input -> EntityType.valueOf(input.readString()), input -> input.readSet(StreamInput::readString)); + this.scopes = in.readSet(StreamInput::readString); } @Override @@ -49,6 +50,11 @@ public void writeTo(final StreamOutput out) throws IOException { @Override public ActionRequestValidationException validate() { + + if (!(this.scopes == null)) { + return Validation.validateScopes(this.scopes); + } + return null; } @@ -56,11 +62,11 @@ public String getResourceId() { return resourceId; } - public Map> getRevokeAccess() { + public Map> getRevokeAccess() { return revokeAccess; } - public List getScopes() { + public Set getScopes() { return scopes; } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java index 85a01d2234..3fe0a2329e 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java @@ -12,6 +12,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import org.opensearch.accesscontrol.resources.EntityType; @@ -47,8 +48,8 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli String resourceId = (String) source.get("resource_id"); @SuppressWarnings("unchecked") - Map> revokeSource = (Map>) source.get("entities"); - Map> revoke = revokeSource.entrySet().stream().collect(Collectors.toMap(entry -> { + Map> revokeSource = (Map>) source.get("entities"); + Map> revoke = revokeSource.entrySet().stream().collect(Collectors.toMap(entry -> { try { return EntityType.fromValue(entry.getKey()); } catch (IllegalArgumentException e) { @@ -58,7 +59,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli } }, Map.Entry::getValue)); @SuppressWarnings("unchecked") - List scopes = source.containsKey("scopes") ? (List) source.get("scopes") : List.of(); + Set scopes = source.containsKey("scopes") ? (Set) source.get("scopes") : Set.of(); final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest(resourceId, revoke, scopes); return channel -> client.executeLocally( RevokeResourceAccessAction.INSTANCE, diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRequest.java index b222364c0c..6c2ed12e73 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRequest.java @@ -9,7 +9,7 @@ package org.opensearch.sample.actions.access.share; import java.io.IOException; -import java.util.Arrays; +import java.util.stream.Collectors; import org.opensearch.accesscontrol.resources.ShareWith; import org.opensearch.accesscontrol.resources.SharedWithScope; @@ -17,7 +17,7 @@ import org.opensearch.action.ActionRequestValidationException; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.sample.SampleResourceScope; +import org.opensearch.sample.utils.Validation; public class ShareResourceRequest extends ActionRequest { @@ -43,19 +43,9 @@ public void writeTo(final StreamOutput out) throws IOException { @Override public ActionRequestValidationException validate() { - for (SharedWithScope s : shareWith.getSharedWithScopes()) { - try { - SampleResourceScope.valueOf(s.getScope()); - } catch (IllegalArgumentException | NullPointerException e) { - ActionRequestValidationException exception = new ActionRequestValidationException(); - exception.addValidationError( - "Invalid scope: " + s.getScope() + ". Scope must be one of: " + Arrays.toString(SampleResourceScope.values()) - ); - return exception; - } - return null; - } - return null; + return Validation.validateScopes( + shareWith.getSharedWithScopes().stream().map(SharedWithScope::getScope).collect(Collectors.toSet()) + ); } public String getResourceId() { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRequest.java index 87c5b5a7f0..b9ab4134c6 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRequest.java @@ -9,13 +9,13 @@ package org.opensearch.sample.actions.access.verify; import java.io.IOException; -import java.util.Arrays; +import java.util.Set; import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionRequestValidationException; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.sample.SampleResourceScope; +import org.opensearch.sample.utils.Validation; public class VerifyResourceAccessRequest extends ActionRequest { @@ -49,16 +49,7 @@ public void writeTo(final StreamOutput out) throws IOException { @Override public ActionRequestValidationException validate() { - try { - SampleResourceScope.valueOf(scope); - } catch (IllegalArgumentException | NullPointerException e) { - ActionRequestValidationException exception = new ActionRequestValidationException(); - exception.addValidationError( - "Invalid scope: " + scope + ". Scope must be one of: " + Arrays.toString(SampleResourceScope.values()) - ); - return exception; - } - return null; + return Validation.validateScopes(Set.of(scope)); } public String getResourceId() { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java index 794675d3f3..2ca748c7d5 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java @@ -8,7 +8,7 @@ package org.opensearch.sample.transport.access; -import java.util.List; +import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -41,7 +41,7 @@ public ListAccessibleResourcesTransportAction(TransportService transportService, protected void doExecute(Task task, ListAccessibleResourcesRequest request, ActionListener listener) { try { ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - List resourceIds = rs.getResourceAccessControlPlugin().listAccessibleResourcesInPlugin(RESOURCE_INDEX_NAME); + Set resourceIds = rs.getResourceAccessControlPlugin().listAccessibleResourcesInPlugin(RESOURCE_INDEX_NAME); log.info("Successfully fetched accessible resources for current user : {}", resourceIds); listener.onResponse(new ListAccessibleResourcesResponse(resourceIds)); } catch (Exception e) { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java new file mode 100644 index 0000000000..13d7761584 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.sample.utils; + +import java.util.Arrays; +import java.util.Set; + +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.sample.SampleResourceScope; + +public class Validation { + public static ActionRequestValidationException validateScopes(Set scopes) { + for (String s : scopes) { + try { + SampleResourceScope.valueOf(s); + } catch (IllegalArgumentException | NullPointerException e) { + ActionRequestValidationException exception = new ActionRequestValidationException(); + exception.addValidationError( + "Invalid scope: " + s + ". Scope must be one of: " + Arrays.toString(SampleResourceScope.values()) + ); + return exception; + } + } + return null; + } +} From bfc39ad849d3a6b583d4d5bb4ca83838ff838224 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 5 Dec 2024 19:05:07 -0500 Subject: [PATCH 035/201] Adds default scopes to validation list Signed-off-by: Darshit Chanpura --- .../opensearch/sample/utils/Validation.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java index 13d7761584..a057d41eed 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java @@ -8,22 +8,26 @@ package org.opensearch.sample.utils; -import java.util.Arrays; +import java.util.HashSet; import java.util.Set; +import org.opensearch.accesscontrol.resources.ResourceAccessScope; import org.opensearch.action.ActionRequestValidationException; import org.opensearch.sample.SampleResourceScope; public class Validation { public static ActionRequestValidationException validateScopes(Set scopes) { + Set validScopes = new HashSet<>(); + for (SampleResourceScope scope : SampleResourceScope.values()) { + validScopes.add(scope.name()); + } + validScopes.add(ResourceAccessScope.READ_ONLY); + validScopes.add(ResourceAccessScope.READ_WRITE); + for (String s : scopes) { - try { - SampleResourceScope.valueOf(s); - } catch (IllegalArgumentException | NullPointerException e) { + if (!validScopes.contains(s)) { ActionRequestValidationException exception = new ActionRequestValidationException(); - exception.addValidationError( - "Invalid scope: " + s + ". Scope must be one of: " + Arrays.toString(SampleResourceScope.values()) - ); + exception.addValidationError("Invalid scope: " + s + ". Scope must be one of: " + validScopes); return exception; } } From acc22c4ea9ca018916b1b619444b8aabfb0e0abe Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 5 Dec 2024 19:16:27 -0500 Subject: [PATCH 036/201] Fixes ClassCastException Signed-off-by: Darshit Chanpura --- .../access/revoke/RevokeResourceAccessRestAction.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java index 3fe0a2329e..1145457863 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java @@ -9,10 +9,7 @@ package org.opensearch.sample.actions.access.revoke; import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import org.opensearch.accesscontrol.resources.EntityType; @@ -59,7 +56,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli } }, Map.Entry::getValue)); @SuppressWarnings("unchecked") - Set scopes = source.containsKey("scopes") ? (Set) source.get("scopes") : Set.of(); + Set scopes = new HashSet<>(source.containsKey("scopes") ? (List) source.get("scopes") : List.of()); final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest(resourceId, revoke, scopes); return channel -> client.executeLocally( RevokeResourceAccessAction.INSTANCE, From 6d7f4c0e25cefef3986e52d712aea3ba7a9c60eb Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 5 Dec 2024 19:48:35 -0500 Subject: [PATCH 037/201] Explicitly casts painless entries to set to avoid duplicates Signed-off-by: Darshit Chanpura --- .../resources/ResourceSharingIndexHandler.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 309daa93d6..3c1dd9771f 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -10,7 +10,10 @@ package org.opensearch.security.resources; import java.io.IOException; -import java.util.*; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; import com.fasterxml.jackson.core.type.TypeReference; @@ -697,7 +700,9 @@ public ResourceSharing updateResourceSharingInfo(String resourceId, String sourc } // Atomic operation - Script updateScript = new Script(ScriptType.INLINE, "painless", """ + // TODO check if this script can be updated to replace magic identifiers (i.e. users, roles and backend_roles) with the ones + // supplied in shareWith + Script updateScript = new Script(ScriptType.INLINE, """ if (ctx._source.share_with == null) { ctx._source.share_with = [:]; } @@ -710,18 +715,20 @@ public ResourceSharing updateResourceSharingInfo(String resourceId, String sourc if (existingScope.users == null) { existingScope.users = new HashSet(); } + existingScope.users = new HashSet<>(existingScope.users); existingScope.users.addAll(newScope.users); } - if (newScope.roles != null) { if (existingScope.roles == null) { existingScope.roles = new HashSet(); } + existingScope.roles = new HashSet<>(existingScope.roles); existingScope.roles.addAll(newScope.roles); } if (newScope.backend_roles != null) { if (existingScope.backend_roles == null) { existingScope.backend_roles = new HashSet(); } + existingScope.backend_roles = new HashSet<>(existingScope.backend_roles); existingScope.backend_roles.addAll(newScope.backend_roles); } } else { @@ -738,7 +745,7 @@ public ResourceSharing updateResourceSharingInfo(String resourceId, String sourc ctx._source.share_with.put(scopeName, newScopeEntry); } } - """, Collections.singletonMap("shareWith", shareWithMap)); + """, "painless", Collections.singletonMap("shareWith", shareWithMap)); boolean success = updateByQueryResourceSharing(sourceIdx, resourceId, updateScript); return success ? new ResourceSharing(resourceId, sourceIdx, createdBy, shareWith) : null; @@ -899,7 +906,7 @@ public ResourceSharing revokeAccess( "painless", """ if (ctx._source.share_with != null) { - Set scopesToProcess = params.scopes == null || params.scopes.isEmpty() ? ctx._source.share_with.keySet() : params.scopes; + Set scopesToProcess = new HashSet(params.scopes == null || params.scopes.isEmpty() ? ctx._source.share_with.keySet() : params.scopes); for (def scopeName : scopesToProcess) { if (ctx._source.share_with.containsKey(scopeName)) { From 9d4ca1e2c607126bd27f9add0ec90e1b3e269df0 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 5 Dec 2024 19:51:30 -0500 Subject: [PATCH 038/201] Fixes revoke access script Signed-off-by: Darshit Chanpura --- .../security/resources/ResourceSharingIndexHandler.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 3c1dd9771f..b460fdc964 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -918,15 +918,8 @@ public ResourceSharing revokeAccess( if (existingScope.containsKey(entityType) && existingScope[entityType] != null) { existingScope[entityType].removeAll(entitiesToRemove); - if (existingScope[entityType].isEmpty()) { - existingScope.remove(entityType); - } } } - - if (existingScope.isEmpty()) { - ctx._source.share_with.remove(scopeName); - } } } } From b4b22d6ff6357c2d345d40c99a2eb98452d87d17 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 6 Dec 2024 15:11:30 -0500 Subject: [PATCH 039/201] Fixes revoke script to handle duplicates Signed-off-by: Darshit Chanpura --- .../ResourceSharingIndexHandler.java | 121 ++++++++++-------- 1 file changed, 67 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index b460fdc964..cce026b8be 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -10,8 +10,11 @@ package org.opensearch.security.resources; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; @@ -702,50 +705,45 @@ public ResourceSharing updateResourceSharingInfo(String resourceId, String sourc // Atomic operation // TODO check if this script can be updated to replace magic identifiers (i.e. users, roles and backend_roles) with the ones // supplied in shareWith - Script updateScript = new Script(ScriptType.INLINE, """ + Script updateScript = new Script(ScriptType.INLINE, "painless", """ if (ctx._source.share_with == null) { ctx._source.share_with = [:]; } + for (def entry : params.shareWith.entrySet()) { def scopeName = entry.getKey(); def newScope = entry.getValue(); - if (ctx._source.share_with.containsKey(scopeName)) { - def existingScope = ctx._source.share_with.get(scopeName); - if (newScope.users != null) { - if (existingScope.users == null) { - existingScope.users = new HashSet(); - } - existingScope.users = new HashSet<>(existingScope.users); - existingScope.users.addAll(newScope.users); - } - if (existingScope.roles == null) { - existingScope.roles = new HashSet(); - } - existingScope.roles = new HashSet<>(existingScope.roles); - existingScope.roles.addAll(newScope.roles); - } - if (newScope.backend_roles != null) { - if (existingScope.backend_roles == null) { - existingScope.backend_roles = new HashSet(); + + if (!ctx._source.share_with.containsKey(scopeName)) { + def newScopeEntry = [:]; + for (def field : newScope.entrySet()) { + if (field.getValue() != null && !field.getValue().isEmpty()) { + newScopeEntry[field.getKey()] = new HashSet(field.getValue()); } - existingScope.backend_roles = new HashSet<>(existingScope.backend_roles); - existingScope.backend_roles.addAll(newScope.backend_roles); } + ctx._source.share_with[scopeName] = newScopeEntry; } else { - def newScopeEntry = [:]; - if (newScope.users != null) { - newScopeEntry.users = new HashSet(newScope.users); - } - if (newScope.roles != null) { - newScopeEntry.roles = new HashSet(newScope.roles); - } - if (newScope.backend_roles != null) { - newScopeEntry.backend_roles = new HashSet(newScope.backend_roles); + def existingScope = ctx._source.share_with[scopeName]; + + for (def field : newScope.entrySet()) { + def fieldName = field.getKey(); + def newValues = field.getValue(); + + if (newValues != null && !newValues.isEmpty()) { + if (!existingScope.containsKey(fieldName)) { + existingScope[fieldName] = new HashSet(); + } + + for (def value : newValues) { + if (!existingScope[fieldName].contains(value)) { + existingScope[fieldName].add(value); + } + } + } } - ctx._source.share_with.put(scopeName, newScopeEntry); } } - """, "painless", Collections.singletonMap("shareWith", shareWithMap)); + """, Collections.singletonMap("shareWith", shareWithMap)); boolean success = updateByQueryResourceSharing(sourceIdx, resourceId, updateScript); return success ? new ResourceSharing(resourceId, sourceIdx, createdBy, shareWith) : null; @@ -900,34 +898,49 @@ public ResourceSharing revokeAccess( LOGGER.debug("Revoking access for resource {} in {} for entities: {} and scopes: {}", resourceId, sourceIdx, revokeAccess, scopes); try { - // Revoke resource access - Script revokeScript = new Script( - ScriptType.INLINE, - "painless", - """ - if (ctx._source.share_with != null) { - Set scopesToProcess = new HashSet(params.scopes == null || params.scopes.isEmpty() ? ctx._source.share_with.keySet() : params.scopes); - - for (def scopeName : scopesToProcess) { - if (ctx._source.share_with.containsKey(scopeName)) { - def existingScope = ctx._source.share_with.get(scopeName); - - for (def entry : params.revokeAccess.entrySet()) { - def entityType = entry.getKey(); - def entitiesToRemove = entry.getValue(); - - if (existingScope.containsKey(entityType) && existingScope[entityType] != null) { - existingScope[entityType].removeAll(entitiesToRemove); - } + Map revoke = new HashMap<>(); + for (Map.Entry> entry : revokeAccess.entrySet()) { + revoke.put(entry.getKey().name().toLowerCase(), new ArrayList<>(entry.getValue())); + } + + List scopesToUse = scopes != null ? new ArrayList<>(scopes) : new ArrayList<>(); + + Script revokeScript = new Script(ScriptType.INLINE, "painless", """ + if (ctx._source.share_with != null) { + Set scopesToProcess = new HashSet(params.scopes.isEmpty() ? ctx._source.share_with.keySet() : params.scopes); + + for (def scopeName : scopesToProcess) { + if (ctx._source.share_with.containsKey(scopeName)) { + def existingScope = ctx._source.share_with.get(scopeName); + + for (def entry : params.revokeAccess.entrySet()) { + def entityType = entry.getKey(); + def entitiesToRemove = entry.getValue(); + + if (existingScope.containsKey(entityType) && existingScope[entityType] != null) { + if (!(existingScope[entityType] instanceof HashSet)) { + existingScope[entityType] = new HashSet(existingScope[entityType]); + } + + existingScope[entityType].removeAll(entitiesToRemove); + + if (existingScope[entityType].isEmpty()) { + existingScope.remove(entityType); } } } + + if (existingScope.isEmpty()) { + ctx._source.share_with.remove(scopeName); + } } - """, - Map.of("revokeAccess", revokeAccess, "scopes", scopes) - ); + } + } + """, Map.of("revokeAccess", revoke, "scopes", scopesToUse)); + // Execute updateByQuery boolean success = updateByQueryResourceSharing(sourceIdx, resourceId, revokeScript); + return success ? fetchDocumentById(sourceIdx, resourceId) : null; } catch (Exception e) { From e87bb80d9e899e0eae8dbb98c6e18a15355226ed Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 7 Dec 2024 00:47:15 -0500 Subject: [PATCH 040/201] Updates logger statement Signed-off-by: Darshit Chanpura --- .../resources/ResourceAccessHandler.java | 27 ++++++++----------- .../ResourceSharingIndexHandler.java | 3 --- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 7812778981..74d83db8c1 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -96,9 +96,9 @@ public boolean hasPermission(String resourceId, String systemIndexName, String s if (isSharedWithEveryone(document) || isOwnerOfResource(document, user.getName()) - || isSharedWithUser(document, user.getName(), scope) - || isSharedWithGroup(document, userRoles, scope) - || isSharedWithGroup(document, userBackendRoles, scope)) { + || isSharedWithEntity(document, EntityType.USERS, Set.of(user.getName()), scope) + || isSharedWithEntity(document, EntityType.ROLES, userRoles, scope) + || isSharedWithEntity(document, EntityType.BACKEND_ROLES, userBackendRoles, scope)) { LOGGER.info("User {} has {} access to {}", user.getName(), scope, resourceId); return true; } @@ -122,7 +122,7 @@ public ResourceSharing revokeAccess( Set scopes ) { final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); - LOGGER.info("Revoking access to resource {} created by {} for {}", resourceId, user.getName(), revokeAccess); + LOGGER.info("User {} revoking access to resource {} for {} for scopes {} ", user.getName(), resourceId, revokeAccess, scopes); return this.resourceSharingIndexHandler.revokeAccess(resourceId, systemIndexName, revokeAccess, scopes); } @@ -169,13 +169,9 @@ private boolean isOwnerOfResource(ResourceSharing document, String userName) { return document.getCreatedBy() != null && document.getCreatedBy().getUser().equals(userName); } - private boolean isSharedWithUser(ResourceSharing document, String userName, String scope) { - return checkSharing(document, "users", userName, scope); - } - - private boolean isSharedWithGroup(ResourceSharing document, Set roles, String scope) { + private boolean isSharedWithEntity(ResourceSharing document, EntityType entityType, Set roles, String scope) { for (String role : roles) { - if (checkSharing(document, "roles", role, scope)) { + if (checkSharing(document, entityType, role, scope)) { return true; } } @@ -187,7 +183,7 @@ private boolean isSharedWithEveryone(ResourceSharing document) { && document.getShareWith().getSharedWithScopes().stream().anyMatch(sharedWithScope -> sharedWithScope.getScope().equals("*")); } - private boolean checkSharing(ResourceSharing document, String sharingType, String identifier, String scope) { + private boolean checkSharing(ResourceSharing document, EntityType entityType, String identifier, String scope) { if (document.getShareWith() == null) { return false; } @@ -200,11 +196,10 @@ private boolean checkSharing(ResourceSharing document, String sharingType, Strin .map(sharedWithScope -> { SharedWithScope.SharedWithPerScope scopePermissions = sharedWithScope.getSharedWithPerScope(); - return switch (sharingType) { - case "users" -> scopePermissions.getUsers().contains(identifier); - case "roles" -> scopePermissions.getRoles().contains(identifier); - case "backend_roles" -> scopePermissions.getBackendRoles().contains(identifier); - default -> false; + return switch (entityType) { + case EntityType.USERS -> scopePermissions.getUsers().contains(identifier); + case EntityType.ROLES -> scopePermissions.getRoles().contains(identifier); + case EntityType.BACKEND_ROLES -> scopePermissions.getBackendRoles().contains(identifier); }; }) .orElse(false); // Return false if no matching scope is found diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index cce026b8be..bfc47a907e 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -405,7 +405,6 @@ public Set fetchDocumentsForAGivenScope(String pluginIndex, Set BoolQueryBuilder shouldQuery = QueryBuilders.boolQuery(); if ("*".equals(scope)) { - // Wildcard behavior: Match any scope dynamically for (String entity : entities) { shouldQuery.should( QueryBuilders.multiMatchQuery(entity, "share_with.*." + entityType + ".keyword") @@ -413,7 +412,6 @@ public Set fetchDocumentsForAGivenScope(String pluginIndex, Set ); } } else { - // Match the specific scope for (String entity : entities) { shouldQuery.should(QueryBuilders.termQuery("share_with." + scope + "." + entityType + ".keyword", entity)); } @@ -938,7 +936,6 @@ public ResourceSharing revokeAccess( } """, Map.of("revokeAccess", revoke, "scopes", scopesToUse)); - // Execute updateByQuery boolean success = updateByQueryResourceSharing(sourceIdx, resourceId, revokeScript); return success ? fetchDocumentById(sourceIdx, resourceId) : null; From 5ad813bcb0d4b7e3516a5fdd29f6340db6e42bf4 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 7 Dec 2024 18:27:11 -0500 Subject: [PATCH 041/201] Adds validation for resource ownership when granting and revoking access Signed-off-by: Darshit Chanpura --- .../resources/ResourceAccessHandler.java | 8 +- .../ResourceSharingIndexHandler.java | 146 ++++++++++-------- 2 files changed, 85 insertions(+), 69 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 74d83db8c1..d060ce6f38 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -19,7 +19,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.accesscontrol.resources.CreatedBy; import org.opensearch.accesscontrol.resources.EntityType; import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.accesscontrol.resources.ShareWith; @@ -109,10 +108,9 @@ public boolean hasPermission(String resourceId, String systemIndexName, String s public ResourceSharing shareWith(String resourceId, String systemIndexName, ShareWith shareWith) { final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); - LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user, shareWith.toString()); + LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith.toString()); - CreatedBy createdBy = new CreatedBy(user.getName()); - return this.resourceSharingIndexHandler.updateResourceSharingInfo(resourceId, systemIndexName, createdBy, shareWith); + return this.resourceSharingIndexHandler.updateResourceSharingInfo(resourceId, systemIndexName, user.getName(), shareWith); } public ResourceSharing revokeAccess( @@ -124,7 +122,7 @@ public ResourceSharing revokeAccess( final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("User {} revoking access to resource {} for {} for scopes {} ", user.getName(), resourceId, revokeAccess, scopes); - return this.resourceSharingIndexHandler.revokeAccess(resourceId, systemIndexName, revokeAccess, scopes); + return this.resourceSharingIndexHandler.revokeAccess(resourceId, systemIndexName, revokeAccess, scopes, user.getName()); } public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index bfc47a907e..6f07f608c9 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -148,7 +148,6 @@ public void createResourceSharingIndexIfAbsent(Callable callable) { public ResourceSharing indexResourceSharing(String resourceId, String resourceIndex, CreatedBy createdBy, ShareWith shareWith) throws IOException { - try { ResourceSharing entry = new ResourceSharing(resourceIndex, resourceId, createdBy, shareWith); @@ -516,7 +515,6 @@ public Set fetchDocumentsByField(String systemIndex, String field, Strin LOGGER.info("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); return resourceIds; - } catch (Exception e) { LOGGER.error("Failed to fetch documents from {} where {} = {}", resourceSharingIndex, field, value, e); throw new RuntimeException("Failed to fetch documents: " + e.getMessage(), e); @@ -579,7 +577,6 @@ public Set fetchDocumentsByField(String systemIndex, String field, Strin */ public ResourceSharing fetchDocumentById(String pluginIndex, String resourceId) { - // Input validation if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(resourceId)) { throw new IllegalArgumentException("systemIndexName and resourceId must not be null or empty"); } @@ -668,12 +665,13 @@ private void executeSearchRequest(Set resourceIds, Scroll scroll, Search /** * Updates the sharing configuration for an existing resource in the resource sharing index. - * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map, Set)} + * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map, Set, String)} * This method modifies the sharing permissions for a specific resource identified by its * resource ID and source index. * * @param resourceId The unique identifier of the resource whose sharing configuration needs to be updated * @param sourceIdx The source index where the original resource is stored + * @param requestUserName The user requesting to share the resource * @param shareWith Updated sharing configuration object containing access control settings: * { * "scope": { @@ -685,7 +683,7 @@ private void executeSearchRequest(Set resourceIds, Scroll scroll, Search * @return ResourceSharing Returns resourceSharing object if the update was successful, null otherwise * @throws RuntimeException if there's an error during the update operation */ - public ResourceSharing updateResourceSharingInfo(String resourceId, String sourceIdx, CreatedBy createdBy, ShareWith shareWith) { + public ResourceSharing updateResourceSharingInfo(String resourceId, String sourceIdx, String requestUserName, ShareWith shareWith) { XContentBuilder builder; Map shareWithMap; try { @@ -700,9 +698,22 @@ public ResourceSharing updateResourceSharingInfo(String resourceId, String sourc return null; } + // Check if the user requesting to share is the owner of the resource + // TODO Add a way for users who are not creators to be able to share the resource + ResourceSharing currentSharingInfo = fetchDocumentById(sourceIdx, resourceId); + if (currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getUser().equals(requestUserName)) { + LOGGER.error("User {} is not authorized to share resource {}", requestUserName, resourceId); + return null; + } + + CreatedBy createdBy; + if (currentSharingInfo == null) { + createdBy = new CreatedBy(requestUserName); + } else { + createdBy = currentSharingInfo.getCreatedBy(); + } + // Atomic operation - // TODO check if this script can be updated to replace magic identifiers (i.e. users, roles and backend_roles) with the ones - // supplied in shareWith Script updateScript = new Script(ScriptType.INLINE, "painless", """ if (ctx._source.share_with == null) { ctx._source.share_with = [:]; @@ -792,8 +803,8 @@ public ResourceSharing updateResourceSharingInfo(String resourceId, String sourc * "query": { * "bool": { * "must": [ - * { "term": { "source_idx": sourceIdx } }, - * { "term": { "resource_id": resourceId } } + * { "term": { "source_idx.keyword": sourceIdx } }, + * { "term": { "resource_id.keyword": resourceId } } * ] * } * } @@ -810,8 +821,6 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId .setScript(updateScript) .setRefresh(true); - LOGGER.debug("Update By Query Request: {}", ubq.toString()); - BulkByScrollResponse response = client.execute(UpdateByQueryAction.INSTANCE, ubq).actionGet(); if (response.getUpdated() > 0) { @@ -866,6 +875,7 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId * @param revokeAccess A map containing entity types (USER, ROLE, BACKEND_ROLE) and their corresponding * values to be removed from the sharing configuration * @param scopes A list of scopes to revoke access from. If null or empty, access is revoked from all scopes + * @param requestUserName The user trying to revoke the accesses * @return The updated ResourceSharing object after revoking access, or null if the document doesn't exist * @throws IllegalArgumentException if resourceId, sourceIdx is null/empty, or if revokeAccess is null/empty * @throws RuntimeException if the update operation fails or encounters an error @@ -887,12 +897,20 @@ public ResourceSharing revokeAccess( String resourceId, String sourceIdx, Map> revokeAccess, - Set scopes + Set scopes, + String requestUserName ) { if (StringUtils.isBlank(resourceId) || StringUtils.isBlank(sourceIdx) || revokeAccess == null || revokeAccess.isEmpty()) { throw new IllegalArgumentException("resourceId, sourceIdx, and revokeAccess must not be null or empty"); } + // TODO Check if access can be revoked by non-creator + ResourceSharing currentSharingInfo = fetchDocumentById(sourceIdx, resourceId); + if (currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getUser().equals(requestUserName)) { + LOGGER.error("User {} is not authorized to revoke access to resource {}", requestUserName, resourceId); + return null; + } + LOGGER.debug("Revoking access for resource {} in {} for entities: {} and scopes: {}", resourceId, sourceIdx, revokeAccess, scopes); try { @@ -1019,58 +1037,58 @@ public boolean deleteResourceSharingRecord(String resourceId, String sourceIdx) } /** - * Deletes all resource sharing records that were created by a specific user. - * This method performs a delete-by-query operation to remove all documents where - * the created_by.user field matches the specified username. - * - *

    The method executes the following steps: - *

      - *
    1. Validates the input username parameter
    2. - *
    3. Creates a delete-by-query request with term query matching
    4. - *
    5. Executes the delete operation with immediate refresh
    6. - *
    7. Returns the operation status based on number of deleted documents
    8. - *
    - * - *

    Example query structure: - *

    -        * {
    -        *   "query": {
    -        *     "term": {
    -        *       "created_by.user": "username"
    -        *     }
    -        *   }
    -        * }
    -        * 
    - * - * @param name The username to match against the created_by.user field - * @return boolean indicating whether the deletion was successful: - *
      - *
    • true - if one or more documents were deleted
    • - *
    • false - if no documents were found
    • - *
    • false - if the operation failed due to an error
    • - *
    - * - * @throws IllegalArgumentException if name is null or empty - * - * - * @implNote Implementation details: - *
      - *
    • Uses DeleteByQueryRequest for efficient bulk deletion
    • - *
    • Sets refresh=true for immediate consistency
    • - *
    • Uses term query for exact username matching
    • - *
    • Implements comprehensive error handling and logging
    • - *
    - * - * Example usage: - *
    -        * boolean success = deleteAllRecordsForUser("john.doe");
    -        * if (success) {
    -        *     // Records were successfully deleted
    -        * } else {
    -        *     // No matching records found or operation failed
    -        * }
    -        * 
    - */ + * Deletes all resource sharing records that were created by a specific user. + * This method performs a delete-by-query operation to remove all documents where + * the created_by.user field matches the specified username. + * + *

    The method executes the following steps: + *

      + *
    1. Validates the input username parameter
    2. + *
    3. Creates a delete-by-query request with term query matching
    4. + *
    5. Executes the delete operation with immediate refresh
    6. + *
    7. Returns the operation status based on number of deleted documents
    8. + *
    + * + *

    Example query structure: + *

    +    * {
    +    *   "query": {
    +    *     "term": {
    +    *       "created_by.user": "username"
    +    *     }
    +    *   }
    +    * }
    +    * 
    + * + * @param name The username to match against the created_by.user field + * @return boolean indicating whether the deletion was successful: + *
      + *
    • true - if one or more documents were deleted
    • + *
    • false - if no documents were found
    • + *
    • false - if the operation failed due to an error
    • + *
    + * + * @throws IllegalArgumentException if name is null or empty + * + * + * @implNote Implementation details: + *
      + *
    • Uses DeleteByQueryRequest for efficient bulk deletion
    • + *
    • Sets refresh=true for immediate consistency
    • + *
    • Uses term query for exact username matching
    • + *
    • Implements comprehensive error handling and logging
    • + *
    + * + * Example usage: + *
    +    * boolean success = deleteAllRecordsForUser("john.doe");
    +    * if (success) {
    +    *     // Records were successfully deleted
    +    * } else {
    +    *     // No matching records found or operation failed
    +    * }
    +    * 
    + */ public boolean deleteAllRecordsForUser(String name) { if (StringUtils.isBlank(name)) { throw new IllegalArgumentException("Username must not be null or empty"); From 4dc7597d452f0b3d799e46bd95506cb061fbea9d Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 7 Dec 2024 18:28:27 -0500 Subject: [PATCH 042/201] Adds plugin specific exception class Signed-off-by: Darshit Chanpura --- .../access/ShareResourceTransportAction.java | 27 +++++++++---------- .../utils/SampleResourcePluginException.java | 17 ++++++++++++ 2 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/utils/SampleResourcePluginException.java diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ShareResourceTransportAction.java index e99a9abf24..3288352d0b 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ShareResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ShareResourceTransportAction.java @@ -21,6 +21,7 @@ import org.opensearch.sample.actions.access.share.ShareResourceAction; import org.opensearch.sample.actions.access.share.ShareResourceRequest; import org.opensearch.sample.actions.access.share.ShareResourceResponse; +import org.opensearch.sample.utils.SampleResourcePluginException; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; @@ -36,26 +37,24 @@ public ShareResourceTransportAction(TransportService transportService, ActionFil @Override protected void doExecute(Task task, ShareResourceRequest request, ActionListener listener) { + ResourceSharing sharing = null; try { - shareResource(request); + sharing = shareResource(request); + if (sharing == null) { + log.error("Failed to share resource {}", request.getResourceId()); + SampleResourcePluginException se = new SampleResourcePluginException("Failed to share resource " + request.getResourceId()); + listener.onFailure(se); + return; + } + log.info("Shared resource : {} with {}", request.getResourceId(), sharing.toString()); listener.onResponse(new ShareResourceResponse("Resource " + request.getResourceId() + " shared successfully.")); } catch (Exception e) { listener.onFailure(e); } } - private void shareResource(ShareResourceRequest request) throws Exception { - try { - ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - ResourceSharing sharing = rs.getResourceAccessControlPlugin() - .shareWith(request.getResourceId(), RESOURCE_INDEX_NAME, request.getShareWith()); - if (sharing == null) { - throw new Exception("Failed to share resource " + request.getResourceId()); - } - log.info("Shared resource : {} with {}", request.getResourceId(), sharing.toString()); - } catch (Exception e) { - log.info("Failed to share resource {}", request.getResourceId(), e); - throw e; - } + private ResourceSharing shareResource(ShareResourceRequest request) throws Exception { + ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); + return rs.getResourceAccessControlPlugin().shareWith(request.getResourceId(), RESOURCE_INDEX_NAME, request.getShareWith()); } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/SampleResourcePluginException.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/SampleResourcePluginException.java new file mode 100644 index 0000000000..1ac2baaaae --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/SampleResourcePluginException.java @@ -0,0 +1,17 @@ +/* + * 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. + */ + +package org.opensearch.sample.utils; + +import org.opensearch.OpenSearchException; + +public class SampleResourcePluginException extends OpenSearchException { + public SampleResourcePluginException(String msg, Object... args) { + super(msg, args); + } +} From 034953732f352d1a4cc4e87a71acbf6da197ea7a Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 7 Dec 2024 19:20:02 -0500 Subject: [PATCH 043/201] Fixes NPE Signed-off-by: Darshit Chanpura --- .../RevokeResourceAccessTransportAction.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java index dd7757e4f2..027e1fffe3 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java @@ -21,6 +21,7 @@ import org.opensearch.sample.actions.access.revoke.RevokeResourceAccessAction; import org.opensearch.sample.actions.access.revoke.RevokeResourceAccessRequest; import org.opensearch.sample.actions.access.revoke.RevokeResourceAccessResponse; +import org.opensearch.sample.utils.SampleResourcePluginException; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; @@ -37,22 +38,25 @@ public RevokeResourceAccessTransportAction(TransportService transportService, Ac @Override protected void doExecute(Task task, RevokeResourceAccessRequest request, ActionListener listener) { try { - revokeAccess(request); + ResourceSharing revoke = revokeAccess(request); + if (revoke == null) { + log.error("Failed to revoke access to resource {}", request.getResourceId()); + SampleResourcePluginException se = new SampleResourcePluginException( + "Failed to revoke access to resource " + request.getResourceId() + ); + listener.onFailure(se); + return; + } + log.info("Revoked resource access for resource: {} with {}", request.getResourceId(), revoke.toString()); listener.onResponse(new RevokeResourceAccessResponse("Resource " + request.getResourceId() + " access revoked successfully.")); } catch (Exception e) { listener.onFailure(e); } } - private void revokeAccess(RevokeResourceAccessRequest request) { - try { - ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - ResourceSharing revoke = rs.getResourceAccessControlPlugin() - .revokeAccess(request.getResourceId(), RESOURCE_INDEX_NAME, request.getRevokeAccess(), request.getScopes()); - log.info("Revoked resource access for resource: {} with {}", request.getResourceId(), revoke.toString()); - } catch (Exception e) { - log.info("Failed to revoke access for resource {}", request.getResourceId(), e); - throw e; - } + private ResourceSharing revokeAccess(RevokeResourceAccessRequest request) { + ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); + return rs.getResourceAccessControlPlugin() + .revokeAccess(request.getResourceId(), RESOURCE_INDEX_NAME, request.getRevokeAccess(), request.getScopes()); } } From c08a99273ccf3cd88d26f3421ba24c56599277c8 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 9 Dec 2024 21:45:06 -0500 Subject: [PATCH 044/201] Adds super-admin bypass Signed-off-by: Darshit Chanpura --- .../resources/ResourceAccessHandler.java | 16 +++++++++++++-- .../ResourceSharingIndexHandler.java | 20 +++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index d060ce6f38..721692c85a 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -82,8 +82,14 @@ public Set listAccessibleResourcesInPlugin(String pluginIndex) { public boolean hasPermission(String resourceId, String systemIndexName, String scope) { final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); + LOGGER.info("Checking if {} has {} permission to resource {}", user.getName(), scope, resourceId); + // check if user is admin, if yes the user has permission + if (adminDNs.isAdmin(user)) { + return true; + } + Set userRoles = user.getSecurityRoles(); Set userBackendRoles = user.getRoles(); @@ -110,7 +116,10 @@ public ResourceSharing shareWith(String resourceId, String systemIndexName, Shar final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith.toString()); - return this.resourceSharingIndexHandler.updateResourceSharingInfo(resourceId, systemIndexName, user.getName(), shareWith); + // check if user is admin, if yes the user has permission + boolean isAdmin = adminDNs.isAdmin(user); + + return this.resourceSharingIndexHandler.updateResourceSharingInfo(resourceId, systemIndexName, user.getName(), shareWith, isAdmin); } public ResourceSharing revokeAccess( @@ -122,7 +131,10 @@ public ResourceSharing revokeAccess( final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("User {} revoking access to resource {} for {} for scopes {} ", user.getName(), resourceId, revokeAccess, scopes); - return this.resourceSharingIndexHandler.revokeAccess(resourceId, systemIndexName, revokeAccess, scopes, user.getName()); + // check if user is admin, if yes the user has permission + boolean isAdmin = adminDNs.isAdmin(user); + + return this.resourceSharingIndexHandler.revokeAccess(resourceId, systemIndexName, revokeAccess, scopes, user.getName(), isAdmin); } public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 6f07f608c9..a4ef90e492 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -145,7 +145,6 @@ public void createResourceSharingIndexIfAbsent(Callable callable) { * @return ResourceSharing Returns resourceSharing object if the operation was successful, null otherwise * @throws IOException if there are issues with index operations or JSON processing */ - public ResourceSharing indexResourceSharing(String resourceId, String resourceIndex, CreatedBy createdBy, ShareWith shareWith) throws IOException { try { @@ -665,7 +664,7 @@ private void executeSearchRequest(Set resourceIds, Scroll scroll, Search /** * Updates the sharing configuration for an existing resource in the resource sharing index. - * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map, Set, String)} + * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map, Set, String, boolean)} * This method modifies the sharing permissions for a specific resource identified by its * resource ID and source index. * @@ -680,10 +679,17 @@ private void executeSearchRequest(Set resourceIds, Scroll scroll, Search * "backend_roles": ["backend_role1"] * } * } + * @param isAdmin Boolean indicating whether the user requesting to share is an admin or not * @return ResourceSharing Returns resourceSharing object if the update was successful, null otherwise * @throws RuntimeException if there's an error during the update operation */ - public ResourceSharing updateResourceSharingInfo(String resourceId, String sourceIdx, String requestUserName, ShareWith shareWith) { + public ResourceSharing updateResourceSharingInfo( + String resourceId, + String sourceIdx, + String requestUserName, + ShareWith shareWith, + boolean isAdmin + ) { XContentBuilder builder; Map shareWithMap; try { @@ -701,7 +707,7 @@ public ResourceSharing updateResourceSharingInfo(String resourceId, String sourc // Check if the user requesting to share is the owner of the resource // TODO Add a way for users who are not creators to be able to share the resource ResourceSharing currentSharingInfo = fetchDocumentById(sourceIdx, resourceId); - if (currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getUser().equals(requestUserName)) { + if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getUser().equals(requestUserName)) { LOGGER.error("User {} is not authorized to share resource {}", requestUserName, resourceId); return null; } @@ -876,6 +882,7 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId * values to be removed from the sharing configuration * @param scopes A list of scopes to revoke access from. If null or empty, access is revoked from all scopes * @param requestUserName The user trying to revoke the accesses + * @param isAdmin Boolean indicating whether the user is an admin or not * @return The updated ResourceSharing object after revoking access, or null if the document doesn't exist * @throws IllegalArgumentException if resourceId, sourceIdx is null/empty, or if revokeAccess is null/empty * @throws RuntimeException if the update operation fails or encounters an error @@ -898,7 +905,8 @@ public ResourceSharing revokeAccess( String sourceIdx, Map> revokeAccess, Set scopes, - String requestUserName + String requestUserName, + boolean isAdmin ) { if (StringUtils.isBlank(resourceId) || StringUtils.isBlank(sourceIdx) || revokeAccess == null || revokeAccess.isEmpty()) { throw new IllegalArgumentException("resourceId, sourceIdx, and revokeAccess must not be null or empty"); @@ -906,7 +914,7 @@ public ResourceSharing revokeAccess( // TODO Check if access can be revoked by non-creator ResourceSharing currentSharingInfo = fetchDocumentById(sourceIdx, resourceId); - if (currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getUser().equals(requestUserName)) { + if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getUser().equals(requestUserName)) { LOGGER.error("User {} is not authorized to revoke access to resource {}", requestUserName, resourceId); return null; } From 8e3d41c2f24df3155cf02c474c2c2955f3ba70d6 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 11 Dec 2024 15:18:02 -0500 Subject: [PATCH 045/201] Updates method names and adds missing java doc Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 4 +- .../resources/ResourceAccessHandler.java | 155 ++++++++++++++---- .../ResourceSharingIndexHandler.java | 4 + .../ResourceSharingIndexListener.java | 21 +++ 4 files changed, 151 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 0e3e612e20..e0293a7abf 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2208,8 +2208,8 @@ private void tryAddSecurityProvider() { } @Override - public Set listAccessibleResourcesInPlugin(String systemIndexName) { - return this.resourceAccessHandler.listAccessibleResourcesInPlugin(systemIndexName); + public Set getAccessibleResourcesForCurrentUser(String systemIndexName) { + return this.resourceAccessHandler.getAccessibleResourcesForCurrentUser(systemIndexName); } @Override diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 721692c85a..5d5b39b697 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -29,6 +29,11 @@ import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; +/** + * This class handles resource access permissions for users and roles. + * It provides methods to check if a user has permission to access a resource + * based on the resource sharing configuration. + */ public class ResourceAccessHandler { private static final Logger LOGGER = LogManager.getLogger(ResourceAccessHandler.class); @@ -47,40 +52,54 @@ public ResourceAccessHandler( this.adminDNs = adminDns; } - public Set listAccessibleResourcesInPlugin(String pluginIndex) { + /** + * Returns a set of accessible resources for the current user within the specified resource index. + * + * @param resourceIndex The resource index to check for accessible resources. + * @return A set of accessible resource IDs. + */ + public Set getAccessibleResourcesForCurrentUser(String resourceIndex) { final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); if (user == null) { LOGGER.info("Unable to fetch user details "); return Collections.emptySet(); } - LOGGER.info("Listing accessible resource within a system index {} for : {}", pluginIndex, user.getName()); + LOGGER.info("Listing accessible resources within a resource index {} for : {}", resourceIndex, user.getName()); // check if user is admin, if yes all resources should be accessible if (adminDNs.isAdmin(user)) { - return loadAllResources(pluginIndex); + return loadAllResources(resourceIndex); } Set result = new HashSet<>(); // 0. Own resources - result.addAll(loadOwnResources(pluginIndex, user.getName())); + result.addAll(loadOwnResources(resourceIndex, user.getName())); // 1. By username - result.addAll(loadSharedWithResources(pluginIndex, Set.of(user.getName()), EntityType.USERS.toString())); + result.addAll(loadSharedWithResources(resourceIndex, Set.of(user.getName()), EntityType.USERS.toString())); // 2. By roles Set roles = user.getSecurityRoles(); - result.addAll(loadSharedWithResources(pluginIndex, roles, EntityType.ROLES.toString())); + result.addAll(loadSharedWithResources(resourceIndex, roles, EntityType.ROLES.toString())); // 3. By backend_roles Set backendRoles = user.getRoles(); - result.addAll(loadSharedWithResources(pluginIndex, backendRoles, EntityType.BACKEND_ROLES.toString())); + result.addAll(loadSharedWithResources(resourceIndex, backendRoles, EntityType.BACKEND_ROLES.toString())); return result; } - public boolean hasPermission(String resourceId, String systemIndexName, String scope) { + /** + * Checks whether current user has given permission (scope) to access given resource. + * + * @param resourceId The resource ID to check access for. + * @param resourceIndex The resource index containing the resource. + * @param scope The permission scope to check. + * @return True if the user has the specified permission, false otherwise. + */ + public boolean hasPermission(String resourceId, String resourceIndex, String scope) { final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Checking if {} has {} permission to resource {}", user.getName(), scope, resourceId); @@ -93,9 +112,9 @@ public boolean hasPermission(String resourceId, String systemIndexName, String s Set userRoles = user.getSecurityRoles(); Set userBackendRoles = user.getRoles(); - ResourceSharing document = this.resourceSharingIndexHandler.fetchDocumentById(systemIndexName, resourceId); + ResourceSharing document = this.resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId); if (document == null) { - LOGGER.warn("Resource {} not found in index {}", resourceId, systemIndexName); + LOGGER.warn("Resource {} not found in index {}", resourceId, resourceIndex); return false; // If the document doesn't exist, no permissions can be granted } @@ -112,19 +131,34 @@ public boolean hasPermission(String resourceId, String systemIndexName, String s return false; } - public ResourceSharing shareWith(String resourceId, String systemIndexName, ShareWith shareWith) { + /** + * Shares a resource with the specified users, roles, and backend roles. + * @param resourceId The resource ID to share. + * @param resourceIndex The index where resource is store + * @param shareWith The users, roles, and backend roles as well as scope to share the resource with. + * @return The updated ResourceSharing document. + */ + public ResourceSharing shareWith(String resourceId, String resourceIndex, ShareWith shareWith) { final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith.toString()); // check if user is admin, if yes the user has permission boolean isAdmin = adminDNs.isAdmin(user); - return this.resourceSharingIndexHandler.updateResourceSharingInfo(resourceId, systemIndexName, user.getName(), shareWith, isAdmin); + return this.resourceSharingIndexHandler.updateResourceSharingInfo(resourceId, resourceIndex, user.getName(), shareWith, isAdmin); } + /** + * Revokes access to a resource for the specified users, roles, and backend roles. + * @param resourceId The resource ID to revoke access from. + * @param resourceIndex The index where resource is store + * @param revokeAccess The users, roles, and backend roles to revoke access for. + * @param scopes The permission scopes to revoke access for. + * @return The updated ResourceSharing document. + */ public ResourceSharing revokeAccess( String resourceId, - String systemIndexName, + String resourceIndex, Map> revokeAccess, Set scopes ) { @@ -134,25 +168,35 @@ public ResourceSharing revokeAccess( // check if user is admin, if yes the user has permission boolean isAdmin = adminDNs.isAdmin(user); - return this.resourceSharingIndexHandler.revokeAccess(resourceId, systemIndexName, revokeAccess, scopes, user.getName(), isAdmin); + return this.resourceSharingIndexHandler.revokeAccess(resourceId, resourceIndex, revokeAccess, scopes, user.getName(), isAdmin); } - public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { + /** + * Deletes a resource sharing record by its ID and the resource index it belongs to. + * @param resourceId The resource ID to delete. + * @param resourceIndex The resource index containing the resource. + * @return True if the record was successfully deleted, false otherwise. + */ + public boolean deleteResourceSharingRecord(String resourceId, String resourceIndex) { final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); - LOGGER.info("Deleting resource sharing record for resource {} in {} created by {}", resourceId, systemIndexName, user.getName()); + LOGGER.info("Deleting resource sharing record for resource {} in {} created by {}", resourceId, resourceIndex, user.getName()); - ResourceSharing document = this.resourceSharingIndexHandler.fetchDocumentById(systemIndexName, resourceId); + ResourceSharing document = this.resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId); if (document == null) { - LOGGER.info("Document {} does not exist in index {}", resourceId, systemIndexName); + LOGGER.info("Document {} does not exist in index {}", resourceId, resourceIndex); return false; } if (!(adminDNs.isAdmin(user) || isOwnerOfResource(document, user.getName()))) { LOGGER.info("User {} does not have access to delete the record {} ", user.getName(), resourceId); return false; } - return this.resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, systemIndexName); + return this.resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex); } + /** + * Deletes all resource sharing records for the current user. + * @return True if all records were successfully deleted, false otherwise. + */ public boolean deleteAllResourceSharingRecordsForCurrentUser() { final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Deleting all resource sharing records for resource {}", user.getName()); @@ -160,39 +204,88 @@ public boolean deleteAllResourceSharingRecordsForCurrentUser() { return this.resourceSharingIndexHandler.deleteAllRecordsForUser(user.getName()); } - // Helper methods - - private Set loadAllResources(String systemIndex) { - return this.resourceSharingIndexHandler.fetchAllDocuments(systemIndex); + /** + * Loads all resources within the specified resource index. + * + * @param resourceIndex The resource index to load resources from. + * @return A set of resource IDs. + */ + private Set loadAllResources(String resourceIndex) { + return this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex); } - private Set loadOwnResources(String systemIndex, String username) { - // TODO check if this magic variable can be replaced - return this.resourceSharingIndexHandler.fetchDocumentsByField(systemIndex, "created_by.user", username); + /** + * Loads resources owned by the specified user within the given resource index. + * + * @param resourceIndex The resource index to load resources from. + * @param userName The username of the owner. + * @return A set of resource IDs owned by the user. + */ + private Set loadOwnResources(String resourceIndex, String userName) { + return this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName); } - private Set loadSharedWithResources(String systemIndex, Set entities, String shareWithType) { - return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(systemIndex, entities, shareWithType); + /** + * Loads resources shared with the specified entities within the given resource index. + * + * @param resourceIndex The resource index to load resources from. + * @param entities The set of entities to check for shared resources. + * @param entityType The type of entity (e.g., users, roles, backend_roles). + * @return A set of resource IDs shared with the specified entities. + */ + private Set loadSharedWithResources(String resourceIndex, Set entities, String entityType) { + return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entities, entityType); } + /** + * Checks if the given resource is owned by the specified user. + * + * @param document The ResourceSharing document to check. + * @param userName The username to check ownership against. + * @return True if the resource is owned by the user, false otherwise. + */ private boolean isOwnerOfResource(ResourceSharing document, String userName) { return document.getCreatedBy() != null && document.getCreatedBy().getUser().equals(userName); } - private boolean isSharedWithEntity(ResourceSharing document, EntityType entityType, Set roles, String scope) { - for (String role : roles) { - if (checkSharing(document, entityType, role, scope)) { + /** + * Checks if the given resource is shared with the specified entities and scope. + * + * @param document The ResourceSharing document to check. + * @param entityType The type of entity (e.g., users, roles, backend_roles). + * @param entities The set of entities to check for sharing. + * @param scope The permission scope to check. + * @return True if the resource is shared with the entities and scope, false otherwise. + */ + private boolean isSharedWithEntity(ResourceSharing document, EntityType entityType, Set entities, String scope) { + for (String entity : entities) { + if (checkSharing(document, entityType, entity, scope)) { return true; } } return false; } + /** + * Checks if the given resource is shared with everyone. + * + * @param document The ResourceSharing document to check. + * @return True if the resource is shared with everyone, false otherwise. + */ private boolean isSharedWithEveryone(ResourceSharing document) { return document.getShareWith() != null && document.getShareWith().getSharedWithScopes().stream().anyMatch(sharedWithScope -> sharedWithScope.getScope().equals("*")); } + /** + * Checks if the given resource is shared with the specified entity and scope. + * + * @param document The ResourceSharing document to check. + * @param entityType The type of entity (e.g., users, roles, backend_roles). + * @param identifier The identifier of the entity to check for sharing. + * @param scope The permission scope to check. + * @return True if the resource is shared with the entity and scope, false otherwise. + */ private boolean checkSharing(ResourceSharing document, EntityType entityType, String identifier, String scope) { if (document.getShareWith() == null) { return false; diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index a4ef90e492..ec75515985 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -67,6 +67,10 @@ import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; +/** + * This class handles the creation and management of the resource sharing index. + * It provides methods to create the index, index resource sharing entries along with updates and deletion, retrieve shared resources. + */ public class ResourceSharingIndexHandler { private static final Logger LOGGER = LogManager.getLogger(ResourceSharingIndexHandler.class); diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java index d7b149a2fb..2fad56fc1b 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -46,6 +46,13 @@ public static ResourceSharingIndexListener getInstance() { } + /** + * Initializes the ResourceSharingIndexListener with the provided ThreadPool and Client. + * This method is called during the plugin's initialization process. + * + * @param threadPool The ThreadPool instance to be used for executing operations. + * @param client The Client instance to be used for interacting with OpenSearch. + */ public void initialize(ThreadPool threadPool, Client client) { if (initialized) { @@ -66,6 +73,13 @@ public boolean isInitialized() { return initialized; } + /** + * This method is called after an index operation is performed. + * It creates a resource sharing entry in the dedicated resource sharing index. + * @param shardId The shard ID of the index where the operation was performed. + * @param index The index where the operation was performed. + * @param result The result of the index operation. + */ @Override public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { @@ -89,6 +103,13 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re } } + /** + * This method is called after a delete operation is performed. + * It deletes the corresponding resource sharing entry from the dedicated resource sharing index. + * @param shardId The shard ID of the index where the delete operation was performed. + * @param delete The delete operation that was performed. + * @param result The result of the delete operation. + */ @Override public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { From 334b50d438853d61b5409abc77de190151b01cc2 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 11 Dec 2024 15:38:51 -0500 Subject: [PATCH 046/201] Updates method name to corresponding to changes in core Signed-off-by: Darshit Chanpura --- sample-resource-plugin/README.md | 146 ++++++++++++++++++ .../sample/SampleResourcePlugin.java | 5 +- ...istAccessibleResourcesTransportAction.java | 2 +- 3 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 sample-resource-plugin/README.md diff --git a/sample-resource-plugin/README.md b/sample-resource-plugin/README.md new file mode 100644 index 0000000000..d40d792f68 --- /dev/null +++ b/sample-resource-plugin/README.md @@ -0,0 +1,146 @@ +# Resource Sharing and Access Control Plugin + +This plugin demonstrates resource sharing and access control functionality, providing APIs to create, manage, and verify access to resources. The plugin enables fine-grained permissions for sharing and accessing resources, making it suitable for systems requiring robust security and collaboration. + +## Features + +- Create and delete resources. +- Share resources with specific users, roles and/or backend_roles with specific scope(s). +- Revoke access to shared resources for a list of or all scopes. +- Verify access permissions for a given user within a given scope. +- List all resources accessible to current user. + +## API Endpoints + +The plugin exposes the following six API endpoints: + +### 1. Create Resource +- **Endpoint:** `POST /_plugins/sample_resource_sharing/create` +- **Description:** Creates a new resource. Also creates a resource sharing entry if security plugin is enabled. +- **Request Body:** + ```json + { + "name": "" + } + ``` +- **Response:** + ```json + { + "resource_id": "", + "status": "created" + } + ``` + +### 2. Delete Resource +- **Endpoint:** `DELETE /api/resource/{resource_id}` +- **Description:** Deletes a specified resource owned by the requesting user. +- **Response:** + ```json + { + "resource_id": "", + "status": "deleted" + } + ``` + +### 3. Share Resource +- **Endpoint:** `POST /api/resource/{resource_id}/share` +- **Description:** Shares a resource with specified users or roles with defined permissions. +- **Request Body:** + ```json + { + "share_with": [ + { "type": "user", "id": "user123", "permission": "read_write" }, + { "type": "role", "id": "admin", "permission": "read_only" } + ] + } + ``` +- **Response:** + ```json + { + "resource_id": "", + "status": "shared" + } + ``` + +### 4. Revoke Access +- **Endpoint:** `DELETE /api/resource/{resource_id}/revoke` +- **Description:** Revokes access to a resource for specified users or roles. +- **Request Body:** + ```json + { + "revoke_from": [ "user123", "role:admin" ] + } + ``` +- **Response:** + ```json + { + "resource_id": "", + "status": "access_revoked" + } + ``` + +### 5. Verify Access +- **Endpoint:** `GET /api/resource/{resource_id}/verify` +- **Description:** Verifies if a user or role has access to a specific resource. +- **Query Parameters:** + - `user_id` (optional): ID of the user. + - `role` (optional): Role to verify. +- **Response:** + ```json + { + "resource_id": "", + "access": true, + "permissions": "read_only" + } + ``` + +### 6. List Accessible Resources +- **Endpoint:** `GET /api/resources/accessible` +- **Description:** Lists all resources accessible to the requesting user or role. +- **Response:** + ```json + [ + { + "resource_id": "", + "name": "", + "permissions": "read_write" + }, + { + "resource_id": "", + "name": "", + "permissions": "read_only" + } + ] + ``` + +## Installation + +1. Clone the repository: + ```bash + git clone + ``` + +2. Navigate to the project directory: + ```bash + cd resource-access-plugin + ``` + +3. Build and deploy the plugin: + ```bash + + ``` + +4. Configure the plugin in your environment. + +## Configuration + +- Ensure that the appropriate access control settings are enabled in your system. +- Define user roles and permissions to match your use case. + +## License + +This code is licensed under the Apache 2.0 License. + +## Copyright + +Copyright OpenSearch Contributors. diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index 90a62f7286..3119e2203a 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -76,8 +76,6 @@ public class SampleResourcePlugin extends Plugin implements ActionPlugin, SystemIndexPlugin, ResourcePlugin { private static final Logger log = LogManager.getLogger(SampleResourcePlugin.class); - private Client client; - @Override public Collection createComponents( Client client, @@ -92,7 +90,6 @@ public Collection createComponents( IndexNameExpressionResolver indexNameExpressionResolver, Supplier repositoriesServiceSupplier ) { - this.client = client; log.info("Loaded SampleResourcePlugin components."); return Collections.emptyList(); } @@ -131,7 +128,7 @@ public List getRestHandlers( @Override public Collection getSystemIndexDescriptors(Settings settings) { - final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(RESOURCE_INDEX_NAME, "Example index with resources"); + final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(RESOURCE_INDEX_NAME, "Sample index with resources"); return Collections.singletonList(systemIndexDescriptor); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java index 2ca748c7d5..2c021d6c27 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java @@ -41,7 +41,7 @@ public ListAccessibleResourcesTransportAction(TransportService transportService, protected void doExecute(Task task, ListAccessibleResourcesRequest request, ActionListener listener) { try { ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - Set resourceIds = rs.getResourceAccessControlPlugin().listAccessibleResourcesInPlugin(RESOURCE_INDEX_NAME); + Set resourceIds = rs.getResourceAccessControlPlugin().getAccessibleResourcesForCurrentUser(RESOURCE_INDEX_NAME); log.info("Successfully fetched accessible resources for current user : {}", resourceIds); listener.onResponse(new ListAccessibleResourcesResponse(resourceIds)); } catch (Exception e) { From 0f60c917a319356490fd277ef3228edec1ec7d5a Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 11 Dec 2024 16:09:31 -0500 Subject: [PATCH 047/201] Updates API route Signed-off-by: Darshit Chanpura --- .../actions/access/list/ListAccessibleResourcesRestAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRestAction.java index 2eee67e0f1..c387eacf90 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRestAction.java @@ -24,7 +24,7 @@ public ListAccessibleResourcesRestAction() {} @Override public List routes() { - return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/resource")); + return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/list")); } @Override From 5ca5dec141e1a5aa0e2d05f84705db9eb023b0d7 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 11 Dec 2024 16:09:44 -0500 Subject: [PATCH 048/201] Adds README Signed-off-by: Darshit Chanpura --- sample-resource-plugin/README.md | 118 ++++++++++++++++--------------- 1 file changed, 62 insertions(+), 56 deletions(-) diff --git a/sample-resource-plugin/README.md b/sample-resource-plugin/README.md index d40d792f68..ccd73db983 100644 --- a/sample-resource-plugin/README.md +++ b/sample-resource-plugin/README.md @@ -26,117 +26,123 @@ The plugin exposes the following six API endpoints: - **Response:** ```json { - "resource_id": "", - "status": "created" + "message": "Resource created successfully." } ``` ### 2. Delete Resource -- **Endpoint:** `DELETE /api/resource/{resource_id}` +- **Endpoint:** `DELETE /_plugins/sample_resource_sharing/{resource_id}` - **Description:** Deletes a specified resource owned by the requesting user. - **Response:** ```json { - "resource_id": "", - "status": "deleted" + "message": "Resource deleted successfully." } ``` ### 3. Share Resource -- **Endpoint:** `POST /api/resource/{resource_id}/share` -- **Description:** Shares a resource with specified users or roles with defined permissions. +- **Endpoint:** `POST /_plugins/sample_resource_sharing/share` +- **Description:** Shares a resource with specified users or roles with defined scope. - **Request Body:** ```json - { - "share_with": [ - { "type": "user", "id": "user123", "permission": "read_write" }, - { "type": "role", "id": "admin", "permission": "read_only" } - ] - } + { + "resource_id" : "{{ADMIN_RESOURCE_ID}}", + "share_with" : { + "SAMPLE_FULL_ACCESS": { + "users": ["test"], + "roles": ["test_role"], + "backend_roles": ["test_backend_role"] + }, + "READ_ONLY": { + "users": ["test"], + "roles": ["test_role"], + "backend_roles": ["test_backend_role"] + }, + "READ_WRITE": { + "users": ["test"], + "roles": ["test_role"], + "backend_roles": ["test_backend_role"] + } + } + } ``` - **Response:** ```json - { - "resource_id": "", - "status": "shared" - } + { + "message": "Resource shared successfully." + } ``` ### 4. Revoke Access -- **Endpoint:** `DELETE /api/resource/{resource_id}/revoke` +- **Endpoint:** `POST /_plugins/sample_resource_sharing/revoke` - **Description:** Revokes access to a resource for specified users or roles. - **Request Body:** ```json - { - "revoke_from": [ "user123", "role:admin" ] - } + { + "resource_id" : "", + "entities" : { + "users": ["test", "admin"], + "roles": ["test_role", "all_access"], + "backend_roles": ["test_backend_role", "admin"] + }, + "scopes": ["SAMPLE_FULL_ACCESS", "READ_ONLY", "READ_WRITE"] + } ``` - **Response:** ```json - { - "resource_id": "", - "status": "access_revoked" - } + { + "message": "Resource access revoked successfully." + } ``` ### 5. Verify Access -- **Endpoint:** `GET /api/resource/{resource_id}/verify` -- **Description:** Verifies if a user or role has access to a specific resource. -- **Query Parameters:** - - `user_id` (optional): ID of the user. - - `role` (optional): Role to verify. +- **Endpoint:** `GET /_plugins/sample_resource_sharing/verify_resource_access` +- **Description:** Verifies if a user or role has access to a specific resource with a specific scope. +- **Request Body:** + ```json + { + "resource_id": "", + "scope": "SAMPLE_FULL_ACCESS" + } + ``` - **Response:** ```json { - "resource_id": "", - "access": true, - "permissions": "read_only" + "message": "User has requested scope SAMPLE_FULL_ACCESS access to " } ``` ### 6. List Accessible Resources -- **Endpoint:** `GET /api/resources/accessible` +- **Endpoint:** `GET /_plugins/sample_resource_sharing/list` - **Description:** Lists all resources accessible to the requesting user or role. - **Response:** ```json - [ - { - "resource_id": "", - "name": "", - "permissions": "read_write" - }, - { - "resource_id": "", - "name": "", - "permissions": "read_only" - } - ] + { + "resource-ids": [ + "", + "" + ] + } ``` ## Installation 1. Clone the repository: ```bash - git clone + git clone git@github.com:opensearch-project/security.git ``` 2. Navigate to the project directory: ```bash - cd resource-access-plugin + cd sample-resource-plugin ``` 3. Build and deploy the plugin: ```bash - + $ ./gradlew clean build -x test -x integrationTest -x spotbugsIntegrationTest + $ ./bin/opensearch-plugin install file: /sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-3.0.0.0-SNAPSHOT.zip ``` -4. Configure the plugin in your environment. - -## Configuration - -- Ensure that the appropriate access control settings are enabled in your system. -- Define user roles and permissions to match your use case. - ## License This code is licensed under the Apache 2.0 License. From cabbcd60d557103c263f56412c79600a4773a423 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 12 Dec 2024 16:48:28 -0500 Subject: [PATCH 049/201] Updates methods to return actual resources instead of resource ids Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 4 +- .../resources/ResourceAccessHandler.java | 26 +++---- .../ResourceSharingIndexHandler.java | 74 ++++++++++++++----- 3 files changed, 72 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index e0293a7abf..118b5bc88b 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2208,8 +2208,8 @@ private void tryAddSecurityProvider() { } @Override - public Set getAccessibleResourcesForCurrentUser(String systemIndexName) { - return this.resourceAccessHandler.getAccessibleResourcesForCurrentUser(systemIndexName); + public Set getAccessibleResourcesForCurrentUser(String systemIndexName, Class clazz) { + return this.resourceAccessHandler.getAccessibleResourcesForCurrentUser(systemIndexName, clazz); } @Override diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 5d5b39b697..6837f88cbf 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -58,7 +58,7 @@ public ResourceAccessHandler( * @param resourceIndex The resource index to check for accessible resources. * @return A set of accessible resource IDs. */ - public Set getAccessibleResourcesForCurrentUser(String resourceIndex) { + public Set getAccessibleResourcesForCurrentUser(String resourceIndex, Class clazz) { final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); if (user == null) { LOGGER.info("Unable to fetch user details "); @@ -69,24 +69,24 @@ public Set getAccessibleResourcesForCurrentUser(String resourceIndex) { // check if user is admin, if yes all resources should be accessible if (adminDNs.isAdmin(user)) { - return loadAllResources(resourceIndex); + return loadAllResources(resourceIndex, clazz); } - Set result = new HashSet<>(); + Set result = new HashSet<>(); // 0. Own resources - result.addAll(loadOwnResources(resourceIndex, user.getName())); + result.addAll(loadOwnResources(resourceIndex, user.getName(), clazz)); // 1. By username - result.addAll(loadSharedWithResources(resourceIndex, Set.of(user.getName()), EntityType.USERS.toString())); + result.addAll(loadSharedWithResources(resourceIndex, Set.of(user.getName()), EntityType.USERS.toString(), clazz)); // 2. By roles Set roles = user.getSecurityRoles(); - result.addAll(loadSharedWithResources(resourceIndex, roles, EntityType.ROLES.toString())); + result.addAll(loadSharedWithResources(resourceIndex, roles, EntityType.ROLES.toString(), clazz)); // 3. By backend_roles Set backendRoles = user.getRoles(); - result.addAll(loadSharedWithResources(resourceIndex, backendRoles, EntityType.BACKEND_ROLES.toString())); + result.addAll(loadSharedWithResources(resourceIndex, backendRoles, EntityType.BACKEND_ROLES.toString(), clazz)); return result; } @@ -210,8 +210,8 @@ public boolean deleteAllResourceSharingRecordsForCurrentUser() { * @param resourceIndex The resource index to load resources from. * @return A set of resource IDs. */ - private Set loadAllResources(String resourceIndex) { - return this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex); + private Set loadAllResources(String resourceIndex, Class clazz) { + return this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex, clazz); } /** @@ -221,8 +221,8 @@ private Set loadAllResources(String resourceIndex) { * @param userName The username of the owner. * @return A set of resource IDs owned by the user. */ - private Set loadOwnResources(String resourceIndex, String userName) { - return this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName); + private Set loadOwnResources(String resourceIndex, String userName, Class clazz) { + return this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName, clazz); } /** @@ -233,8 +233,8 @@ private Set loadOwnResources(String resourceIndex, String userName) { * @param entityType The type of entity (e.g., users, roles, backend_roles). * @return A set of resource IDs shared with the specified entities. */ - private Set loadSharedWithResources(String resourceIndex, Set entities, String entityType) { - return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entities, entityType); + private Set loadSharedWithResources(String resourceIndex, Set entities, String entityType, Class clazz) { + return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entities, entityType, clazz); } /** diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index ec75515985..e53c1f1a56 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -30,6 +30,9 @@ import org.opensearch.accesscontrol.resources.ShareWith; import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.action.get.MultiGetItemResponse; +import org.opensearch.action.get.MultiGetRequest; +import org.opensearch.action.get.MultiGetResponse; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.index.IndexResponse; import org.opensearch.action.search.ClearScrollRequest; @@ -214,7 +217,7 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn *
  • Returns an empty list instead of throwing exceptions
  • * */ - public Set fetchAllDocuments(String pluginIndex) { + public Set fetchAllDocuments(String pluginIndex, Class clazz) { LOGGER.debug("Fetching all documents from {} where source_idx = {}", resourceSharingIndex, pluginIndex); try { @@ -242,7 +245,7 @@ public Set fetchAllDocuments(String pluginIndex) { LOGGER.debug("Found {} documents in {} for source_idx: {}", resourceIds.size(), resourceSharingIndex, pluginIndex); - return resourceIds; + return getResourcesFromIds(resourceIds, pluginIndex, clazz); } catch (Exception e) { LOGGER.error("Failed to fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); @@ -316,9 +319,9 @@ public Set fetchAllDocuments(String pluginIndex) { * */ - public Set fetchDocumentsForAllScopes(String pluginIndex, Set entities, String entityType) { + public Set fetchDocumentsForAllScopes(String pluginIndex, Set entities, String entityType, Class clazz) { // "*" must match all scopes - return fetchDocumentsForAGivenScope(pluginIndex, entities, entityType, "*"); + return fetchDocumentsForAGivenScope(pluginIndex, entities, entityType, "*", clazz); } /** @@ -387,7 +390,13 @@ public Set fetchDocumentsForAllScopes(String pluginIndex, Set en *
  • Properly cleans up scroll context after use
  • * */ - public Set fetchDocumentsForAGivenScope(String pluginIndex, Set entities, String entityType, String scope) { + public Set fetchDocumentsForAGivenScope( + String pluginIndex, + Set entities, + String entityType, + String scope, + Class clazz + ) { LOGGER.debug( "Fetching documents from index: {}, where share_with.{}.{} contains any of {}", pluginIndex, @@ -426,11 +435,11 @@ public Set fetchDocumentsForAGivenScope(String pluginIndex, Set LOGGER.debug("Found {} documents matching the criteria in {}", resourceIds.size(), resourceSharingIndex); - return resourceIds; + return getResourcesFromIds(resourceIds, pluginIndex, clazz); } catch (Exception e) { LOGGER.error( - "Failed to fetch documents from {} for criteria - systemIndex: {}, scope: {}, entityType: {}, entities: {}", + "Failed to fetch documents from {} for criteria - pluginIndex: {}, scope: {}, entityType: {}, entities: {}", resourceSharingIndex, pluginIndex, scope, @@ -472,7 +481,7 @@ public Set fetchDocumentsForAGivenScope(String pluginIndex, Set * } * * - * @param systemIndex The source index to match against the source_idx field + * @param pluginIndex The source index to match against the source_idx field * @param field The field name to search in. Must be a valid field in the index mapping * @param value The value to match for the specified field. Performs exact term matching * @return Set List of resource IDs that match the criteria. Returns an empty list @@ -495,12 +504,12 @@ public Set fetchDocumentsForAGivenScope(String pluginIndex, Set * Set resources = fetchDocumentsByField("myIndex", "status", "active"); * */ - public Set fetchDocumentsByField(String systemIndex, String field, String value) { - if (StringUtils.isBlank(systemIndex) || StringUtils.isBlank(field) || StringUtils.isBlank(value)) { - throw new IllegalArgumentException("systemIndex, field, and value must not be null or empty"); + public Set fetchDocumentsByField(String pluginIndex, String field, String value, Class clazz) { + if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(field) || StringUtils.isBlank(value)) { + throw new IllegalArgumentException("pluginIndex, field, and value must not be null or empty"); } - LOGGER.debug("Fetching documents from index: {}, where {} = {}", systemIndex, field, value); + LOGGER.debug("Fetching documents from index: {}, where {} = {}", pluginIndex, field, value); Set resourceIds = new HashSet<>(); final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); @@ -510,14 +519,14 @@ public Set fetchDocumentsByField(String systemIndex, String field, Strin searchRequest.scroll(scroll); BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery("source_idx.keyword", systemIndex)) + .must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)) .must(QueryBuilders.termQuery(field + ".keyword", value)); executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery); LOGGER.info("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); - return resourceIds; + return getResourcesFromIds(resourceIds, pluginIndex, clazz); } catch (Exception e) { LOGGER.error("Failed to fetch documents from {} where {} = {}", resourceSharingIndex, field, value, e); throw new RuntimeException("Failed to fetch documents: " + e.getMessage(), e); @@ -557,7 +566,7 @@ public Set fetchDocumentsByField(String systemIndex, String field, Strin * @return ResourceSharing object if a matching document is found, null if no document * matches the criteria * - * @throws IllegalArgumentException if systemIndexName or resourceId is null or empty + * @throws IllegalArgumentException if pluginIndexName or resourceId is null or empty * @throws RuntimeException if the search operation fails or parsing errors occur, * wrapping the underlying exception * @@ -581,7 +590,7 @@ public Set fetchDocumentsByField(String systemIndex, String field, Strin public ResourceSharing fetchDocumentById(String pluginIndex, String resourceId) { if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(resourceId)) { - throw new IllegalArgumentException("systemIndexName and resourceId must not be null or empty"); + throw new IllegalArgumentException("pluginIndexName and resourceId must not be null or empty"); } LOGGER.debug("Fetching document from index: {}, with resourceId: {}", pluginIndex, resourceId); @@ -901,7 +910,7 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId * Map> revokeAccess = new HashMap<>(); * revokeAccess.put(EntityType.USER, Set.of("user1", "user2")); * revokeAccess.put(EntityType.ROLE, Set.of("role1")); - * ResourceSharing updated = revokeAccess("resourceId", "systemIndex", revokeAccess); + * ResourceSharing updated = revokeAccess("resourceId", "pluginIndex", revokeAccess); * */ public ResourceSharing revokeAccess( @@ -1131,4 +1140,35 @@ public boolean deleteAllRecordsForUser(String name) { } } + /** + * Fetches all documents from the specified resource index and deserializes them into the specified class. + * + * @param resourceIndex The resource index to fetch documents from. + * @param clazz The class to deserialize the documents into. + * @return A set of deserialized documents. + */ + private Set getResourcesFromIds(Set resourceIds, String resourceIndex, Class clazz) { + + Set result = new HashSet<>(); + try { + MultiGetRequest request = new MultiGetRequest(); + for (String id : resourceIds) { + request.add(new MultiGetRequest.Item(resourceIndex, id)); + } + + MultiGetResponse response = client.multiGet(request).actionGet(); + + for (MultiGetItemResponse itemResponse : response.getResponses()) { + if (!itemResponse.isFailed() && itemResponse.getResponse().isExists()) { + String sourceAsString = itemResponse.getResponse().getSourceAsString(); + T resource = DefaultObjectMapper.readValue(sourceAsString, clazz); + result.add(resource); + } + } + } catch (Exception e) { + LOGGER.error("Failed to fetch resources with ids {} from index {}", resourceIds, resourceIndex, e); + } + + return result; + } } From ca377f6311b6b9adc0d44baeac3a8184c9c3d459 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 12 Dec 2024 17:25:45 -0500 Subject: [PATCH 050/201] Returns actual resource when listing the resource Signed-off-by: Darshit Chanpura --- .../resource/create => }/SampleResource.java | 5 ++--- .../list/ListAccessibleResourcesResponse.java | 13 +++++++------ .../resource/create/CreateResourceRestAction.java | 1 + .../ListAccessibleResourcesTransportAction.java | 8 +++++--- 4 files changed, 15 insertions(+), 12 deletions(-) rename sample-resource-plugin/src/main/java/org/opensearch/sample/{actions/resource/create => }/SampleResource.java (90%) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java similarity index 90% rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/SampleResource.java rename to sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java index db475b7018..07441d96b8 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java @@ -9,14 +9,13 @@ * GitHub history for details. */ -package org.opensearch.sample.actions.resource.create; +package org.opensearch.sample; import java.io.IOException; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.sample.Resource; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; @@ -26,7 +25,7 @@ public class SampleResource implements Resource { public SampleResource() {} - SampleResource(StreamInput in) throws IOException { + public SampleResource(StreamInput in) throws IOException { this.name = in.readString(); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java index fb1112bc1d..9c5d2a3e8a 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java @@ -16,30 +16,31 @@ import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.sample.SampleResource; /** * Response to a ListAccessibleResourcesRequest */ public class ListAccessibleResourcesResponse extends ActionResponse implements ToXContentObject { - private final Set resourceIds; + private final Set resources; - public ListAccessibleResourcesResponse(Set resourceIds) { - this.resourceIds = resourceIds; + public ListAccessibleResourcesResponse(Set resources) { + this.resources = resources; } @Override public void writeTo(StreamOutput out) throws IOException { - out.writeStringArray(resourceIds.toArray(new String[0])); + out.writeCollection(resources); } public ListAccessibleResourcesResponse(final StreamInput in) throws IOException { - resourceIds = in.readSet(StreamInput::readString); + this.resources = in.readSet(SampleResource::new); } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field("resource-ids", resourceIds); + builder.field("resources", resources); builder.endObject(); return builder; } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java index 171c539a7c..f56f61d010 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java @@ -17,6 +17,7 @@ import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.RestRequest; import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.sample.SampleResource; import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.POST; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java index 2c021d6c27..57c2c7889f 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java @@ -18,6 +18,7 @@ import org.opensearch.action.support.HandledTransportAction; import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.SampleResource; import org.opensearch.sample.SampleResourcePlugin; import org.opensearch.sample.actions.access.list.ListAccessibleResourcesAction; import org.opensearch.sample.actions.access.list.ListAccessibleResourcesRequest; @@ -41,9 +42,10 @@ public ListAccessibleResourcesTransportAction(TransportService transportService, protected void doExecute(Task task, ListAccessibleResourcesRequest request, ActionListener listener) { try { ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - Set resourceIds = rs.getResourceAccessControlPlugin().getAccessibleResourcesForCurrentUser(RESOURCE_INDEX_NAME); - log.info("Successfully fetched accessible resources for current user : {}", resourceIds); - listener.onResponse(new ListAccessibleResourcesResponse(resourceIds)); + Set resources = rs.getResourceAccessControlPlugin() + .getAccessibleResourcesForCurrentUser(RESOURCE_INDEX_NAME, SampleResource.class); + log.info("Successfully fetched accessible resources for current user : {}", resources); + listener.onResponse(new ListAccessibleResourcesResponse(resources)); } catch (Exception e) { log.info("Failed to list accessible resources for current user: ", e); listener.onFailure(e); From dc964aca7eaa375377b04c6d7354557a31202264 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 12 Dec 2024 20:33:27 -0500 Subject: [PATCH 051/201] Stash context to fetch resources from a system index Signed-off-by: Darshit Chanpura --- .../security/resources/ResourceSharingIndexHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index e53c1f1a56..92ef31402a 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -1150,7 +1150,8 @@ public boolean deleteAllRecordsForUser(String name) { private Set getResourcesFromIds(Set resourceIds, String resourceIndex, Class clazz) { Set result = new HashSet<>(); - try { + // stashing Context to avoid permission issues in-case resourceIndex is a system index + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { MultiGetRequest request = new MultiGetRequest(); for (String id : resourceIds) { request.add(new MultiGetRequest.Item(resourceIndex, id)); From cc973c6864be903f5acba05c28834cce42d6a248 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 12 Dec 2024 20:55:43 -0500 Subject: [PATCH 052/201] Optimize call to fetch resources Signed-off-by: Darshit Chanpura --- .../security/resources/ResourceSharingIndexHandler.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 92ef31402a..cf622b46a1 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -245,7 +245,7 @@ public Set fetchAllDocuments(String pluginIndex, Class clazz) { LOGGER.debug("Found {} documents in {} for source_idx: {}", resourceIds.size(), resourceSharingIndex, pluginIndex); - return getResourcesFromIds(resourceIds, pluginIndex, clazz); + return resourceIds.isEmpty() ? Set.of() : getResourcesFromIds(resourceIds, pluginIndex, clazz); } catch (Exception e) { LOGGER.error("Failed to fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); @@ -435,7 +435,7 @@ public Set fetchDocumentsForAGivenScope( LOGGER.debug("Found {} documents matching the criteria in {}", resourceIds.size(), resourceSharingIndex); - return getResourcesFromIds(resourceIds, pluginIndex, clazz); + return resourceIds.isEmpty() ? Set.of() : getResourcesFromIds(resourceIds, pluginIndex, clazz); } catch (Exception e) { LOGGER.error( @@ -526,7 +526,7 @@ public Set fetchDocumentsByField(String pluginIndex, String field, String LOGGER.info("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); - return getResourcesFromIds(resourceIds, pluginIndex, clazz); + return resourceIds.isEmpty() ? Set.of() : getResourcesFromIds(resourceIds, pluginIndex, clazz); } catch (Exception e) { LOGGER.error("Failed to fetch documents from {} where {} = {}", resourceSharingIndex, field, value, e); throw new RuntimeException("Failed to fetch documents: " + e.getMessage(), e); From 3ce3d92735a927b78e641b155a1c6104a61e781b Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 13 Dec 2024 01:28:44 -0500 Subject: [PATCH 053/201] Updates javadoc Signed-off-by: Darshit Chanpura --- .../security/resources/ResourceSharingIndexHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index cf622b46a1..c0f6ea2bd0 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -304,6 +304,7 @@ public Set fetchAllDocuments(String pluginIndex, Class clazz) { *
  • "roles" - for role-based access
  • *
  • "backend_roles" - for backend role-based access
  • * + * @param clazz Class to deserialize each document from Response into * @return Set List of resource IDs that match the criteria. The list may be empty * if no matches are found * @@ -376,6 +377,7 @@ public Set fetchDocumentsForAllScopes(String pluginIndex, Set ent *
  • "backend_roles" - for backend role-based access
  • * * @param scope The scope of the access. Should be implementation of {@link org.opensearch.accesscontrol.resources.ResourceAccessScope} + * @param clazz Class to deserialize each document from Response into * @return Set List of resource IDs that match the criteria. The list may be empty * if no matches are found * @@ -484,6 +486,7 @@ public Set fetchDocumentsForAGivenScope( * @param pluginIndex The source index to match against the source_idx field * @param field The field name to search in. Must be a valid field in the index mapping * @param value The value to match for the specified field. Performs exact term matching + * @param clazz Class to deserialize each document from Response into * @return Set List of resource IDs that match the criteria. Returns an empty list * if no matches are found * From 428e11e204492d91c91161ee5b6aa7838abb28f3 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 13 Dec 2024 01:43:22 -0500 Subject: [PATCH 054/201] Adds input validation Signed-off-by: Darshit Chanpura --- .../resources/ResourceAccessHandler.java | 32 +++++++++++++++++++ .../ResourceSharingIndexHandler.java | 1 - 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 6837f88cbf..41b999c009 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -59,6 +59,9 @@ public ResourceAccessHandler( * @return A set of accessible resource IDs. */ public Set getAccessibleResourcesForCurrentUser(String resourceIndex, Class clazz) { + if (areArgumentsInvalid(resourceIndex, clazz)) { + return Collections.emptySet(); + } final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); if (user == null) { LOGGER.info("Unable to fetch user details "); @@ -100,6 +103,9 @@ public Set getAccessibleResourcesForCurrentUser(String resourceIndex, Cla * @return True if the user has the specified permission, false otherwise. */ public boolean hasPermission(String resourceId, String resourceIndex, String scope) { + if (areArgumentsInvalid(resourceId, resourceIndex, scope)) { + return false; + } final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Checking if {} has {} permission to resource {}", user.getName(), scope, resourceId); @@ -139,6 +145,9 @@ public boolean hasPermission(String resourceId, String resourceIndex, String sco * @return The updated ResourceSharing document. */ public ResourceSharing shareWith(String resourceId, String resourceIndex, ShareWith shareWith) { + if (areArgumentsInvalid(resourceId, resourceIndex, shareWith)) { + return null; + } final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith.toString()); @@ -162,6 +171,9 @@ public ResourceSharing revokeAccess( Map> revokeAccess, Set scopes ) { + if (areArgumentsInvalid(resourceId, resourceIndex, revokeAccess, scopes)) { + return null; + } final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("User {} revoking access to resource {} for {} for scopes {} ", user.getName(), resourceId, revokeAccess, scopes); @@ -178,6 +190,9 @@ public ResourceSharing revokeAccess( * @return True if the record was successfully deleted, false otherwise. */ public boolean deleteResourceSharingRecord(String resourceId, String resourceIndex) { + if (areArgumentsInvalid(resourceId, resourceIndex)) { + return false; + } final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Deleting resource sharing record for resource {} in {} created by {}", resourceId, resourceIndex, user.getName()); @@ -198,6 +213,7 @@ public boolean deleteResourceSharingRecord(String resourceId, String resourceInd * @return True if all records were successfully deleted, false otherwise. */ public boolean deleteAllResourceSharingRecordsForCurrentUser() { + final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Deleting all resource sharing records for resource {}", user.getName()); @@ -308,4 +324,20 @@ private boolean checkSharing(ResourceSharing document, EntityType entityType, St .orElse(false); // Return false if no matching scope is found } + private boolean areArgumentsInvalid(Object... args) { + if (args == null) { + return true; + } + for (Object arg : args) { + if (arg == null) { + return true; + } + // Additional check for String type arguments + if (arg instanceof String && ((String) arg).trim().isEmpty()) { + return true; + } + } + return false; + } + } diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index c0f6ea2bd0..839af57f9c 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -1151,7 +1151,6 @@ public boolean deleteAllRecordsForUser(String name) { * @return A set of deserialized documents. */ private Set getResourcesFromIds(Set resourceIds, String resourceIndex, Class clazz) { - Set result = new HashSet<>(); // stashing Context to avoid permission issues in-case resourceIndex is a system index try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { From 8845812df90124f0e4043bf32daa752cc06d8623 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 13 Dec 2024 13:06:04 -0500 Subject: [PATCH 055/201] Updates SampleResource class structure Signed-off-by: Darshit Chanpura --- .../org/opensearch/sample/SampleResource.java | 21 ++++++++++++++++--- .../create/CreateResourceRestAction.java | 4 ++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java index 07441d96b8..c384dc770e 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java @@ -12,6 +12,7 @@ package org.opensearch.sample; import java.io.IOException; +import java.util.Map; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; @@ -22,11 +23,15 @@ public class SampleResource implements Resource { private String name; + private String description; + private Map attributes; public SampleResource() {} public SampleResource(StreamInput in) throws IOException { this.name = in.readString(); + this.description = in.readString(); + this.attributes = in.readMap(StreamInput::readString, StreamInput::readString); } @Override @@ -41,12 +46,14 @@ public String getResourceName() { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return builder.startObject().field("name", name).endObject(); + return builder.startObject().field("name", name).field("description", description).field("attributes", attributes).endObject(); } @Override - public void writeTo(StreamOutput streamOutput) throws IOException { - streamOutput.writeString(name); + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeString(description); + out.writeMap(attributes, StreamOutput::writeString, StreamOutput::writeString); } @Override @@ -57,4 +64,12 @@ public String getWriteableName() { public void setName(String name) { this.name = name; } + + public void setDescription(String description) { + this.description = description; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java index f56f61d010..f7aa1c76b5 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java @@ -44,8 +44,12 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli } String name = (String) source.get("name"); + String description = source.containsKey("description") ? (String) source.get("description") : null; + Map attributes = source.containsKey("attributes") ? (Map) source.get("attributes") : null; SampleResource resource = new SampleResource(); resource.setName(name); + resource.setDescription(description); + resource.setAttributes(attributes); final CreateResourceRequest createSampleResourceRequest = new CreateResourceRequest(resource); return channel -> client.executeLocally( CreateResourceAction.INSTANCE, From a55ac2263d4359a1ca77cf4d2ac8553fc8a9b710 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 20 Dec 2024 11:49:25 -0500 Subject: [PATCH 056/201] Adds auditlog capability and conform to changes in core Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 17 +++++++++++++---- .../resources/ResourceAccessHandler.java | 2 +- .../resources/ResourceSharingIndexHandler.java | 6 +++++- .../resources/ResourceSharingIndexListener.java | 6 ++++-- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 118b5bc88b..a2cfded4cc 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -726,7 +726,7 @@ public void onIndexModule(IndexModule indexModule) { if (this.indicesToListen.contains(indexModule.getIndex().getName())) { ResourceSharingIndexListener resourceSharingIndexListener = ResourceSharingIndexListener.getInstance(); - resourceSharingIndexListener.initialize(threadPool, localClient); + resourceSharingIndexListener.initialize(threadPool, localClient, auditLog); indexModule.addIndexOperationListener(resourceSharingIndexListener); log.warn("Security plugin started listening to operations on index {}", indexModule.getIndex().getName()); } @@ -1215,7 +1215,12 @@ public Collection createComponents( } final var resourceSharingIndex = ConfigConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; - ResourceSharingIndexHandler rsIndexHandler = new ResourceSharingIndexHandler(resourceSharingIndex, localClient, threadPool); + ResourceSharingIndexHandler rsIndexHandler = new ResourceSharingIndexHandler( + resourceSharingIndex, + localClient, + threadPool, + auditLog + ); resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns); rmr = ResourceSharingIndexManagementRepository.create(rsIndexHandler); @@ -2150,8 +2155,12 @@ public Collection getSystemIndexDescriptors(Settings sett ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX ); - final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(indexPattern, "Security index"); - return Collections.singletonList(systemIndexDescriptor); + final SystemIndexDescriptor securityIndexDescriptor = new SystemIndexDescriptor(indexPattern, "Security index"); + final SystemIndexDescriptor resourceSharingIndexDescriptor = new SystemIndexDescriptor( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_INDEX, + "Resource Sharing index" + ); + return List.of(securityIndexDescriptor, resourceSharingIndexDescriptor); } @Override diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 41b999c009..f4b9a937c1 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -313,7 +313,7 @@ private boolean checkSharing(ResourceSharing document, EntityType entityType, St .filter(sharedWithScope -> sharedWithScope.getScope().equals(scope)) .findFirst() .map(sharedWithScope -> { - SharedWithScope.SharedWithPerScope scopePermissions = sharedWithScope.getSharedWithPerScope(); + SharedWithScope.ScopeRecipients scopePermissions = sharedWithScope.getSharedWithPerScope(); return switch (entityType) { case EntityType.USERS -> scopePermissions.getUsers().contains(identifier); diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 839af57f9c..de802ac485 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -66,6 +66,7 @@ import org.opensearch.search.SearchHit; import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.auditlog.AuditLog; import org.opensearch.threadpool.ThreadPool; import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; @@ -84,10 +85,13 @@ public class ResourceSharingIndexHandler { private final ThreadPool threadPool; - public ResourceSharingIndexHandler(final String indexName, final Client client, ThreadPool threadPool) { + private final AuditLog auditLog; + + public ResourceSharingIndexHandler(final String indexName, final Client client, final ThreadPool threadPool, final AuditLog auditLog) { this.resourceSharingIndex = indexName; this.client = client; this.threadPool = threadPool; + this.auditLog = auditLog; } public final static Map INDEX_SETTINGS = Map.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java index 2fad56fc1b..6be230f752 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -19,6 +19,7 @@ import org.opensearch.core.index.shard.ShardId; import org.opensearch.index.engine.Engine; import org.opensearch.index.shard.IndexingOperationListener; +import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; @@ -53,7 +54,7 @@ public static ResourceSharingIndexListener getInstance() { * @param threadPool The ThreadPool instance to be used for executing operations. * @param client The Client instance to be used for interacting with OpenSearch. */ - public void initialize(ThreadPool threadPool, Client client) { + public void initialize(ThreadPool threadPool, Client client, AuditLog auditLog) { if (initialized) { return; @@ -64,7 +65,8 @@ public void initialize(ThreadPool threadPool, Client client) { this.resourceSharingIndexHandler = new ResourceSharingIndexHandler( ConfigConstants.OPENSEARCH_RESOURCE_SHARING_INDEX, client, - threadPool + threadPool, + auditLog ); } From 6269d940dd817be2a4a55212e2ef38c5849af235 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 20 Dec 2024 12:02:32 -0500 Subject: [PATCH 057/201] Adds new scope named public Signed-off-by: Darshit Chanpura --- .../main/java/org/opensearch/sample/SampleResourceScope.java | 4 +++- .../transport/resource/DeleteResourceTransportAction.java | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java index 90df0d3764..1d6de8c1f7 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java @@ -19,7 +19,9 @@ */ public enum SampleResourceScope implements ResourceAccessScope { - SAMPLE_FULL_ACCESS("sample_full_access"); + SAMPLE_FULL_ACCESS("sample_full_access"), + + PUBLIC("public"); private final String name; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/DeleteResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/DeleteResourceTransportAction.java index bdc19ab8b3..bb403e3704 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/DeleteResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/DeleteResourceTransportAction.java @@ -54,9 +54,9 @@ protected void doExecute(Task task, DeleteResourceRequest request, ActionListene try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { deleteResource(request, ActionListener.wrap(deleteResponse -> { if (deleteResponse.getResult() == DocWriteResponse.Result.NOT_FOUND) { - listener.onFailure(new ResourceNotFoundException("Resource " + request.getResourceId() + " not found")); + listener.onFailure(new ResourceNotFoundException("Resource " + request.getResourceId() + " not found.")); } else { - listener.onResponse(new DeleteResourceResponse("Resource " + request.getResourceId() + " deleted successfully")); + listener.onResponse(new DeleteResourceResponse("Resource " + request.getResourceId() + " deleted successfully.")); } }, exception -> { log.error("Failed to delete resource: " + request.getResourceId(), exception); From 1c62d367b91d3ab62f1f8a358fe5ed75ab2c9f78 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 20 Dec 2024 12:27:52 -0500 Subject: [PATCH 058/201] Conforms to type bounding change introduced in core Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 3 ++- .../security/resources/ResourceAccessHandler.java | 14 ++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index a2cfded4cc..544141b8bb 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -70,6 +70,7 @@ import org.opensearch.SpecialPermission; import org.opensearch.Version; import org.opensearch.accesscontrol.resources.EntityType; +import org.opensearch.accesscontrol.resources.Resource; import org.opensearch.accesscontrol.resources.ResourceService; import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.accesscontrol.resources.ShareWith; @@ -2217,7 +2218,7 @@ private void tryAddSecurityProvider() { } @Override - public Set getAccessibleResourcesForCurrentUser(String systemIndexName, Class clazz) { + public Set getAccessibleResourcesForCurrentUser(String systemIndexName, Class clazz) { return this.resourceAccessHandler.getAccessibleResourcesForCurrentUser(systemIndexName, clazz); } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index f4b9a937c1..782e4b040b 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -20,6 +20,7 @@ import org.apache.logging.log4j.Logger; import org.opensearch.accesscontrol.resources.EntityType; +import org.opensearch.accesscontrol.resources.Resource; import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.accesscontrol.resources.ShareWith; import org.opensearch.accesscontrol.resources.SharedWithScope; @@ -58,7 +59,7 @@ public ResourceAccessHandler( * @param resourceIndex The resource index to check for accessible resources. * @return A set of accessible resource IDs. */ - public Set getAccessibleResourcesForCurrentUser(String resourceIndex, Class clazz) { + public Set getAccessibleResourcesForCurrentUser(String resourceIndex, Class clazz) { if (areArgumentsInvalid(resourceIndex, clazz)) { return Collections.emptySet(); } @@ -226,7 +227,7 @@ public boolean deleteAllResourceSharingRecordsForCurrentUser() { * @param resourceIndex The resource index to load resources from. * @return A set of resource IDs. */ - private Set loadAllResources(String resourceIndex, Class clazz) { + private Set loadAllResources(String resourceIndex, Class clazz) { return this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex, clazz); } @@ -237,7 +238,7 @@ private Set loadAllResources(String resourceIndex, Class clazz) { * @param userName The username of the owner. * @return A set of resource IDs owned by the user. */ - private Set loadOwnResources(String resourceIndex, String userName, Class clazz) { + private Set loadOwnResources(String resourceIndex, String userName, Class clazz) { return this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName, clazz); } @@ -249,7 +250,12 @@ private Set loadOwnResources(String resourceIndex, String userName, Class * @param entityType The type of entity (e.g., users, roles, backend_roles). * @return A set of resource IDs shared with the specified entities. */ - private Set loadSharedWithResources(String resourceIndex, Set entities, String entityType, Class clazz) { + private Set loadSharedWithResources( + String resourceIndex, + Set entities, + String entityType, + Class clazz + ) { return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entities, entityType, clazz); } From d8969e57d4ac9abfd7fb0c90553d702663b554ab Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 20 Dec 2024 12:44:05 -0500 Subject: [PATCH 059/201] Updates Resource type Signed-off-by: Darshit Chanpura --- .../java/org/opensearch/sample/Resource.java | 21 ------------------- .../org/opensearch/sample/SampleResource.java | 18 ++++++---------- .../create/CreateResourceRequest.java | 2 +- .../CreateResourceTransportAction.java | 2 +- 4 files changed, 8 insertions(+), 35 deletions(-) delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java deleted file mode 100644 index 4ddb56f395..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/Resource.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.sample; - -import org.opensearch.core.common.io.stream.NamedWriteable; -import org.opensearch.core.xcontent.ToXContentFragment; - -public interface Resource extends NamedWriteable, ToXContentFragment { - String getResourceIndex(); - - String getResourceName(); -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java index c384dc770e..abef02ff35 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java @@ -14,12 +14,11 @@ import java.io.IOException; import java.util.Map; +import org.opensearch.accesscontrol.resources.Resource; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.XContentBuilder; -import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; - public class SampleResource implements Resource { private String name; @@ -34,16 +33,6 @@ public SampleResource(StreamInput in) throws IOException { this.attributes = in.readMap(StreamInput::readString, StreamInput::readString); } - @Override - public String getResourceIndex() { - return RESOURCE_INDEX_NAME; - } - - @Override - public String getResourceName() { - return this.name; - } - @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { return builder.startObject().field("name", name).field("description", description).field("attributes", attributes).endObject(); @@ -72,4 +61,9 @@ public void setDescription(String description) { public void setAttributes(Map attributes) { this.attributes = attributes; } + + @Override + public String getResourceName() { + return name; + } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java index 3f330d9719..abad5cd1c3 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java @@ -10,11 +10,11 @@ import java.io.IOException; +import org.opensearch.accesscontrol.resources.Resource; import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionRequestValidationException; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.sample.Resource; /** * Request object for CreateSampleResource transport action diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java index 9a764b61de..052783a90b 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java @@ -13,6 +13,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.accesscontrol.resources.Resource; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; @@ -23,7 +24,6 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.sample.Resource; import org.opensearch.sample.actions.resource.create.CreateResourceAction; import org.opensearch.sample.actions.resource.create.CreateResourceRequest; import org.opensearch.sample.actions.resource.create.CreateResourceResponse; From c24323c1d39d2fa7bfe573350c584d016d71aaaa Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 20 Dec 2024 15:37:22 -0500 Subject: [PATCH 060/201] Stashes context while fetching resource sharing record Signed-off-by: Darshit Chanpura --- .../ResourceSharingIndexHandler.java | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index de802ac485..755793c698 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -118,6 +118,7 @@ public ResourceSharingIndexHandler(final String indexName, final Client client, public void createResourceSharingIndexIfAbsent(Callable callable) { // TODO: Once stashContext is replaced with switchContext this call will have to be modified try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + CreateIndexRequest cir = new CreateIndexRequest(resourceSharingIndex).settings(INDEX_SETTINGS).waitForActiveShards(1); ActionListener cirListener = ActionListener.wrap(response -> { LOGGER.info("Resource sharing index {} created.", resourceSharingIndex); @@ -158,7 +159,8 @@ public void createResourceSharingIndexIfAbsent(Callable callable) { */ public ResourceSharing indexResourceSharing(String resourceId, String resourceIndex, CreatedBy createdBy, ShareWith shareWith) throws IOException { - try { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { ResourceSharing entry = new ResourceSharing(resourceIndex, resourceId, createdBy, shareWith); IndexRequest ir = client.prepareIndex(resourceSharingIndex) @@ -224,7 +226,8 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn public Set fetchAllDocuments(String pluginIndex, Class clazz) { LOGGER.debug("Fetching all documents from {} where source_idx = {}", resourceSharingIndex, pluginIndex); - try { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); @@ -414,7 +417,8 @@ public Set fetchDocumentsForAGivenScope( Set resourceIds = new HashSet<>(); final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); - try { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); searchRequest.scroll(scroll); @@ -521,7 +525,8 @@ public Set fetchDocumentsByField(String pluginIndex, String field, String Set resourceIds = new HashSet<>(); final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); - try { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); searchRequest.scroll(scroll); @@ -602,7 +607,8 @@ public ResourceSharing fetchDocumentById(String pluginIndex, String resourceId) LOGGER.debug("Fetching document from index: {}, with resourceId: {}", pluginIndex, resourceId); - try { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() @@ -838,7 +844,8 @@ public ResourceSharing updateResourceSharingInfo( * */ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId, Script updateScript) { - try { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { BoolQueryBuilder query = QueryBuilders.boolQuery() .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); @@ -941,7 +948,8 @@ public ResourceSharing revokeAccess( LOGGER.debug("Revoking access for resource {} in {} for entities: {} and scopes: {}", resourceId, sourceIdx, revokeAccess, scopes); - try { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { Map revoke = new HashMap<>(); for (Map.Entry> entry : revokeAccess.entrySet()) { revoke.put(entry.getKey().name().toLowerCase(), new ArrayList<>(entry.getValue())); @@ -1036,7 +1044,8 @@ public ResourceSharing revokeAccess( public boolean deleteResourceSharingRecord(String resourceId, String sourceIdx) { LOGGER.debug("Deleting documents from {} where source_idx = {} and resource_id = {}", resourceSharingIndex, sourceIdx, resourceId); - try { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { DeleteByQueryRequest dbq = new DeleteByQueryRequest(resourceSharingIndex).setQuery( QueryBuilders.boolQuery() .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) @@ -1124,7 +1133,8 @@ public boolean deleteAllRecordsForUser(String name) { LOGGER.debug("Deleting all records for user {}", name); - try { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { DeleteByQueryRequest deleteRequest = new DeleteByQueryRequest(resourceSharingIndex).setQuery( QueryBuilders.termQuery("created_by.user", name) ).setRefresh(true); @@ -1157,6 +1167,7 @@ public boolean deleteAllRecordsForUser(String name) { private Set getResourcesFromIds(Set resourceIds, String resourceIndex, Class clazz) { Set result = new HashSet<>(); // stashing Context to avoid permission issues in-case resourceIndex is a system index + // TODO: Once stashContext is replaced with switchContext this call will have to be modified try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { MultiGetRequest request = new MultiGetRequest(); for (String id : resourceIds) { From f514859d757a606dd1627c22a7ff4ea4ad7dca7e Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 20 Dec 2024 15:37:51 -0500 Subject: [PATCH 061/201] Fixes accessDeclaredMembers error caused in AuditConfig class Signed-off-by: Darshit Chanpura --- .../security/DefaultObjectMapper.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/opensearch/security/DefaultObjectMapper.java b/src/main/java/org/opensearch/security/DefaultObjectMapper.java index 68a537c669..05ceabb86c 100644 --- a/src/main/java/org/opensearch/security/DefaultObjectMapper.java +++ b/src/main/java/org/opensearch/security/DefaultObjectMapper.java @@ -287,12 +287,26 @@ public static TypeFactory getTypeFactory() { return objectMapper.getTypeFactory(); } + @SuppressWarnings("removal") public static Set getFields(Class cls) { - return objectMapper.getSerializationConfig() - .introspect(getTypeFactory().constructType(cls)) - .findProperties() - .stream() - .map(BeanPropertyDefinition::getName) - .collect(ImmutableSet.toImmutableSet()); + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged( + (PrivilegedExceptionAction>) () -> objectMapper.getSerializationConfig() + .introspect(getTypeFactory().constructType(cls)) + .findProperties() + .stream() + .map(BeanPropertyDefinition::getName) + .collect(ImmutableSet.toImmutableSet()) + ); + } catch (final PrivilegedActionException e) { + throw (RuntimeException) e.getCause(); + } + } } From 193e846758cbf340f56aa94a84337d19a0a1f0f0 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 20 Dec 2024 17:44:50 -0500 Subject: [PATCH 062/201] Changes log levels and improves log statements Signed-off-by: Darshit Chanpura --- .../org/opensearch/security/OpenSearchSecurityPlugin.java | 3 +-- .../security/resources/ResourceSharingIndexListener.java | 6 +----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 3a31718de4..74025ea4b9 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2128,7 +2128,7 @@ public void onNodeStarted(DiscoveryNode localNode) { String resourceIndex = resourcePlugin.getResourceIndex(); this.indicesToListen.add(resourceIndex); - log.info("Preparing to listen to index: {} of plugin: {}", resourceIndex, resourcePlugin); + log.warn("Security plugin started listening to index: {} of plugin: {}", resourceIndex, resourcePlugin); } final Set securityModules = ReflectionHelper.getModulesLoaded(); @@ -2148,7 +2148,6 @@ public Collection> getGuiceServiceClasses() final List> services = new ArrayList<>(1); services.add(GuiceHolder.class); - log.info("Guice service classes loaded"); return services; } diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java index 6be230f752..1c6950b9ae 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -42,9 +42,7 @@ public class ResourceSharingIndexListener implements IndexingOperationListener { private ResourceSharingIndexListener() {} public static ResourceSharingIndexListener getInstance() { - return ResourceSharingIndexListener.INSTANCE; - } /** @@ -122,11 +120,9 @@ public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResul boolean success = this.resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex); if (success) { - log.info("Successfully deleted resource sharing entries for resource {}", resourceId); + log.info("Successfully deleted resource sharing entry for resource {}", resourceId); } else { log.info("Failed to delete resource sharing entry for resource {}", resourceId); } - } - } From 13fdb81445b716f39387e81ae32c462297e434d0 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 30 Dec 2024 20:48:37 -0500 Subject: [PATCH 063/201] Bring user notion to security plugin Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 9 +-- .../security/resources/Creator.java | 15 ++++ .../security/resources/Recipient.java | 17 +++++ .../resources/ResourceAccessHandler.java | 57 +++++++++------ .../ResourceSharingIndexHandler.java | 73 +++++++++---------- .../ResourceSharingIndexListener.java | 2 +- ...ourceSharingIndexManagementRepository.java | 1 - 7 files changed, 108 insertions(+), 66 deletions(-) create mode 100644 src/main/java/org/opensearch/security/resources/Creator.java create mode 100644 src/main/java/org/opensearch/security/resources/Recipient.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 74025ea4b9..4153d9749d 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -69,11 +69,7 @@ import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; import org.opensearch.Version; -import org.opensearch.accesscontrol.resources.EntityType; -import org.opensearch.accesscontrol.resources.Resource; -import org.opensearch.accesscontrol.resources.ResourceService; -import org.opensearch.accesscontrol.resources.ResourceSharing; -import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.accesscontrol.resources.*; import org.opensearch.action.ActionRequest; import org.opensearch.action.search.PitService; import org.opensearch.action.search.SearchScrollAction; @@ -1230,6 +1226,7 @@ public Collection createComponents( auditLog ); resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns); + resourceAccessHandler.initializeRecipientTypes(); rmr = ResourceSharingIndexManagementRepository.create(rsIndexHandler); @@ -2255,7 +2252,7 @@ public ResourceSharing shareWith(String resourceId, String systemIndexName, Shar public ResourceSharing revokeAccess( String resourceId, String systemIndexName, - Map> entities, + Map> entities, Set scopes ) { return this.resourceAccessHandler.revokeAccess(resourceId, systemIndexName, entities, scopes); diff --git a/src/main/java/org/opensearch/security/resources/Creator.java b/src/main/java/org/opensearch/security/resources/Creator.java new file mode 100644 index 0000000000..84a00756c1 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/Creator.java @@ -0,0 +1,15 @@ +package org.opensearch.security.resources; + +public enum Creator { + USER("user"); + + private final String name; + + Creator(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/org/opensearch/security/resources/Recipient.java b/src/main/java/org/opensearch/security/resources/Recipient.java new file mode 100644 index 0000000000..7cd2ed76ad --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/Recipient.java @@ -0,0 +1,17 @@ +package org.opensearch.security.resources; + +public enum Recipient { + USERS("users"), + ROLES("roles"), + BACKEND_ROLES("backend_roles"); + + private final String name; + + Recipient(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 782e4b040b..eb9a81408d 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -19,7 +19,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.accesscontrol.resources.EntityType; +import org.opensearch.accesscontrol.resources.RecipientType; +import org.opensearch.accesscontrol.resources.RecipientTypeRegistry; import org.opensearch.accesscontrol.resources.Resource; import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.accesscontrol.resources.ShareWith; @@ -53,6 +54,19 @@ public ResourceAccessHandler( this.adminDNs = adminDns; } + /** + * Initializes the recipient types for users, roles, and backend roles. + * These recipient types are used to identify the types of recipients for resource sharing. + */ + public void initializeRecipientTypes() { + RecipientTypeRegistry.registerRecipientType(Recipient.USERS.getName(), new RecipientType(Recipient.USERS.getName())); + RecipientTypeRegistry.registerRecipientType(Recipient.ROLES.getName(), new RecipientType(Recipient.ROLES.getName())); + RecipientTypeRegistry.registerRecipientType( + Recipient.BACKEND_ROLES.getName(), + new RecipientType(Recipient.BACKEND_ROLES.getName()) + ); + } + /** * Returns a set of accessible resources for the current user within the specified resource index. * @@ -82,15 +96,15 @@ public Set getAccessibleResourcesForCurrentUser(String r result.addAll(loadOwnResources(resourceIndex, user.getName(), clazz)); // 1. By username - result.addAll(loadSharedWithResources(resourceIndex, Set.of(user.getName()), EntityType.USERS.toString(), clazz)); + result.addAll(loadSharedWithResources(resourceIndex, Set.of(user.getName()), Recipient.USERS.toString(), clazz)); // 2. By roles Set roles = user.getSecurityRoles(); - result.addAll(loadSharedWithResources(resourceIndex, roles, EntityType.ROLES.toString(), clazz)); + result.addAll(loadSharedWithResources(resourceIndex, roles, Recipient.ROLES.toString(), clazz)); // 3. By backend_roles Set backendRoles = user.getRoles(); - result.addAll(loadSharedWithResources(resourceIndex, backendRoles, EntityType.BACKEND_ROLES.toString(), clazz)); + result.addAll(loadSharedWithResources(resourceIndex, backendRoles, Recipient.BACKEND_ROLES.toString(), clazz)); return result; } @@ -127,9 +141,9 @@ public boolean hasPermission(String resourceId, String resourceIndex, String sco if (isSharedWithEveryone(document) || isOwnerOfResource(document, user.getName()) - || isSharedWithEntity(document, EntityType.USERS, Set.of(user.getName()), scope) - || isSharedWithEntity(document, EntityType.ROLES, userRoles, scope) - || isSharedWithEntity(document, EntityType.BACKEND_ROLES, userBackendRoles, scope)) { + || isSharedWithEntity(document, Recipient.USERS, Set.of(user.getName()), scope) + || isSharedWithEntity(document, Recipient.ROLES, userRoles, scope) + || isSharedWithEntity(document, Recipient.BACKEND_ROLES, userBackendRoles, scope)) { LOGGER.info("User {} has {} access to {}", user.getName(), scope, resourceId); return true; } @@ -169,7 +183,7 @@ public ResourceSharing shareWith(String resourceId, String resourceIndex, ShareW public ResourceSharing revokeAccess( String resourceId, String resourceIndex, - Map> revokeAccess, + Map> revokeAccess, Set scopes ) { if (areArgumentsInvalid(resourceId, resourceIndex, revokeAccess, scopes)) { @@ -247,16 +261,16 @@ private Set loadOwnResources(String resourceIndex, Strin * * @param resourceIndex The resource index to load resources from. * @param entities The set of entities to check for shared resources. - * @param entityType The type of entity (e.g., users, roles, backend_roles). + * @param RecipientType The type of entity (e.g., users, roles, backend_roles). * @return A set of resource IDs shared with the specified entities. */ private Set loadSharedWithResources( String resourceIndex, Set entities, - String entityType, + String RecipientType, Class clazz ) { - return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entities, entityType, clazz); + return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entities, RecipientType, clazz); } /** @@ -267,21 +281,21 @@ private Set loadSharedWithResources( * @return True if the resource is owned by the user, false otherwise. */ private boolean isOwnerOfResource(ResourceSharing document, String userName) { - return document.getCreatedBy() != null && document.getCreatedBy().getUser().equals(userName); + return document.getCreatedBy() != null && document.getCreatedBy().getCreator().equals(userName); } /** * Checks if the given resource is shared with the specified entities and scope. * * @param document The ResourceSharing document to check. - * @param entityType The type of entity (e.g., users, roles, backend_roles). + * @param recipient The recipient entity * @param entities The set of entities to check for sharing. * @param scope The permission scope to check. * @return True if the resource is shared with the entities and scope, false otherwise. */ - private boolean isSharedWithEntity(ResourceSharing document, EntityType entityType, Set entities, String scope) { + private boolean isSharedWithEntity(ResourceSharing document, Recipient recipient, Set entities, String scope) { for (String entity : entities) { - if (checkSharing(document, entityType, entity, scope)) { + if (checkSharing(document, recipient, entity, scope)) { return true; } } @@ -303,12 +317,12 @@ private boolean isSharedWithEveryone(ResourceSharing document) { * Checks if the given resource is shared with the specified entity and scope. * * @param document The ResourceSharing document to check. - * @param entityType The type of entity (e.g., users, roles, backend_roles). + * @param recipient The recipient entity * @param identifier The identifier of the entity to check for sharing. * @param scope The permission scope to check. * @return True if the resource is shared with the entity and scope, false otherwise. */ - private boolean checkSharing(ResourceSharing document, EntityType entityType, String identifier, String scope) { + private boolean checkSharing(ResourceSharing document, Recipient recipient, String identifier, String scope) { if (document.getShareWith() == null) { return false; } @@ -320,11 +334,12 @@ private boolean checkSharing(ResourceSharing document, EntityType entityType, St .findFirst() .map(sharedWithScope -> { SharedWithScope.ScopeRecipients scopePermissions = sharedWithScope.getSharedWithPerScope(); + Map> recipients = scopePermissions.getRecipients(); - return switch (entityType) { - case EntityType.USERS -> scopePermissions.getUsers().contains(identifier); - case EntityType.ROLES -> scopePermissions.getRoles().contains(identifier); - case EntityType.BACKEND_ROLES -> scopePermissions.getBackendRoles().contains(identifier); + return switch (recipient) { + case Recipient.USERS, Recipient.ROLES, Recipient.BACKEND_ROLES -> recipients.get( + RecipientTypeRegistry.fromValue(recipient.getName()) + ).contains(identifier); }; }) .orElse(false); // Return false if no matching scope is found diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 755793c698..83341b1ff2 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -25,7 +25,7 @@ import org.apache.logging.log4j.Logger; import org.opensearch.accesscontrol.resources.CreatedBy; -import org.opensearch.accesscontrol.resources.EntityType; +import org.opensearch.accesscontrol.resources.RecipientType; import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.accesscontrol.resources.ShareWith; import org.opensearch.action.admin.indices.create.CreateIndexRequest; @@ -266,7 +266,7 @@ public Set fetchAllDocuments(String pluginIndex, Class clazz) { * *

    The method executes the following steps: *

      - *
    1. Validates the entityType parameter
    2. + *
    3. Validates the RecipientType parameter
    4. *
    5. Creates a scrolling search request with a compound query
    6. *
    7. Processes results in batches using scroll API
    8. *
    9. Collects all matching resource IDs
    10. @@ -285,9 +285,9 @@ public Set fetchAllDocuments(String pluginIndex, Class clazz) { * "should": [ * { * "nested": { - * "path": "share_with.*.entityType", + * "path": "share_with.*.RecipientType", * "query": { - * "term": { "share_with.*.entityType": "entity_value" } + * "term": { "share_with.*.RecipientType": "entity_value" } * } * } * } @@ -304,8 +304,8 @@ public Set fetchAllDocuments(String pluginIndex, Class clazz) { * * * @param pluginIndex The source index to match against the source_idx field - * @param entities Set of values to match in the specified entityType field - * @param entityType The type of association with the resource. Must be one of: + * @param entities Set of values to match in the specified RecipientType field + * @param RecipientType The type of association with the resource. Must be one of: *
        *
      • "users" - for user-based access
      • *
      • "roles" - for role-based access
      • @@ -327,9 +327,9 @@ public Set fetchAllDocuments(String pluginIndex, Class clazz) { *
      */ - public Set fetchDocumentsForAllScopes(String pluginIndex, Set entities, String entityType, Class clazz) { + public Set fetchDocumentsForAllScopes(String pluginIndex, Set entities, String RecipientType, Class clazz) { // "*" must match all scopes - return fetchDocumentsForAGivenScope(pluginIndex, entities, entityType, "*", clazz); + return fetchDocumentsForAGivenScope(pluginIndex, entities, RecipientType, "*", clazz); } /** @@ -338,7 +338,7 @@ public Set fetchDocumentsForAllScopes(String pluginIndex, Set ent * *

      The method executes the following steps: *

        - *
      1. Validates the entityType parameter
      2. + *
      3. Validates the RecipientType parameter
      4. *
      5. Creates a scrolling search request with a compound query
      6. *
      7. Processes results in batches using scroll API
      8. *
      9. Collects all matching resource IDs
      10. @@ -357,9 +357,9 @@ public Set fetchDocumentsForAllScopes(String pluginIndex, Set ent * "should": [ * { * "nested": { - * "path": "share_with.scope.entityType", + * "path": "share_with.scope.RecipientType", * "query": { - * "term": { "share_with.scope.entityType": "entity_value" } + * "term": { "share_with.scope.RecipientType": "entity_value" } * } * } * } @@ -376,8 +376,8 @@ public Set fetchDocumentsForAllScopes(String pluginIndex, Set ent * * * @param pluginIndex The source index to match against the source_idx field - * @param entities Set of values to match in the specified entityType field - * @param entityType The type of association with the resource. Must be one of: + * @param entities Set of values to match in the specified RecipientType field + * @param RecipientType The type of association with the resource. Must be one of: *
          *
        • "users" - for user-based access
        • *
        • "roles" - for role-based access
        • @@ -402,7 +402,7 @@ public Set fetchDocumentsForAllScopes(String pluginIndex, Set ent public Set fetchDocumentsForAGivenScope( String pluginIndex, Set entities, - String entityType, + String RecipientType, String scope, Class clazz ) { @@ -410,7 +410,7 @@ public Set fetchDocumentsForAGivenScope( "Fetching documents from index: {}, where share_with.{}.{} contains any of {}", pluginIndex, scope, - entityType, + RecipientType, entities ); @@ -428,13 +428,13 @@ public Set fetchDocumentsForAGivenScope( if ("*".equals(scope)) { for (String entity : entities) { shouldQuery.should( - QueryBuilders.multiMatchQuery(entity, "share_with.*." + entityType + ".keyword") + QueryBuilders.multiMatchQuery(entity, "share_with.*." + RecipientType + ".keyword") .type(MultiMatchQueryBuilder.Type.BEST_FIELDS) ); } } else { for (String entity : entities) { - shouldQuery.should(QueryBuilders.termQuery("share_with." + scope + "." + entityType + ".keyword", entity)); + shouldQuery.should(QueryBuilders.termQuery("share_with." + scope + "." + RecipientType + ".keyword", entity)); } } shouldQuery.minimumShouldMatch(1); @@ -449,11 +449,11 @@ public Set fetchDocumentsForAGivenScope( } catch (Exception e) { LOGGER.error( - "Failed to fetch documents from {} for criteria - pluginIndex: {}, scope: {}, entityType: {}, entities: {}", + "Failed to fetch documents from {} for criteria - pluginIndex: {}, scope: {}, RecipientType: {}, entities: {}", resourceSharingIndex, pluginIndex, scope, - entityType, + RecipientType, entities, e ); @@ -618,7 +618,6 @@ public ResourceSharing fetchDocumentById(String pluginIndex, String resourceId) SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery).size(1); // We only need one document since // a resource must have only one // sharing entry - searchRequest.source(searchSourceBuilder); SearchResponse searchResponse = client.search(searchRequest).actionGet(); @@ -733,14 +732,14 @@ public ResourceSharing updateResourceSharingInfo( // Check if the user requesting to share is the owner of the resource // TODO Add a way for users who are not creators to be able to share the resource ResourceSharing currentSharingInfo = fetchDocumentById(sourceIdx, resourceId); - if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getUser().equals(requestUserName)) { + if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { LOGGER.error("User {} is not authorized to share resource {}", requestUserName, resourceId); return null; } CreatedBy createdBy; if (currentSharingInfo == null) { - createdBy = new CreatedBy(requestUserName); + createdBy = new CreatedBy(Creator.USER.getName(), requestUserName); } else { createdBy = currentSharingInfo.getCreatedBy(); } @@ -914,23 +913,23 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId * @throws IllegalArgumentException if resourceId, sourceIdx is null/empty, or if revokeAccess is null/empty * @throws RuntimeException if the update operation fails or encounters an error * - * @see EntityType + * @see RecipientType * @see ResourceSharing * * @apiNote This method modifies the existing document. If no modifications are needed (i.e., specified * entities don't exist in the current configuration), the original document is returned unchanged. * @example *
          -     * Map> revokeAccess = new HashMap<>();
          -     * revokeAccess.put(EntityType.USER, Set.of("user1", "user2"));
          -     * revokeAccess.put(EntityType.ROLE, Set.of("role1"));
          +     * Map> revokeAccess = new HashMap<>();
          +     * revokeAccess.put(RecipientType.USER, Set.of("user1", "user2"));
          +     * revokeAccess.put(RecipientType.ROLE, Set.of("role1"));
                * ResourceSharing updated = revokeAccess("resourceId", "pluginIndex", revokeAccess);
                * 
          */ public ResourceSharing revokeAccess( String resourceId, String sourceIdx, - Map> revokeAccess, + Map> revokeAccess, Set scopes, String requestUserName, boolean isAdmin @@ -941,7 +940,7 @@ public ResourceSharing revokeAccess( // TODO Check if access can be revoked by non-creator ResourceSharing currentSharingInfo = fetchDocumentById(sourceIdx, resourceId); - if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getUser().equals(requestUserName)) { + if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { LOGGER.error("User {} is not authorized to revoke access to resource {}", requestUserName, resourceId); return null; } @@ -951,8 +950,8 @@ public ResourceSharing revokeAccess( // TODO: Once stashContext is replaced with switchContext this call will have to be modified try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { Map revoke = new HashMap<>(); - for (Map.Entry> entry : revokeAccess.entrySet()) { - revoke.put(entry.getKey().name().toLowerCase(), new ArrayList<>(entry.getValue())); + for (Map.Entry> entry : revokeAccess.entrySet()) { + revoke.put(entry.getKey().getType().toLowerCase(), new ArrayList<>(entry.getValue())); } List scopesToUse = scopes != null ? new ArrayList<>(scopes) : new ArrayList<>(); @@ -966,18 +965,18 @@ public ResourceSharing revokeAccess( def existingScope = ctx._source.share_with.get(scopeName); for (def entry : params.revokeAccess.entrySet()) { - def entityType = entry.getKey(); + def RecipientType = entry.getKey(); def entitiesToRemove = entry.getValue(); - if (existingScope.containsKey(entityType) && existingScope[entityType] != null) { - if (!(existingScope[entityType] instanceof HashSet)) { - existingScope[entityType] = new HashSet(existingScope[entityType]); + if (existingScope.containsKey(RecipientType) && existingScope[RecipientType] != null) { + if (!(existingScope[RecipientType] instanceof HashSet)) { + existingScope[RecipientType] = new HashSet(existingScope[RecipientType]); } - existingScope[entityType].removeAll(entitiesToRemove); + existingScope[RecipientType].removeAll(entitiesToRemove); - if (existingScope[entityType].isEmpty()) { - existingScope.remove(entityType); + if (existingScope[RecipientType].isEmpty()) { + existingScope.remove(RecipientType); } } } diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java index 1c6950b9ae..58fe4cccf4 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -94,7 +94,7 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re ResourceSharing sharing = this.resourceSharingIndexHandler.indexResourceSharing( resourceId, resourceIndex, - new CreatedBy(user.getName()), + new CreatedBy(Creator.USER.getName(), user.getName()), null ); log.info("Successfully created a resource sharing entry {}", sharing); diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java index 60cb48145f..17f57269be 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java @@ -35,5 +35,4 @@ public void createResourceSharingIndexIfAbsent() { this.resourceSharingIndexHandler.createResourceSharingIndexIfAbsent(() -> null); } - } From 413bb0b92ab353dcb4ac9fc5e244ad268dab4e43 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 30 Dec 2024 21:21:19 -0500 Subject: [PATCH 064/201] Conforms to changes in core Signed-off-by: Darshit Chanpura --- .../revoke/RevokeResourceAccessRequest.java | 12 ++++++------ .../revoke/RevokeResourceAccessRestAction.java | 15 +++++---------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java index e97a2d1244..f7b4e7b5d7 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java @@ -12,7 +12,7 @@ import java.util.Map; import java.util.Set; -import org.opensearch.accesscontrol.resources.EntityType; +import org.opensearch.accesscontrol.resources.RecipientType; import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionRequestValidationException; import org.opensearch.core.common.io.stream.StreamInput; @@ -22,10 +22,10 @@ public class RevokeResourceAccessRequest extends ActionRequest { private final String resourceId; - private final Map> revokeAccess; + private final Map> revokeAccess; private final Set scopes; - public RevokeResourceAccessRequest(String resourceId, Map> revokeAccess, Set scopes) { + public RevokeResourceAccessRequest(String resourceId, Map> revokeAccess, Set scopes) { this.resourceId = resourceId; this.revokeAccess = revokeAccess; this.scopes = scopes; @@ -33,7 +33,7 @@ public RevokeResourceAccessRequest(String resourceId, Map EntityType.valueOf(input.readString()), input -> input.readSet(StreamInput::readString)); + this.revokeAccess = in.readMap(input -> new RecipientType(input.readString()), input -> input.readSet(StreamInput::readString)); this.scopes = in.readSet(StreamInput::readString); } @@ -42,7 +42,7 @@ public void writeTo(final StreamOutput out) throws IOException { out.writeString(resourceId); out.writeMap( revokeAccess, - (streamOutput, entityType) -> streamOutput.writeString(entityType.name()), + (streamOutput, recipientType) -> streamOutput.writeString(recipientType.getType()), StreamOutput::writeStringCollection ); out.writeStringCollection(scopes); @@ -62,7 +62,7 @@ public String getResourceId() { return resourceId; } - public Map> getRevokeAccess() { + public Map> getRevokeAccess() { return revokeAccess; } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java index 1145457863..387d02502f 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java @@ -12,7 +12,8 @@ import java.util.*; import java.util.stream.Collectors; -import org.opensearch.accesscontrol.resources.EntityType; +import org.opensearch.accesscontrol.resources.RecipientType; +import org.opensearch.accesscontrol.resources.RecipientTypeRegistry; import org.opensearch.client.node.NodeClient; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.rest.BaseRestHandler; @@ -46,15 +47,9 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli String resourceId = (String) source.get("resource_id"); @SuppressWarnings("unchecked") Map> revokeSource = (Map>) source.get("entities"); - Map> revoke = revokeSource.entrySet().stream().collect(Collectors.toMap(entry -> { - try { - return EntityType.fromValue(entry.getKey()); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException( - "Invalid entity type: " + entry.getKey() + ". Valid values are: " + Arrays.toString(EntityType.values()) - ); - } - }, Map.Entry::getValue)); + Map> revoke = revokeSource.entrySet() + .stream() + .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); @SuppressWarnings("unchecked") Set scopes = new HashSet<>(source.containsKey("scopes") ? (List) source.get("scopes") : List.of()); final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest(resourceId, revoke, scopes); From 3ab92bb02d203170106a8865e5b1cfcbaea29785 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 7 Jan 2025 15:51:57 -0500 Subject: [PATCH 065/201] Makes changes to conform to SPI model Signed-off-by: Darshit Chanpura --- sample-resource-plugin/build.gradle | 23 ++++++ .../org/opensearch/sample/SampleResource.java | 7 +- .../sample/SampleResourceParser.java | 41 ++++++++++ .../sample/SampleResourcePlugin.java | 35 +++------ .../sample/SampleResourceScope.java | 7 +- .../list/ListAccessibleResourcesAction.java | 29 ------- .../list/ListAccessibleResourcesRequest.java | 39 ---------- .../list/ListAccessibleResourcesResponse.java | 47 ------------ .../ListAccessibleResourcesRestAction.java | 44 ----------- .../revoke/RevokeResourceAccessAction.java | 21 ------ .../revoke/RevokeResourceAccessRequest.java | 72 ------------------ .../revoke/RevokeResourceAccessResponse.java | 42 ----------- .../RevokeResourceAccessRestAction.java | 62 --------------- .../access/share/ShareResourceAction.java | 26 ------- .../access/share/ShareResourceRequest.java | 58 -------------- .../access/share/ShareResourceResponse.java | 52 ------------- .../access/share/ShareResourceRestAction.java | 75 ------------------- .../create/CreateResourceRequest.java | 2 +- ...istAccessibleResourcesTransportAction.java | 55 -------------- .../RevokeResourceAccessTransportAction.java | 62 --------------- .../access/ShareResourceTransportAction.java | 60 --------------- .../VerifyResourceAccessTransportAction.java | 5 +- .../CreateResourceTransportAction.java | 2 +- .../opensearch/sample/utils/Validation.java | 2 +- 24 files changed, 91 insertions(+), 777 deletions(-) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceParser.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRequest.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRestAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessResponse.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRequest.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceResponse.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRestAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ShareResourceTransportAction.java diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle index e9822c1f22..efdf700599 100644 --- a/sample-resource-plugin/build.gradle +++ b/sample-resource-plugin/build.gradle @@ -9,6 +9,10 @@ apply plugin: 'opensearch.java-rest-test' import org.opensearch.gradle.test.RestIntegTestTask +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} opensearchplugin { name 'opensearch-sample-resource-plugin' @@ -20,6 +24,20 @@ ext { projectSubstitutions = [:] licenseFile = rootProject.file('LICENSE.txt') noticeFile = rootProject.file('NOTICE.txt') + opensearch_version = System.getProperty("opensearch.version", "3.0.0-SNAPSHOT") + isSnapshot = "true" == System.getProperty("build.snapshot", "true") + buildVersionQualifier = System.getProperty("build.version_qualifier", "") + + version_tokens = opensearch_version.tokenize('-') + opensearch_build = version_tokens[0] + '.0' + + + if (buildVersionQualifier) { + opensearch_build += "-${buildVersionQualifier}" + } + if (isSnapshot) { + opensearch_build += "-SNAPSHOT" + } } repositories { @@ -29,8 +47,13 @@ repositories { } dependencies { + implementation "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" + implementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" } +dependencyLicenses.enabled = false +thirdPartyAudit.enabled = false + def es_tmp_dir = rootProject.file('build/private/es_tmp').absoluteFile es_tmp_dir.mkdirs() diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java index abef02ff35..a265f0cdaa 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java @@ -14,10 +14,10 @@ import java.io.IOException; import java.util.Map; -import org.opensearch.accesscontrol.resources.Resource; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.security.spi.resources.Resource; public class SampleResource implements Resource { @@ -66,4 +66,9 @@ public void setAttributes(Map attributes) { public String getResourceName() { return name; } + + @Override + public Resource readFrom(StreamInput streamInput) throws IOException { + return new SampleResource(streamInput); + } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceParser.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceParser.java new file mode 100644 index 0000000000..4bb80fe0e4 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceParser.java @@ -0,0 +1,41 @@ +/* + * 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.sample; + +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.opensearch.SpecialPermission; +import org.opensearch.security.spi.resources.ResourceParser; + +@SuppressWarnings("removal") +public class SampleResourceParser implements ResourceParser { + @Override + public SampleResource parse(String s) throws IOException { + ObjectMapper obj = new ObjectMapper(); + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> obj.readValue(s, SampleResource.class)); + } catch (final PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index 3119e2203a..4c0ab20ffa 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -17,7 +17,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.accesscontrol.resources.ResourceService; import org.opensearch.action.ActionRequest; import org.opensearch.client.Client; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; @@ -39,30 +38,23 @@ import org.opensearch.indices.SystemIndexDescriptor; import org.opensearch.plugins.ActionPlugin; import org.opensearch.plugins.Plugin; -import org.opensearch.plugins.ResourcePlugin; import org.opensearch.plugins.SystemIndexPlugin; import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; -import org.opensearch.sample.actions.access.list.ListAccessibleResourcesAction; -import org.opensearch.sample.actions.access.list.ListAccessibleResourcesRestAction; -import org.opensearch.sample.actions.access.revoke.RevokeResourceAccessAction; -import org.opensearch.sample.actions.access.revoke.RevokeResourceAccessRestAction; -import org.opensearch.sample.actions.access.share.ShareResourceAction; -import org.opensearch.sample.actions.access.share.ShareResourceRestAction; import org.opensearch.sample.actions.access.verify.VerifyResourceAccessAction; import org.opensearch.sample.actions.access.verify.VerifyResourceAccessRestAction; import org.opensearch.sample.actions.resource.create.CreateResourceAction; import org.opensearch.sample.actions.resource.create.CreateResourceRestAction; import org.opensearch.sample.actions.resource.delete.DeleteResourceAction; import org.opensearch.sample.actions.resource.delete.DeleteResourceRestAction; -import org.opensearch.sample.transport.access.ListAccessibleResourcesTransportAction; -import org.opensearch.sample.transport.access.RevokeResourceAccessTransportAction; -import org.opensearch.sample.transport.access.ShareResourceTransportAction; import org.opensearch.sample.transport.access.VerifyResourceAccessTransportAction; import org.opensearch.sample.transport.resource.CreateResourceTransportAction; import org.opensearch.sample.transport.resource.DeleteResourceTransportAction; import org.opensearch.script.ScriptService; +import org.opensearch.security.spi.resources.ResourceParser; +import org.opensearch.security.spi.resources.ResourceService; +import org.opensearch.security.spi.resources.ResourceSharingExtension; import org.opensearch.threadpool.ThreadPool; import org.opensearch.watcher.ResourceWatcherService; @@ -73,7 +65,7 @@ * It uses ".sample_resources" index to manage its resources, and exposes a REST API * */ -public class SampleResourcePlugin extends Plugin implements ActionPlugin, SystemIndexPlugin, ResourcePlugin { +public class SampleResourcePlugin extends Plugin implements ActionPlugin, SystemIndexPlugin, ResourceSharingExtension { private static final Logger log = LogManager.getLogger(SampleResourcePlugin.class); @Override @@ -104,23 +96,13 @@ public List getRestHandlers( IndexNameExpressionResolver indexNameExpressionResolver, Supplier nodesInCluster ) { - return List.of( - new CreateResourceRestAction(), - new ListAccessibleResourcesRestAction(), - new VerifyResourceAccessRestAction(), - new RevokeResourceAccessRestAction(), - new ShareResourceRestAction(), - new DeleteResourceRestAction() - ); + return List.of(new CreateResourceRestAction(), new VerifyResourceAccessRestAction(), new DeleteResourceRestAction()); } @Override public List> getActions() { return List.of( new ActionHandler<>(CreateResourceAction.INSTANCE, CreateResourceTransportAction.class), - new ActionHandler<>(ListAccessibleResourcesAction.INSTANCE, ListAccessibleResourcesTransportAction.class), - new ActionHandler<>(ShareResourceAction.INSTANCE, ShareResourceTransportAction.class), - new ActionHandler<>(RevokeResourceAccessAction.INSTANCE, RevokeResourceAccessTransportAction.class), new ActionHandler<>(VerifyResourceAccessAction.INSTANCE, VerifyResourceAccessTransportAction.class), new ActionHandler<>(DeleteResourceAction.INSTANCE, DeleteResourceTransportAction.class) ); @@ -134,7 +116,7 @@ public Collection getSystemIndexDescriptors(Settings sett @Override public String getResourceType() { - return ""; + return SampleResource.class.getCanonicalName(); } @Override @@ -142,6 +124,11 @@ public String getResourceIndex() { return RESOURCE_INDEX_NAME; } + @Override + public ResourceParser getResourceParser() { + return new SampleResourceParser(); + } + @Override public Collection> getGuiceServiceClasses() { final List> services = new ArrayList<>(1); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java index 1d6de8c1f7..cfec368aa7 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java @@ -11,13 +11,13 @@ package org.opensearch.sample; -import org.opensearch.accesscontrol.resources.ResourceAccessScope; +import org.opensearch.security.spi.resources.ResourceAccessScope; /** * This class demonstrates a sample implementation of Basic Access Scopes to fit each plugin's use-case. * The plugin then uses this scope when seeking access evaluation for a user on a particular resource. */ -public enum SampleResourceScope implements ResourceAccessScope { +public enum SampleResourceScope implements ResourceAccessScope { SAMPLE_FULL_ACCESS("sample_full_access"), @@ -29,7 +29,8 @@ public enum SampleResourceScope implements ResourceAccessScope { this.name = scopeName; } - public String getName() { + @Override + public String value() { return name; } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesAction.java deleted file mode 100644 index 3bea515a19..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesAction.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.access.list; - -import org.opensearch.action.ActionType; - -/** - * Action to list sample resources - */ -public class ListAccessibleResourcesAction extends ActionType { - /** - * List sample resource action instance - */ - public static final ListAccessibleResourcesAction INSTANCE = new ListAccessibleResourcesAction(); - /** - * List sample resource action name - */ - public static final String NAME = "cluster:admin/sample-resource-plugin/list"; - - private ListAccessibleResourcesAction() { - super(NAME, ListAccessibleResourcesResponse::new); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRequest.java deleted file mode 100644 index 4a9315bfd9..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRequest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.access.list; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; - -/** - * Request object for ListSampleResource transport action - */ -public class ListAccessibleResourcesRequest extends ActionRequest { - - public ListAccessibleResourcesRequest() {} - - /** - * Constructor with stream input - * @param in the stream input - * @throws IOException IOException - */ - public ListAccessibleResourcesRequest(final StreamInput in) throws IOException {} - - @Override - public void writeTo(final StreamOutput out) throws IOException {} - - @Override - public ActionRequestValidationException validate() { - return null; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java deleted file mode 100644 index 9c5d2a3e8a..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesResponse.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.access.list; - -import java.io.IOException; -import java.util.Set; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.sample.SampleResource; - -/** - * Response to a ListAccessibleResourcesRequest - */ -public class ListAccessibleResourcesResponse extends ActionResponse implements ToXContentObject { - private final Set resources; - - public ListAccessibleResourcesResponse(Set resources) { - this.resources = resources; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeCollection(resources); - } - - public ListAccessibleResourcesResponse(final StreamInput in) throws IOException { - this.resources = in.readSet(SampleResource::new); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("resources", resources); - builder.endObject(); - return builder; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRestAction.java deleted file mode 100644 index c387eacf90..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/list/ListAccessibleResourcesRestAction.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.access.list; - -import java.util.List; - -import org.opensearch.client.node.NodeClient; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; - -import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.GET; - -public class ListAccessibleResourcesRestAction extends BaseRestHandler { - - public ListAccessibleResourcesRestAction() {} - - @Override - public List routes() { - return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/list")); - } - - @Override - public String getName() { - return "list_sample_resources"; - } - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { - final ListAccessibleResourcesRequest listAccessibleResourcesRequest = new ListAccessibleResourcesRequest(); - return channel -> client.executeLocally( - ListAccessibleResourcesAction.INSTANCE, - listAccessibleResourcesRequest, - new RestToXContentListener<>(channel) - ); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessAction.java deleted file mode 100644 index a040cb0732..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessAction.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.access.revoke; - -import org.opensearch.action.ActionType; - -public class RevokeResourceAccessAction extends ActionType { - public static final RevokeResourceAccessAction INSTANCE = new RevokeResourceAccessAction(); - - public static final String NAME = "cluster:admin/sample-resource-plugin/revoke"; - - private RevokeResourceAccessAction() { - super(NAME, RevokeResourceAccessResponse::new); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java deleted file mode 100644 index f7b4e7b5d7..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRequest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.access.revoke; - -import java.io.IOException; -import java.util.Map; -import java.util.Set; - -import org.opensearch.accesscontrol.resources.RecipientType; -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.sample.utils.Validation; - -public class RevokeResourceAccessRequest extends ActionRequest { - - private final String resourceId; - private final Map> revokeAccess; - private final Set scopes; - - public RevokeResourceAccessRequest(String resourceId, Map> revokeAccess, Set scopes) { - this.resourceId = resourceId; - this.revokeAccess = revokeAccess; - this.scopes = scopes; - } - - public RevokeResourceAccessRequest(StreamInput in) throws IOException { - this.resourceId = in.readString(); - this.revokeAccess = in.readMap(input -> new RecipientType(input.readString()), input -> input.readSet(StreamInput::readString)); - this.scopes = in.readSet(StreamInput::readString); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeString(resourceId); - out.writeMap( - revokeAccess, - (streamOutput, recipientType) -> streamOutput.writeString(recipientType.getType()), - StreamOutput::writeStringCollection - ); - out.writeStringCollection(scopes); - } - - @Override - public ActionRequestValidationException validate() { - - if (!(this.scopes == null)) { - return Validation.validateScopes(this.scopes); - } - - return null; - } - - public String getResourceId() { - return resourceId; - } - - public Map> getRevokeAccess() { - return revokeAccess; - } - - public Set getScopes() { - return scopes; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessResponse.java deleted file mode 100644 index 4cfd3d74e5..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.access.revoke; - -import java.io.IOException; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; - -public class RevokeResourceAccessResponse extends ActionResponse implements ToXContentObject { - private final String message; - - public RevokeResourceAccessResponse(String message) { - this.message = message; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(message); - } - - public RevokeResourceAccessResponse(final StreamInput in) throws IOException { - message = in.readString(); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("message", message); - builder.endObject(); - return builder; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java deleted file mode 100644 index 387d02502f..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/revoke/RevokeResourceAccessRestAction.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.access.revoke; - -import java.io.IOException; -import java.util.*; -import java.util.stream.Collectors; - -import org.opensearch.accesscontrol.resources.RecipientType; -import org.opensearch.accesscontrol.resources.RecipientTypeRegistry; -import org.opensearch.client.node.NodeClient; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; - -import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.POST; - -public class RevokeResourceAccessRestAction extends BaseRestHandler { - - public RevokeResourceAccessRestAction() {} - - @Override - public List routes() { - return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/revoke")); - } - - @Override - public String getName() { - return "revoke_sample_resources_access"; - } - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - - String resourceId = (String) source.get("resource_id"); - @SuppressWarnings("unchecked") - Map> revokeSource = (Map>) source.get("entities"); - Map> revoke = revokeSource.entrySet() - .stream() - .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); - @SuppressWarnings("unchecked") - Set scopes = new HashSet<>(source.containsKey("scopes") ? (List) source.get("scopes") : List.of()); - final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest(resourceId, revoke, scopes); - return channel -> client.executeLocally( - RevokeResourceAccessAction.INSTANCE, - revokeResourceAccessRequest, - new RestToXContentListener<>(channel) - ); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceAction.java deleted file mode 100644 index 768a811e27..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceAction.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.access.share; - -import org.opensearch.action.ActionType; - -public class ShareResourceAction extends ActionType { - /** - * List sample resource action instance - */ - public static final ShareResourceAction INSTANCE = new ShareResourceAction(); - /** - * List sample resource action name - */ - public static final String NAME = "cluster:admin/sample-resource-plugin/share"; - - private ShareResourceAction() { - super(NAME, ShareResourceResponse::new); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRequest.java deleted file mode 100644 index 6c2ed12e73..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRequest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.access.share; - -import java.io.IOException; -import java.util.stream.Collectors; - -import org.opensearch.accesscontrol.resources.ShareWith; -import org.opensearch.accesscontrol.resources.SharedWithScope; -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.sample.utils.Validation; - -public class ShareResourceRequest extends ActionRequest { - - private final String resourceId; - private final ShareWith shareWith; - - public ShareResourceRequest(String resourceId, ShareWith shareWith) { - this.resourceId = resourceId; - this.shareWith = shareWith; - } - - public ShareResourceRequest(StreamInput in) throws IOException { - this.resourceId = in.readString(); - this.shareWith = in.readNamedWriteable(ShareWith.class); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeString(resourceId); - out.writeNamedWriteable(shareWith); - } - - @Override - public ActionRequestValidationException validate() { - - return Validation.validateScopes( - shareWith.getSharedWithScopes().stream().map(SharedWithScope::getScope).collect(Collectors.toSet()) - ); - } - - public String getResourceId() { - return resourceId; - } - - public ShareWith getShareWith() { - return shareWith; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceResponse.java deleted file mode 100644 index 035a9a245e..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceResponse.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.access.share; - -import java.io.IOException; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; - -public class ShareResourceResponse extends ActionResponse implements ToXContentObject { - private final String message; - - /** - * Default constructor - * - * @param message The message - */ - public ShareResourceResponse(String message) { - this.message = message; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(message); - } - - /** - * Constructor with StreamInput - * - * @param in the stream input - */ - public ShareResourceResponse(final StreamInput in) throws IOException { - message = in.readString(); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("message", message); - builder.endObject(); - return builder; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRestAction.java deleted file mode 100644 index 0db4208c05..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/share/ShareResourceRestAction.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.actions.access.share; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import org.opensearch.accesscontrol.resources.ShareWith; -import org.opensearch.client.node.NodeClient; -import org.opensearch.common.xcontent.LoggingDeprecationHandler; -import org.opensearch.common.xcontent.XContentFactory; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; - -import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.POST; - -public class ShareResourceRestAction extends BaseRestHandler { - - public ShareResourceRestAction() {} - - @Override - public List routes() { - return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/share")); - } - - @Override - public String getName() { - return "share_sample_resources"; - } - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - - String resourceId = (String) source.get("resource_id"); - - ShareWith shareWith = parseShareWith(source); - final ShareResourceRequest shareResourceRequest = new ShareResourceRequest(resourceId, shareWith); - return channel -> client.executeLocally(ShareResourceAction.INSTANCE, shareResourceRequest, new RestToXContentListener<>(channel)); - } - - private ShareWith parseShareWith(Map source) throws IOException { - @SuppressWarnings("unchecked") - Map shareWithMap = (Map) source.get("share_with"); - if (shareWithMap == null || shareWithMap.isEmpty()) { - throw new IllegalArgumentException("share_with is required and cannot be empty"); - } - - String jsonString = XContentFactory.jsonBuilder().map(shareWithMap).toString(); - - try ( - XContentParser parser = XContentType.JSON.xContent() - .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, jsonString) - ) { - return ShareWith.fromXContent(parser); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); - } - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java index abad5cd1c3..fe579ff0d1 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java @@ -10,11 +10,11 @@ import java.io.IOException; -import org.opensearch.accesscontrol.resources.Resource; import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionRequestValidationException; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.security.spi.resources.Resource; /** * Request object for CreateSampleResource transport action diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java deleted file mode 100644 index 57c2c7889f..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ListAccessibleResourcesTransportAction.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.transport.access; - -import java.util.Set; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.accesscontrol.resources.ResourceService; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.common.inject.Inject; -import org.opensearch.core.action.ActionListener; -import org.opensearch.sample.SampleResource; -import org.opensearch.sample.SampleResourcePlugin; -import org.opensearch.sample.actions.access.list.ListAccessibleResourcesAction; -import org.opensearch.sample.actions.access.list.ListAccessibleResourcesRequest; -import org.opensearch.sample.actions.access.list.ListAccessibleResourcesResponse; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; - -public class ListAccessibleResourcesTransportAction extends HandledTransportAction< - ListAccessibleResourcesRequest, - ListAccessibleResourcesResponse> { - private static final Logger log = LogManager.getLogger(ListAccessibleResourcesTransportAction.class); - - @Inject - public ListAccessibleResourcesTransportAction(TransportService transportService, ActionFilters actionFilters) { - super(ListAccessibleResourcesAction.NAME, transportService, actionFilters, ListAccessibleResourcesRequest::new); - } - - @Override - protected void doExecute(Task task, ListAccessibleResourcesRequest request, ActionListener listener) { - try { - ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - Set resources = rs.getResourceAccessControlPlugin() - .getAccessibleResourcesForCurrentUser(RESOURCE_INDEX_NAME, SampleResource.class); - log.info("Successfully fetched accessible resources for current user : {}", resources); - listener.onResponse(new ListAccessibleResourcesResponse(resources)); - } catch (Exception e) { - log.info("Failed to list accessible resources for current user: ", e); - listener.onFailure(e); - } - - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java deleted file mode 100644 index 027e1fffe3..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/RevokeResourceAccessTransportAction.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.transport.access; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.accesscontrol.resources.ResourceService; -import org.opensearch.accesscontrol.resources.ResourceSharing; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.common.inject.Inject; -import org.opensearch.core.action.ActionListener; -import org.opensearch.sample.SampleResourcePlugin; -import org.opensearch.sample.actions.access.revoke.RevokeResourceAccessAction; -import org.opensearch.sample.actions.access.revoke.RevokeResourceAccessRequest; -import org.opensearch.sample.actions.access.revoke.RevokeResourceAccessResponse; -import org.opensearch.sample.utils.SampleResourcePluginException; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; - -public class RevokeResourceAccessTransportAction extends HandledTransportAction { - private static final Logger log = LogManager.getLogger(RevokeResourceAccessTransportAction.class); - - @Inject - public RevokeResourceAccessTransportAction(TransportService transportService, ActionFilters actionFilters) { - super(RevokeResourceAccessAction.NAME, transportService, actionFilters, RevokeResourceAccessRequest::new); - } - - @Override - protected void doExecute(Task task, RevokeResourceAccessRequest request, ActionListener listener) { - try { - ResourceSharing revoke = revokeAccess(request); - if (revoke == null) { - log.error("Failed to revoke access to resource {}", request.getResourceId()); - SampleResourcePluginException se = new SampleResourcePluginException( - "Failed to revoke access to resource " + request.getResourceId() - ); - listener.onFailure(se); - return; - } - log.info("Revoked resource access for resource: {} with {}", request.getResourceId(), revoke.toString()); - listener.onResponse(new RevokeResourceAccessResponse("Resource " + request.getResourceId() + " access revoked successfully.")); - } catch (Exception e) { - listener.onFailure(e); - } - } - - private ResourceSharing revokeAccess(RevokeResourceAccessRequest request) { - ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - return rs.getResourceAccessControlPlugin() - .revokeAccess(request.getResourceId(), RESOURCE_INDEX_NAME, request.getRevokeAccess(), request.getScopes()); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ShareResourceTransportAction.java deleted file mode 100644 index 3288352d0b..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/ShareResourceTransportAction.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.transport.access; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.accesscontrol.resources.ResourceService; -import org.opensearch.accesscontrol.resources.ResourceSharing; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.common.inject.Inject; -import org.opensearch.core.action.ActionListener; -import org.opensearch.sample.SampleResourcePlugin; -import org.opensearch.sample.actions.access.share.ShareResourceAction; -import org.opensearch.sample.actions.access.share.ShareResourceRequest; -import org.opensearch.sample.actions.access.share.ShareResourceResponse; -import org.opensearch.sample.utils.SampleResourcePluginException; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; - -public class ShareResourceTransportAction extends HandledTransportAction { - private static final Logger log = LogManager.getLogger(ShareResourceTransportAction.class); - - @Inject - public ShareResourceTransportAction(TransportService transportService, ActionFilters actionFilters) { - super(ShareResourceAction.NAME, transportService, actionFilters, ShareResourceRequest::new); - } - - @Override - protected void doExecute(Task task, ShareResourceRequest request, ActionListener listener) { - ResourceSharing sharing = null; - try { - sharing = shareResource(request); - if (sharing == null) { - log.error("Failed to share resource {}", request.getResourceId()); - SampleResourcePluginException se = new SampleResourcePluginException("Failed to share resource " + request.getResourceId()); - listener.onFailure(se); - return; - } - log.info("Shared resource : {} with {}", request.getResourceId(), sharing.toString()); - listener.onResponse(new ShareResourceResponse("Resource " + request.getResourceId() + " shared successfully.")); - } catch (Exception e) { - listener.onFailure(e); - } - } - - private ResourceSharing shareResource(ShareResourceRequest request) throws Exception { - ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); - return rs.getResourceAccessControlPlugin().shareWith(request.getResourceId(), RESOURCE_INDEX_NAME, request.getShareWith()); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/VerifyResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/VerifyResourceAccessTransportAction.java index 681e4546cc..13954dbe2b 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/VerifyResourceAccessTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/VerifyResourceAccessTransportAction.java @@ -11,16 +11,17 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.accesscontrol.resources.ResourceService; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.client.Client; import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; import org.opensearch.sample.SampleResourcePlugin; +import org.opensearch.sample.SampleResourceScope; import org.opensearch.sample.actions.access.verify.VerifyResourceAccessAction; import org.opensearch.sample.actions.access.verify.VerifyResourceAccessRequest; import org.opensearch.sample.actions.access.verify.VerifyResourceAccessResponse; +import org.opensearch.security.spi.resources.ResourceService; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; @@ -39,7 +40,7 @@ protected void doExecute(Task task, VerifyResourceAccessRequest request, ActionL try { ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService(); boolean hasRequestedScopeAccess = rs.getResourceAccessControlPlugin() - .hasPermission(request.getResourceId(), RESOURCE_INDEX_NAME, request.getScope()); + .hasPermission(request.getResourceId(), RESOURCE_INDEX_NAME, SampleResourceScope.valueOf(request.getScope())); StringBuilder sb = new StringBuilder(); sb.append("User "); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java index 052783a90b..ad82e19576 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java @@ -13,7 +13,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.accesscontrol.resources.Resource; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; @@ -27,6 +26,7 @@ import org.opensearch.sample.actions.resource.create.CreateResourceAction; import org.opensearch.sample.actions.resource.create.CreateResourceRequest; import org.opensearch.sample.actions.resource.create.CreateResourceResponse; +import org.opensearch.security.spi.resources.Resource; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java index a057d41eed..fac032402c 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java @@ -11,9 +11,9 @@ import java.util.HashSet; import java.util.Set; -import org.opensearch.accesscontrol.resources.ResourceAccessScope; import org.opensearch.action.ActionRequestValidationException; import org.opensearch.sample.SampleResourceScope; +import org.opensearch.security.spi.resources.ResourceAccessScope; public class Validation { public static ActionRequestValidationException validateScopes(Set scopes) { From 0fe83bad7dba9af04a2fc287c9e84738b8290cca Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 7 Jan 2025 16:56:33 -0500 Subject: [PATCH 066/201] Adds an SPI Model instead of changes in core Signed-off-by: Darshit Chanpura --- build.gradle | 1 + settings.gradle | 3 + spi/README.md | 152 ++++++++++ spi/build.gradle | 74 +++++ .../security/spi/resources/Resource.java | 31 ++ .../ResourceAccessControlPlugin.java | 21 ++ .../spi/resources/ResourceAccessScope.java | 38 +++ .../spi/resources/ResourceParser.java | 21 ++ .../spi/resources/ResourceProvider.java | 33 ++ .../spi/resources/ResourceService.java | 54 ++++ .../resources/ResourceSharingExtension.java | 33 ++ .../DefaultResourceAccessControlPlugin.java | 28 ++ .../spi/resources/fallback/package-info.java | 14 + .../security/spi/resources/package-info.java | 14 + ...faultResourceAccessControlPluginTests.java | 123 ++++++++ .../spi/resources/ResourceServiceTests.java | 220 ++++++++++++++ .../security/OpenSearchSecurityPlugin.java | 70 ++--- .../security/resources/CreatedBy.java | 89 ++++++ .../security/resources/Creator.java | 8 + .../security/resources/Recipient.java | 8 + .../security/resources/RecipientType.java | 32 ++ .../resources/RecipientTypeRegistry.java | 33 ++ .../resources/ResourceAccessHandler.java | 80 +++-- .../security/resources/ResourceSharing.java | 207 +++++++++++++ .../ResourceSharingIndexHandler.java | 59 ++-- .../ResourceSharingIndexListener.java | 4 +- .../security/resources/ShareWith.java | 104 +++++++ .../security/resources/SharedWithScope.java | 169 +++++++++++ .../list/ListAccessibleResourcesAction.java | 25 ++ .../list/ListAccessibleResourcesRequest.java | 51 ++++ .../list/ListAccessibleResourcesResponse.java | 49 +++ .../RestListAccessibleResourcesAction.java | 56 ++++ .../RestRevokeResourceAccessAction.java | 74 +++++ .../revoke/RevokeResourceAccessAction.java | 21 ++ .../revoke/RevokeResourceAccessRequest.java | 79 +++++ .../revoke/RevokeResourceAccessResponse.java | 42 +++ .../access/share/RestShareResourceAction.java | 79 +++++ .../access/share/ShareResourceAction.java | 25 ++ .../access/share/ShareResourceRequest.java | 61 ++++ .../access/share/ShareResourceResponse.java | 42 +++ .../RestVerifyResourceAccessAction.java | 59 ++++ .../verify/VerifyResourceAccessAction.java | 25 ++ .../verify/VerifyResourceAccessRequest.java | 69 +++++ .../verify/VerifyResourceAccessResponse.java | 52 ++++ ...istAccessibleResourcesTransportAction.java | 56 ++++ .../RevokeResourceAccessTransportAction.java | 65 ++++ .../access/ShareResourceTransportAction.java | 61 ++++ .../VerifyResourceAccessTransportAction.java | 66 ++++ .../security/util/ResourceValidation.java | 34 +++ .../security/resources/CreatedByTests.java | 286 ++++++++++++++++++ .../resources/RecipientTypeRegistryTests.java | 33 ++ .../security/resources/ShareWithTests.java | 263 ++++++++++++++++ .../transport/SecurityInterceptorTests.java | 4 +- 53 files changed, 3285 insertions(+), 115 deletions(-) create mode 100644 spi/README.md create mode 100644 spi/build.gradle create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/Resource.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessControlPlugin.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceProvider.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceService.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/fallback/DefaultResourceAccessControlPlugin.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/fallback/package-info.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/package-info.java create mode 100644 spi/src/tests/java/opensearch/security/spi/resources/DefaultResourceAccessControlPluginTests.java create mode 100644 spi/src/tests/java/opensearch/security/spi/resources/ResourceServiceTests.java create mode 100644 src/main/java/org/opensearch/security/resources/CreatedBy.java create mode 100644 src/main/java/org/opensearch/security/resources/RecipientType.java create mode 100644 src/main/java/org/opensearch/security/resources/RecipientTypeRegistry.java create mode 100644 src/main/java/org/opensearch/security/resources/ResourceSharing.java create mode 100644 src/main/java/org/opensearch/security/resources/ShareWith.java create mode 100644 src/main/java/org/opensearch/security/resources/SharedWithScope.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesAction.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesRequest.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/list/RestListAccessibleResourcesAction.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/revoke/RestRevokeResourceAccessAction.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessAction.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessRequest.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessResponse.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/share/RestShareResourceAction.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceAction.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceRequest.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceResponse.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/verify/RestVerifyResourceAccessAction.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessAction.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessRequest.java create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessResponse.java create mode 100644 src/main/java/org/opensearch/security/transport/resources/access/ListAccessibleResourcesTransportAction.java create mode 100644 src/main/java/org/opensearch/security/transport/resources/access/RevokeResourceAccessTransportAction.java create mode 100644 src/main/java/org/opensearch/security/transport/resources/access/ShareResourceTransportAction.java create mode 100644 src/main/java/org/opensearch/security/transport/resources/access/VerifyResourceAccessTransportAction.java create mode 100644 src/main/java/org/opensearch/security/util/ResourceValidation.java create mode 100644 src/test/java/org/opensearch/security/resources/CreatedByTests.java create mode 100644 src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java create mode 100644 src/test/java/org/opensearch/security/resources/ShareWithTests.java diff --git a/build.gradle b/build.gradle index cacfec77c5..2124b0d9de 100644 --- a/build.gradle +++ b/build.gradle @@ -574,6 +574,7 @@ tasks.integrationTest.finalizedBy(jacocoTestReport) // report is always generate check.dependsOn integrationTest dependencies { + implementation project(path: ":opensearch-resource-sharing-spi") implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" implementation "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}" implementation "org.apache.httpcomponents.client5:httpclient5-cache:${versions.httpclient5}" diff --git a/settings.gradle b/settings.gradle index 1c3e7ff5aa..193587dee7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,3 +5,6 @@ */ rootProject.name = 'opensearch-security' + +include "spi" +project(":spi").name = "opensearch-resource-sharing-spi" diff --git a/spi/README.md b/spi/README.md new file mode 100644 index 0000000000..ccd73db983 --- /dev/null +++ b/spi/README.md @@ -0,0 +1,152 @@ +# Resource Sharing and Access Control Plugin + +This plugin demonstrates resource sharing and access control functionality, providing APIs to create, manage, and verify access to resources. The plugin enables fine-grained permissions for sharing and accessing resources, making it suitable for systems requiring robust security and collaboration. + +## Features + +- Create and delete resources. +- Share resources with specific users, roles and/or backend_roles with specific scope(s). +- Revoke access to shared resources for a list of or all scopes. +- Verify access permissions for a given user within a given scope. +- List all resources accessible to current user. + +## API Endpoints + +The plugin exposes the following six API endpoints: + +### 1. Create Resource +- **Endpoint:** `POST /_plugins/sample_resource_sharing/create` +- **Description:** Creates a new resource. Also creates a resource sharing entry if security plugin is enabled. +- **Request Body:** + ```json + { + "name": "" + } + ``` +- **Response:** + ```json + { + "message": "Resource created successfully." + } + ``` + +### 2. Delete Resource +- **Endpoint:** `DELETE /_plugins/sample_resource_sharing/{resource_id}` +- **Description:** Deletes a specified resource owned by the requesting user. +- **Response:** + ```json + { + "message": "Resource deleted successfully." + } + ``` + +### 3. Share Resource +- **Endpoint:** `POST /_plugins/sample_resource_sharing/share` +- **Description:** Shares a resource with specified users or roles with defined scope. +- **Request Body:** + ```json + { + "resource_id" : "{{ADMIN_RESOURCE_ID}}", + "share_with" : { + "SAMPLE_FULL_ACCESS": { + "users": ["test"], + "roles": ["test_role"], + "backend_roles": ["test_backend_role"] + }, + "READ_ONLY": { + "users": ["test"], + "roles": ["test_role"], + "backend_roles": ["test_backend_role"] + }, + "READ_WRITE": { + "users": ["test"], + "roles": ["test_role"], + "backend_roles": ["test_backend_role"] + } + } + } + ``` +- **Response:** + ```json + { + "message": "Resource shared successfully." + } + ``` + +### 4. Revoke Access +- **Endpoint:** `POST /_plugins/sample_resource_sharing/revoke` +- **Description:** Revokes access to a resource for specified users or roles. +- **Request Body:** + ```json + { + "resource_id" : "", + "entities" : { + "users": ["test", "admin"], + "roles": ["test_role", "all_access"], + "backend_roles": ["test_backend_role", "admin"] + }, + "scopes": ["SAMPLE_FULL_ACCESS", "READ_ONLY", "READ_WRITE"] + } + ``` +- **Response:** + ```json + { + "message": "Resource access revoked successfully." + } + ``` + +### 5. Verify Access +- **Endpoint:** `GET /_plugins/sample_resource_sharing/verify_resource_access` +- **Description:** Verifies if a user or role has access to a specific resource with a specific scope. +- **Request Body:** + ```json + { + "resource_id": "", + "scope": "SAMPLE_FULL_ACCESS" + } + ``` +- **Response:** + ```json + { + "message": "User has requested scope SAMPLE_FULL_ACCESS access to " + } + ``` + +### 6. List Accessible Resources +- **Endpoint:** `GET /_plugins/sample_resource_sharing/list` +- **Description:** Lists all resources accessible to the requesting user or role. +- **Response:** + ```json + { + "resource-ids": [ + "", + "" + ] + } + ``` + +## Installation + +1. Clone the repository: + ```bash + git clone git@github.com:opensearch-project/security.git + ``` + +2. Navigate to the project directory: + ```bash + cd sample-resource-plugin + ``` + +3. Build and deploy the plugin: + ```bash + $ ./gradlew clean build -x test -x integrationTest -x spotbugsIntegrationTest + $ ./bin/opensearch-plugin install file: /sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-3.0.0.0-SNAPSHOT.zip + ``` + +## License + +This code is licensed under the Apache 2.0 License. + +## Copyright + +Copyright OpenSearch Contributors. diff --git a/spi/build.gradle b/spi/build.gradle new file mode 100644 index 0000000000..2cfe1a0d21 --- /dev/null +++ b/spi/build.gradle @@ -0,0 +1,74 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + id 'java' + id 'maven-publish' +} + +ext { + opensearch_version = System.getProperty("opensearch.version", "3.0.0-SNAPSHOT") +} + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } +} + +dependencies { + compileOnly "org.opensearch:opensearch:${opensearch_version}" + testImplementation "org.opensearch.test:framework:${opensearch_version}" +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +task sourcesJar(type: Jar) { + archiveClassifier.set 'sources' + from sourceSets.main.allJava +} + +task javadocJar(type: Jar) { + archiveClassifier.set 'javadoc' + from tasks.javadoc +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar + pom { + name.set("OpenSearch Resource Sharing SPI") + description.set("OpenSearch Security Resource Sharing") + url.set("https://github.com/opensearch-project/security") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + scm { + connection.set("scm:git@github.com:opensearch-project/security.git") + developerConnection.set("scm:git@github.com:opensearch-project/security.git") + url.set("https://github.com/opensearch-project/security.git") + } + developers { + developer { + name.set("OpenSearch Contributors") + url.set("https://github.com/opensearch-project") + } + } + } + } + } + repositories { + mavenLocal() + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java b/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java new file mode 100644 index 0000000000..9116ed0a9e --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java @@ -0,0 +1,31 @@ +/* + * 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. + */ + +package org.opensearch.security.spi.resources; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.xcontent.ToXContentFragment; + +/** + * Marker interface for all resources + */ +public interface Resource extends NamedWriteable, ToXContentFragment { + /** + * Get the resource name + * @return resource name + */ + String getResourceName(); + + // For de-serialization + Resource readFrom(StreamInput in) throws IOException; + + // TODO: Next iteration, check if getResourceType() should be implemented +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessControlPlugin.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessControlPlugin.java new file mode 100644 index 0000000000..5f9c2558c2 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessControlPlugin.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +package org.opensearch.security.spi.resources; + +/** + * This plugin allows to control access to resources. It is used by the ResourcePlugins to check whether a user has access to a resource defined by that plugin. + * It also defines java APIs to list, share or revoke resources with other users. + * User information will be fetched from the ThreadContext. + * + * @opensearch.experimental + */ +public interface ResourceAccessControlPlugin { + + boolean hasPermission(String resourceId, String resourceIndex, ResourceAccessScope> scope); +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java new file mode 100644 index 0000000000..b8dab4ff67 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java @@ -0,0 +1,38 @@ +/* + * 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. + */ + +package org.opensearch.security.spi.resources; + +import java.util.Arrays; + +/** + * This interface defines the two basic access scopes for resource-access. + * Each plugin must implement their own scopes and manage them. + * These access scopes will then be used to verify the type of access being requested. + * + * @opensearch.experimental + */ +public interface ResourceAccessScope> { + String READ_ONLY = "read_only"; + String READ_WRITE = "read_write"; + + static & ResourceAccessScope> E fromValue(Class enumClass, String value) { + for (E enumConstant : enumClass.getEnumConstants()) { + if (enumConstant.value().equalsIgnoreCase(value)) { + return enumConstant; + } + } + throw new IllegalArgumentException("Unknown value: " + value); + } + + String value(); + + static & ResourceAccessScope> String[] values(Class enumClass) { + return Arrays.stream(enumClass.getEnumConstants()).map(ResourceAccessScope::value).toArray(String[]::new); + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java new file mode 100644 index 0000000000..b3c2d0079d --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +package org.opensearch.security.spi.resources; + +import java.io.IOException; + +public interface ResourceParser { + /** + * Parse stringified json input to a desired Resource type + * @param source the stringified json input + * @return the parsed object of Resource type + * @throws IOException if something went wrong while parsing + */ + T parse(String source) throws IOException; +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceProvider.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceProvider.java new file mode 100644 index 0000000000..d6bde36a75 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceProvider.java @@ -0,0 +1,33 @@ +/* + * 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. + */ + +package org.opensearch.security.spi.resources; + +public class ResourceProvider { + private final String resourceType; + private final String resourceIndexName; + private final ResourceParser resourceParser; + + public ResourceParser getResourceParser() { + return resourceParser; + } + + public String getResourceIndexName() { + return resourceIndexName; + } + + public String getResourceType() { + return resourceType; + } + + public ResourceProvider(String resourceType, String resourceIndexName, ResourceParser resourceParser) { + this.resourceType = resourceType; + this.resourceIndexName = resourceIndexName; + this.resourceParser = resourceParser; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceService.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceService.java new file mode 100644 index 0000000000..19d24b97e6 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceService.java @@ -0,0 +1,54 @@ +/* + * 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. + */ + +package org.opensearch.security.spi.resources; + +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.inject.Inject; +import org.opensearch.security.spi.resources.fallback.DefaultResourceAccessControlPlugin; + +/** + * Service to get the current ResourceSharingExtension to perform authorization. + * + * @opensearch.experimental + */ +public class ResourceService { + private static final Logger log = LogManager.getLogger(ResourceService.class); + + private final ResourceAccessControlPlugin resourceACPlugin; + + @Inject + public ResourceService(final List resourceACPlugins) { + + if (resourceACPlugins.isEmpty()) { + log.info("Security plugin disabled: Using DefaultResourceAccessControlPlugin"); + resourceACPlugin = new DefaultResourceAccessControlPlugin(); + } else if (resourceACPlugins.size() == 1) { + log.info("Security plugin enabled: Using OpenSearchSecurityPlugin"); + resourceACPlugin = resourceACPlugins.get(0); + } else { + throw new OpenSearchException( + "Multiple resource access control plugins are not supported, found: " + + resourceACPlugins.stream().map(Object::getClass).map(Class::getName).collect(Collectors.joining(",")) + ); + } + } + + /** + * Gets the ResourceAccessControlPlugin in-effect to perform authorization + */ + public ResourceAccessControlPlugin getResourceAccessControlPlugin() { + return resourceACPlugin; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java new file mode 100644 index 0000000000..f6eb1d35e8 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java @@ -0,0 +1,33 @@ +/* + * 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. + */ + +package org.opensearch.security.spi.resources; + +/** + * This interface should be implemented by all the plugins that define one or more resources. + * + * @opensearch.experimental + */ +public interface ResourceSharingExtension { + + /** + * Type of the resource + * @return a string containing the type of the resource + */ + String getResourceType(); + + /** + * The index where resource meta-data is stored + * @return the name of the parent index where resource meta-data is stored + */ + String getResourceIndex(); + + default ResourceParser getResourceParser() { + return null; + }; +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/fallback/DefaultResourceAccessControlPlugin.java b/spi/src/main/java/org/opensearch/security/spi/resources/fallback/DefaultResourceAccessControlPlugin.java new file mode 100644 index 0000000000..379aa15d5d --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/fallback/DefaultResourceAccessControlPlugin.java @@ -0,0 +1,28 @@ +/* + * 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. + */ + +package org.opensearch.security.spi.resources.fallback; + +import org.opensearch.security.spi.resources.ResourceAccessControlPlugin; +import org.opensearch.security.spi.resources.ResourceAccessScope; + +/** + * A default plugin for resource access control + */ +public class DefaultResourceAccessControlPlugin implements ResourceAccessControlPlugin { + /** + * @param resourceId the resource on which access is to be checked + * @param resourceIndex where the resource exists + * @param scope the scope being requested + * @return true always since this is a passthrough implementation + */ + @Override + public boolean hasPermission(String resourceId, String resourceIndex, ResourceAccessScope> scope) { + return true; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/fallback/package-info.java b/spi/src/main/java/org/opensearch/security/spi/resources/fallback/package-info.java new file mode 100644 index 0000000000..2dd2803b38 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/fallback/package-info.java @@ -0,0 +1,14 @@ +/* + * 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. + */ + +/** + * This package defines a pass-through implementation of ResourceAccessControlPlugin. + * + * @opensearch.experimental + */ +package main.java.org.opensearch.security.spi.resources.fallback; diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/package-info.java b/spi/src/main/java/org/opensearch/security/spi/resources/package-info.java new file mode 100644 index 0000000000..8990889429 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/package-info.java @@ -0,0 +1,14 @@ +/* + * 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. + */ + +/** + * This package defines class required to implement resource access control in OpenSearch. + * + * @opensearch.experimental + */ +package main.java.org.opensearch.security.spi.resources; diff --git a/spi/src/tests/java/opensearch/security/spi/resources/DefaultResourceAccessControlPluginTests.java b/spi/src/tests/java/opensearch/security/spi/resources/DefaultResourceAccessControlPluginTests.java new file mode 100644 index 0000000000..686f8484b9 --- /dev/null +++ b/spi/src/tests/java/opensearch/security/spi/resources/DefaultResourceAccessControlPluginTests.java @@ -0,0 +1,123 @@ +/* + * 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. + */ + +package tests.java.opensearch.security.spi.resources; + +public class DefaultResourceAccessControlPluginTests { + // @Override + // protected Collection> nodePlugins() { + // return List.of(TestResourcePlugin.class); + // } + // + // public void testGetResources() throws IOException { + // final Client client = client(); + // + // createIndex(SAMPLE_TEST_INDEX); + // indexSampleDocuments(); + // + // Set resources; + // try ( + // DefaultResourceAccessControlExtension plugin = new DefaultResourceAccessControlExtension( + // client, + // internalCluster().getInstance(ThreadPool.class) + // ) + // ) { + // resources = plugin.getAccessibleResourcesForCurrentUser(SAMPLE_TEST_INDEX, TestResourcePlugin.TestResource.class); + // + // assertNotNull(resources); + // MatcherAssert.assertThat(resources, hasSize(2)); + // + // MatcherAssert.assertThat(resources, hasItem(hasProperty("id", is("1")))); + // MatcherAssert.assertThat(resources, hasItem(hasProperty("id", is("2")))); + // } + // } + // + // public void testSampleResourcePluginListResources() throws IOException { + // createIndex(SAMPLE_TEST_INDEX); + // indexSampleDocuments(); + // + // ResourceAccessControlPlugin racPlugin = TestResourcePlugin.GuiceHolder.getResourceService().getResourceAccessControlPlugin(); + // MatcherAssert.assertThat(racPlugin.getClass(), is(DefaultResourceAccessControlExtension.class)); + // + // Set resources = racPlugin.getAccessibleResourcesForCurrentUser( + // SAMPLE_TEST_INDEX, + // TestResourcePlugin.TestResource.class + // ); + // + // assertNotNull(resources); + // MatcherAssert.assertThat(resources, hasSize(2)); + // MatcherAssert.assertThat(resources, hasItem(hasProperty("id", is("1")))); + // MatcherAssert.assertThat(resources, hasItem(hasProperty("id", is("2")))); + // } + // + // public void testSampleResourcePluginCallsHasPermission() { + // + // ResourceAccessControlPlugin racPlugin = TestResourcePlugin.GuiceHolder.getResourceService().getResourceAccessControlPlugin(); + // MatcherAssert.assertThat(racPlugin.getClass(), is(DefaultResourceAccessControlExtension.class)); + // + // boolean canAccess = racPlugin.hasPermission("1", SAMPLE_TEST_INDEX, null); + // + // MatcherAssert.assertThat(canAccess, is(true)); + // + // } + // + // public void testSampleResourcePluginCallsShareWith() { + // + // ResourceAccessControlPlugin racPlugin = TestResourcePlugin.GuiceHolder.getResourceService().getResourceAccessControlPlugin(); + // MatcherAssert.assertThat(racPlugin.getClass(), is(DefaultResourceAccessControlExtension.class)); + // + // ResourceSharing sharingInfo = racPlugin.shareWith("1", SAMPLE_TEST_INDEX, new ShareWith(Set.of())); + // + // MatcherAssert.assertThat(sharingInfo, is(nullValue())); + // } + // + // public void testSampleResourcePluginCallsRevokeAccess() { + // + // ResourceAccessControlPlugin racPlugin = TestResourcePlugin.GuiceHolder.getResourceService().getResourceAccessControlPlugin(); + // MatcherAssert.assertThat(racPlugin.getClass(), is(DefaultResourceAccessControlExtension.class)); + // + // ResourceSharing sharingInfo = racPlugin.revokeAccess("1", SAMPLE_TEST_INDEX, Map.of(), Set.of("some_scope")); + // + // MatcherAssert.assertThat(sharingInfo, is(nullValue())); + // } + // + // public void testSampleResourcePluginCallsDeleteResourceSharingRecord() { + // ResourceAccessControlPlugin racPlugin = TestResourcePlugin.GuiceHolder.getResourceService().getResourceAccessControlPlugin(); + // MatcherAssert.assertThat(racPlugin.getClass(), is(DefaultResourceAccessControlExtension.class)); + // + // boolean recordDeleted = racPlugin.deleteResourceSharingRecord("1", SAMPLE_TEST_INDEX); + // + // // no record to delete + // MatcherAssert.assertThat(recordDeleted, is(false)); + // } + // + // public void testSampleResourcePluginCallsDeleteAllResourceSharingRecordsForCurrentUser() { + // ResourceAccessControlPlugin racPlugin = TestResourcePlugin.GuiceHolder.getResourceService().getResourceAccessControlPlugin(); + // MatcherAssert.assertThat(racPlugin.getClass(), is(DefaultResourceAccessControlExtension.class)); + // + // boolean recordDeleted = racPlugin.deleteAllResourceSharingRecordsForCurrentUser(); + // + // // no records to delete + // MatcherAssert.assertThat(recordDeleted, is(false)); + // } + // + // private void indexSampleDocuments() throws IOException { + // XContentBuilder doc1 = jsonBuilder().startObject().field("id", "1").field("name", "Test Document 1").endObject(); + // + // XContentBuilder doc2 = jsonBuilder().startObject().field("id", "2").field("name", "Test Document 2").endObject(); + // + // try (Client client = client()) { + // + // client.prepareIndex(SAMPLE_TEST_INDEX).setId("1").setSource(doc1).get(); + // + // client.prepareIndex(SAMPLE_TEST_INDEX).setId("2").setSource(doc2).get(); + // + // client.admin().indices().prepareRefresh(SAMPLE_TEST_INDEX).get(); + // } + // } +} diff --git a/spi/src/tests/java/opensearch/security/spi/resources/ResourceServiceTests.java b/spi/src/tests/java/opensearch/security/spi/resources/ResourceServiceTests.java new file mode 100644 index 0000000000..e537dc1697 --- /dev/null +++ b/spi/src/tests/java/opensearch/security/spi/resources/ResourceServiceTests.java @@ -0,0 +1,220 @@ +/// * +// * 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. +// */ +// +// package tests.java.opensearch.security.spi.resources; +// +// import org.hamcrest.MatcherAssert; +// import org.mockito.Mock; +// import org.mockito.MockitoAnnotations; +// import org.opensearch.OpenSearchException; +// import org.opensearch.accesscontrol.resources.fallback.DefaultResourceAccessControlExtension; +// import org.opensearch.client.Client; +// import org.opensearch.plugins.ResourceAccessControlPlugin; +// import org.opensearch.plugins.ResourceSharingExtension; +// import org.opensearch.test.OpenSearchTestCase; +// import org.opensearch.threadpool.ThreadPool; +// +// import java.util.ArrayList; +// import java.util.Arrays; +// import java.util.Collections; +// import java.util.List; +// +// import static org.hamcrest.Matchers.*; +// import static org.mockito.Mockito.mock; +// +// public class ResourceServiceTests extends OpenSearchTestCase { +// +// @Mock +// private Client client; +// +// @Mock +// private ThreadPool threadPool; +// +// public void setup() { +// MockitoAnnotations.openMocks(this); +// } +// +// public void testGetResourceAccessControlPluginReturnsInitializedPlugin() { +// setup(); +// Client mockClient = mock(Client.class); +// ThreadPool mockThreadPool = mock(ThreadPool.class); +// +// ResourceAccessControlPlugin mockPlugin = mock(ResourceAccessControlPlugin.class); +// List plugins = new ArrayList<>(); +// plugins.add(mockPlugin); +// +// List resourcePlugins = new ArrayList<>(); +// +// ResourceService resourceService = new ResourceService(plugins, resourcePlugins, mockClient, mockThreadPool); +// +// ResourceAccessControlPlugin result = resourceService.getResourceAccessControlPlugin(); +// +// MatcherAssert.assertThat(mockPlugin, equalTo(result)); +// } +// +// public void testGetResourceAccessControlPlugin_NoPlugins() { +// setup(); +// List emptyPlugins = new ArrayList<>(); +// List resourcePlugins = new ArrayList<>(); +// +// ResourceService resourceService = new ResourceService(emptyPlugins, resourcePlugins, client, threadPool); +// +// ResourceAccessControlPlugin result = resourceService.getResourceAccessControlPlugin(); +// +// assertNotNull(result); +// MatcherAssert.assertThat(result, instanceOf(DefaultResourceAccessControlExtension.class)); +// } +// +// public void testGetResourceAccessControlPlugin_SinglePlugin() { +// setup(); +// ResourceAccessControlPlugin mockPlugin = mock(ResourceAccessControlPlugin.class); +// List singlePlugin = Arrays.asList(mockPlugin); +// List resourcePlugins = new ArrayList<>(); +// +// ResourceService resourceService = new ResourceService(singlePlugin, resourcePlugins, client, threadPool); +// +// ResourceAccessControlPlugin result = resourceService.getResourceAccessControlPlugin(); +// +// assertNotNull(result); +// assertSame(mockPlugin, result); +// } +// +// public void testListResourcePluginsReturnsPluginList() { +// setup(); +// List resourceACPlugins = new ArrayList<>(); +// List expectedResourcePlugins = new ArrayList<>(); +// expectedResourcePlugins.add(mock(ResourceSharingExtension.class)); +// expectedResourcePlugins.add(mock(ResourceSharingExtension.class)); +// +// ResourceService resourceService = new ResourceService(resourceACPlugins, expectedResourcePlugins, client, threadPool); +// +// List actualResourcePlugins = resourceService.listResourcePlugins(); +// +// MatcherAssert.assertThat(expectedResourcePlugins, equalTo(actualResourcePlugins)); +// } +// +// public void testListResourcePlugins_concurrentModification() { +// setup(); +// List emptyACPlugins = Collections.emptyList(); +// List resourcePlugins = new ArrayList<>(); +// resourcePlugins.add(mock(ResourceSharingExtension.class)); +// +// ResourceService resourceService = new ResourceService(emptyACPlugins, resourcePlugins, client, threadPool); +// +// Thread modifierThread = new Thread(() -> { resourcePlugins.add(mock(ResourceSharingExtension.class)); }); +// +// modifierThread.start(); +// +// List result = resourceService.listResourcePlugins(); +// +// assertNotNull(result); +// // The size could be either 1 or 2 depending on the timing of the concurrent modification +// assertTrue(result.size() == 1 || result.size() == 2); +// } +// +// public void testListResourcePlugins_emptyList() { +// setup(); +// List emptyACPlugins = Collections.emptyList(); +// List emptyResourcePlugins = Collections.emptyList(); +// +// ResourceService resourceService = new ResourceService(emptyACPlugins, emptyResourcePlugins, client, threadPool); +// +// List result = resourceService.listResourcePlugins(); +// +// assertNotNull(result); +// MatcherAssert.assertThat(result, is(empty())); +// } +// +// public void testListResourcePlugins_immutability() { +// setup(); +// List emptyACPlugins = Collections.emptyList(); +// List resourcePlugins = new ArrayList<>(); +// resourcePlugins.add(mock(ResourceSharingExtension.class)); +// +// ResourceService resourceService = new ResourceService(emptyACPlugins, resourcePlugins, client, threadPool); +// +// List result = resourceService.listResourcePlugins(); +// +// assertThrows(UnsupportedOperationException.class, () -> { result.add(mock(ResourceSharingExtension.class)); }); +// } +// +// public void testResourceServiceConstructorWithMultiplePlugins() { +// setup(); +// ResourceAccessControlPlugin plugin1 = mock(ResourceAccessControlPlugin.class); +// ResourceAccessControlPlugin plugin2 = mock(ResourceAccessControlPlugin.class); +// List resourceACPlugins = Arrays.asList(plugin1, plugin2); +// List resourcePlugins = Arrays.asList(mock(ResourceSharingExtension.class)); +// +// assertThrows(OpenSearchException.class, () -> { new ResourceService(resourceACPlugins, resourcePlugins, client, threadPool); }); +// } +// +// public void testResourceServiceConstructor_MultiplePlugins() { +// setup(); +// ResourceAccessControlPlugin mockPlugin1 = mock(ResourceAccessControlPlugin.class); +// ResourceAccessControlPlugin mockPlugin2 = mock(ResourceAccessControlPlugin.class); +// List multiplePlugins = Arrays.asList(mockPlugin1, mockPlugin2); +// List resourcePlugins = new ArrayList<>(); +// +// assertThrows( +// org.opensearch.OpenSearchException.class, +// () -> { new ResourceService(multiplePlugins, resourcePlugins, client, threadPool); } +// ); +// } +// +// public void testResourceServiceWithMultipleResourceACPlugins() { +// setup(); +// List multipleResourceACPlugins = Arrays.asList( +// mock(ResourceAccessControlPlugin.class), +// mock(ResourceAccessControlPlugin.class) +// ); +// List resourcePlugins = new ArrayList<>(); +// +// assertThrows( +// OpenSearchException.class, +// () -> { new ResourceService(multipleResourceACPlugins, resourcePlugins, client, threadPool); } +// ); +// } +// +// public void testResourceServiceWithNoAccessControlPlugin() { +// setup(); +// List resourceACPlugins = new ArrayList<>(); +// List resourcePlugins = new ArrayList<>(); +// Client client = mock(Client.class); +// ThreadPool threadPool = mock(ThreadPool.class); +// +// ResourceService resourceService = new ResourceService(resourceACPlugins, resourcePlugins, client, threadPool); +// +// MatcherAssert.assertThat(resourceService.getResourceAccessControlPlugin(), instanceOf(DefaultResourceAccessControlExtension.class)); +// MatcherAssert.assertThat(resourcePlugins, equalTo(resourceService.listResourcePlugins())); +// } +// +// public void testResourceServiceWithNoResourceACPlugins() { +// setup(); +// List emptyResourceACPlugins = new ArrayList<>(); +// List resourcePlugins = new ArrayList<>(); +// +// ResourceService resourceService = new ResourceService(emptyResourceACPlugins, resourcePlugins, client, threadPool); +// +// assertNotNull(resourceService.getResourceAccessControlPlugin()); +// } +// +// public void testResourceServiceWithSingleResourceAccessControlPlugin() { +// setup(); +// List resourceACPlugins = new ArrayList<>(); +// ResourceAccessControlPlugin mockPlugin = mock(ResourceAccessControlPlugin.class); +// resourceACPlugins.add(mockPlugin); +// +// List resourcePlugins = new ArrayList<>(); +// +// ResourceService resourceService = new ResourceService(resourceACPlugins, resourcePlugins, client, threadPool); +// +// assertNotNull(resourceService); +// MatcherAssert.assertThat(mockPlugin, equalTo(resourceService.getResourceAccessControlPlugin())); +// MatcherAssert.assertThat(resourcePlugins, equalTo(resourceService.listResourcePlugins())); +// } +// } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 4153d9749d..14cd439566 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -69,7 +69,6 @@ import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; import org.opensearch.Version; -import org.opensearch.accesscontrol.resources.*; import org.opensearch.action.ActionRequest; import org.opensearch.action.search.PitService; import org.opensearch.action.search.SearchScrollAction; @@ -118,12 +117,11 @@ import org.opensearch.indices.IndicesService; import org.opensearch.indices.SystemIndexDescriptor; import org.opensearch.plugins.ClusterPlugin; +import org.opensearch.plugins.ExtensiblePlugin; import org.opensearch.plugins.ExtensionAwarePlugin; import org.opensearch.plugins.IdentityPlugin; import org.opensearch.plugins.MapperPlugin; import org.opensearch.plugins.Plugin; -import org.opensearch.plugins.ResourceAccessControlPlugin; -import org.opensearch.plugins.ResourcePlugin; import org.opensearch.plugins.SecureHttpTransportSettingsProvider; import org.opensearch.plugins.SecureSettingsFactory; import org.opensearch.plugins.SecureTransportSettingsProvider; @@ -191,6 +189,12 @@ import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.setting.TransportPassiveAuthSetting; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.ResourceAccessControlPlugin; +import org.opensearch.security.spi.resources.ResourceAccessScope; +import org.opensearch.security.spi.resources.ResourceParser; +import org.opensearch.security.spi.resources.ResourceProvider; +import org.opensearch.security.spi.resources.ResourceSharingExtension; import org.opensearch.security.ssl.ExternalSecurityKeyStore; import org.opensearch.security.ssl.OpenSearchSecureSettingsFactory; import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; @@ -244,7 +248,8 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin IdentityPlugin, ResourceAccessControlPlugin, // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings - ExtensionAwarePlugin + ExtensionAwarePlugin, + ExtensiblePlugin // CS-ENFORCE-SINGLE { @@ -283,6 +288,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private ResourceSharingIndexManagementRepository rmr; private ResourceAccessHandler resourceAccessHandler; private final Set indicesToListen = new HashSet<>(); + private static final Map resourceProviders = new HashMap<>(); public static boolean isActionTraceEnabled() { @@ -2121,13 +2127,6 @@ public void onNodeStarted(DiscoveryNode localNode) { // create resource sharing index if absent rmr.createResourceSharingIndexIfAbsent(); - for (ResourcePlugin resourcePlugin : OpenSearchSecurityPlugin.GuiceHolder.getResourceService().listResourcePlugins()) { - String resourceIndex = resourcePlugin.getResourceIndex(); - - this.indicesToListen.add(resourceIndex); - log.warn("Security plugin started listening to index: {} of plugin: {}", resourceIndex, resourcePlugin); - } - final Set securityModules = ReflectionHelper.getModulesLoaded(); log.info("{} OpenSearch Security modules loaded so far: {}", securityModules.size(), securityModules); } @@ -2233,40 +2232,32 @@ private void tryAddSecurityProvider() { }); } - @Override - public Set getAccessibleResourcesForCurrentUser(String systemIndexName, Class clazz) { - return this.resourceAccessHandler.getAccessibleResourcesForCurrentUser(systemIndexName, clazz); + public static Map getResourceProviders() { + return resourceProviders; } @Override - public boolean hasPermission(String resourceId, String systemIndexName, String scope) { - return this.resourceAccessHandler.hasPermission(resourceId, systemIndexName, scope); + public boolean hasPermission(String resourceId, String resourceIndex, ResourceAccessScope> scope) { + return this.resourceAccessHandler.hasPermission(resourceId, resourceIndex, scope.value()); } + // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings @Override - public ResourceSharing shareWith(String resourceId, String systemIndexName, ShareWith shareWith) { - return this.resourceAccessHandler.shareWith(resourceId, systemIndexName, shareWith); - } + public void loadExtensions(ExtensiblePlugin.ExtensionLoader loader) { - @Override - public ResourceSharing revokeAccess( - String resourceId, - String systemIndexName, - Map> entities, - Set scopes - ) { - return this.resourceAccessHandler.revokeAccess(resourceId, systemIndexName, entities, scopes); - } + for (ResourceSharingExtension extension : loader.loadExtensions(ResourceSharingExtension.class)) { + String resourceType = extension.getResourceType(); + String resourceIndexName = extension.getResourceIndex(); + ResourceParser resourceParser = extension.getResourceParser(); - @Override - public boolean deleteResourceSharingRecord(String resourceId, String systemIndexName) { - return this.resourceAccessHandler.deleteResourceSharingRecord(resourceId, systemIndexName); - } + this.indicesToListen.add(resourceIndexName); - @Override - public boolean deleteAllResourceSharingRecordsForCurrentUser() { - return this.resourceAccessHandler.deleteAllResourceSharingRecordsForCurrentUser(); + ResourceProvider resourceProvider = new ResourceProvider(resourceType, resourceIndexName, resourceParser); + resourceProviders.put(resourceIndexName, resourceProvider); + log.info("Loaded resource provider extension: {}, index: {}", resourceType, resourceIndexName); + } } + // CS-ENFORCE-SINGLE public static class GuiceHolder implements LifecycleComponent { @@ -2274,7 +2265,6 @@ public static class GuiceHolder implements LifecycleComponent { private static RemoteClusterService remoteClusterService; private static IndicesService indicesService; private static PitService pitService; - private static ResourceService resourceService; // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions manager used to allow/disallow TLS connections to extensions private static ExtensionsManager extensionsManager; @@ -2285,15 +2275,13 @@ public GuiceHolder( final TransportService remoteClusterService, IndicesService indicesService, PitService pitService, - ExtensionsManager extensionsManager, - ResourceService resourceService + ExtensionsManager extensionsManager ) { GuiceHolder.repositoriesService = repositoriesService; GuiceHolder.remoteClusterService = remoteClusterService.getRemoteClusterService(); GuiceHolder.indicesService = indicesService; GuiceHolder.pitService = pitService; GuiceHolder.extensionsManager = extensionsManager; - GuiceHolder.resourceService = resourceService; } // CS-ENFORCE-SINGLE @@ -2319,10 +2307,6 @@ public static ExtensionsManager getExtensionsManager() { } // CS-ENFORCE-SINGLE - public static ResourceService getResourceService() { - return resourceService; - } - @Override public void close() {} diff --git a/src/main/java/org/opensearch/security/resources/CreatedBy.java b/src/main/java/org/opensearch/security/resources/CreatedBy.java new file mode 100644 index 0000000000..3790d56a72 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/CreatedBy.java @@ -0,0 +1,89 @@ +/* + * 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. + */ + +package org.opensearch.security.resources; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +/** + * This class is used to store information about the creator of a resource. + * Concrete implementation will be provided by security plugin + * + * @opensearch.experimental + */ +public class CreatedBy implements ToXContentFragment, NamedWriteable { + + private final String creatorType; + private final String creator; + + public CreatedBy(String creatorType, String creator) { + this.creatorType = creatorType; + this.creator = creator; + } + + public CreatedBy(StreamInput in) throws IOException { + this.creatorType = in.readString(); + this.creator = in.readString(); + } + + public String getCreator() { + return creator; + } + + public String getCreatorType() { + return creatorType; + } + + @Override + public String toString() { + return "CreatedBy {" + this.creatorType + "='" + this.creator + '\'' + '}'; + } + + @Override + public String getWriteableName() { + return "created_by"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(creatorType); + out.writeString(creator); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().field(creatorType, creator).endObject(); + } + + public static CreatedBy fromXContent(XContentParser parser) throws IOException { + String creator = null; + String creatorType = null; + XContentParser.Token token; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + creatorType = parser.currentName(); + } else if (token == XContentParser.Token.VALUE_STRING) { + creator = parser.text(); + } + } + + if (creator == null) { + throw new IllegalArgumentException(creatorType + " is required"); + } + + return new CreatedBy(creatorType, creator); + } +} diff --git a/src/main/java/org/opensearch/security/resources/Creator.java b/src/main/java/org/opensearch/security/resources/Creator.java index 84a00756c1..c7a913d4de 100644 --- a/src/main/java/org/opensearch/security/resources/Creator.java +++ b/src/main/java/org/opensearch/security/resources/Creator.java @@ -1,3 +1,11 @@ +/* + * 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. + */ + package org.opensearch.security.resources; public enum Creator { diff --git a/src/main/java/org/opensearch/security/resources/Recipient.java b/src/main/java/org/opensearch/security/resources/Recipient.java index 7cd2ed76ad..354f75fc0f 100644 --- a/src/main/java/org/opensearch/security/resources/Recipient.java +++ b/src/main/java/org/opensearch/security/resources/Recipient.java @@ -1,3 +1,11 @@ +/* + * 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. + */ + package org.opensearch.security.resources; public enum Recipient { diff --git a/src/main/java/org/opensearch/security/resources/RecipientType.java b/src/main/java/org/opensearch/security/resources/RecipientType.java new file mode 100644 index 0000000000..6ed3004b7e --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/RecipientType.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.resources; + +/** + * This class determines a type of recipient a resource can be shared with. + * An example type would be a user or a role. + * This class is used to determine the type of recipient a resource can be shared with. + * @opensearch.experimental + */ +public class RecipientType { + private final String type; + + public RecipientType(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + @Override + public String toString() { + return type; + } +} diff --git a/src/main/java/org/opensearch/security/resources/RecipientTypeRegistry.java b/src/main/java/org/opensearch/security/resources/RecipientTypeRegistry.java new file mode 100644 index 0000000000..95da5debef --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/RecipientTypeRegistry.java @@ -0,0 +1,33 @@ +/* + * 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. + */ + +package org.opensearch.security.resources; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class determines a collection of recipient types a resource can be shared with. + * + * @opensearch.experimental + */ +public class RecipientTypeRegistry { + private static final Map REGISTRY = new HashMap<>(); + + public static void registerRecipientType(String key, RecipientType recipientType) { + REGISTRY.put(key, recipientType); + } + + public static RecipientType fromValue(String value) { + RecipientType type = REGISTRY.get(value); + if (type == null) { + throw new IllegalArgumentException("Unknown RecipientType: " + value + ". Must be 1 of these: " + REGISTRY.values()); + } + return type; + } +} diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index eb9a81408d..149e058752 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -19,14 +19,11 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.accesscontrol.resources.RecipientType; -import org.opensearch.accesscontrol.resources.RecipientTypeRegistry; -import org.opensearch.accesscontrol.resources.Resource; -import org.opensearch.accesscontrol.resources.ResourceSharing; -import org.opensearch.accesscontrol.resources.ShareWith; -import org.opensearch.accesscontrol.resources.SharedWithScope; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.ResourceParser; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; @@ -73,11 +70,12 @@ public void initializeRecipientTypes() { * @param resourceIndex The resource index to check for accessible resources. * @return A set of accessible resource IDs. */ - public Set getAccessibleResourcesForCurrentUser(String resourceIndex, Class clazz) { - if (areArgumentsInvalid(resourceIndex, clazz)) { - return Collections.emptySet(); - } - final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); + @SuppressWarnings("unchecked") + public Set getAccessibleResourcesForCurrentUser(String resourceIndex) { + validateArguments(resourceIndex); + ResourceParser parser = OpenSearchSecurityPlugin.getResourceProviders().get(resourceIndex).getResourceParser(); + + final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); if (user == null) { LOGGER.info("Unable to fetch user details "); return Collections.emptySet(); @@ -87,24 +85,24 @@ public Set getAccessibleResourcesForCurrentUser(String r // check if user is admin, if yes all resources should be accessible if (adminDNs.isAdmin(user)) { - return loadAllResources(resourceIndex, clazz); + return loadAllResources(resourceIndex, parser); } Set result = new HashSet<>(); // 0. Own resources - result.addAll(loadOwnResources(resourceIndex, user.getName(), clazz)); + result.addAll(loadOwnResources(resourceIndex, user.getName(), parser)); // 1. By username - result.addAll(loadSharedWithResources(resourceIndex, Set.of(user.getName()), Recipient.USERS.toString(), clazz)); + result.addAll(loadSharedWithResources(resourceIndex, Set.of(user.getName()), Recipient.USERS.toString(), parser)); // 2. By roles Set roles = user.getSecurityRoles(); - result.addAll(loadSharedWithResources(resourceIndex, roles, Recipient.ROLES.toString(), clazz)); + result.addAll(loadSharedWithResources(resourceIndex, roles, Recipient.ROLES.toString(), parser)); // 3. By backend_roles Set backendRoles = user.getRoles(); - result.addAll(loadSharedWithResources(resourceIndex, backendRoles, Recipient.BACKEND_ROLES.toString(), clazz)); + result.addAll(loadSharedWithResources(resourceIndex, backendRoles, Recipient.BACKEND_ROLES.toString(), parser)); return result; } @@ -118,10 +116,9 @@ public Set getAccessibleResourcesForCurrentUser(String r * @return True if the user has the specified permission, false otherwise. */ public boolean hasPermission(String resourceId, String resourceIndex, String scope) { - if (areArgumentsInvalid(resourceId, resourceIndex, scope)) { - return false; - } - final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); + validateArguments(resourceId, resourceIndex, scope); + + final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Checking if {} has {} permission to resource {}", user.getName(), scope, resourceId); @@ -160,10 +157,9 @@ public boolean hasPermission(String resourceId, String resourceIndex, String sco * @return The updated ResourceSharing document. */ public ResourceSharing shareWith(String resourceId, String resourceIndex, ShareWith shareWith) { - if (areArgumentsInvalid(resourceId, resourceIndex, shareWith)) { - return null; - } - final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); + validateArguments(resourceId, resourceIndex, shareWith); + + final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith.toString()); // check if user is admin, if yes the user has permission @@ -186,10 +182,8 @@ public ResourceSharing revokeAccess( Map> revokeAccess, Set scopes ) { - if (areArgumentsInvalid(resourceId, resourceIndex, revokeAccess, scopes)) { - return null; - } - final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); + validateArguments(resourceId, resourceIndex, revokeAccess, scopes); + final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("User {} revoking access to resource {} for {} for scopes {} ", user.getName(), resourceId, revokeAccess, scopes); // check if user is admin, if yes the user has permission @@ -205,10 +199,9 @@ public ResourceSharing revokeAccess( * @return True if the record was successfully deleted, false otherwise. */ public boolean deleteResourceSharingRecord(String resourceId, String resourceIndex) { - if (areArgumentsInvalid(resourceId, resourceIndex)) { - return false; - } - final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); + validateArguments(resourceId, resourceIndex); + + final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Deleting resource sharing record for resource {} in {} created by {}", resourceId, resourceIndex, user.getName()); ResourceSharing document = this.resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId); @@ -229,7 +222,7 @@ public boolean deleteResourceSharingRecord(String resourceId, String resourceInd */ public boolean deleteAllResourceSharingRecordsForCurrentUser() { - final User user = threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); + final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); LOGGER.info("Deleting all resource sharing records for resource {}", user.getName()); return this.resourceSharingIndexHandler.deleteAllRecordsForUser(user.getName()); @@ -241,8 +234,8 @@ public boolean deleteAllResourceSharingRecordsForCurrentUser() { * @param resourceIndex The resource index to load resources from. * @return A set of resource IDs. */ - private Set loadAllResources(String resourceIndex, Class clazz) { - return this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex, clazz); + private Set loadAllResources(String resourceIndex, ResourceParser parser) { + return this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex, parser); } /** @@ -252,8 +245,8 @@ private Set loadAllResources(String resourceIndex, Class * @param userName The username of the owner. * @return A set of resource IDs owned by the user. */ - private Set loadOwnResources(String resourceIndex, String userName, Class clazz) { - return this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName, clazz); + private Set loadOwnResources(String resourceIndex, String userName, ResourceParser parser) { + return this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName, parser); } /** @@ -268,9 +261,9 @@ private Set loadSharedWithResources( String resourceIndex, Set entities, String RecipientType, - Class clazz + ResourceParser parser ) { - return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entities, RecipientType, clazz); + return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entities, RecipientType, parser); } /** @@ -345,20 +338,19 @@ private boolean checkSharing(ResourceSharing document, Recipient recipient, Stri .orElse(false); // Return false if no matching scope is found } - private boolean areArgumentsInvalid(Object... args) { + private void validateArguments(Object... args) { if (args == null) { - return true; + throw new IllegalArgumentException("Arguments cannot be null"); } for (Object arg : args) { if (arg == null) { - return true; + throw new IllegalArgumentException("Argument cannot be null"); } // Additional check for String type arguments if (arg instanceof String && ((String) arg).trim().isEmpty()) { - return true; + throw new IllegalArgumentException("Arguments cannot be empty"); } } - return false; } } diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharing.java b/src/main/java/org/opensearch/security/resources/ResourceSharing.java new file mode 100644 index 0000000000..6dd6734a87 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/ResourceSharing.java @@ -0,0 +1,207 @@ +/* + * 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. + */ + +package org.opensearch.security.resources; + +import java.io.IOException; +import java.util.Objects; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +/** + * Represents a resource sharing configuration that manages access control for OpenSearch resources. + * This class holds information about shared resources including their source, creator, and sharing permissions. + * + *

          This class implements {@link ToXContentFragment} for JSON serialization and {@link NamedWriteable} + * for stream-based serialization.

          + * + * The class maintains information about: + *
            + *
          • The source index where the resource is defined
          • + *
          • The unique identifier of the resource
          • + *
          • The creator's information
          • + *
          • The sharing permissions and recipients
          • + *
          + * + * + * @see org.opensearch.security.resources.CreatedBy + * @see org.opensearch.security.resources.ShareWith + * @opensearch.experimental + */ +public class ResourceSharing implements ToXContentFragment, NamedWriteable { + + /** + * The index where the resource is defined + */ + private String sourceIdx; + + /** + * The unique identifier of the resource + */ + private String resourceId; + + /** + * Information about who created the resource + */ + private CreatedBy createdBy; + + /** + * Information about with whom the resource is shared with + */ + private ShareWith shareWith; + + public ResourceSharing(String sourceIdx, String resourceId, CreatedBy createdBy, ShareWith shareWith) { + this.sourceIdx = sourceIdx; + this.resourceId = resourceId; + this.createdBy = createdBy; + this.shareWith = shareWith; + } + + public String getSourceIdx() { + return sourceIdx; + } + + public void setSourceIdx(String sourceIdx) { + this.sourceIdx = sourceIdx; + } + + public String getResourceId() { + return resourceId; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + public CreatedBy getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(CreatedBy createdBy) { + this.createdBy = createdBy; + } + + public ShareWith getShareWith() { + return shareWith; + } + + public void setShareWith(ShareWith shareWith) { + this.shareWith = shareWith; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ResourceSharing resourceSharing = (ResourceSharing) o; + return Objects.equals(getSourceIdx(), resourceSharing.getSourceIdx()) + && Objects.equals(getResourceId(), resourceSharing.getResourceId()) + && Objects.equals(getCreatedBy(), resourceSharing.getCreatedBy()) + && Objects.equals(getShareWith(), resourceSharing.getShareWith()); + } + + @Override + public int hashCode() { + return Objects.hash(getSourceIdx(), getResourceId(), getCreatedBy(), getShareWith()); + } + + @Override + public String toString() { + return "Resource {" + + "sourceIdx='" + + sourceIdx + + '\'' + + ", resourceId='" + + resourceId + + '\'' + + ", createdBy=" + + createdBy + + ", sharedWith=" + + shareWith + + '}'; + } + + @Override + public String getWriteableName() { + return "resource_sharing"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(sourceIdx); + out.writeString(resourceId); + createdBy.writeTo(out); + if (shareWith != null) { + out.writeBoolean(true); + shareWith.writeTo(out); + } else { + out.writeBoolean(false); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject().field("source_idx", sourceIdx).field("resource_id", resourceId).field("created_by"); + createdBy.toXContent(builder, params); + if (shareWith != null && !shareWith.getSharedWithScopes().isEmpty()) { + builder.field("share_with"); + shareWith.toXContent(builder, params); + } + return builder.endObject(); + } + + public static ResourceSharing fromXContent(XContentParser parser) throws IOException { + String sourceIdx = null; + String resourceId = null; + CreatedBy createdBy = null; + ShareWith shareWith = null; + + String currentFieldName = null; + XContentParser.Token token; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else { + switch (Objects.requireNonNull(currentFieldName)) { + case "source_idx": + sourceIdx = parser.text(); + break; + case "resource_id": + resourceId = parser.text(); + break; + case "created_by": + createdBy = CreatedBy.fromXContent(parser); + break; + case "share_with": + shareWith = ShareWith.fromXContent(parser); + break; + default: + parser.skipChildren(); + break; + } + } + } + + validateRequiredField("source_idx", sourceIdx); + validateRequiredField("resource_id", resourceId); + validateRequiredField("created_by", createdBy); + + return new ResourceSharing(sourceIdx, resourceId, createdBy, shareWith); + } + + private static void validateRequiredField(String field, T value) { + if (value == null) { + throw new IllegalArgumentException(field + " is required"); + } + } +} diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 83341b1ff2..c44fe452d2 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -24,10 +24,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.accesscontrol.resources.CreatedBy; -import org.opensearch.accesscontrol.resources.RecipientType; -import org.opensearch.accesscontrol.resources.ResourceSharing; -import org.opensearch.accesscontrol.resources.ShareWith; +import org.opensearch.OpenSearchException; import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.action.admin.indices.create.CreateIndexResponse; import org.opensearch.action.get.MultiGetItemResponse; @@ -52,6 +49,7 @@ import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.index.IndexNotFoundException; import org.opensearch.index.query.BoolQueryBuilder; import org.opensearch.index.query.MultiMatchQueryBuilder; import org.opensearch.index.query.QueryBuilders; @@ -67,6 +65,8 @@ import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.ResourceParser; import org.opensearch.threadpool.ThreadPool; import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; @@ -178,7 +178,7 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn return entry; } catch (Exception e) { LOGGER.info("Failed to create {} entry.", resourceSharingIndex, e); - return null; + throw new OpenSearchException("Failed to create " + resourceSharingIndex + " entry.", e); } } @@ -223,7 +223,7 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn *
        • Returns an empty list instead of throwing exceptions
        • *
        */ - public Set fetchAllDocuments(String pluginIndex, Class clazz) { + public Set fetchAllDocuments(String pluginIndex, ResourceParser parser) { LOGGER.debug("Fetching all documents from {} where source_idx = {}", resourceSharingIndex, pluginIndex); // TODO: Once stashContext is replaced with switchContext this call will have to be modified @@ -252,7 +252,7 @@ public Set fetchAllDocuments(String pluginIndex, Class clazz) { LOGGER.debug("Found {} documents in {} for source_idx: {}", resourceIds.size(), resourceSharingIndex, pluginIndex); - return resourceIds.isEmpty() ? Set.of() : getResourcesFromIds(resourceIds, pluginIndex, clazz); + return resourceIds.isEmpty() ? Set.of() : getResourcesFromIds(resourceIds, pluginIndex, parser); } catch (Exception e) { LOGGER.error("Failed to fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); @@ -327,9 +327,14 @@ public Set fetchAllDocuments(String pluginIndex, Class clazz) { * */ - public Set fetchDocumentsForAllScopes(String pluginIndex, Set entities, String RecipientType, Class clazz) { + public Set fetchDocumentsForAllScopes( + String pluginIndex, + Set entities, + String RecipientType, + ResourceParser parser + ) { // "*" must match all scopes - return fetchDocumentsForAGivenScope(pluginIndex, entities, RecipientType, "*", clazz); + return fetchDocumentsForAGivenScope(pluginIndex, entities, RecipientType, "*", parser); } /** @@ -383,7 +388,7 @@ public Set fetchDocumentsForAllScopes(String pluginIndex, Set ent *
      11. "roles" - for role-based access
      12. *
      13. "backend_roles" - for backend role-based access
      14. * - * @param scope The scope of the access. Should be implementation of {@link org.opensearch.accesscontrol.resources.ResourceAccessScope} + * @param scope The scope of the access. Should be implementation of {@link org.opensearch.security.spi.resources.ResourceAccessScope} * @param clazz Class to deserialize each document from Response into * @return Set List of resource IDs that match the criteria. The list may be empty * if no matches are found @@ -399,12 +404,12 @@ public Set fetchDocumentsForAllScopes(String pluginIndex, Set ent *
      15. Properly cleans up scroll context after use
      16. * */ - public Set fetchDocumentsForAGivenScope( + public Set fetchDocumentsForAGivenScope( String pluginIndex, Set entities, String RecipientType, String scope, - Class clazz + ResourceParser parser ) { LOGGER.debug( "Fetching documents from index: {}, where share_with.{}.{} contains any of {}", @@ -445,7 +450,7 @@ public Set fetchDocumentsForAGivenScope( LOGGER.debug("Found {} documents matching the criteria in {}", resourceIds.size(), resourceSharingIndex); - return resourceIds.isEmpty() ? Set.of() : getResourcesFromIds(resourceIds, pluginIndex, clazz); + return resourceIds.isEmpty() ? Set.of() : getResourcesFromIds(resourceIds, pluginIndex, parser); } catch (Exception e) { LOGGER.error( @@ -515,7 +520,7 @@ public Set fetchDocumentsForAGivenScope( * Set resources = fetchDocumentsByField("myIndex", "status", "active"); * */ - public Set fetchDocumentsByField(String pluginIndex, String field, String value, Class clazz) { + public Set fetchDocumentsByField(String pluginIndex, String field, String value, ResourceParser parser) { if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(field) || StringUtils.isBlank(value)) { throw new IllegalArgumentException("pluginIndex, field, and value must not be null or empty"); } @@ -538,7 +543,7 @@ public Set fetchDocumentsByField(String pluginIndex, String field, String LOGGER.info("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); - return resourceIds.isEmpty() ? Set.of() : getResourcesFromIds(resourceIds, pluginIndex, clazz); + return resourceIds.isEmpty() ? Set.of() : getResourcesFromIds(resourceIds, pluginIndex, parser); } catch (Exception e) { LOGGER.error("Failed to fetch documents from {} where {} = {}", resourceSharingIndex, field, value, e); throw new RuntimeException("Failed to fetch documents: " + e.getMessage(), e); @@ -645,7 +650,7 @@ public ResourceSharing fetchDocumentById(String pluginIndex, String resourceId) } catch (Exception e) { LOGGER.error("Failed to fetch document for resourceId: {} from index: {}", resourceId, pluginIndex, e); - throw new RuntimeException("Failed to fetch document: " + e.getMessage(), e); + throw new OpenSearchException("Failed to fetch document for resourceId: " + resourceId + " from index: " + pluginIndex, e); } } @@ -726,7 +731,7 @@ public ResourceSharing updateResourceSharingInfo( } catch (IOException e) { LOGGER.error("Failed to build json content", e); - return null; + throw new OpenSearchException("Failed to build json content", e); } // Check if the user requesting to share is the owner of the resource @@ -734,7 +739,7 @@ public ResourceSharing updateResourceSharingInfo( ResourceSharing currentSharingInfo = fetchDocumentById(sourceIdx, resourceId); if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { LOGGER.error("User {} is not authorized to share resource {}", requestUserName, resourceId); - return null; + throw new OpenSearchException("User " + requestUserName + " is not authorized to share resource " + resourceId); } CreatedBy createdBy; @@ -786,7 +791,12 @@ public ResourceSharing updateResourceSharingInfo( """, Collections.singletonMap("shareWith", shareWithMap)); boolean success = updateByQueryResourceSharing(sourceIdx, resourceId, updateScript); - return success ? new ResourceSharing(resourceId, sourceIdx, createdBy, shareWith) : null; + if (!success) { + LOGGER.error("Failed to update resource sharing info for resource {}", resourceId); + throw new OpenSearchException("Failed to update resource sharing info for resource " + resourceId); + } + + return new ResourceSharing(resourceId, sourceIdx, createdBy, shareWith); } /** @@ -942,7 +952,7 @@ public ResourceSharing revokeAccess( ResourceSharing currentSharingInfo = fetchDocumentById(sourceIdx, resourceId); if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { LOGGER.error("User {} is not authorized to revoke access to resource {}", requestUserName, resourceId); - return null; + throw new OpenSearchException("User " + requestUserName + " is not authorized to revoke access to resource " + resourceId); } LOGGER.debug("Revoking access for resource {} in {} for entities: {} and scopes: {}", resourceId, sourceIdx, revokeAccess, scopes); @@ -1163,7 +1173,7 @@ public boolean deleteAllRecordsForUser(String name) { * @param clazz The class to deserialize the documents into. * @return A set of deserialized documents. */ - private Set getResourcesFromIds(Set resourceIds, String resourceIndex, Class clazz) { + private Set getResourcesFromIds(Set resourceIds, String resourceIndex, ResourceParser parser) { Set result = new HashSet<>(); // stashing Context to avoid permission issues in-case resourceIndex is a system index // TODO: Once stashContext is replaced with switchContext this call will have to be modified @@ -1178,12 +1188,17 @@ private Set getResourcesFromIds(Set resourceIds, String resourceI for (MultiGetItemResponse itemResponse : response.getResponses()) { if (!itemResponse.isFailed() && itemResponse.getResponse().isExists()) { String sourceAsString = itemResponse.getResponse().getSourceAsString(); - T resource = DefaultObjectMapper.readValue(sourceAsString, clazz); + // T resource = DefaultObjectMapper.readValue(sourceAsString, clazz); + T resource = parser.parse(sourceAsString); result.add(resource); } } + } catch (IndexNotFoundException e) { + LOGGER.error("Index {} does not exist", resourceIndex, e); + throw e; } catch (Exception e) { LOGGER.error("Failed to fetch resources with ids {} from index {}", resourceIds, resourceIndex, e); + throw new OpenSearchException("Failed to fetch resources: " + e.getMessage(), e); } return result; diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java index 58fe4cccf4..649a21dfb1 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -13,8 +13,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.accesscontrol.resources.CreatedBy; -import org.opensearch.accesscontrol.resources.ResourceSharing; import org.opensearch.client.Client; import org.opensearch.core.index.shard.ShardId; import org.opensearch.index.engine.Engine; @@ -88,7 +86,7 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re String resourceId = index.id(); - User user = threadPool.getThreadContext().getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); + User user = (User) threadPool.getThreadContext().getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); try { ResourceSharing sharing = this.resourceSharingIndexHandler.indexResourceSharing( diff --git a/src/main/java/org/opensearch/security/resources/ShareWith.java b/src/main/java/org/opensearch/security/resources/ShareWith.java new file mode 100644 index 0000000000..2a8e047761 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/ShareWith.java @@ -0,0 +1,104 @@ +/* + * 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. + */ + +package org.opensearch.security.resources; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +/** + * + * This class contains information about whom a resource is shared with and at what scope. + * Example: + * "share_with": { + * "read_only": { + * "users": [], + * "roles": [], + * "backend_roles": [] + * }, + * "read_write": { + * "users": [], + * "roles": [], + * "backend_roles": [] + * } + * } + * + * @opensearch.experimental + */ +public class ShareWith implements ToXContentFragment, NamedWriteable { + + /** + * A set of objects representing the scopes and their associated users, roles, and backend roles. + */ + private final Set sharedWithScopes; + + public ShareWith(Set sharedWithScopes) { + this.sharedWithScopes = sharedWithScopes; + } + + public ShareWith(StreamInput in) throws IOException { + this.sharedWithScopes = in.readSet(SharedWithScope::new); + } + + public Set getSharedWithScopes() { + return sharedWithScopes; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + + for (SharedWithScope scope : sharedWithScopes) { + scope.toXContent(builder, params); + } + + return builder.endObject(); + } + + public static ShareWith fromXContent(XContentParser parser) throws IOException { + Set sharedWithScopes = new HashSet<>(); + + if (parser.currentToken() != XContentParser.Token.START_OBJECT) { + parser.nextToken(); + } + + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + // Each field in the object represents a SharedWithScope + if (token == XContentParser.Token.FIELD_NAME) { + SharedWithScope scope = SharedWithScope.fromXContent(parser); + sharedWithScopes.add(scope); + } + } + + return new ShareWith(sharedWithScopes); + } + + @Override + public String getWriteableName() { + return "share_with"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeCollection(sharedWithScopes); + } + + @Override + public String toString() { + return "ShareWith " + sharedWithScopes; + } +} diff --git a/src/main/java/org/opensearch/security/resources/SharedWithScope.java b/src/main/java/org/opensearch/security/resources/SharedWithScope.java new file mode 100644 index 0000000000..02e3db854f --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/SharedWithScope.java @@ -0,0 +1,169 @@ +/* + * 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. + */ + +package org.opensearch.security.resources; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +/** + * This class represents the scope at which a resource is shared with. + * Example: + * "read_only": { + * "users": [], + * "roles": [], + * "backend_roles": [] + * } + * where "users", "roles" and "backend_roles" are the recipient entities + * + * @opensearch.experimental + */ +public class SharedWithScope implements ToXContentFragment, NamedWriteable { + + private final String scope; + + private final ScopeRecipients scopeRecipients; + + public SharedWithScope(String scope, ScopeRecipients scopeRecipients) { + this.scope = scope; + this.scopeRecipients = scopeRecipients; + } + + public SharedWithScope(StreamInput in) throws IOException { + this.scope = in.readString(); + this.scopeRecipients = new ScopeRecipients(in); + } + + public String getScope() { + return scope; + } + + public ScopeRecipients getSharedWithPerScope() { + return scopeRecipients; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field(scope); + builder.startObject(); + + scopeRecipients.toXContent(builder, params); + + return builder.endObject(); + } + + public static SharedWithScope fromXContent(XContentParser parser) throws IOException { + String scope = parser.currentName(); + + parser.nextToken(); + + ScopeRecipients scopeRecipients = ScopeRecipients.fromXContent(parser); + + return new SharedWithScope(scope, scopeRecipients); + } + + @Override + public String toString() { + return "{" + scope + ": " + scopeRecipients + '}'; + } + + @Override + public String getWriteableName() { + return "shared_with_scope"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(scope); + out.writeNamedWriteable(scopeRecipients); + } + + /** + * This class represents the entities with whom a resource is shared with for a given scope. + * + * @opensearch.experimental + */ + public static class ScopeRecipients implements ToXContentFragment, NamedWriteable { + + private final Map> recipients; + + public ScopeRecipients(Map> recipients) { + if (recipients == null) { + throw new IllegalArgumentException("Recipients map cannot be null"); + } + this.recipients = recipients; + } + + public ScopeRecipients(StreamInput in) throws IOException { + this.recipients = in.readMap( + key -> RecipientTypeRegistry.fromValue(key.readString()), + input -> input.readSet(StreamInput::readString) + ); + } + + public Map> getRecipients() { + return recipients; + } + + @Override + public String getWriteableName() { + return "scope_recipients"; + } + + public static ScopeRecipients fromXContent(XContentParser parser) throws IOException { + Map> recipients = new HashMap<>(); + + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + String fieldName = parser.currentName(); + RecipientType recipientType = RecipientTypeRegistry.fromValue(fieldName); + + parser.nextToken(); + Set values = new HashSet<>(); + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + values.add(parser.text()); + } + recipients.put(recipientType, values); + } + } + + return new ScopeRecipients(recipients); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeMap( + recipients, + (streamOutput, recipientType) -> streamOutput.writeString(recipientType.getType()), + (streamOutput, strings) -> streamOutput.writeCollection(strings, StreamOutput::writeString) + ); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (recipients.isEmpty()) { + return builder; + } + for (Map.Entry> entry : recipients.entrySet()) { + builder.array(entry.getKey().getType(), entry.getValue().toArray()); + } + return builder; + } + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesAction.java b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesAction.java new file mode 100644 index 0000000000..3a8aa6ae59 --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesAction.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.list; + +import org.opensearch.action.ActionType; + +/** + * Action to list resources + */ +public class ListAccessibleResourcesAction extends ActionType { + + public static final ListAccessibleResourcesAction INSTANCE = new ListAccessibleResourcesAction(); + + public static final String NAME = "cluster:admin/security/resources/list"; + + private ListAccessibleResourcesAction() { + super(NAME, ListAccessibleResourcesResponse::new); + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesRequest.java b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesRequest.java new file mode 100644 index 0000000000..f16887f12b --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesRequest.java @@ -0,0 +1,51 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.list; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +/** + * Request object for ListSampleResource transport action + */ +public class ListAccessibleResourcesRequest extends ActionRequest { + + private String resourceIndex; + + public ListAccessibleResourcesRequest(String resourceIndex) { + this.resourceIndex = resourceIndex; + } + + /** + * Constructor with stream input + * @param in the stream input + * @throws IOException IOException + */ + public ListAccessibleResourcesRequest(final StreamInput in) throws IOException { + this.resourceIndex = in.readString(); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(this.resourceIndex); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceIndex() { + return resourceIndex; + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java new file mode 100644 index 0000000000..1a678ac2ce --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java @@ -0,0 +1,49 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.list; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.security.spi.resources.Resource; + +/** + * Response to a ListAccessibleResourcesRequest + */ +public class ListAccessibleResourcesResponse extends ActionResponse implements ToXContentObject { + private final Set resources; + + public ListAccessibleResourcesResponse(Set resources) { + this.resources = resources; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeCollection(resources); + } + + public ListAccessibleResourcesResponse(StreamInput in) { + // TODO need to fix this to return correct value + this.resources = new HashSet<>(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("resources", resources); + builder.endObject(); + return builder; + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/list/RestListAccessibleResourcesAction.java b/src/main/java/org/opensearch/security/rest/resources/access/list/RestListAccessibleResourcesAction.java new file mode 100644 index 0000000000..61935ee709 --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/list/RestListAccessibleResourcesAction.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.list; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; + +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +public class RestListAccessibleResourcesAction extends BaseRestHandler { + + public RestListAccessibleResourcesAction() {} + + @Override + public List routes() { + return addRoutesPrefix(ImmutableList.of(new Route(GET, "/resources/list")), PLUGIN_ROUTE_PREFIX); + } + + @Override + public String getName() { + return "list_accessible_resources"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String resourceIndex = (String) source.get("resource_index"); + final ListAccessibleResourcesRequest listAccessibleResourcesRequest = new ListAccessibleResourcesRequest(resourceIndex); + return channel -> client.executeLocally( + ListAccessibleResourcesAction.INSTANCE, + listAccessibleResourcesRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RestRevokeResourceAccessAction.java b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RestRevokeResourceAccessAction.java new file mode 100644 index 0000000000..2bde557884 --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RestRevokeResourceAccessAction.java @@ -0,0 +1,74 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.revoke; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableList; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.security.resources.RecipientType; +import org.opensearch.security.resources.RecipientTypeRegistry; + +import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +public class RestRevokeResourceAccessAction extends BaseRestHandler { + + public RestRevokeResourceAccessAction() {} + + @Override + public List routes() { + return addRoutesPrefix(ImmutableList.of(new Route(POST, "/resources/revoke")), PLUGIN_ROUTE_PREFIX); + } + + @Override + public String getName() { + return "revoke_resources_access"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String resourceId = (String) source.get("resource_id"); + String resourceIndex = (String) source.get("resource_index"); + @SuppressWarnings("unchecked") + Map> revokeSource = (Map>) source.get("entities"); + Map> revoke = revokeSource.entrySet() + .stream() + .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); + @SuppressWarnings("unchecked") + Set scopes = new HashSet<>(source.containsKey("scopes") ? (List) source.get("scopes") : List.of()); + final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest( + resourceId, + resourceIndex, + revoke, + scopes + ); + return channel -> client.executeLocally( + RevokeResourceAccessAction.INSTANCE, + revokeResourceAccessRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessAction.java b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessAction.java new file mode 100644 index 0000000000..e27ce05a2b --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessAction.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.revoke; + +import org.opensearch.action.ActionType; + +public class RevokeResourceAccessAction extends ActionType { + public static final RevokeResourceAccessAction INSTANCE = new RevokeResourceAccessAction(); + + public static final String NAME = "cluster:admin/security/resources/revoke"; + + private RevokeResourceAccessAction() { + super(NAME, RevokeResourceAccessResponse::new); + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessRequest.java b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessRequest.java new file mode 100644 index 0000000000..667f1670dd --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessRequest.java @@ -0,0 +1,79 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.revoke; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.security.resources.RecipientType; + +public class RevokeResourceAccessRequest extends ActionRequest { + + private final String resourceId; + private final String resourceIndex; + private final Map> revokeAccess; + private final Set scopes; + + public RevokeResourceAccessRequest( + String resourceId, + String resourceIndex, + Map> revokeAccess, + Set scopes + ) { + this.resourceId = resourceId; + this.resourceIndex = resourceIndex; + this.revokeAccess = revokeAccess; + this.scopes = scopes; + } + + public RevokeResourceAccessRequest(StreamInput in) throws IOException { + this.resourceId = in.readString(); + this.resourceIndex = in.readString(); + this.revokeAccess = in.readMap(input -> new RecipientType(input.readString()), input -> input.readSet(StreamInput::readString)); + this.scopes = in.readSet(StreamInput::readString); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(resourceId); + out.writeString(resourceIndex); + out.writeMap( + revokeAccess, + (streamOutput, recipientType) -> streamOutput.writeString(recipientType.getType()), + StreamOutput::writeStringCollection + ); + out.writeStringCollection(scopes); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceId() { + return resourceId; + } + + public String getResourceIndex() { + return resourceIndex; + } + + public Map> getRevokeAccess() { + return revokeAccess; + } + + public Set getScopes() { + return scopes; + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessResponse.java b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessResponse.java new file mode 100644 index 0000000000..090dfb54d0 --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessResponse.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.revoke; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +public class RevokeResourceAccessResponse extends ActionResponse implements ToXContentObject { + private final String message; + + public RevokeResourceAccessResponse(String message) { + this.message = message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(message); + } + + public RevokeResourceAccessResponse(final StreamInput in) throws IOException { + message = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("message", message); + builder.endObject(); + return builder; + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/share/RestShareResourceAction.java b/src/main/java/org/opensearch/security/rest/resources/access/share/RestShareResourceAction.java new file mode 100644 index 0000000000..3559ced3aa --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/share/RestShareResourceAction.java @@ -0,0 +1,79 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.share; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.security.resources.ShareWith; + +import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +public class RestShareResourceAction extends BaseRestHandler { + + public RestShareResourceAction() {} + + @Override + public List routes() { + return addRoutesPrefix(ImmutableList.of(new Route(POST, "/resources/share")), PLUGIN_ROUTE_PREFIX); + } + + @Override + public String getName() { + return "share_resources"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String resourceId = (String) source.get("resource_id"); + String resourceIndex = (String) source.get("resource_index"); + + ShareWith shareWith = parseShareWith(source); + final ShareResourceRequest shareResourceRequest = new ShareResourceRequest(resourceId, resourceIndex, shareWith); + return channel -> client.executeLocally(ShareResourceAction.INSTANCE, shareResourceRequest, new RestToXContentListener<>(channel)); + } + + private ShareWith parseShareWith(Map source) throws IOException { + @SuppressWarnings("unchecked") + Map shareWithMap = (Map) source.get("share_with"); + if (shareWithMap == null || shareWithMap.isEmpty()) { + throw new IllegalArgumentException("share_with is required and cannot be empty"); + } + + String jsonString = XContentFactory.jsonBuilder().map(shareWithMap).toString(); + + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, jsonString) + ) { + return ShareWith.fromXContent(parser); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceAction.java b/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceAction.java new file mode 100644 index 0000000000..a112108bf1 --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceAction.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.share; + +import org.opensearch.action.ActionType; + +/** + * Share resource + */ +public class ShareResourceAction extends ActionType { + + public static final ShareResourceAction INSTANCE = new ShareResourceAction(); + + public static final String NAME = "cluster:admin/security/resources/share"; + + private ShareResourceAction() { + super(NAME, ShareResourceResponse::new); + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceRequest.java b/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceRequest.java new file mode 100644 index 0000000000..560e2967ba --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceRequest.java @@ -0,0 +1,61 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.share; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.security.resources.ShareWith; + +public class ShareResourceRequest extends ActionRequest { + + private final String resourceId; + private final String resourceIndex; + private final ShareWith shareWith; + + public ShareResourceRequest(String resourceId, String resourceIndex, ShareWith shareWith) { + this.resourceId = resourceId; + this.resourceIndex = resourceIndex; + this.shareWith = shareWith; + } + + public ShareResourceRequest(StreamInput in) throws IOException { + this.resourceId = in.readString(); + this.resourceIndex = in.readString(); + this.shareWith = in.readNamedWriteable(ShareWith.class); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(resourceId); + out.writeString(resourceIndex); + out.writeNamedWriteable(shareWith); + } + + @Override + public ActionRequestValidationException validate() { + + return null; + } + + public String getResourceId() { + return resourceId; + } + + public String getResourceIndex() { + return resourceIndex; + } + + public ShareWith getShareWith() { + return shareWith; + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceResponse.java b/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceResponse.java new file mode 100644 index 0000000000..15b83c8d6f --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceResponse.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.share; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +public class ShareResourceResponse extends ActionResponse implements ToXContentObject { + private final String message; + + public ShareResourceResponse(String message) { + this.message = message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(message); + } + + public ShareResourceResponse(final StreamInput in) throws IOException { + message = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("message", message); + builder.endObject(); + return builder; + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/verify/RestVerifyResourceAccessAction.java b/src/main/java/org/opensearch/security/rest/resources/access/verify/RestVerifyResourceAccessAction.java new file mode 100644 index 0000000000..3a7e713a83 --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/verify/RestVerifyResourceAccessAction.java @@ -0,0 +1,59 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.verify; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; + +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +public class RestVerifyResourceAccessAction extends BaseRestHandler { + + public RestVerifyResourceAccessAction() {} + + @Override + public List routes() { + return addRoutesPrefix(ImmutableList.of(new Route(GET, "/resources/verify_access")), PLUGIN_ROUTE_PREFIX); + } + + @Override + public String getName() { + return "verify_resource_access"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String resourceId = (String) source.get("resource_id"); + String resourceIndex = (String) source.get("resource_index"); + String scope = (String) source.get("scope"); + + final VerifyResourceAccessRequest verifyResourceAccessRequest = new VerifyResourceAccessRequest(resourceId, resourceIndex, scope); + return channel -> client.executeLocally( + VerifyResourceAccessAction.INSTANCE, + verifyResourceAccessRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessAction.java b/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessAction.java new file mode 100644 index 0000000000..ff07b1e455 --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessAction.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.verify; + +import org.opensearch.action.ActionType; + +/** + * Action to verify resource access for current user + */ +public class VerifyResourceAccessAction extends ActionType { + + public static final VerifyResourceAccessAction INSTANCE = new VerifyResourceAccessAction(); + + public static final String NAME = "cluster:admin/sample-resource-plugin/verify/resource_access"; + + private VerifyResourceAccessAction() { + super(NAME, VerifyResourceAccessResponse::new); + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessRequest.java b/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessRequest.java new file mode 100644 index 0000000000..529db51830 --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessRequest.java @@ -0,0 +1,69 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.verify; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +public class VerifyResourceAccessRequest extends ActionRequest { + + private final String resourceId; + + private final String resourceIndex; + + private final String scope; + + /** + * Default constructor + */ + public VerifyResourceAccessRequest(String resourceId, String resourceIndex, String scope) { + this.resourceId = resourceId; + this.resourceIndex = resourceIndex; + this.scope = scope; + } + + /** + * Constructor with stream input + * @param in the stream input + * @throws IOException IOException + */ + public VerifyResourceAccessRequest(final StreamInput in) throws IOException { + this.resourceId = in.readString(); + this.resourceIndex = in.readString(); + this.scope = in.readString(); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(resourceId); + out.writeString(resourceIndex); + out.writeString(scope); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceId() { + return resourceId; + } + + public String getResourceIndex() { + return resourceIndex; + } + + public String getScope() { + return scope; + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessResponse.java b/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessResponse.java new file mode 100644 index 0000000000..a7fa7a2de4 --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessResponse.java @@ -0,0 +1,52 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access.verify; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +public class VerifyResourceAccessResponse extends ActionResponse implements ToXContentObject { + private final String message; + + /** + * Default constructor + * + * @param message The message + */ + public VerifyResourceAccessResponse(String message) { + this.message = message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(message); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public VerifyResourceAccessResponse(final StreamInput in) throws IOException { + message = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("message", message); + builder.endObject(); + return builder; + } +} diff --git a/src/main/java/org/opensearch/security/transport/resources/access/ListAccessibleResourcesTransportAction.java b/src/main/java/org/opensearch/security/transport/resources/access/ListAccessibleResourcesTransportAction.java new file mode 100644 index 0000000000..0e3ca2f1c4 --- /dev/null +++ b/src/main/java/org/opensearch/security/transport/resources/access/ListAccessibleResourcesTransportAction.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package org.opensearch.security.transport.resources.access; + +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.security.resources.ResourceAccessHandler; +import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesAction; +import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesRequest; +import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesResponse; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +public class ListAccessibleResourcesTransportAction extends HandledTransportAction< + ListAccessibleResourcesRequest, + ListAccessibleResourcesResponse> { + private static final Logger log = LogManager.getLogger(ListAccessibleResourcesTransportAction.class); + private final ResourceAccessHandler resourceAccessHandler; + + @Inject + public ListAccessibleResourcesTransportAction( + TransportService transportService, + ActionFilters actionFilters, + ResourceAccessHandler resourceAccessHandler + ) { + super(ListAccessibleResourcesAction.NAME, transportService, actionFilters, ListAccessibleResourcesRequest::new); + this.resourceAccessHandler = resourceAccessHandler; + } + + @Override + protected void doExecute(Task task, ListAccessibleResourcesRequest request, ActionListener listener) { + try { + Set resources = resourceAccessHandler.getAccessibleResourcesForCurrentUser(request.getResourceIndex()); + log.info("Successfully fetched accessible resources for current user : {}", resources); + listener.onResponse(new ListAccessibleResourcesResponse(resources)); + } catch (Exception e) { + log.info("Failed to list accessible resources for current user: ", e); + listener.onFailure(e); + } + + } +} diff --git a/src/main/java/org/opensearch/security/transport/resources/access/RevokeResourceAccessTransportAction.java b/src/main/java/org/opensearch/security/transport/resources/access/RevokeResourceAccessTransportAction.java new file mode 100644 index 0000000000..fd7324dca1 --- /dev/null +++ b/src/main/java/org/opensearch/security/transport/resources/access/RevokeResourceAccessTransportAction.java @@ -0,0 +1,65 @@ +/* + * 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. + */ + +package org.opensearch.security.transport.resources.access; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.security.resources.ResourceAccessHandler; +import org.opensearch.security.resources.ResourceSharing; +import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessAction; +import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessRequest; +import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessResponse; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +public class RevokeResourceAccessTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(RevokeResourceAccessTransportAction.class); + private final ResourceAccessHandler resourceAccessHandler; + + @Inject + public RevokeResourceAccessTransportAction( + TransportService transportService, + ActionFilters actionFilters, + ResourceAccessHandler resourceAccessHandler + ) { + super(RevokeResourceAccessAction.NAME, transportService, actionFilters, RevokeResourceAccessRequest::new); + this.resourceAccessHandler = resourceAccessHandler; + } + + @Override + protected void doExecute(Task task, RevokeResourceAccessRequest request, ActionListener listener) { + try { + ResourceSharing revoke = revokeAccess(request); + if (revoke == null) { + log.error("Failed to revoke access to resource {}", request.getResourceId()); + listener.onFailure(new OpenSearchException("Failed to revoke access to resource " + request.getResourceId())); + return; + } + log.info("Revoked resource access for resource: {} with {}", request.getResourceId(), revoke.toString()); + listener.onResponse(new RevokeResourceAccessResponse("Resource " + request.getResourceId() + " access revoked successfully.")); + } catch (Exception e) { + listener.onFailure(e); + } + } + + private ResourceSharing revokeAccess(RevokeResourceAccessRequest request) { + return this.resourceAccessHandler.revokeAccess( + request.getResourceId(), + request.getResourceIndex(), + request.getRevokeAccess(), + request.getScopes() + ); + } +} diff --git a/src/main/java/org/opensearch/security/transport/resources/access/ShareResourceTransportAction.java b/src/main/java/org/opensearch/security/transport/resources/access/ShareResourceTransportAction.java new file mode 100644 index 0000000000..1d8111f1b6 --- /dev/null +++ b/src/main/java/org/opensearch/security/transport/resources/access/ShareResourceTransportAction.java @@ -0,0 +1,61 @@ +/* + * 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. + */ + +package org.opensearch.security.transport.resources.access; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.security.resources.ResourceAccessHandler; +import org.opensearch.security.resources.ResourceSharing; +import org.opensearch.security.rest.resources.access.share.ShareResourceAction; +import org.opensearch.security.rest.resources.access.share.ShareResourceRequest; +import org.opensearch.security.rest.resources.access.share.ShareResourceResponse; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +public class ShareResourceTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(ShareResourceTransportAction.class); + private final ResourceAccessHandler resourceAccessHandler; + + @Inject + public ShareResourceTransportAction( + TransportService transportService, + ActionFilters actionFilters, + ResourceAccessHandler resourceAccessHandler + ) { + super(ShareResourceAction.NAME, transportService, actionFilters, ShareResourceRequest::new); + this.resourceAccessHandler = resourceAccessHandler; + } + + @Override + protected void doExecute(Task task, ShareResourceRequest request, ActionListener listener) { + ResourceSharing sharing = null; + try { + sharing = shareResource(request); + if (sharing == null) { + log.error("Failed to share resource {}", request.getResourceId()); + listener.onFailure(new OpenSearchException("Failed to share resource " + request.getResourceId())); + return; + } + log.info("Shared resource : {} with {}", request.getResourceId(), sharing.toString()); + listener.onResponse(new ShareResourceResponse("Resource " + request.getResourceId() + " shared successfully.")); + } catch (Exception e) { + listener.onFailure(e); + } + } + + private ResourceSharing shareResource(ShareResourceRequest request) throws Exception { + return this.resourceAccessHandler.shareWith(request.getResourceId(), request.getResourceIndex(), request.getShareWith()); + } +} diff --git a/src/main/java/org/opensearch/security/transport/resources/access/VerifyResourceAccessTransportAction.java b/src/main/java/org/opensearch/security/transport/resources/access/VerifyResourceAccessTransportAction.java new file mode 100644 index 0000000000..f608453c02 --- /dev/null +++ b/src/main/java/org/opensearch/security/transport/resources/access/VerifyResourceAccessTransportAction.java @@ -0,0 +1,66 @@ +/* + * 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. + */ + +package org.opensearch.security.transport.resources.access; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.security.resources.ResourceAccessHandler; +import org.opensearch.security.rest.resources.access.verify.VerifyResourceAccessAction; +import org.opensearch.security.rest.resources.access.verify.VerifyResourceAccessRequest; +import org.opensearch.security.rest.resources.access.verify.VerifyResourceAccessResponse; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +public class VerifyResourceAccessTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(VerifyResourceAccessTransportAction.class); + private final ResourceAccessHandler resourceAccessHandler; + + @Inject + public VerifyResourceAccessTransportAction( + TransportService transportService, + ActionFilters actionFilters, + Client nodeClient, + ResourceAccessHandler resourceAccessHandler + ) { + super(VerifyResourceAccessAction.NAME, transportService, actionFilters, VerifyResourceAccessRequest::new); + this.resourceAccessHandler = resourceAccessHandler; + } + + @Override + protected void doExecute(Task task, VerifyResourceAccessRequest request, ActionListener listener) { + try { + boolean hasRequestedScopeAccess = this.resourceAccessHandler.hasPermission( + request.getResourceId(), + request.getResourceIndex(), + request.getScope() + ); + + StringBuilder sb = new StringBuilder(); + sb.append("User "); + sb.append(hasRequestedScopeAccess ? "has" : "does not have"); + sb.append(" requested scope "); + sb.append(request.getScope()); + sb.append(" access to "); + sb.append(request.getResourceId()); + + log.info(sb.toString()); + listener.onResponse(new VerifyResourceAccessResponse(sb.toString())); + } catch (Exception e) { + log.info("Failed to check user permissions for resource {}", request.getResourceId(), e); + listener.onFailure(e); + } + } + +} diff --git a/src/main/java/org/opensearch/security/util/ResourceValidation.java b/src/main/java/org/opensearch/security/util/ResourceValidation.java new file mode 100644 index 0000000000..3850087e4e --- /dev/null +++ b/src/main/java/org/opensearch/security/util/ResourceValidation.java @@ -0,0 +1,34 @@ +/* + * 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. + */ + +package org.opensearch.security.util; + +import java.util.HashSet; +import java.util.Set; + +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.security.spi.resources.ResourceAccessScope; + +public class ResourceValidation { + public static ActionRequestValidationException validateScopes(Set scopes) { + Set validScopes = new HashSet<>(); + validScopes.add(ResourceAccessScope.READ_ONLY); + validScopes.add(ResourceAccessScope.READ_WRITE); + + // TODO See if we can add custom scopes as part of this validation routine + + for (String s : scopes) { + if (!validScopes.contains(s)) { + ActionRequestValidationException exception = new ActionRequestValidationException(); + exception.addValidationError("Invalid scope: " + s + ". Scope must be one of: " + validScopes); + return exception; + } + } + return null; + } +} diff --git a/src/test/java/org/opensearch/security/resources/CreatedByTests.java b/src/test/java/org/opensearch/security/resources/CreatedByTests.java new file mode 100644 index 0000000000..6b183ccbc7 --- /dev/null +++ b/src/test/java/org/opensearch/security/resources/CreatedByTests.java @@ -0,0 +1,286 @@ +/* + * 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. + */ + +package org.opensearch.security.resources; + +import java.io.IOException; + +import org.hamcrest.MatcherAssert; + +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.test.OpenSearchTestCase; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CreatedByTests extends OpenSearchTestCase { + + private static final String CREATOR_TYPE = "user"; + + public void testCreatedByConstructorWithValidUser() { + String expectedUser = "testUser"; + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, expectedUser); + + MatcherAssert.assertThat(expectedUser, is(equalTo(createdBy.getCreator()))); + } + + public void testCreatedByFromStreamInput() throws IOException { + String expectedUser = "testUser"; + + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.writeString(CREATOR_TYPE); + out.writeString(expectedUser); + + StreamInput in = out.bytes().streamInput(); + + CreatedBy createdBy = new CreatedBy(in); + + MatcherAssert.assertThat(expectedUser, is(equalTo(createdBy.getCreator()))); + } + } + + public void testCreatedByWithEmptyStreamInput() throws IOException { + + try (StreamInput mockStreamInput = mock(StreamInput.class)) { + when(mockStreamInput.readString()).thenThrow(new IOException("EOF")); + + assertThrows(IOException.class, () -> new CreatedBy(mockStreamInput)); + } + } + + public void testCreatedByWithEmptyUser() { + + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, ""); + MatcherAssert.assertThat("", equalTo(createdBy.getCreator())); + } + + public void testCreatedByWithIOException() throws IOException { + + try (StreamInput mockStreamInput = mock(StreamInput.class)) { + when(mockStreamInput.readString()).thenThrow(new IOException("Test IOException")); + + assertThrows(IOException.class, () -> new CreatedBy(mockStreamInput)); + } + } + + public void testCreatedByWithLongUsername() { + String longUsername = "a".repeat(10000); + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, longUsername); + MatcherAssert.assertThat(longUsername, equalTo(createdBy.getCreator())); + } + + public void testCreatedByWithUnicodeCharacters() { + String unicodeUsername = "η”¨ζˆ·γ“γ‚“γ«γ‘γ―"; + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, unicodeUsername); + MatcherAssert.assertThat(unicodeUsername, equalTo(createdBy.getCreator())); + } + + public void testFromXContentThrowsExceptionWhenUserFieldIsMissing() throws IOException { + String emptyJson = "{}"; + IllegalArgumentException exception; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, emptyJson)) { + + exception = assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + + MatcherAssert.assertThat("null is required", equalTo(exception.getMessage())); + } + + public void testFromXContentWithEmptyInput() throws IOException { + String emptyJson = "{}"; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, emptyJson)) { + + assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + } + + public void testFromXContentWithExtraFields() throws IOException { + String jsonWithExtraFields = "{\"user\": \"testUser\", \"extraField\": \"value\"}"; + XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, jsonWithExtraFields); + + CreatedBy.fromXContent(parser); + } + + public void testFromXContentWithIncorrectFieldType() throws IOException { + String jsonWithIncorrectType = "{\"user\": 12345}"; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, jsonWithIncorrectType)) { + + assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + } + + public void testFromXContentWithEmptyUser() throws IOException { + String emptyJson = "{\"" + CREATOR_TYPE + "\": \"\" }"; + CreatedBy createdBy; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, emptyJson)) { + parser.nextToken(); + + createdBy = CreatedBy.fromXContent(parser); + } + + MatcherAssert.assertThat(CREATOR_TYPE, equalTo(createdBy.getCreatorType())); + MatcherAssert.assertThat("", equalTo(createdBy.getCreator())); + } + + public void testFromXContentWithNullUserValue() throws IOException { + String jsonWithNullUser = "{\"user\": null}"; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, jsonWithNullUser)) { + + assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + } + + public void testFromXContentWithValidUser() throws IOException { + String json = "{\"user\":\"testUser\"}"; + XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, json); + + CreatedBy createdBy = CreatedBy.fromXContent(parser); + + MatcherAssert.assertThat(createdBy, notNullValue()); + MatcherAssert.assertThat("testUser", equalTo(createdBy.getCreator())); + } + + public void testGetCreatorReturnsCorrectValue() { + String expectedUser = "testUser"; + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, expectedUser); + + String actualUser = createdBy.getCreator(); + + MatcherAssert.assertThat(expectedUser, equalTo(actualUser)); + } + + public void testGetCreatorWithNullString() { + + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, null); + MatcherAssert.assertThat(createdBy.getCreator(), nullValue()); + } + + public void testGetWriteableNameReturnsCorrectString() { + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, "testUser"); + MatcherAssert.assertThat("created_by", equalTo(createdBy.getWriteableName())); + } + + public void testToStringWithEmptyUser() { + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, ""); + String result = createdBy.toString(); + MatcherAssert.assertThat("CreatedBy {user=''}", equalTo(result)); + } + + public void testToStringWithNullUser() { + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, (String) null); + String result = createdBy.toString(); + MatcherAssert.assertThat("CreatedBy {user='null'}", equalTo(result)); + } + + public void testToStringWithLongUserName() { + + String longUserName = "a".repeat(1000); + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, longUserName); + String result = createdBy.toString(); + MatcherAssert.assertThat(result.startsWith("CreatedBy {user='"), is(true)); + MatcherAssert.assertThat(result.endsWith("'}"), is(true)); + MatcherAssert.assertThat(1019, equalTo(result.length())); + } + + public void testToXContentWithEmptyUser() throws IOException { + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, ""); + XContentBuilder builder = JsonXContent.contentBuilder(); + + createdBy.toXContent(builder, null); + String result = builder.toString(); + MatcherAssert.assertThat("{\"user\":\"\"}", equalTo(result)); + } + + public void testWriteToWithExceptionInStreamOutput() throws IOException { + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, "user1"); + try (StreamOutput failingOutput = new StreamOutput() { + @Override + public void writeByte(byte b) throws IOException { + throw new IOException("Simulated IO exception"); + } + + @Override + public void writeBytes(byte[] b, int offset, int length) throws IOException { + throw new IOException("Simulated IO exception"); + } + + @Override + public void flush() throws IOException { + + } + + @Override + public void close() throws IOException { + + } + + @Override + public void reset() throws IOException { + + } + }) { + + assertThrows(IOException.class, () -> createdBy.writeTo(failingOutput)); + } + } + + public void testWriteToWithLongUserName() throws IOException { + String longUserName = "a".repeat(65536); + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, longUserName); + BytesStreamOutput out = new BytesStreamOutput(); + createdBy.writeTo(out); + MatcherAssert.assertThat(out.size(), greaterThan(65536)); + } + + public void test_createdByToStringReturnsCorrectFormat() { + String testUser = "testUser"; + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, testUser); + + String expected = "CreatedBy {user='" + testUser + "'}"; + String actual = createdBy.toString(); + + MatcherAssert.assertThat(expected, equalTo(actual)); + } + + public void test_toXContent_serializesCorrectly() throws IOException { + String expectedUser = "testUser"; + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, expectedUser); + XContentBuilder builder = XContentFactory.jsonBuilder(); + + createdBy.toXContent(builder, null); + + String expectedJson = "{\"user\":\"testUser\"}"; + MatcherAssert.assertThat(expectedJson, equalTo(builder.toString())); + } + + public void test_writeTo_writesUserCorrectly() throws IOException { + String expectedUser = "testUser"; + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, expectedUser); + + BytesStreamOutput out = new BytesStreamOutput(); + createdBy.writeTo(out); + + StreamInput in = out.bytes().streamInput(); + in.readString(); + String actualUser = in.readString(); + + MatcherAssert.assertThat(expectedUser, equalTo(actualUser)); + } + +} diff --git a/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java b/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java new file mode 100644 index 0000000000..394bae608e --- /dev/null +++ b/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java @@ -0,0 +1,33 @@ +/* + * 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. + */ + +package org.opensearch.security.resources; + +import org.hamcrest.MatcherAssert; + +import org.opensearch.test.OpenSearchTestCase; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class RecipientTypeRegistryTests extends OpenSearchTestCase { + + public void testFromValue() { + RecipientTypeRegistry.registerRecipientType("ble1", new RecipientType("ble1")); + RecipientTypeRegistry.registerRecipientType("ble2", new RecipientType("ble2")); + + // Valid Value + RecipientType type = RecipientTypeRegistry.fromValue("ble1"); + assertNotNull(type); + assertEquals("ble1", type.getType()); + + // Invalid Value + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> RecipientTypeRegistry.fromValue("bleble")); + MatcherAssert.assertThat("Unknown RecipientType: bleble. Must be 1 of these: [ble1, ble2]", is(equalTo(exception.getMessage()))); + } +} diff --git a/src/test/java/org/opensearch/security/resources/ShareWithTests.java b/src/test/java/org/opensearch/security/resources/ShareWithTests.java new file mode 100644 index 0000000000..7c7b634e86 --- /dev/null +++ b/src/test/java/org/opensearch/security/resources/ShareWithTests.java @@ -0,0 +1,263 @@ +/* + * 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. + */ + +package org.opensearch.security.resources; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.hamcrest.MatcherAssert; +import org.junit.Before; + +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.security.spi.resources.ResourceAccessScope; +import org.opensearch.test.OpenSearchTestCase; + +import org.mockito.Mockito; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ShareWithTests extends OpenSearchTestCase { + + @Before + public void setupResourceRecipientTypes() { + initializeRecipientTypes(); + } + + public void testFromXContentWhenCurrentTokenIsNotStartObject() throws IOException { + String json = "{\"read_only\": {\"users\": [\"user1\"], \"roles\": [], \"backend_roles\": []}}"; + XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, json); + + parser.nextToken(); + + ShareWith shareWith = ShareWith.fromXContent(parser); + + assertNotNull(shareWith); + Set sharedWithScopes = shareWith.getSharedWithScopes(); + assertNotNull(sharedWithScopes); + MatcherAssert.assertThat(1, equalTo(sharedWithScopes.size())); + + SharedWithScope scope = sharedWithScopes.iterator().next(); + MatcherAssert.assertThat("read_only", equalTo(scope.getScope())); + + SharedWithScope.ScopeRecipients scopeRecipients = scope.getSharedWithPerScope(); + assertNotNull(scopeRecipients); + Map> recipients = scopeRecipients.getRecipients(); + MatcherAssert.assertThat(recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.USERS.getName())).size(), is(1)); + MatcherAssert.assertThat(recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.USERS.getName())), contains("user1")); + MatcherAssert.assertThat(recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.ROLES.getName())).size(), is(0)); + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.BACKEND_ROLES.getName())).size(), + is(0) + ); + } + + public void testFromXContentWithEmptyInput() throws IOException { + String emptyJson = "{}"; + XContentParser parser = XContentType.JSON.xContent().createParser(xContentRegistry(), null, emptyJson); + + ShareWith result = ShareWith.fromXContent(parser); + + assertNotNull(result); + MatcherAssert.assertThat(result.getSharedWithScopes(), is(empty())); + } + + public void testFromXContentWithStartObject() throws IOException { + XContentParser parser; + try (XContentBuilder builder = XContentFactory.jsonBuilder()) { + builder.startObject() + .startObject(ResourceAccessScope.READ_ONLY) + .array("users", "user1", "user2") + .array("roles", "role1") + .array("backend_roles", "backend_role1") + .endObject() + .startObject(ResourceAccessScope.READ_WRITE) + .array("users", "user3") + .array("roles", "role2", "role3") + .array("backend_roles") + .endObject() + .endObject(); + + parser = JsonXContent.jsonXContent.createParser(null, null, builder.toString()); + } + + parser.nextToken(); + + ShareWith shareWith = ShareWith.fromXContent(parser); + + assertNotNull(shareWith); + Set scopes = shareWith.getSharedWithScopes(); + MatcherAssert.assertThat(scopes.size(), equalTo(2)); + + for (SharedWithScope scope : scopes) { + SharedWithScope.ScopeRecipients perScope = scope.getSharedWithPerScope(); + Map> recipients = perScope.getRecipients(); + if (scope.getScope().equals(ResourceAccessScope.READ_ONLY)) { + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.USERS.getName())).size(), + is(2) + ); + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.ROLES.getName())).size(), + is(1) + ); + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.BACKEND_ROLES.getName())).size(), + is(1) + ); + } else if (scope.getScope().equals(ResourceAccessScope.READ_WRITE)) { + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.USERS.getName())).size(), + is(1) + ); + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.ROLES.getName())).size(), + is(2) + ); + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.BACKEND_ROLES.getName())).size(), + is(0) + ); + } + } + } + + public void testFromXContentWithUnexpectedEndOfInput() throws IOException { + XContentParser mockParser = mock(XContentParser.class); + when(mockParser.currentToken()).thenReturn(XContentParser.Token.START_OBJECT); + when(mockParser.nextToken()).thenReturn(XContentParser.Token.END_OBJECT, (XContentParser.Token) null); + + ShareWith result = ShareWith.fromXContent(mockParser); + + assertNotNull(result); + MatcherAssert.assertThat(result.getSharedWithScopes(), is(empty())); + } + + public void testToXContentBuildsCorrectly() throws IOException { + SharedWithScope scope = new SharedWithScope( + "scope1", + new SharedWithScope.ScopeRecipients(Map.of(new RecipientType("users"), Set.of("bleh"))) + ); + + Set scopes = new HashSet<>(); + scopes.add(scope); + + ShareWith shareWith = new ShareWith(scopes); + + XContentBuilder builder = JsonXContent.contentBuilder(); + + shareWith.toXContent(builder, ToXContent.EMPTY_PARAMS); + + String result = builder.toString(); + + String expected = "{\"scope1\":{\"users\":[\"bleh\"]}}"; + + MatcherAssert.assertThat(expected.length(), equalTo(result.length())); + MatcherAssert.assertThat(expected, equalTo(result)); + } + + public void testWriteToWithEmptySet() throws IOException { + Set emptySet = Collections.emptySet(); + ShareWith shareWith = new ShareWith(emptySet); + StreamOutput mockOutput = Mockito.mock(StreamOutput.class); + + shareWith.writeTo(mockOutput); + + verify(mockOutput).writeCollection(emptySet); + } + + public void testWriteToWithIOException() throws IOException { + Set set = new HashSet<>(); + set.add(new SharedWithScope("test", new SharedWithScope.ScopeRecipients(Map.of()))); + ShareWith shareWith = new ShareWith(set); + StreamOutput mockOutput = Mockito.mock(StreamOutput.class); + + doThrow(new IOException("Simulated IO exception")).when(mockOutput).writeCollection(set); + + assertThrows(IOException.class, () -> shareWith.writeTo(mockOutput)); + } + + public void testWriteToWithLargeSet() throws IOException { + Set largeSet = new HashSet<>(); + for (int i = 0; i < 10000; i++) { + largeSet.add(new SharedWithScope("scope" + i, new SharedWithScope.ScopeRecipients(Map.of()))); + } + ShareWith shareWith = new ShareWith(largeSet); + StreamOutput mockOutput = Mockito.mock(StreamOutput.class); + + shareWith.writeTo(mockOutput); + + verify(mockOutput).writeCollection(largeSet); + } + + public void test_fromXContent_emptyObject() throws IOException { + XContentParser parser; + try (XContentBuilder builder = XContentFactory.jsonBuilder()) { + builder.startObject().endObject(); + parser = XContentType.JSON.xContent().createParser(null, null, builder.toString()); + } + + ShareWith shareWith = ShareWith.fromXContent(parser); + + MatcherAssert.assertThat(shareWith.getSharedWithScopes(), is(empty())); + } + + public void test_writeSharedWithScopesToStream() throws IOException { + StreamOutput mockStreamOutput = Mockito.mock(StreamOutput.class); + + Set sharedWithScopes = new HashSet<>(); + sharedWithScopes.add(new SharedWithScope(ResourceAccessScope.READ_ONLY, new SharedWithScope.ScopeRecipients(Map.of()))); + sharedWithScopes.add(new SharedWithScope(ResourceAccessScope.READ_WRITE, new SharedWithScope.ScopeRecipients(Map.of()))); + + ShareWith shareWith = new ShareWith(sharedWithScopes); + + shareWith.writeTo(mockStreamOutput); + + verify(mockStreamOutput, times(1)).writeCollection(eq(sharedWithScopes)); + } + + private void initializeRecipientTypes() { + RecipientTypeRegistry.registerRecipientType("users", new RecipientType("users")); + RecipientTypeRegistry.registerRecipientType("roles", new RecipientType("roles")); + RecipientTypeRegistry.registerRecipientType("backend_roles", new RecipientType("backend_roles")); + } +} + +enum DefaultRecipientType { + USERS("users"), + ROLES("roles"), + BACKEND_ROLES("backend_roles"); + + private final String name; + + DefaultRecipientType(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java b/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java index 0f7d5c59c5..d12fafb247 100644 --- a/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java +++ b/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java @@ -20,7 +20,6 @@ import org.junit.Test; import org.opensearch.Version; -import org.opensearch.accesscontrol.resources.ResourceService; import org.opensearch.action.search.PitService; import org.opensearch.cluster.ClusterName; import org.opensearch.cluster.node.DiscoveryNode; @@ -172,8 +171,7 @@ public void setup() { transportService, mock(IndicesService.class), mock(PitService.class), - mock(ExtensionsManager.class), - mock(ResourceService.class) + mock(ExtensionsManager.class) ); // CS-ENFORCE-SINGLE From 9b508de92f3595a6b88b1f484fb8227d90200ba4 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 8 Jan 2025 18:21:18 -0500 Subject: [PATCH 067/201] Adds a bunch of REST APIs and modifies DLS query to support resource permission filter Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 59 ++++++-- .../SecurityFlsDlsIndexSearcherWrapper.java | 40 ++++- .../resources/ResourceAccessHandler.java | 78 ++++++---- .../ResourceSharingIndexHandler.java | 141 +++++++++--------- .../list/ListAccessibleResourcesRequest.java | 2 +- .../list/ListAccessibleResourcesResponse.java | 24 ++- .../verify/VerifyResourceAccessAction.java | 2 +- ...ansportListAccessibleResourcesAction.java} | 10 +- ... TransportRevokeResourceAccessAction.java} | 6 +- ...java => TransportShareResourceAction.java} | 6 +- ... TransportVerifyResourceAccessAction.java} | 6 +- 11 files changed, 246 insertions(+), 128 deletions(-) rename src/main/java/org/opensearch/security/transport/resources/access/{ListAccessibleResourcesTransportAction.java => TransportListAccessibleResourcesAction.java} (83%) rename src/main/java/org/opensearch/security/transport/resources/access/{RevokeResourceAccessTransportAction.java => TransportRevokeResourceAccessAction.java} (92%) rename src/main/java/org/opensearch/security/transport/resources/access/{ShareResourceTransportAction.java => TransportShareResourceAction.java} (92%) rename src/main/java/org/opensearch/security/transport/resources/access/{VerifyResourceAccessTransportAction.java => TransportVerifyResourceAccessAction.java} (92%) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 14cd439566..a5f2bdfea4 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -58,6 +58,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -185,6 +187,14 @@ import org.opensearch.security.rest.SecurityInfoAction; import org.opensearch.security.rest.SecurityWhoAmIAction; import org.opensearch.security.rest.TenantInfoAction; +import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesAction; +import org.opensearch.security.rest.resources.access.list.RestListAccessibleResourcesAction; +import org.opensearch.security.rest.resources.access.revoke.RestRevokeResourceAccessAction; +import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessAction; +import org.opensearch.security.rest.resources.access.share.RestShareResourceAction; +import org.opensearch.security.rest.resources.access.share.ShareResourceAction; +import org.opensearch.security.rest.resources.access.verify.RestVerifyResourceAccessAction; +import org.opensearch.security.rest.resources.access.verify.VerifyResourceAccessAction; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.setting.OpensearchDynamicSetting; @@ -212,6 +222,10 @@ import org.opensearch.security.transport.DefaultInterClusterRequestEvaluator; import org.opensearch.security.transport.InterClusterRequestEvaluator; import org.opensearch.security.transport.SecurityInterceptor; +import org.opensearch.security.transport.resources.access.TransportListAccessibleResourcesAction; +import org.opensearch.security.transport.resources.access.TransportRevokeResourceAccessAction; +import org.opensearch.security.transport.resources.access.TransportShareResourceAction; +import org.opensearch.security.transport.resources.access.TransportVerifyResourceAccessAction; import org.opensearch.security.user.User; import org.opensearch.security.user.UserService; import org.opensearch.tasks.Task; @@ -288,7 +302,8 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private ResourceSharingIndexManagementRepository rmr; private ResourceAccessHandler resourceAccessHandler; private final Set indicesToListen = new HashSet<>(); - private static final Map resourceProviders = new HashMap<>(); + private static final Map RESOURCE_PROVIDERS = new HashMap<>(); + private static final Set RESOURCE_INDICES = new HashSet<>(); public static boolean isActionTraceEnabled() { @@ -679,6 +694,16 @@ public List getRestHandlers( passwordHasher ) ); + + // Adds rest handlers for resource-access-control actions + handlers.addAll( + List.of( + new RestShareResourceAction(), + new RestRevokeResourceAccessAction(), + new RestListAccessibleResourcesAction(), + new RestVerifyResourceAccessAction() + ) + ); log.debug("Added {} rest handler(s)", handlers.size()); } } @@ -706,6 +731,16 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre actions.add(new ActionHandler<>(CertificatesActionType.INSTANCE, TransportCertificatesInfoNodesAction.class)); } actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class)); + + // Resource-access-control related actions + actions.addAll( + List.of( + new ActionHandler<>(ShareResourceAction.INSTANCE, TransportShareResourceAction.class), + new ActionHandler<>(RevokeResourceAccessAction.INSTANCE, TransportRevokeResourceAccessAction.class), + new ActionHandler<>(ListAccessibleResourcesAction.INSTANCE, TransportListAccessibleResourcesAction.class), + new ActionHandler<>(VerifyResourceAccessAction.INSTANCE, TransportVerifyResourceAccessAction.class) + ) + ); } return actions; } @@ -730,15 +765,17 @@ public void onIndexModule(IndexModule indexModule) { ciol, evaluator, dlsFlsValve::getCurrentConfig, - dlsFlsBaseContext + dlsFlsBaseContext, + resourceAccessHandler ) ); - if (this.indicesToListen.contains(indexModule.getIndex().getName())) { - ResourceSharingIndexListener resourceSharingIndexListener = ResourceSharingIndexListener.getInstance(); - resourceSharingIndexListener.initialize(threadPool, localClient, auditLog); + // Listening on POST and DELETE operations in resource indices + ResourceSharingIndexListener resourceSharingIndexListener = ResourceSharingIndexListener.getInstance(); + resourceSharingIndexListener.initialize(threadPool, localClient, auditLog); + if (RESOURCE_INDICES.contains(indexModule.getIndex().getName())) { indexModule.addIndexOperationListener(resourceSharingIndexListener); - log.warn("Security plugin started listening to operations on index {}", indexModule.getIndex().getName()); + log.warn("Security plugin started listening to operations on resource-index {}", indexModule.getIndex().getName()); } indexModule.forceQueryCacheProvider((indexSettings, nodeCache) -> new QueryCache() { @@ -2233,7 +2270,11 @@ private void tryAddSecurityProvider() { } public static Map getResourceProviders() { - return resourceProviders; + return ImmutableMap.copyOf(RESOURCE_PROVIDERS); + } + + public static Set getResourceIndices() { + return ImmutableSet.copyOf(RESOURCE_INDICES); } @Override @@ -2250,10 +2291,10 @@ public void loadExtensions(ExtensiblePlugin.ExtensionLoader loader) { String resourceIndexName = extension.getResourceIndex(); ResourceParser resourceParser = extension.getResourceParser(); - this.indicesToListen.add(resourceIndexName); + RESOURCE_INDICES.add(resourceIndexName); ResourceProvider resourceProvider = new ResourceProvider(resourceType, resourceIndexName, resourceParser); - resourceProviders.put(resourceIndexName, resourceProvider); + RESOURCE_PROVIDERS.put(resourceIndexName, resourceProvider); log.info("Loaded resource provider extension: {}, index: {}", resourceType, resourceIndexName); } } diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java index 4f7a412097..bb06604829 100644 --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java @@ -34,6 +34,7 @@ import org.opensearch.index.mapper.SeqNoFieldMapper; import org.opensearch.index.query.QueryShardContext; import org.opensearch.index.shard.ShardUtils; +import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.compliance.ComplianceIndexingOperationListener; import org.opensearch.security.privileges.DocumentAllowList; @@ -45,8 +46,11 @@ import org.opensearch.security.privileges.dlsfls.DlsRestriction; import org.opensearch.security.privileges.dlsfls.FieldMasking; import org.opensearch.security.privileges.dlsfls.FieldPrivileges; +import org.opensearch.security.resources.ResourceAccessHandler; import org.opensearch.security.support.ConfigConstants; +import joptsimple.internal.Strings; + public class SecurityFlsDlsIndexSearcherWrapper extends SystemIndexSearcherWrapper { public final Logger log = LogManager.getLogger(this.getClass()); @@ -61,6 +65,7 @@ public class SecurityFlsDlsIndexSearcherWrapper extends SystemIndexSearcherWrapp private final LongSupplier nowInMillis; private final Supplier dlsFlsProcessedConfigSupplier; private final DlsFlsBaseContext dlsFlsBaseContext; + private final ResourceAccessHandler resourceAccessHandler; public SecurityFlsDlsIndexSearcherWrapper( final IndexService indexService, @@ -71,7 +76,8 @@ public SecurityFlsDlsIndexSearcherWrapper( final ComplianceIndexingOperationListener ciol, final PrivilegesEvaluator evaluator, final Supplier dlsFlsProcessedConfigSupplier, - final DlsFlsBaseContext dlsFlsBaseContext + final DlsFlsBaseContext dlsFlsBaseContext, + final ResourceAccessHandler resourceAccessHandler ) { super(indexService, settings, adminDNs, evaluator); Set metadataFieldsCopy; @@ -103,6 +109,7 @@ public SecurityFlsDlsIndexSearcherWrapper( log.debug("FLS/DLS {} enabled for index {}", this, indexService.index().getName()); this.dlsFlsProcessedConfigSupplier = dlsFlsProcessedConfigSupplier; this.dlsFlsBaseContext = dlsFlsBaseContext; + this.resourceAccessHandler = resourceAccessHandler; } @SuppressWarnings("unchecked") @@ -116,7 +123,36 @@ protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdm log.trace("dlsFlsWrap(); index: {}; privilegeEvaluationContext: {}", index.getName(), privilegesEvaluationContext); } - if (isAdmin || privilegesEvaluationContext == null) { + String indexName = shardId != null ? shardId.getIndexName() : null; + Set resourceIds = null; + if (!Strings.isNullOrEmpty(indexName) && OpenSearchSecurityPlugin.getResourceIndices().contains(indexName)) { + resourceIds = this.resourceAccessHandler.getAccessibleResourceIdsForCurrentUser(indexName); + if (resourceIds.isEmpty()) { + return new EmptyFilterLeafReader.EmptyDirectoryReader(reader); + } + // Create a resource DLS query for the current user + QueryShardContext queryShardContext = this.indexService.newQueryShardContext(shardId.getId(), null, nowInMillis, null); + Query resourceQuery = this.resourceAccessHandler.createResourceDlsQuery(resourceIds, queryShardContext); + + // TODO the FlsRule must still be checked + return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader( + reader, + FieldPrivileges.FlsRule.ALLOW_ALL, + resourceQuery, + indexService, + threadContext, + clusterService, + auditlog, + FieldMasking.FieldMaskingRule.ALLOW_ALL, + shardId, + metaFields + ); + } + + // resourceIds == null indicates that the index is not a resource index + // resourceIds.isEmpty() indicates that the index is a resource index but the user does not have access to any resource under the + // index + if (isAdmin || privilegesEvaluationContext == null || resourceIds == null) { return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader( reader, FieldPrivileges.FlsRule.ALLOW_ALL, diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 149e058752..361342e611 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -11,6 +11,7 @@ package org.opensearch.security.resources; +import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.Map; @@ -18,8 +19,13 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.lucene.search.Query; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.ConstantScoreQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.index.query.QueryShardContext; import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.spi.resources.Resource; @@ -65,16 +71,11 @@ public void initializeRecipientTypes() { } /** - * Returns a set of accessible resources for the current user within the specified resource index. - * + * Returns a set of accessible resource IDs for the current user within the specified resource index. * @param resourceIndex The resource index to check for accessible resources. * @return A set of accessible resource IDs. */ - @SuppressWarnings("unchecked") - public Set getAccessibleResourcesForCurrentUser(String resourceIndex) { - validateArguments(resourceIndex); - ResourceParser parser = OpenSearchSecurityPlugin.getResourceProviders().get(resourceIndex).getResourceParser(); - + public Set getAccessibleResourceIdsForCurrentUser(String resourceIndex) { final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); if (user == null) { LOGGER.info("Unable to fetch user details "); @@ -83,28 +84,45 @@ public Set getAccessibleResourcesForCurrentUser(String r LOGGER.info("Listing accessible resources within a resource index {} for : {}", resourceIndex, user.getName()); + Set resourceIds = new HashSet<>(); + // check if user is admin, if yes all resources should be accessible if (adminDNs.isAdmin(user)) { - return loadAllResources(resourceIndex, parser); + resourceIds.addAll(loadAllResources(resourceIndex)); + return resourceIds; } - Set result = new HashSet<>(); - // 0. Own resources - result.addAll(loadOwnResources(resourceIndex, user.getName(), parser)); + resourceIds.addAll(loadOwnResources(resourceIndex, user.getName())); // 1. By username - result.addAll(loadSharedWithResources(resourceIndex, Set.of(user.getName()), Recipient.USERS.toString(), parser)); + resourceIds.addAll(loadSharedWithResources(resourceIndex, Set.of(user.getName()), Recipient.USERS.toString())); // 2. By roles Set roles = user.getSecurityRoles(); - result.addAll(loadSharedWithResources(resourceIndex, roles, Recipient.ROLES.toString(), parser)); + resourceIds.addAll(loadSharedWithResources(resourceIndex, roles, Recipient.ROLES.toString())); // 3. By backend_roles Set backendRoles = user.getRoles(); - result.addAll(loadSharedWithResources(resourceIndex, backendRoles, Recipient.BACKEND_ROLES.toString(), parser)); + resourceIds.addAll(loadSharedWithResources(resourceIndex, backendRoles, Recipient.BACKEND_ROLES.toString())); + + return resourceIds; + } - return result; + /** + * Returns a set of accessible resources for the current user within the specified resource index. + * + * @param resourceIndex The resource index to check for accessible resources. + * @return A set of accessible resource IDs. + */ + @SuppressWarnings("unchecked") + public Set getAccessibleResourcesForCurrentUser(String resourceIndex) { + validateArguments(resourceIndex); + ResourceParser parser = OpenSearchSecurityPlugin.getResourceProviders().get(resourceIndex).getResourceParser(); + Set resourceIds = getAccessibleResourceIdsForCurrentUser(resourceIndex); + return resourceIds.isEmpty() + ? Set.of() + : this.resourceSharingIndexHandler.getResourceDocumentsFromIds(resourceIds, resourceIndex, parser); } /** @@ -234,8 +252,8 @@ public boolean deleteAllResourceSharingRecordsForCurrentUser() { * @param resourceIndex The resource index to load resources from. * @return A set of resource IDs. */ - private Set loadAllResources(String resourceIndex, ResourceParser parser) { - return this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex, parser); + private Set loadAllResources(String resourceIndex) { + return this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex); } /** @@ -245,8 +263,8 @@ private Set loadAllResources(String resourceIndex, Resou * @param userName The username of the owner. * @return A set of resource IDs owned by the user. */ - private Set loadOwnResources(String resourceIndex, String userName, ResourceParser parser) { - return this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName, parser); + private Set loadOwnResources(String resourceIndex, String userName) { + return this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName); } /** @@ -257,13 +275,8 @@ private Set loadOwnResources(String resourceIndex, Strin * @param RecipientType The type of entity (e.g., users, roles, backend_roles). * @return A set of resource IDs shared with the specified entities. */ - private Set loadSharedWithResources( - String resourceIndex, - Set entities, - String RecipientType, - ResourceParser parser - ) { - return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entities, RecipientType, parser); + private Set loadSharedWithResources(String resourceIndex, Set entities, String RecipientType) { + return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entities, RecipientType); } /** @@ -353,4 +366,17 @@ private void validateArguments(Object... args) { } } + /** + * Creates a DLS query for the given resource IDs. + * @param resourceIds The resource IDs to create the query for. + * @param queryShardContext The query shard context. + * @return The DLS query. + * @throws IOException If an I/O error occurs. + */ + public Query createResourceDlsQuery(Set resourceIds, QueryShardContext queryShardContext) throws IOException { + BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); + boolQueryBuilder.filter(QueryBuilders.termsQuery("_id", resourceIds)); + ConstantScoreQueryBuilder builder = new ConstantScoreQueryBuilder(boolQueryBuilder); + return builder.toQuery(queryShardContext); + } } diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index c44fe452d2..7d4a55b8ca 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -25,6 +25,7 @@ import org.apache.logging.log4j.Logger; import org.opensearch.OpenSearchException; +import org.opensearch.action.DocWriteRequest; import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.action.admin.indices.create.CreateIndexResponse; import org.opensearch.action.get.MultiGetItemResponse; @@ -42,7 +43,6 @@ import org.opensearch.common.unit.TimeValue; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.common.xcontent.LoggingDeprecationHandler; -import org.opensearch.common.xcontent.XContentFactory; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.action.ActionListener; import org.opensearch.core.xcontent.NamedXContentRegistry; @@ -66,6 +66,7 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.ResourceAccessScope; import org.opensearch.security.spi.resources.ResourceParser; import org.opensearch.threadpool.ThreadPool; @@ -166,6 +167,7 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn IndexRequest ir = client.prepareIndex(resourceSharingIndex) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) .setSource(entry.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) + .setOpType(DocWriteRequest.OpType.CREATE) // only create if an entry doesn't exist .request(); ActionListener irListener = ActionListener.wrap(idxResponse -> { @@ -183,47 +185,47 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn } /** - * Fetches all resource sharing records that match the specified system index. This method retrieves - * a list of resource IDs associated with the given system index from the resource sharing index. - * - *

        The method executes the following steps: - *

          - *
        1. Creates a search request with term query matching the system index
        2. - *
        3. Applies source filtering to only fetch resource_id field
        4. - *
        5. Executes the search with a limit of 10000 documents
        6. - *
        7. Processes the results to extract resource IDs
        8. - *
        - * - *

        Example query structure: - *

        -        * {
        -        *   "query": {
        -        *     "term": {
        -        *       "source_idx": "system_index_name"
        -        *     }
        -        *   },
        -        *   "_source": ["resource_id"],
        -        *   "size": 10000
        -        * }
        -        * 
        - * - * @param pluginIndex The source index to match against the source_idx field - * @return Set containing resource IDs that belong to the specified system index. - * Returns an empty list if: - *
          - *
        • No matching documents are found
        • - *
        • An error occurs during the search operation
        • - *
        • The system index parameter is invalid
        • - *
        - * - * @apiNote This method: - *
          - *
        • Uses source filtering for optimal performance
        • - *
        • Performs exact matching on the source_idx field
        • - *
        • Returns an empty list instead of throwing exceptions
        • - *
        - */ - public Set fetchAllDocuments(String pluginIndex, ResourceParser parser) { + * Fetches all resource sharing records that match the specified system index. This method retrieves + * a list of resource IDs associated with the given system index from the resource sharing index. + * + *

        The method executes the following steps: + *

          + *
        1. Creates a search request with term query matching the system index
        2. + *
        3. Applies source filtering to only fetch resource_id field
        4. + *
        5. Executes the search with a limit of 10000 documents
        6. + *
        7. Processes the results to extract resource IDs
        8. + *
        + * + *

        Example query structure: + *

        +    * {
        +    *   "query": {
        +    *     "term": {
        +    *       "source_idx": "resource_index_name"
        +    *     }
        +    *   },
        +    *   "_source": ["resource_id"],
        +    *   "size": 10000
        +    * }
        +    * 
        + * + * @param pluginIndex The source index to match against the source_idx field + * @return Set containing resource IDs that belong to the specified system index. + * Returns an empty list if: + *
          + *
        • No matching documents are found
        • + *
        • An error occurs during the search operation
        • + *
        • The system index parameter is invalid
        • + *
        + * + * @apiNote This method: + *
          + *
        • Uses source filtering for optimal performance
        • + *
        • Performs exact matching on the source_idx field
        • + *
        • Returns an empty list instead of throwing exceptions
        • + *
        + */ + public Set fetchAllDocuments(String pluginIndex) { LOGGER.debug("Fetching all documents from {} where source_idx = {}", resourceSharingIndex, pluginIndex); // TODO: Once stashContext is replaced with switchContext this call will have to be modified @@ -252,7 +254,7 @@ public Set fetchAllDocuments(String pluginIndex, Resourc LOGGER.debug("Found {} documents in {} for source_idx: {}", resourceIds.size(), resourceSharingIndex, pluginIndex); - return resourceIds.isEmpty() ? Set.of() : getResourcesFromIds(resourceIds, pluginIndex, parser); + return resourceIds; } catch (Exception e) { LOGGER.error("Failed to fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); @@ -279,7 +281,7 @@ public Set fetchAllDocuments(String pluginIndex, Resourc * "query": { * "bool": { * "must": [ - * { "term": { "source_idx": "system_index_name" } }, + * { "term": { "source_idx": "resource_index_name" } }, * { * "bool": { * "should": [ @@ -311,7 +313,6 @@ public Set fetchAllDocuments(String pluginIndex, Resourc *
      17. "roles" - for role-based access
      18. *
      19. "backend_roles" - for backend role-based access
      20. * - * @param clazz Class to deserialize each document from Response into * @return Set List of resource IDs that match the criteria. The list may be empty * if no matches are found * @@ -327,14 +328,9 @@ public Set fetchAllDocuments(String pluginIndex, Resourc * */ - public Set fetchDocumentsForAllScopes( - String pluginIndex, - Set entities, - String RecipientType, - ResourceParser parser - ) { + public Set fetchDocumentsForAllScopes(String pluginIndex, Set entities, String RecipientType) { // "*" must match all scopes - return fetchDocumentsForAGivenScope(pluginIndex, entities, RecipientType, "*", parser); + return fetchDocumentsForAGivenScope(pluginIndex, entities, RecipientType, "*"); } /** @@ -356,7 +352,7 @@ public Set fetchDocumentsForAllScopes( * "query": { * "bool": { * "must": [ - * { "term": { "source_idx": "system_index_name" } }, + * { "term": { "source_idx": "resource_index_name" } }, * { * "bool": { * "should": [ @@ -388,8 +384,7 @@ public Set fetchDocumentsForAllScopes( *
      21. "roles" - for role-based access
      22. *
      23. "backend_roles" - for backend role-based access
      24. * - * @param scope The scope of the access. Should be implementation of {@link org.opensearch.security.spi.resources.ResourceAccessScope} - * @param clazz Class to deserialize each document from Response into + * @param scope The scope of the access. Should be implementation of {@link ResourceAccessScope} * @return Set List of resource IDs that match the criteria. The list may be empty * if no matches are found * @@ -404,13 +399,7 @@ public Set fetchDocumentsForAllScopes( *
      25. Properly cleans up scroll context after use
      26. * */ - public Set fetchDocumentsForAGivenScope( - String pluginIndex, - Set entities, - String RecipientType, - String scope, - ResourceParser parser - ) { + public Set fetchDocumentsForAGivenScope(String pluginIndex, Set entities, String RecipientType, String scope) { LOGGER.debug( "Fetching documents from index: {}, where share_with.{}.{} contains any of {}", pluginIndex, @@ -419,6 +408,9 @@ public Set fetchDocumentsForAGivenScope( entities ); + // To allow "public" resources to be matched for any user, role, backend_role + entities.add("*"); + Set resourceIds = new HashSet<>(); final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); @@ -450,7 +442,7 @@ public Set fetchDocumentsForAGivenScope( LOGGER.debug("Found {} documents matching the criteria in {}", resourceIds.size(), resourceSharingIndex); - return resourceIds.isEmpty() ? Set.of() : getResourcesFromIds(resourceIds, pluginIndex, parser); + return resourceIds; } catch (Exception e) { LOGGER.error( @@ -499,7 +491,6 @@ public Set fetchDocumentsForAGivenScope( * @param pluginIndex The source index to match against the source_idx field * @param field The field name to search in. Must be a valid field in the index mapping * @param value The value to match for the specified field. Performs exact term matching - * @param clazz Class to deserialize each document from Response into * @return Set List of resource IDs that match the criteria. Returns an empty list * if no matches are found * @@ -520,7 +511,7 @@ public Set fetchDocumentsForAGivenScope( * Set resources = fetchDocumentsByField("myIndex", "status", "active"); * */ - public Set fetchDocumentsByField(String pluginIndex, String field, String value, ResourceParser parser) { + public Set fetchDocumentsByField(String pluginIndex, String field, String value) { if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(field) || StringUtils.isBlank(value)) { throw new IllegalArgumentException("pluginIndex, field, and value must not be null or empty"); } @@ -543,7 +534,7 @@ public Set fetchDocumentsByField(String pluginIndex, Str LOGGER.info("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); - return resourceIds.isEmpty() ? Set.of() : getResourcesFromIds(resourceIds, pluginIndex, parser); + return resourceIds; } catch (Exception e) { LOGGER.error("Failed to fetch documents from {} where {} = {}", resourceSharingIndex, field, value, e); throw new RuntimeException("Failed to fetch documents: " + e.getMessage(), e); @@ -569,7 +560,7 @@ public Set fetchDocumentsByField(String pluginIndex, Str * "query": { * "bool": { * "must": [ - * { "term": { "source_idx": "system_index_name" } }, + * { "term": { "source_idx": "resource_index_name" } }, * { "term": { "resource_id": "resource_id_value" } } * ] * } @@ -723,7 +714,7 @@ public ResourceSharing updateResourceSharingInfo( XContentBuilder builder; Map shareWithMap; try { - builder = XContentFactory.jsonBuilder(); + builder = jsonBuilder(); shareWith.toXContent(builder, ToXContent.EMPTY_PARAMS); String json = builder.toString(); shareWithMap = DefaultObjectMapper.readValue(json, new TypeReference<>() { @@ -900,7 +891,7 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId *

        Example document structure: *

              * {
        -     *   "source_idx": "system_index_name",
        +     *   "source_idx": "resource_index_name",
              *   "resource_id": "resource_id",
              *   "share_with": {
              *     "scope": {
        @@ -1170,11 +1161,19 @@ public boolean deleteAllRecordsForUser(String name) {
              * Fetches all documents from the specified resource index and deserializes them into the specified class.
              *
              * @param resourceIndex The resource index to fetch documents from.
        -     * @param clazz The class to deserialize the documents into.
        +     * @param parser The class to deserialize the documents into a specified type defined by the parser.
              * @return A set of deserialized documents.
              */
        -    private  Set getResourcesFromIds(Set resourceIds, String resourceIndex, ResourceParser parser) {
        +    public  Set getResourceDocumentsFromIds(
        +        Set resourceIds,
        +        String resourceIndex,
        +        ResourceParser parser
        +    ) {
                 Set result = new HashSet<>();
        +        if (resourceIds.isEmpty()) {
        +            return result;
        +        }
        +
                 // stashing Context to avoid permission issues in-case resourceIndex is a system index
                 // TODO: Once stashContext is replaced with switchContext this call will have to be modified
                 try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) {
        diff --git a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesRequest.java b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesRequest.java
        index f16887f12b..414e25e305 100644
        --- a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesRequest.java
        +++ b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesRequest.java
        @@ -20,7 +20,7 @@
          */
         public class ListAccessibleResourcesRequest extends ActionRequest {
         
        -    private String resourceIndex;
        +    private final String resourceIndex;
         
             public ListAccessibleResourcesRequest(String resourceIndex) {
                 this.resourceIndex = resourceIndex;
        diff --git a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java
        index 1a678ac2ce..e242b9b353 100644
        --- a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java
        +++ b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java
        @@ -9,7 +9,7 @@
         package org.opensearch.security.rest.resources.access.list;
         
         import java.io.IOException;
        -import java.util.HashSet;
        +import java.lang.reflect.InvocationTargetException;
         import java.util.Set;
         
         import org.opensearch.core.action.ActionResponse;
        @@ -24,19 +24,33 @@
          */
         public class ListAccessibleResourcesResponse extends ActionResponse implements ToXContentObject {
             private final Set resources;
        +    private final String resourceClass;
         
        -    public ListAccessibleResourcesResponse(Set resources) {
        +    public ListAccessibleResourcesResponse(String resourceClass, Set resources) {
        +        this.resourceClass = resourceClass;
                 this.resources = resources;
             }
         
             @Override
             public void writeTo(StreamOutput out) throws IOException {
        +        out.writeString(resourceClass);
                 out.writeCollection(resources);
             }
         
        -    public ListAccessibleResourcesResponse(StreamInput in) {
        -        // TODO need to fix this to return correct value
        -        this.resources = new HashSet<>();
        +    public ListAccessibleResourcesResponse(StreamInput in) throws IOException, ClassNotFoundException {
        +        this.resourceClass = in.readString();
        +
        +        // TODO check if there is a better way to handle this
        +        Class clazz = Class.forName(this.resourceClass);
        +        @SuppressWarnings("unchecked")
        +        Class resourceClass = (Class) clazz;
        +        this.resources = in.readSet(i -> {
        +            try {
        +                return resourceClass.getDeclaredConstructor(StreamInput.class).newInstance(i);
        +            } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
        +                throw new RuntimeException(e);
        +            }
        +        });
             }
         
             @Override
        diff --git a/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessAction.java b/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessAction.java
        index ff07b1e455..1f1f189ee1 100644
        --- a/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessAction.java
        +++ b/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessAction.java
        @@ -17,7 +17,7 @@ public class VerifyResourceAccessAction extends ActionType {
        -    private static final Logger log = LogManager.getLogger(ListAccessibleResourcesTransportAction.class);
        +    private static final Logger log = LogManager.getLogger(TransportListAccessibleResourcesAction.class);
             private final ResourceAccessHandler resourceAccessHandler;
         
             @Inject
        -    public ListAccessibleResourcesTransportAction(
        +    public TransportListAccessibleResourcesAction(
                 TransportService transportService,
                 ActionFilters actionFilters,
                 ResourceAccessHandler resourceAccessHandler
        @@ -46,7 +47,8 @@ protected void doExecute(Task task, ListAccessibleResourcesRequest request, Acti
                 try {
                     Set resources = resourceAccessHandler.getAccessibleResourcesForCurrentUser(request.getResourceIndex());
                     log.info("Successfully fetched accessible resources for current user : {}", resources);
        -            listener.onResponse(new ListAccessibleResourcesResponse(resources));
        +            String resourceType = OpenSearchSecurityPlugin.getResourceProviders().get(request.getResourceIndex()).getResourceType();
        +            listener.onResponse(new ListAccessibleResourcesResponse(resourceType, resources));
                 } catch (Exception e) {
                     log.info("Failed to list accessible resources for current user: ", e);
                     listener.onFailure(e);
        diff --git a/src/main/java/org/opensearch/security/transport/resources/access/RevokeResourceAccessTransportAction.java b/src/main/java/org/opensearch/security/transport/resources/access/TransportRevokeResourceAccessAction.java
        similarity index 92%
        rename from src/main/java/org/opensearch/security/transport/resources/access/RevokeResourceAccessTransportAction.java
        rename to src/main/java/org/opensearch/security/transport/resources/access/TransportRevokeResourceAccessAction.java
        index fd7324dca1..7a04e5d46f 100644
        --- a/src/main/java/org/opensearch/security/transport/resources/access/RevokeResourceAccessTransportAction.java
        +++ b/src/main/java/org/opensearch/security/transport/resources/access/TransportRevokeResourceAccessAction.java
        @@ -24,12 +24,12 @@
         import org.opensearch.tasks.Task;
         import org.opensearch.transport.TransportService;
         
        -public class RevokeResourceAccessTransportAction extends HandledTransportAction {
        -    private static final Logger log = LogManager.getLogger(RevokeResourceAccessTransportAction.class);
        +public class TransportRevokeResourceAccessAction extends HandledTransportAction {
        +    private static final Logger log = LogManager.getLogger(TransportRevokeResourceAccessAction.class);
             private final ResourceAccessHandler resourceAccessHandler;
         
             @Inject
        -    public RevokeResourceAccessTransportAction(
        +    public TransportRevokeResourceAccessAction(
                 TransportService transportService,
                 ActionFilters actionFilters,
                 ResourceAccessHandler resourceAccessHandler
        diff --git a/src/main/java/org/opensearch/security/transport/resources/access/ShareResourceTransportAction.java b/src/main/java/org/opensearch/security/transport/resources/access/TransportShareResourceAction.java
        similarity index 92%
        rename from src/main/java/org/opensearch/security/transport/resources/access/ShareResourceTransportAction.java
        rename to src/main/java/org/opensearch/security/transport/resources/access/TransportShareResourceAction.java
        index 1d8111f1b6..4959de2ab2 100644
        --- a/src/main/java/org/opensearch/security/transport/resources/access/ShareResourceTransportAction.java
        +++ b/src/main/java/org/opensearch/security/transport/resources/access/TransportShareResourceAction.java
        @@ -24,12 +24,12 @@
         import org.opensearch.tasks.Task;
         import org.opensearch.transport.TransportService;
         
        -public class ShareResourceTransportAction extends HandledTransportAction {
        -    private static final Logger log = LogManager.getLogger(ShareResourceTransportAction.class);
        +public class TransportShareResourceAction extends HandledTransportAction {
        +    private static final Logger log = LogManager.getLogger(TransportShareResourceAction.class);
             private final ResourceAccessHandler resourceAccessHandler;
         
             @Inject
        -    public ShareResourceTransportAction(
        +    public TransportShareResourceAction(
                 TransportService transportService,
                 ActionFilters actionFilters,
                 ResourceAccessHandler resourceAccessHandler
        diff --git a/src/main/java/org/opensearch/security/transport/resources/access/VerifyResourceAccessTransportAction.java b/src/main/java/org/opensearch/security/transport/resources/access/TransportVerifyResourceAccessAction.java
        similarity index 92%
        rename from src/main/java/org/opensearch/security/transport/resources/access/VerifyResourceAccessTransportAction.java
        rename to src/main/java/org/opensearch/security/transport/resources/access/TransportVerifyResourceAccessAction.java
        index f608453c02..0b732a1cb1 100644
        --- a/src/main/java/org/opensearch/security/transport/resources/access/VerifyResourceAccessTransportAction.java
        +++ b/src/main/java/org/opensearch/security/transport/resources/access/TransportVerifyResourceAccessAction.java
        @@ -23,12 +23,12 @@
         import org.opensearch.tasks.Task;
         import org.opensearch.transport.TransportService;
         
        -public class VerifyResourceAccessTransportAction extends HandledTransportAction {
        -    private static final Logger log = LogManager.getLogger(VerifyResourceAccessTransportAction.class);
        +public class TransportVerifyResourceAccessAction extends HandledTransportAction {
        +    private static final Logger log = LogManager.getLogger(TransportVerifyResourceAccessAction.class);
             private final ResourceAccessHandler resourceAccessHandler;
         
             @Inject
        -    public VerifyResourceAccessTransportAction(
        +    public TransportVerifyResourceAccessAction(
                 TransportService transportService,
                 ActionFilters actionFilters,
                 Client nodeClient,
        
        From 31f3e8245c60f4c509fe6b8aed097100f2e9d462 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Wed, 8 Jan 2025 18:21:41 -0500
        Subject: [PATCH 068/201] Updates Resource to be an abstract class
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../security/spi/resources/Resource.java         | 16 +++++++++-------
         1 file changed, 9 insertions(+), 7 deletions(-)
        
        diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java b/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java
        index 9116ed0a9e..18de796c8e 100644
        --- a/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java
        +++ b/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java
        @@ -17,15 +17,17 @@
         /**
          * Marker interface for all resources
          */
        -public interface Resource extends NamedWriteable, ToXContentFragment {
        +public abstract class Resource implements NamedWriteable, ToXContentFragment {
             /**
        -     * Get the resource name
        +     * Abstract method to get the resource name.
        +     * Must be implemented by subclasses.
        +     *
              * @return resource name
              */
        -    String getResourceName();
        +    public abstract String getResourceName();
         
        -    // For de-serialization
        -    Resource readFrom(StreamInput in) throws IOException;
        -
        -    // TODO: Next iteration, check if getResourceType() should be implemented
        +    /**
        +     * Enforces that all subclasses have a constructor accepting StreamInput.
        +     */
        +    protected Resource(StreamInput in) throws IOException {}
         }
        
        From b97e58cf2d72dcd56b018f25183b53bb2a9369e7 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Wed, 8 Jan 2025 18:37:44 -0500
        Subject: [PATCH 069/201] Adds sample plugin to demonstrate resource sharing
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../org/opensearch/sample/SampleResource.java | 12 ++--
         .../sample/SampleResourcePlugin.java          | 18 +++---
         .../verify/VerifyResourceAccessAction.java    | 25 --------
         .../verify/VerifyResourceAccessRequest.java   | 62 -------------------
         .../verify/VerifyResourceAccessResponse.java  | 52 ----------------
         .../VerifyResourceAccessRestAction.java       | 55 ----------------
         .../rest}/create/CreateResourceAction.java    |  2 +-
         .../rest}/create/CreateResourceRequest.java   |  2 +-
         .../rest}/create/CreateResourceResponse.java  |  2 +-
         .../create/CreateResourceRestAction.java      |  2 +-
         .../rest}/delete/DeleteResourceAction.java    |  2 +-
         .../rest}/delete/DeleteResourceRequest.java   |  2 +-
         .../rest}/delete/DeleteResourceResponse.java  |  2 +-
         .../delete/DeleteResourceRestAction.java      |  2 +-
         .../CreateResourceTransportAction.java        |  8 +--
         .../DeleteResourceTransportAction.java        |  8 +--
         .../VerifyResourceAccessTransportAction.java  | 61 ------------------
         .../opensearch/sample/utils/Validation.java   | 36 -----------
         18 files changed, 28 insertions(+), 325 deletions(-)
         delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessAction.java
         delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRequest.java
         delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessResponse.java
         delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRestAction.java
         rename sample-resource-plugin/src/main/java/org/opensearch/sample/{actions/resource => resource/actions/rest}/create/CreateResourceAction.java (92%)
         rename sample-resource-plugin/src/main/java/org/opensearch/sample/{actions/resource => resource/actions/rest}/create/CreateResourceRequest.java (95%)
         rename sample-resource-plugin/src/main/java/org/opensearch/sample/{actions/resource => resource/actions/rest}/create/CreateResourceResponse.java (95%)
         rename sample-resource-plugin/src/main/java/org/opensearch/sample/{actions/resource => resource/actions/rest}/create/CreateResourceRestAction.java (97%)
         rename sample-resource-plugin/src/main/java/org/opensearch/sample/{actions/resource => resource/actions/rest}/delete/DeleteResourceAction.java (92%)
         rename sample-resource-plugin/src/main/java/org/opensearch/sample/{actions/resource => resource/actions/rest}/delete/DeleteResourceRequest.java (95%)
         rename sample-resource-plugin/src/main/java/org/opensearch/sample/{actions/resource => resource/actions/rest}/delete/DeleteResourceResponse.java (95%)
         rename sample-resource-plugin/src/main/java/org/opensearch/sample/{actions/resource => resource/actions/rest}/delete/DeleteResourceRestAction.java (96%)
         rename sample-resource-plugin/src/main/java/org/opensearch/sample/{transport/resource => resource/actions/transport}/CreateResourceTransportAction.java (91%)
         rename sample-resource-plugin/src/main/java/org/opensearch/sample/{transport/resource => resource/actions/transport}/DeleteResourceTransportAction.java (91%)
         delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/VerifyResourceAccessTransportAction.java
         delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java
        
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java
        index a265f0cdaa..508d8e7597 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java
        @@ -19,15 +19,18 @@
         import org.opensearch.core.xcontent.XContentBuilder;
         import org.opensearch.security.spi.resources.Resource;
         
        -public class SampleResource implements Resource {
        +public class SampleResource extends Resource {
         
             private String name;
             private String description;
             private Map attributes;
         
        -    public SampleResource() {}
        +    public SampleResource() throws IOException {
        +        super(null);
        +    }
         
             public SampleResource(StreamInput in) throws IOException {
        +        super(in);
                 this.name = in.readString();
                 this.description = in.readString();
                 this.attributes = in.readMap(StreamInput::readString, StreamInput::readString);
        @@ -66,9 +69,4 @@ public void setAttributes(Map attributes) {
             public String getResourceName() {
                 return name;
             }
        -
        -    @Override
        -    public Resource readFrom(StreamInput streamInput) throws IOException {
        -        return new SampleResource(streamInput);
        -    }
         }
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java
        index 4c0ab20ffa..6c68ef81ab 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java
        @@ -42,15 +42,12 @@
         import org.opensearch.repositories.RepositoriesService;
         import org.opensearch.rest.RestController;
         import org.opensearch.rest.RestHandler;
        -import org.opensearch.sample.actions.access.verify.VerifyResourceAccessAction;
        -import org.opensearch.sample.actions.access.verify.VerifyResourceAccessRestAction;
        -import org.opensearch.sample.actions.resource.create.CreateResourceAction;
        -import org.opensearch.sample.actions.resource.create.CreateResourceRestAction;
        -import org.opensearch.sample.actions.resource.delete.DeleteResourceAction;
        -import org.opensearch.sample.actions.resource.delete.DeleteResourceRestAction;
        -import org.opensearch.sample.transport.access.VerifyResourceAccessTransportAction;
        -import org.opensearch.sample.transport.resource.CreateResourceTransportAction;
        -import org.opensearch.sample.transport.resource.DeleteResourceTransportAction;
        +import org.opensearch.sample.resource.actions.rest.create.CreateResourceAction;
        +import org.opensearch.sample.resource.actions.rest.create.CreateResourceRestAction;
        +import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceAction;
        +import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceRestAction;
        +import org.opensearch.sample.resource.actions.transport.CreateResourceTransportAction;
        +import org.opensearch.sample.resource.actions.transport.DeleteResourceTransportAction;
         import org.opensearch.script.ScriptService;
         import org.opensearch.security.spi.resources.ResourceParser;
         import org.opensearch.security.spi.resources.ResourceService;
        @@ -96,14 +93,13 @@ public List getRestHandlers(
                 IndexNameExpressionResolver indexNameExpressionResolver,
                 Supplier nodesInCluster
             ) {
        -        return List.of(new CreateResourceRestAction(), new VerifyResourceAccessRestAction(), new DeleteResourceRestAction());
        +        return List.of(new CreateResourceRestAction(), new DeleteResourceRestAction());
             }
         
             @Override
             public List> getActions() {
                 return List.of(
                     new ActionHandler<>(CreateResourceAction.INSTANCE, CreateResourceTransportAction.class),
        -            new ActionHandler<>(VerifyResourceAccessAction.INSTANCE, VerifyResourceAccessTransportAction.class),
                     new ActionHandler<>(DeleteResourceAction.INSTANCE, DeleteResourceTransportAction.class)
                 );
             }
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessAction.java
        deleted file mode 100644
        index 466cc901c6..0000000000
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessAction.java
        +++ /dev/null
        @@ -1,25 +0,0 @@
        -/*
        - * SPDX-License-Identifier: Apache-2.0
        - *
        - * The OpenSearch Contributors require contributions made to
        - * this file be licensed under the Apache-2.0 license or a
        - * compatible open source license.
        - */
        -
        -package org.opensearch.sample.actions.access.verify;
        -
        -import org.opensearch.action.ActionType;
        -
        -/**
        - * Action to verify resource access for current user
        - */
        -public class VerifyResourceAccessAction extends ActionType {
        -
        -    public static final VerifyResourceAccessAction INSTANCE = new VerifyResourceAccessAction();
        -
        -    public static final String NAME = "cluster:admin/sample-resource-plugin/verify/resource_access";
        -
        -    private VerifyResourceAccessAction() {
        -        super(NAME, VerifyResourceAccessResponse::new);
        -    }
        -}
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRequest.java
        deleted file mode 100644
        index b9ab4134c6..0000000000
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRequest.java
        +++ /dev/null
        @@ -1,62 +0,0 @@
        -/*
        - * SPDX-License-Identifier: Apache-2.0
        - *
        - * The OpenSearch Contributors require contributions made to
        - * this file be licensed under the Apache-2.0 license or a
        - * compatible open source license.
        - */
        -
        -package org.opensearch.sample.actions.access.verify;
        -
        -import java.io.IOException;
        -import java.util.Set;
        -
        -import org.opensearch.action.ActionRequest;
        -import org.opensearch.action.ActionRequestValidationException;
        -import org.opensearch.core.common.io.stream.StreamInput;
        -import org.opensearch.core.common.io.stream.StreamOutput;
        -import org.opensearch.sample.utils.Validation;
        -
        -public class VerifyResourceAccessRequest extends ActionRequest {
        -
        -    private final String resourceId;
        -
        -    private final String scope;
        -
        -    /**
        -     * Default constructor
        -     */
        -    public VerifyResourceAccessRequest(String resourceId, String scope) {
        -        this.resourceId = resourceId;
        -        this.scope = scope;
        -    }
        -
        -    /**
        -     * Constructor with stream input
        -     * @param in the stream input
        -     * @throws IOException IOException
        -     */
        -    public VerifyResourceAccessRequest(final StreamInput in) throws IOException {
        -        this.resourceId = in.readString();
        -        this.scope = in.readString();
        -    }
        -
        -    @Override
        -    public void writeTo(final StreamOutput out) throws IOException {
        -        out.writeString(resourceId);
        -        out.writeString(scope);
        -    }
        -
        -    @Override
        -    public ActionRequestValidationException validate() {
        -        return Validation.validateScopes(Set.of(scope));
        -    }
        -
        -    public String getResourceId() {
        -        return resourceId;
        -    }
        -
        -    public String getScope() {
        -        return scope;
        -    }
        -}
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessResponse.java
        deleted file mode 100644
        index f7c419b9d1..0000000000
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessResponse.java
        +++ /dev/null
        @@ -1,52 +0,0 @@
        -/*
        - * SPDX-License-Identifier: Apache-2.0
        - *
        - * The OpenSearch Contributors require contributions made to
        - * this file be licensed under the Apache-2.0 license or a
        - * compatible open source license.
        - */
        -
        -package org.opensearch.sample.actions.access.verify;
        -
        -import java.io.IOException;
        -
        -import org.opensearch.core.action.ActionResponse;
        -import org.opensearch.core.common.io.stream.StreamInput;
        -import org.opensearch.core.common.io.stream.StreamOutput;
        -import org.opensearch.core.xcontent.ToXContentObject;
        -import org.opensearch.core.xcontent.XContentBuilder;
        -
        -public class VerifyResourceAccessResponse extends ActionResponse implements ToXContentObject {
        -    private final String message;
        -
        -    /**
        -     * Default constructor
        -     *
        -     * @param message The message
        -     */
        -    public VerifyResourceAccessResponse(String message) {
        -        this.message = message;
        -    }
        -
        -    @Override
        -    public void writeTo(StreamOutput out) throws IOException {
        -        out.writeString(message);
        -    }
        -
        -    /**
        -     * Constructor with StreamInput
        -     *
        -     * @param in the stream input
        -     */
        -    public VerifyResourceAccessResponse(final StreamInput in) throws IOException {
        -        message = in.readString();
        -    }
        -
        -    @Override
        -    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
        -        builder.startObject();
        -        builder.field("message", message);
        -        builder.endObject();
        -        return builder;
        -    }
        -}
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRestAction.java
        deleted file mode 100644
        index 3118fd54e6..0000000000
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/access/verify/VerifyResourceAccessRestAction.java
        +++ /dev/null
        @@ -1,55 +0,0 @@
        -/*
        - * SPDX-License-Identifier: Apache-2.0
        - *
        - * The OpenSearch Contributors require contributions made to
        - * this file be licensed under the Apache-2.0 license or a
        - * compatible open source license.
        - */
        -
        -package org.opensearch.sample.actions.access.verify;
        -
        -import java.io.IOException;
        -import java.util.List;
        -import java.util.Map;
        -
        -import org.opensearch.client.node.NodeClient;
        -import org.opensearch.core.xcontent.XContentParser;
        -import org.opensearch.rest.BaseRestHandler;
        -import org.opensearch.rest.RestRequest;
        -import org.opensearch.rest.action.RestToXContentListener;
        -
        -import static java.util.Collections.singletonList;
        -import static org.opensearch.rest.RestRequest.Method.GET;
        -
        -public class VerifyResourceAccessRestAction extends BaseRestHandler {
        -
        -    public VerifyResourceAccessRestAction() {}
        -
        -    @Override
        -    public List routes() {
        -        return singletonList(new Route(GET, "/_plugins/sample_resource_sharing/verify_resource_access"));
        -    }
        -
        -    @Override
        -    public String getName() {
        -        return "verify_resource_access";
        -    }
        -
        -    @Override
        -    protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
        -        Map source;
        -        try (XContentParser parser = request.contentParser()) {
        -            source = parser.map();
        -        }
        -
        -        String resourceId = (String) source.get("resource_id");
        -        String scope = (String) source.get("scope");
        -
        -        final VerifyResourceAccessRequest verifyResourceAccessRequest = new VerifyResourceAccessRequest(resourceId, scope);
        -        return channel -> client.executeLocally(
        -            VerifyResourceAccessAction.INSTANCE,
        -            verifyResourceAccessRequest,
        -            new RestToXContentListener<>(channel)
        -        );
        -    }
        -}
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceAction.java
        similarity index 92%
        rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceAction.java
        rename to sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceAction.java
        index a2b91185e1..3e73b95f79 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceAction.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceAction.java
        @@ -6,7 +6,7 @@
          * compatible open source license.
          */
         
        -package org.opensearch.sample.actions.resource.create;
        +package org.opensearch.sample.resource.actions.rest.create;
         
         import org.opensearch.action.ActionType;
         
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRequest.java
        similarity index 95%
        rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java
        rename to sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRequest.java
        index fe579ff0d1..d3e9a7a468 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRequest.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRequest.java
        @@ -6,7 +6,7 @@
          * compatible open source license.
          */
         
        -package org.opensearch.sample.actions.resource.create;
        +package org.opensearch.sample.resource.actions.rest.create;
         
         import java.io.IOException;
         
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceResponse.java
        similarity index 95%
        rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceResponse.java
        rename to sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceResponse.java
        index 6b980c9912..33c8b0b1e6 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceResponse.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceResponse.java
        @@ -6,7 +6,7 @@
          * compatible open source license.
          */
         
        -package org.opensearch.sample.actions.resource.create;
        +package org.opensearch.sample.resource.actions.rest.create;
         
         import java.io.IOException;
         
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java
        similarity index 97%
        rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java
        rename to sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java
        index f7aa1c76b5..bcfa0ae9df 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/create/CreateResourceRestAction.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java
        @@ -6,7 +6,7 @@
          * compatible open source license.
          */
         
        -package org.opensearch.sample.actions.resource.create;
        +package org.opensearch.sample.resource.actions.rest.create;
         
         import java.io.IOException;
         import java.util.List;
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceAction.java
        similarity index 92%
        rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceAction.java
        rename to sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceAction.java
        index ccb31f7ab2..bfb672dfec 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceAction.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceAction.java
        @@ -6,7 +6,7 @@
          * compatible open source license.
          */
         
        -package org.opensearch.sample.actions.resource.delete;
        +package org.opensearch.sample.resource.actions.rest.delete;
         
         import org.opensearch.action.ActionType;
         
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRequest.java
        similarity index 95%
        rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRequest.java
        rename to sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRequest.java
        index 1cb58989d3..d7c4637f31 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRequest.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRequest.java
        @@ -6,7 +6,7 @@
          * compatible open source license.
          */
         
        -package org.opensearch.sample.actions.resource.delete;
        +package org.opensearch.sample.resource.actions.rest.delete;
         
         import java.io.IOException;
         
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceResponse.java
        similarity index 95%
        rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceResponse.java
        rename to sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceResponse.java
        index ba3cddc04b..31bf86ca79 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceResponse.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceResponse.java
        @@ -6,7 +6,7 @@
          * compatible open source license.
          */
         
        -package org.opensearch.sample.actions.resource.delete;
        +package org.opensearch.sample.resource.actions.rest.delete;
         
         import java.io.IOException;
         
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java
        similarity index 96%
        rename from sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRestAction.java
        rename to sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java
        index 9a10ca2a62..6c88fdbc4d 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/actions/resource/delete/DeleteResourceRestAction.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java
        @@ -6,7 +6,7 @@
          * compatible open source license.
          */
         
        -package org.opensearch.sample.actions.resource.delete;
        +package org.opensearch.sample.resource.actions.rest.delete;
         
         import java.util.List;
         
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java
        similarity index 91%
        rename from sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java
        rename to sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java
        index ad82e19576..c20f492985 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/CreateResourceTransportAction.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java
        @@ -6,7 +6,7 @@
          * compatible open source license.
          */
         
        -package org.opensearch.sample.transport.resource;
        +package org.opensearch.sample.resource.actions.transport;
         
         import java.io.IOException;
         
        @@ -23,9 +23,9 @@
         import org.opensearch.core.action.ActionListener;
         import org.opensearch.core.xcontent.ToXContent;
         import org.opensearch.core.xcontent.XContentBuilder;
        -import org.opensearch.sample.actions.resource.create.CreateResourceAction;
        -import org.opensearch.sample.actions.resource.create.CreateResourceRequest;
        -import org.opensearch.sample.actions.resource.create.CreateResourceResponse;
        +import org.opensearch.sample.resource.actions.rest.create.CreateResourceAction;
        +import org.opensearch.sample.resource.actions.rest.create.CreateResourceRequest;
        +import org.opensearch.sample.resource.actions.rest.create.CreateResourceResponse;
         import org.opensearch.security.spi.resources.Resource;
         import org.opensearch.tasks.Task;
         import org.opensearch.transport.TransportService;
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/DeleteResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java
        similarity index 91%
        rename from sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/DeleteResourceTransportAction.java
        rename to sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java
        index bb403e3704..4ce8954bfe 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/resource/DeleteResourceTransportAction.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java
        @@ -6,7 +6,7 @@
          * compatible open source license.
          */
         
        -package org.opensearch.sample.transport.resource;
        +package org.opensearch.sample.resource.actions.transport;
         
         import org.apache.logging.log4j.LogManager;
         import org.apache.logging.log4j.Logger;
        @@ -22,9 +22,9 @@
         import org.opensearch.common.inject.Inject;
         import org.opensearch.common.util.concurrent.ThreadContext;
         import org.opensearch.core.action.ActionListener;
        -import org.opensearch.sample.actions.resource.delete.DeleteResourceAction;
        -import org.opensearch.sample.actions.resource.delete.DeleteResourceRequest;
        -import org.opensearch.sample.actions.resource.delete.DeleteResourceResponse;
        +import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceAction;
        +import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceRequest;
        +import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceResponse;
         import org.opensearch.tasks.Task;
         import org.opensearch.transport.TransportService;
         
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/VerifyResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/VerifyResourceAccessTransportAction.java
        deleted file mode 100644
        index 13954dbe2b..0000000000
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/transport/access/VerifyResourceAccessTransportAction.java
        +++ /dev/null
        @@ -1,61 +0,0 @@
        -/*
        - * SPDX-License-Identifier: Apache-2.0
        - *
        - * The OpenSearch Contributors require contributions made to
        - * this file be licensed under the Apache-2.0 license or a
        - * compatible open source license.
        - */
        -
        -package org.opensearch.sample.transport.access;
        -
        -import org.apache.logging.log4j.LogManager;
        -import org.apache.logging.log4j.Logger;
        -
        -import org.opensearch.action.support.ActionFilters;
        -import org.opensearch.action.support.HandledTransportAction;
        -import org.opensearch.client.Client;
        -import org.opensearch.common.inject.Inject;
        -import org.opensearch.core.action.ActionListener;
        -import org.opensearch.sample.SampleResourcePlugin;
        -import org.opensearch.sample.SampleResourceScope;
        -import org.opensearch.sample.actions.access.verify.VerifyResourceAccessAction;
        -import org.opensearch.sample.actions.access.verify.VerifyResourceAccessRequest;
        -import org.opensearch.sample.actions.access.verify.VerifyResourceAccessResponse;
        -import org.opensearch.security.spi.resources.ResourceService;
        -import org.opensearch.tasks.Task;
        -import org.opensearch.transport.TransportService;
        -
        -import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME;
        -
        -public class VerifyResourceAccessTransportAction extends HandledTransportAction {
        -    private static final Logger log = LogManager.getLogger(VerifyResourceAccessTransportAction.class);
        -
        -    @Inject
        -    public VerifyResourceAccessTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) {
        -        super(VerifyResourceAccessAction.NAME, transportService, actionFilters, VerifyResourceAccessRequest::new);
        -    }
        -
        -    @Override
        -    protected void doExecute(Task task, VerifyResourceAccessRequest request, ActionListener listener) {
        -        try {
        -            ResourceService rs = SampleResourcePlugin.GuiceHolder.getResourceService();
        -            boolean hasRequestedScopeAccess = rs.getResourceAccessControlPlugin()
        -                .hasPermission(request.getResourceId(), RESOURCE_INDEX_NAME, SampleResourceScope.valueOf(request.getScope()));
        -
        -            StringBuilder sb = new StringBuilder();
        -            sb.append("User ");
        -            sb.append(hasRequestedScopeAccess ? "has" : "does not have");
        -            sb.append(" requested scope ");
        -            sb.append(request.getScope());
        -            sb.append(" access to ");
        -            sb.append(request.getResourceId());
        -
        -            log.info(sb.toString());
        -            listener.onResponse(new VerifyResourceAccessResponse(sb.toString()));
        -        } catch (Exception e) {
        -            log.info("Failed to check user permissions for resource {}", request.getResourceId(), e);
        -            listener.onFailure(e);
        -        }
        -    }
        -
        -}
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java
        deleted file mode 100644
        index fac032402c..0000000000
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Validation.java
        +++ /dev/null
        @@ -1,36 +0,0 @@
        -/*
        - * SPDX-License-Identifier: Apache-2.0
        - *
        - * The OpenSearch Contributors require contributions made to
        - * this file be licensed under the Apache-2.0 license or a
        - * compatible open source license.
        - */
        -
        -package org.opensearch.sample.utils;
        -
        -import java.util.HashSet;
        -import java.util.Set;
        -
        -import org.opensearch.action.ActionRequestValidationException;
        -import org.opensearch.sample.SampleResourceScope;
        -import org.opensearch.security.spi.resources.ResourceAccessScope;
        -
        -public class Validation {
        -    public static ActionRequestValidationException validateScopes(Set scopes) {
        -        Set validScopes = new HashSet<>();
        -        for (SampleResourceScope scope : SampleResourceScope.values()) {
        -            validScopes.add(scope.name());
        -        }
        -        validScopes.add(ResourceAccessScope.READ_ONLY);
        -        validScopes.add(ResourceAccessScope.READ_WRITE);
        -
        -        for (String s : scopes) {
        -            if (!validScopes.contains(s)) {
        -                ActionRequestValidationException exception = new ActionRequestValidationException();
        -                exception.addValidationError("Invalid scope: " + s + ". Scope must be one of: " + validScopes);
        -                return exception;
        -            }
        -        }
        -        return null;
        -    }
        -}
        
        From bd148db7a42485ac68741895385c0f299b77eed7 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Wed, 8 Jan 2025 18:39:19 -0500
        Subject: [PATCH 070/201] Updates scope names
        
        Signed-off-by: Darshit Chanpura 
        ---
         build.gradle | 2 +-
         1 file changed, 1 insertion(+), 1 deletion(-)
        
        diff --git a/build.gradle b/build.gradle
        index 2124b0d9de..8e4148c23d 100644
        --- a/build.gradle
        +++ b/build.gradle
        @@ -574,7 +574,7 @@ tasks.integrationTest.finalizedBy(jacocoTestReport) // report is always generate
         check.dependsOn integrationTest
         
         dependencies {
        -    implementation project(path: ":opensearch-resource-sharing-spi")
        +    compileOnly project(path: ":opensearch-resource-sharing-spi")
             implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}"
             implementation "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}"
             implementation "org.apache.httpcomponents.client5:httpclient5-cache:${versions.httpclient5}"
        
        From fce2b9009a048f25c2d46905c5a60b0ebcd52bf2 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Thu, 9 Jan 2025 12:58:15 -0500
        Subject: [PATCH 071/201] Updates settings gradle to add spi
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../spi/resources/ResourceAccessScope.java    |  4 +--
         .../list/ListAccessibleResourcesResponse.java | 31 ++++++++++++-------
         .../security/util/ResourceValidation.java     |  4 +--
         .../security/resources/ShareWithTests.java    | 12 +++----
         4 files changed, 29 insertions(+), 22 deletions(-)
        
        diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java
        index b8dab4ff67..e6fd2a76f6 100644
        --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java
        +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java
        @@ -18,8 +18,8 @@
          * @opensearch.experimental
          */
         public interface ResourceAccessScope> {
        -    String READ_ONLY = "read_only";
        -    String READ_WRITE = "read_write";
        +    String RESTRICTED = "restricted";
        +    String PUBLIC = "public";
         
             static  & ResourceAccessScope> E fromValue(Class enumClass, String value) {
                 for (E enumConstant : enumClass.getEnumConstants()) {
        diff --git a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java
        index e242b9b353..8bb1f0ea02 100644
        --- a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java
        +++ b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java
        @@ -37,20 +37,27 @@ public void writeTo(StreamOutput out) throws IOException {
                 out.writeCollection(resources);
             }
         
        -    public ListAccessibleResourcesResponse(StreamInput in) throws IOException, ClassNotFoundException {
        +    public ListAccessibleResourcesResponse(StreamInput in) throws IOException {
                 this.resourceClass = in.readString();
        +        this.resources = readResourcesFromStream(in);
        +    }
         
        -        // TODO check if there is a better way to handle this
        -        Class clazz = Class.forName(this.resourceClass);
        -        @SuppressWarnings("unchecked")
        -        Class resourceClass = (Class) clazz;
        -        this.resources = in.readSet(i -> {
        -            try {
        -                return resourceClass.getDeclaredConstructor(StreamInput.class).newInstance(i);
        -            } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
        -                throw new RuntimeException(e);
        -            }
        -        });
        +    private Set readResourcesFromStream(StreamInput in) {
        +        try {
        +            // TODO check if there is a better way to handle this
        +            Class clazz = Class.forName(this.resourceClass);
        +            @SuppressWarnings("unchecked")
        +            Class resourceClass = (Class) clazz;
        +            return in.readSet(i -> {
        +                try {
        +                    return resourceClass.getDeclaredConstructor(StreamInput.class).newInstance(i);
        +                } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
        +                    throw new RuntimeException(e);
        +                }
        +            });
        +        } catch (ClassNotFoundException | IOException e) {
        +            return Set.of();
        +        }
             }
         
             @Override
        diff --git a/src/main/java/org/opensearch/security/util/ResourceValidation.java b/src/main/java/org/opensearch/security/util/ResourceValidation.java
        index 3850087e4e..428aae2cf2 100644
        --- a/src/main/java/org/opensearch/security/util/ResourceValidation.java
        +++ b/src/main/java/org/opensearch/security/util/ResourceValidation.java
        @@ -17,8 +17,8 @@
         public class ResourceValidation {
             public static ActionRequestValidationException validateScopes(Set scopes) {
                 Set validScopes = new HashSet<>();
        -        validScopes.add(ResourceAccessScope.READ_ONLY);
        -        validScopes.add(ResourceAccessScope.READ_WRITE);
        +        validScopes.add(ResourceAccessScope.RESTRICTED);
        +        validScopes.add(ResourceAccessScope.PUBLIC);
         
                 // TODO See if we can add custom scopes as part of this validation routine
         
        diff --git a/src/test/java/org/opensearch/security/resources/ShareWithTests.java b/src/test/java/org/opensearch/security/resources/ShareWithTests.java
        index 7c7b634e86..43b2b6f502 100644
        --- a/src/test/java/org/opensearch/security/resources/ShareWithTests.java
        +++ b/src/test/java/org/opensearch/security/resources/ShareWithTests.java
        @@ -89,12 +89,12 @@ public void testFromXContentWithStartObject() throws IOException {
                 XContentParser parser;
                 try (XContentBuilder builder = XContentFactory.jsonBuilder()) {
                     builder.startObject()
        -                .startObject(ResourceAccessScope.READ_ONLY)
        +                .startObject(ResourceAccessScope.RESTRICTED)
                         .array("users", "user1", "user2")
                         .array("roles", "role1")
                         .array("backend_roles", "backend_role1")
                         .endObject()
        -                .startObject(ResourceAccessScope.READ_WRITE)
        +                .startObject(ResourceAccessScope.PUBLIC)
                         .array("users", "user3")
                         .array("roles", "role2", "role3")
                         .array("backend_roles")
        @@ -115,7 +115,7 @@ public void testFromXContentWithStartObject() throws IOException {
                 for (SharedWithScope scope : scopes) {
                     SharedWithScope.ScopeRecipients perScope = scope.getSharedWithPerScope();
                     Map> recipients = perScope.getRecipients();
        -            if (scope.getScope().equals(ResourceAccessScope.READ_ONLY)) {
        +            if (scope.getScope().equals(ResourceAccessScope.RESTRICTED)) {
                         MatcherAssert.assertThat(
                             recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.USERS.getName())).size(),
                             is(2)
        @@ -128,7 +128,7 @@ public void testFromXContentWithStartObject() throws IOException {
                             recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.BACKEND_ROLES.getName())).size(),
                             is(1)
                         );
        -            } else if (scope.getScope().equals(ResourceAccessScope.READ_WRITE)) {
        +            } else if (scope.getScope().equals(ResourceAccessScope.PUBLIC)) {
                         MatcherAssert.assertThat(
                             recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.USERS.getName())).size(),
                             is(1)
        @@ -229,8 +229,8 @@ public void test_writeSharedWithScopesToStream() throws IOException {
                 StreamOutput mockStreamOutput = Mockito.mock(StreamOutput.class);
         
                 Set sharedWithScopes = new HashSet<>();
        -        sharedWithScopes.add(new SharedWithScope(ResourceAccessScope.READ_ONLY, new SharedWithScope.ScopeRecipients(Map.of())));
        -        sharedWithScopes.add(new SharedWithScope(ResourceAccessScope.READ_WRITE, new SharedWithScope.ScopeRecipients(Map.of())));
        +        sharedWithScopes.add(new SharedWithScope(ResourceAccessScope.RESTRICTED, new SharedWithScope.ScopeRecipients(Map.of())));
        +        sharedWithScopes.add(new SharedWithScope(ResourceAccessScope.PUBLIC, new SharedWithScope.ScopeRecipients(Map.of())));
         
                 ShareWith shareWith = new ShareWith(sharedWithScopes);
         
        
        From 1aec47a3f451d84c66caaf9fe12a63e002f64f88 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Thu, 9 Jan 2025 22:08:49 -0500
        Subject: [PATCH 072/201] Updates DLS Search handler to filter out resources
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../security/OpenSearchSecurityPlugin.java    |  3 +-
         .../configuration/DlsFlsValveImpl.java        | 33 ++++++++++++++-----
         .../SecurityFlsDlsIndexSearcherWrapper.java   |  3 +-
         .../privileges/dlsfls/DlsRestriction.java     |  2 +-
         .../privileges/dlsfls/DocumentPrivileges.java |  6 ++--
         .../resources/ResourceAccessHandler.java      | 28 +++++++++++-----
         6 files changed, 51 insertions(+), 24 deletions(-)
        
        diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        index a5f2bdfea4..bc8de0a6c3 100644
        --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        @@ -1194,7 +1194,8 @@ public Collection createComponents(
                         resolver,
                         xContentRegistry,
                         threadPool,
        -                dlsFlsBaseContext
        +                dlsFlsBaseContext,
        +                resourceAccessHandler
                     );
                     cr.subscribeOnChange(configMap -> { ((DlsFlsValveImpl) dlsFlsValve).updateConfiguration(cr.getConfiguration(CType.ROLES)); });
                 }
        diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        index 498b908e5d..9169cad529 100644
        --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        @@ -17,6 +17,7 @@
         import java.util.Comparator;
         import java.util.List;
         import java.util.Objects;
        +import java.util.Set;
         import java.util.concurrent.atomic.AtomicReference;
         import java.util.function.Consumer;
         import java.util.stream.StreamSupport;
        @@ -70,13 +71,9 @@
         import org.opensearch.security.privileges.DocumentAllowList;
         import org.opensearch.security.privileges.PrivilegesEvaluationContext;
         import org.opensearch.security.privileges.PrivilegesEvaluationException;
        -import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext;
        -import org.opensearch.security.privileges.dlsfls.DlsFlsLegacyHeaders;
        -import org.opensearch.security.privileges.dlsfls.DlsFlsProcessedConfig;
        -import org.opensearch.security.privileges.dlsfls.DlsRestriction;
        -import org.opensearch.security.privileges.dlsfls.FieldMasking;
        -import org.opensearch.security.privileges.dlsfls.IndexToRuleMap;
        +import org.opensearch.security.privileges.dlsfls.*;
         import org.opensearch.security.resolver.IndexResolverReplacer;
        +import org.opensearch.security.resources.ResourceAccessHandler;
         import org.opensearch.security.securityconf.DynamicConfigFactory;
         import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration;
         import org.opensearch.security.securityconf.impl.v7.RoleV7;
        @@ -98,6 +95,7 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve {
             private final AtomicReference dlsFlsProcessedConfig = new AtomicReference<>();
             private final FieldMasking.Config fieldMaskingConfig;
             private final Settings settings;
        +    private final ResourceAccessHandler resourceAccessHandler;
         
             public DlsFlsValveImpl(
                 Settings settings,
        @@ -106,7 +104,8 @@ public DlsFlsValveImpl(
                 IndexNameExpressionResolver resolver,
                 NamedXContentRegistry namedXContentRegistry,
                 ThreadPool threadPool,
        -        DlsFlsBaseContext dlsFlsBaseContext
        +        DlsFlsBaseContext dlsFlsBaseContext,
        +        ResourceAccessHandler resourceAccessHandler
             ) {
                 super();
                 this.nodeClient = nodeClient;
        @@ -118,6 +117,7 @@ public DlsFlsValveImpl(
                 this.fieldMaskingConfig = FieldMasking.Config.fromSettings(settings);
                 this.dlsFlsBaseContext = dlsFlsBaseContext;
                 this.settings = settings;
        +        this.resourceAccessHandler = resourceAccessHandler;
         
                 clusterService.addListener(event -> {
                     DlsFlsProcessedConfig config = dlsFlsProcessedConfig.get();
        @@ -349,6 +349,7 @@ public void handleSearchContext(SearchContext searchContext, ThreadPool threadPo
                 try {
                     String index = searchContext.indexShard().indexSettings().getIndex().getName();
         
        +            assert !Strings.isNullOrEmpty(index);
                     if (log.isTraceEnabled()) {
                         log.trace("handleSearchContext(); index: {}", index);
                     }
        @@ -374,13 +375,27 @@ public void handleSearchContext(SearchContext searchContext, ThreadPool threadPo
                     }
         
                     PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext();
        -            if (privilegesEvaluationContext == null) {
        +
        +            if (privilegesEvaluationContext == null || OpenSearchSecurityPlugin.getResourceIndices().contains(index)) {
                         return;
                     }
         
                     DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get();
         
        -            DlsRestriction dlsRestriction = config.getDocumentPrivileges().getRestriction(privilegesEvaluationContext, index);
        +            DlsRestriction dlsRestriction;
        +
        +            Set resourceIds;
        +            if (OpenSearchSecurityPlugin.getResourceIndices().contains(index)) {
        +                resourceIds = this.resourceAccessHandler.getAccessibleResourceIdsForCurrentUser(index);
        +                if (resourceIds.isEmpty()) {
        +                    return;
        +                }
        +                // Create a DLS restriction to filter search results with accessible resources only
        +                dlsRestriction = this.resourceAccessHandler.createResourceDLSRestriction(resourceIds, namedXContentRegistry);
        +
        +            } else {
        +                dlsRestriction = config.getDocumentPrivileges().getRestriction(privilegesEvaluationContext, index);
        +            }
         
                     if (log.isTraceEnabled()) {
                         log.trace("handleSearchContext(); index: {}; dlsRestriction: {}", index, dlsRestriction);
        diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java
        index bb06604829..3b90dbcd2a 100644
        --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java
        +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java
        @@ -29,6 +29,7 @@
         import org.opensearch.cluster.metadata.IndexMetadata;
         import org.opensearch.cluster.service.ClusterService;
         import org.opensearch.common.settings.Settings;
        +import org.opensearch.core.common.Strings;
         import org.opensearch.core.index.shard.ShardId;
         import org.opensearch.index.IndexService;
         import org.opensearch.index.mapper.SeqNoFieldMapper;
        @@ -49,8 +50,6 @@
         import org.opensearch.security.resources.ResourceAccessHandler;
         import org.opensearch.security.support.ConfigConstants;
         
        -import joptsimple.internal.Strings;
        -
         public class SecurityFlsDlsIndexSearcherWrapper extends SystemIndexSearcherWrapper {
         
             public final Logger log = LogManager.getLogger(this.getClass());
        diff --git a/src/main/java/org/opensearch/security/privileges/dlsfls/DlsRestriction.java b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsRestriction.java
        index 242e0000a4..01fccb78e6 100644
        --- a/src/main/java/org/opensearch/security/privileges/dlsfls/DlsRestriction.java
        +++ b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsRestriction.java
        @@ -53,7 +53,7 @@ public class DlsRestriction extends AbstractRuleBasedPrivileges.Rule {
         
             private final ImmutableList queries;
         
        -    DlsRestriction(List queries) {
        +    public DlsRestriction(List queries) {
                 this.queries = ImmutableList.copyOf(queries);
             }
         
        diff --git a/src/main/java/org/opensearch/security/privileges/dlsfls/DocumentPrivileges.java b/src/main/java/org/opensearch/security/privileges/dlsfls/DocumentPrivileges.java
        index 2afcdd4b82..40ebfd7282 100644
        --- a/src/main/java/org/opensearch/security/privileges/dlsfls/DocumentPrivileges.java
        +++ b/src/main/java/org/opensearch/security/privileges/dlsfls/DocumentPrivileges.java
        @@ -92,7 +92,7 @@ protected DlsRestriction compile(PrivilegesEvaluationContext context, Collection
             /**
              * The basic rules of DLS are queries. This class encapsulates single queries.
              */
        -    static abstract class DlsQuery {
        +    public static abstract class DlsQuery {
                 final String queryString;
         
                 DlsQuery(String queryString) {
        @@ -118,7 +118,7 @@ public boolean equals(Object obj) {
                     return Objects.equals(this.queryString, other.queryString);
                 }
         
        -        protected QueryBuilder parseQuery(String queryString, NamedXContentRegistry xContentRegistry)
        +        public static QueryBuilder parseQuery(String queryString, NamedXContentRegistry xContentRegistry)
                     throws PrivilegesConfigurationValidationException {
                     try {
                         XContentParser parser = JsonXContent.jsonXContent.createParser(
        @@ -193,7 +193,7 @@ public static class RenderedDlsQuery {
                 private final QueryBuilder queryBuilder;
                 private final String renderedSource;
         
        -        RenderedDlsQuery(QueryBuilder queryBuilder, String renderedSource) {
        +        public RenderedDlsQuery(QueryBuilder queryBuilder, String renderedSource) {
                     this.queryBuilder = queryBuilder;
                     this.renderedSource = renderedSource;
                 }
        diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        index 361342e611..1602133b46 100644
        --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        @@ -12,22 +12,22 @@
         package org.opensearch.security.resources;
         
         import java.io.IOException;
        -import java.util.Collections;
        -import java.util.HashSet;
        -import java.util.Map;
        -import java.util.Set;
        +import java.util.*;
         
        +import com.fasterxml.jackson.core.JsonProcessingException;
         import org.apache.logging.log4j.LogManager;
         import org.apache.logging.log4j.Logger;
         import org.apache.lucene.search.Query;
         
         import org.opensearch.common.util.concurrent.ThreadContext;
        -import org.opensearch.index.query.BoolQueryBuilder;
        -import org.opensearch.index.query.ConstantScoreQueryBuilder;
        -import org.opensearch.index.query.QueryBuilders;
        -import org.opensearch.index.query.QueryShardContext;
        +import org.opensearch.core.xcontent.NamedXContentRegistry;
        +import org.opensearch.index.query.*;
        +import org.opensearch.security.DefaultObjectMapper;
         import org.opensearch.security.OpenSearchSecurityPlugin;
         import org.opensearch.security.configuration.AdminDNs;
        +import org.opensearch.security.privileges.PrivilegesConfigurationValidationException;
        +import org.opensearch.security.privileges.dlsfls.DlsRestriction;
        +import org.opensearch.security.privileges.dlsfls.DocumentPrivileges;
         import org.opensearch.security.spi.resources.Resource;
         import org.opensearch.security.spi.resources.ResourceParser;
         import org.opensearch.security.support.ConfigConstants;
        @@ -379,4 +379,16 @@ public Query createResourceDlsQuery(Set resourceIds, QueryShardContext q
                 ConstantScoreQueryBuilder builder = new ConstantScoreQueryBuilder(boolQueryBuilder);
                 return builder.toQuery(queryShardContext);
             }
        +
        +    public DlsRestriction createResourceDLSRestriction(Set resourceIds, NamedXContentRegistry xContentRegistry)
        +        throws JsonProcessingException, PrivilegesConfigurationValidationException {
        +        String jsonQuery = String.format(
        +            "{ \"bool\": { \"filter\": [ { \"terms\": { \"_id\": %s } } ] } }",
        +            DefaultObjectMapper.writeValueAsString(resourceIds, true)
        +        );
        +        QueryBuilder queryBuilder = DocumentPrivileges.DlsQuery.parseQuery(jsonQuery, xContentRegistry);
        +        DocumentPrivileges.RenderedDlsQuery renderedDlsQuery = new DocumentPrivileges.RenderedDlsQuery(queryBuilder, jsonQuery);
        +        List documentPrivileges = List.of(renderedDlsQuery);
        +        return new DlsRestriction(documentPrivileges);
        +    }
         }
        
        From 11d8d6d22bdd4f5009d47eede2def37f0653b196 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Fri, 10 Jan 2025 12:19:19 -0500
        Subject: [PATCH 073/201] Updates if clause to match correctly
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../org/opensearch/security/configuration/DlsFlsValveImpl.java  | 2 +-
         1 file changed, 1 insertion(+), 1 deletion(-)
        
        diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        index 9169cad529..f9897baae0 100644
        --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        @@ -376,7 +376,7 @@ public void handleSearchContext(SearchContext searchContext, ThreadPool threadPo
         
                     PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext();
         
        -            if (privilegesEvaluationContext == null || OpenSearchSecurityPlugin.getResourceIndices().contains(index)) {
        +            if (privilegesEvaluationContext == null && !OpenSearchSecurityPlugin.getResourceIndices().contains(index)) {
                         return;
                     }
         
        
        From 07b26b0f76f10d5d31b3db7e7715f7a4ec910302 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Fri, 10 Jan 2025 13:18:41 -0500
        Subject: [PATCH 074/201] Updates resource tests
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../test/resources/security/esnode-key.pem    |  28 ------------------
         .../src/test/resources/security/esnode.pem    |  25 ----------------
         .../src/test/resources/security/kirk-key.pem  |  28 ------------------
         .../src/test/resources/security/kirk.pem      |  27 -----------------
         .../src/test/resources/security/root-ca.pem   |  28 ------------------
         .../src/test/resources/security/sample.pem    |  25 ----------------
         .../src/test/resources/security/test-kirk.jks | Bin 3766 -> 0 bytes
         7 files changed, 161 deletions(-)
         delete mode 100644 sample-resource-plugin/src/test/resources/security/esnode-key.pem
         delete mode 100644 sample-resource-plugin/src/test/resources/security/esnode.pem
         delete mode 100644 sample-resource-plugin/src/test/resources/security/kirk-key.pem
         delete mode 100644 sample-resource-plugin/src/test/resources/security/kirk.pem
         delete mode 100644 sample-resource-plugin/src/test/resources/security/root-ca.pem
         delete mode 100644 sample-resource-plugin/src/test/resources/security/sample.pem
         delete mode 100644 sample-resource-plugin/src/test/resources/security/test-kirk.jks
        
        diff --git a/sample-resource-plugin/src/test/resources/security/esnode-key.pem b/sample-resource-plugin/src/test/resources/security/esnode-key.pem
        deleted file mode 100644
        index e90562be43..0000000000
        --- a/sample-resource-plugin/src/test/resources/security/esnode-key.pem
        +++ /dev/null
        @@ -1,28 +0,0 @@
        ------BEGIN PRIVATE KEY-----
        -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCm93kXteDQHMAv
        -bUPNPW5pyRHKDD42XGWSgq0k1D29C/UdyL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0
        -o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0HGkn47XVu3EwbfrTENg3jFu+Oem6a/50
        -1SzITzJWtS0cn2dIFOBimTVpT/4Zv5qrXA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1
        -MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8ndibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b
        -6l+KLo3IKpfTbAIJXIO+M67FLtWKtttDao94B069skzKk6FPgW/OZh6PRCD0oxOa
        -vV+ld2SjAgMBAAECggEAQK1+uAOZeaSZggW2jQut+MaN4JHLi61RH2cFgU3COLgo
        -FIiNjFn8f2KKU3gpkt1It8PjlmprpYut4wHI7r6UQfuv7ZrmncRiPWHm9PB82+ZQ
        -5MXYqj4YUxoQJ62Cyz4sM6BobZDrjG6HHGTzuwiKvHHkbsEE9jQ4E5m7yfbVvM0O
        -zvwrSOM1tkZihKSTpR0j2+taji914tjBssbn12TMZQL5ItGnhR3luY8mEwT9MNkZ
        -xg0VcREoAH+pu9FE0vPUgLVzhJ3be7qZTTSRqv08bmW+y1plu80GbppePcgYhEow
        -dlW4l6XPJaHVSn1lSFHE6QAx6sqiAnBz0NoTPIaLyQKBgQDZqDOlhCRciMRicSXn
        -7yid9rhEmdMkySJHTVFOidFWwlBcp0fGxxn8UNSBcXdSy7GLlUtH41W9PWl8tp9U
        -hQiiXORxOJ7ZcB80uNKXF01hpPj2DpFPWyHFxpDkWiTAYpZl68rOlYujxZUjJIej
        -VvcykBC2BlEOG9uZv2kxcqLyJwKBgQDEYULTxaTuLIa17wU3nAhaainKB3vHxw9B
        -Ksy5p3ND43UNEKkQm7K/WENx0q47TA1mKD9i+BhaLod98mu0YZ+BCUNgWKcBHK8c
        -uXpauvM/pLhFLXZ2jvEJVpFY3J79FSRK8bwE9RgKfVKMMgEk4zOyZowS8WScOqiy
        -hnQn1vKTJQKBgElhYuAnl9a2qXcC7KOwRsJS3rcKIVxijzL4xzOyVShp5IwIPbOv
        -hnxBiBOH/JGmaNpFYBcBdvORE9JfA4KMQ2fx53agfzWRjoPI1/7mdUk5RFI4gRb/
        -A3jZRBoopgFSe6ArCbnyQxzYzToG48/Wzwp19ZxYrtUR4UyJct6f5n27AoGBAJDh
        -KIpQQDOvCdtjcbfrF4aM2DPCfaGPzENJriwxy6oEPzDaX8Bu/dqI5Ykt43i/zQrX
        -GpyLaHvv4+oZVTiI5UIvcVO9U8hQPyiz9f7F+fu0LHZs6f7hyhYXlbe3XFxeop3f
        -5dTKdWgXuTTRF2L9dABkA2deS9mutRKwezWBMQk5AoGBALPtX0FrT1zIosibmlud
        -tu49A/0KZu4PBjrFMYTSEWGNJez3Fb2VsJwylVl6HivwbP61FhlYfyksCzQQFU71
        -+x7Nmybp7PmpEBECr3deoZKQ/acNHn0iwb0It+YqV5+TquQebqgwK6WCLsMuiYKT
        -bg/ch9Rhxbq22yrVgWHh6epp
        ------END PRIVATE KEY-----
        \ No newline at end of file
        diff --git a/sample-resource-plugin/src/test/resources/security/esnode.pem b/sample-resource-plugin/src/test/resources/security/esnode.pem
        deleted file mode 100644
        index 44101f0b37..0000000000
        --- a/sample-resource-plugin/src/test/resources/security/esnode.pem
        +++ /dev/null
        @@ -1,25 +0,0 @@
        ------BEGIN CERTIFICATE-----
        -MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL
        -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt
        -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl
        -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v
        -dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT
        -AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl
        -MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA
        -A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud
        -yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0
        -HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr
        -XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n
        -dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD
        -ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R
        -BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA
        -AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF
        -BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo
        -wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ
        -KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz
        -pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi
        -7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh
        -hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L
        -camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg
        -PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg=
        ------END CERTIFICATE-----
        \ No newline at end of file
        diff --git a/sample-resource-plugin/src/test/resources/security/kirk-key.pem b/sample-resource-plugin/src/test/resources/security/kirk-key.pem
        deleted file mode 100644
        index 1949c26139..0000000000
        --- a/sample-resource-plugin/src/test/resources/security/kirk-key.pem
        +++ /dev/null
        @@ -1,28 +0,0 @@
        ------BEGIN PRIVATE KEY-----
        -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCVXDgEJQorgfXp
        -gpY0TgF55bD2xuzxN5Dc9rDfgWxrsOvOloMpd7k6FR71bKWjJi1KptSmM/cDElky
        -AWYKSfYWGiGxsQ+EQW+6kwCfEOHXQldn+0+JcWqP+osSPjtJfwRvRN5kRqP69MPo
        -7U0N2kdqenqMWjmG1chDGLRSOEGU5HIBiDxsZtOcvMaJ8b1eaW0lvS+6gFQ80AvB
        -GBkDDCOHHLtDXBylrZk2CQP8AzxNicIZ4B8G3CG3OHA8+nBtEtxZoIihrrkqlMt+
        -b/5N8u8zB0Encew0kdrc4R/2wS//ahr6U+9Siq8T7WsUtGwKj3BJClg6OyDJRhlu
        -y2gFnxoPAgMBAAECggEAP5TOycDkx+megAWVoHV2fmgvgZXkBrlzQwUG/VZQi7V4
        -ZGzBMBVltdqI38wc5MtbK3TCgHANnnKgor9iq02Z4wXDwytPIiti/ycV9CDRKvv0
        -TnD2hllQFjN/IUh5n4thHWbRTxmdM7cfcNgX3aZGkYbLBVVhOMtn4VwyYu/Mxy8j
        -xClZT2xKOHkxqwmWPmdDTbAeZIbSv7RkIGfrKuQyUGUaWhrPslvYzFkYZ0umaDgQ
        -OAthZew5Bz3OfUGOMPLH61SVPuJZh9zN1hTWOvT65WFWfsPd2yStI+WD/5PU1Doo
        -1RyeHJO7s3ug8JPbtNJmaJwHe9nXBb/HXFdqb976yQKBgQDNYhpu+MYSYupaYqjs
        -9YFmHQNKpNZqgZ4ceRFZ6cMJoqpI5dpEMqToFH7tpor72Lturct2U9nc2WR0HeEs
        -/6tiptyMPTFEiMFb1opQlXF2ae7LeJllntDGN0Q6vxKnQV+7VMcXA0Y8F7tvGDy3
        -qJu5lfvB1mNM2I6y/eMxjBuQhwKBgQC6K41DXMFro0UnoO879pOQYMydCErJRmjG
        -/tZSy3Wj4KA/QJsDSViwGfvdPuHZRaG9WtxdL6kn0w1exM9Rb0bBKl36lvi7o7xv
        -M+Lw9eyXMkww8/F5d7YYH77gIhGo+RITkKI3+5BxeBaUnrGvmHrpmpgRXWmINqr0
        -0jsnN3u0OQKBgCf45vIgItSjQb8zonLz2SpZjTFy4XQ7I92gxnq8X0Q5z3B+o7tQ
        -K/4rNwTju/sGFHyXAJlX+nfcK4vZ4OBUJjP+C8CTjEotX4yTNbo3S6zjMyGQqDI5
        -9aIOUY4pb+TzeUFJX7If5gR+DfGyQubvvtcg1K3GHu9u2l8FwLj87sRzAoGAflQF
        -RHuRiG+/AngTPnZAhc0Zq0kwLkpH2Rid6IrFZhGLy8AUL/O6aa0IGoaMDLpSWUJp
        -nBY2S57MSM11/MVslrEgGmYNnI4r1K25xlaqV6K6ztEJv6n69327MS4NG8L/gCU5
        -3pEm38hkUi8pVYU7in7rx4TCkrq94OkzWJYurAkCgYATQCL/rJLQAlJIGulp8s6h
        -mQGwy8vIqMjAdHGLrCS35sVYBXG13knS52LJHvbVee39AbD5/LlWvjJGlQMzCLrw
        -F7oILW5kXxhb8S73GWcuMbuQMFVHFONbZAZgn+C9FW4l7XyRdkrbR1MRZ2km8YMs
        -/AHmo368d4PSNRMMzLHw8Q==
        ------END PRIVATE KEY-----
        \ No newline at end of file
        diff --git a/sample-resource-plugin/src/test/resources/security/kirk.pem b/sample-resource-plugin/src/test/resources/security/kirk.pem
        deleted file mode 100644
        index 36b7e19a75..0000000000
        --- a/sample-resource-plugin/src/test/resources/security/kirk.pem
        +++ /dev/null
        @@ -1,27 +0,0 @@
        ------BEGIN CERTIFICATE-----
        -MIIEmDCCA4CgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLcwDQYJKoZIhvcNAQEL
        -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt
        -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl
        -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v
        -dCBDQTAeFw0yNDAyMjAxNzA0MjRaFw0zNDAyMTcxNzA0MjRaME0xCzAJBgNVBAYT
        -AmRlMQ0wCwYDVQQHDAR0ZXN0MQ8wDQYDVQQKDAZjbGllbnQxDzANBgNVBAsMBmNs
        -aWVudDENMAsGA1UEAwwEa2lyazCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
        -ggEBAJVcOAQlCiuB9emCljROAXnlsPbG7PE3kNz2sN+BbGuw686Wgyl3uToVHvVs
        -paMmLUqm1KYz9wMSWTIBZgpJ9hYaIbGxD4RBb7qTAJ8Q4ddCV2f7T4lxao/6ixI+
        -O0l/BG9E3mRGo/r0w+jtTQ3aR2p6eoxaOYbVyEMYtFI4QZTkcgGIPGxm05y8xonx
        -vV5pbSW9L7qAVDzQC8EYGQMMI4ccu0NcHKWtmTYJA/wDPE2JwhngHwbcIbc4cDz6
        -cG0S3FmgiKGuuSqUy35v/k3y7zMHQSdx7DSR2tzhH/bBL/9qGvpT71KKrxPtaxS0
        -bAqPcEkKWDo7IMlGGW7LaAWfGg8CAwEAAaOCASswggEnMAwGA1UdEwEB/wQCMAAw
        -DgYDVR0PAQH/BAQDAgXgMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMB0GA1UdDgQW
        -BBSjMS8tgguX/V7KSGLoGg7K6XMzIDCBzwYDVR0jBIHHMIHEgBQXh9+gWutmEqfV
        -0Pi6EkU8tysAnKGBlaSBkjCBjzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmS
        -JomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1wbGUgQ29tIEluYy4xITAf
        -BgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEhMB8GA1UEAwwYRXhhbXBs
        -ZSBDb20gSW5jLiBSb290IENBghQNZAmZZn3EFOxBR4630XlhI+mo4jANBgkqhkiG
        -9w0BAQsFAAOCAQEACEUPPE66/Ot3vZqRGpjDjPHAdtOq+ebaglQhvYcnDw8LOZm8
        -Gbh9M88CiO6UxC8ipQLTPh2yyeWArkpJzJK/Pi1eoF1XLiAa0sQ/RaJfQWPm9dvl
        -1ZQeK5vfD4147b3iBobwEV+CR04SKow0YeEEzAJvzr8YdKI6jqr+2GjjVqzxvRBy
        -KRVHWCFiR7bZhHGLq3br8hSu0hwjb3oGa1ZI8dui6ujyZt6nm6BoEkau3G/6+zq9
        -E6vX3+8Fj4HKCAL6i0SwfGmEpTNp5WUhqibK/fMhhmMT4Mx6MxkT+OFnIjdUU0S/
        -e3kgnG8qjficUr38CyEli1U0M7koIXUZI7r+LQ==
        ------END CERTIFICATE-----
        \ No newline at end of file
        diff --git a/sample-resource-plugin/src/test/resources/security/root-ca.pem b/sample-resource-plugin/src/test/resources/security/root-ca.pem
        deleted file mode 100644
        index d33f5f7216..0000000000
        --- a/sample-resource-plugin/src/test/resources/security/root-ca.pem
        +++ /dev/null
        @@ -1,28 +0,0 @@
        ------BEGIN CERTIFICATE-----
        -MIIExjCCA66gAwIBAgIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcNAQEL
        -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt
        -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl
        -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v
        -dCBDQTAeFw0yNDAyMjAxNzAwMzZaFw0zNDAyMTcxNzAwMzZaMIGPMRMwEQYKCZIm
        -iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ
        -RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290
        -IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwggEiMA0GCSqG
        -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEPyN7J9VGPyJcQmCBl5TGwfSzvVdWwoQU
        -j9aEsdfFJ6pBCDQSsj8Lv4RqL0dZra7h7SpZLLX/YZcnjikrYC+rP5OwsI9xEE/4
        -U98CsTBPhIMgqFK6SzNE5494BsAk4cL72dOOc8tX19oDS/PvBULbNkthQ0aAF1dg
        -vbrHvu7hq7LisB5ZRGHVE1k/AbCs2PaaKkn2jCw/b+U0Ml9qPuuEgz2mAqJDGYoA
        -WSR4YXrOcrmPuRqbws464YZbJW898/0Pn/U300ed+4YHiNYLLJp51AMkR4YEw969
        -VRPbWIvLrd0PQBooC/eLrL6rvud/GpYhdQEUx8qcNCKd4bz3OaQ5AgMBAAGjggEW
        -MIIBEjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQU
        -F4ffoFrrZhKn1dD4uhJFPLcrAJwwgc8GA1UdIwSBxzCBxIAUF4ffoFrrZhKn1dD4
        -uhJFPLcrAJyhgZWkgZIwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJ
        -k/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYD
        -VQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUg
        -Q29tIEluYy4gUm9vdCBDQYIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcN
        -AQELBQADggEBAL3Q3AHUhMiLUy6OlLSt8wX9I2oNGDKbBu0atpUNDztk/0s3YLQC
        -YuXgN4KrIcMXQIuAXCx407c+pIlT/T1FNn+VQXwi56PYzxQKtlpoKUL3oPQE1d0V
        -6EoiNk+6UodvyZqpdQu7fXVentRMk1QX7D9otmiiNuX+GSxJhJC2Lyzw65O9EUgG
        -1yVJon6RkUGtqBqKIuLksKwEr//ELnjmXit4LQKSnqKr0FTCB7seIrKJNyb35Qnq
        -qy9a/Unhokrmdda1tr6MbqU8l7HmxLuSd/Ky+L0eDNtYv6YfMewtjg0TtAnFyQov
        -rdXmeq1dy9HLo3Ds4AFz3Gx9076TxcRS/iI=
        ------END CERTIFICATE-----
        \ No newline at end of file
        diff --git a/sample-resource-plugin/src/test/resources/security/sample.pem b/sample-resource-plugin/src/test/resources/security/sample.pem
        deleted file mode 100644
        index 44101f0b37..0000000000
        --- a/sample-resource-plugin/src/test/resources/security/sample.pem
        +++ /dev/null
        @@ -1,25 +0,0 @@
        ------BEGIN CERTIFICATE-----
        -MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL
        -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt
        -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl
        -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v
        -dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT
        -AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl
        -MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA
        -A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud
        -yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0
        -HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr
        -XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n
        -dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD
        -ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R
        -BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA
        -AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF
        -BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo
        -wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ
        -KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz
        -pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi
        -7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh
        -hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L
        -camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg
        -PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg=
        ------END CERTIFICATE-----
        \ No newline at end of file
        diff --git a/sample-resource-plugin/src/test/resources/security/test-kirk.jks b/sample-resource-plugin/src/test/resources/security/test-kirk.jks
        deleted file mode 100644
        index 6c8c5ef77e20980f8c78295b159256b805da6a28..0000000000000000000000000000000000000000
        GIT binary patch
        literal 0
        HcmV?d00001
        
        literal 3766
        zcmd^=c{r47AIImJ%`(PV###wuU&o%k$xbMgr4m`Pk2Tv-j4?=zEwY?!X|aVw)I`=A
        zPAY52Rt6yODkPjhAQ%WsfbL*f;mp!-018Nf*#Q6sf)b!}Nv;s_8gzOC@mTmi+D9F}jyYkhL=#Xk3eYM2csmxKA&W!xAdE{tZ2mEGS
        z;L%QU`DHcrbdbw$3GsKUvmfQu0Z^?sH7B)!W)eLbG*fXB^G$&6CbCnj4~
        z*J>Rkut6vL1EvT!JqAq#X=O~#!JHQ#QVSPuOGlnLrXXB~{{FsGRq?o?I;>^GFEhMB
        zw;z!v1sXap8nq3zz&+prKs-DRPm*XsS4BaP6Z{8tM~n@m|rxMA=p6*i(w=7
        z*2&*Yg-uWU$5|W>>g5h)Fn{3B={`skAJ5_wXB5pDwyj{vG1_{{Y-`wB_i^B!5PA|=
        zrx=_>rprb&75BQ=J)SKPAJI;?(D#46)o+a?SsR^-&qJjXY2ER8S*1ZvU`t7~M6?NKULuzlAZ8C#X9>8j2;WDY
        z(TY-^!`&0%67`u|U_-Y(knWVcSlh-kwZQ6KG@S?L`W!iVl>Gyd(LnpMc@C!QeY{(E
        z)uAwF_CcqH#00}jer2dQk3}R|p^87XCxR8`n4c@g9rASTt9$8}SuGW!!+QQ&w&G!P
        zvv5Mft<&pzv^&XuuQAj&ieoa*3nI-hx}0`4kym=(cd>?v6yM3v43y@5@;yPeJ_N{@
        z622W$@5Z4VqliMF3GAf_RcB;$HX^%cwTCgxg^4)5I0?*&oW|giBB@nUNBO+IX=iON
        zo~;L}HOwhyeqH4GHvAQ5i=|0c+_5*661aDyT_tr=I#+Zog%!9nRiuBb8m&SS4qp2fv7HJMG
        zwJFuqV*Hoq3`|Mayml;So|9W4Um6Lu8(k+(Hc2}p@&>?!7!7H~9*O%@BrKNAOa-~e
        z$e6#G)fJ+wNz5x9zU;#>&V}d
        z?!F1W_eNN;&LI9$!kWa0Zqa)0CVM4D=x(r>aXgW=XQ)PTRsJJ&MC?WjjoMwLRh`-I
        z8yD|^&(r#NU|pRpRF%wn&t%X`)8HQe%uxEKnXxIu9yui1s$eH0*YZ^Wvt25yOg6{5
        zPefKstjqam-PRDz=&-BVb^xZe>{C{$cza!_sV&3M*l0ocMJVr!l~TlJi4JChDn9Nn
        zc&la1caY}0P&Ho=r;)l;mKBf$V<6A*R6XC}s98g%I7ZIAFI=e6SqQ4;oevw)nw0%^
        zKq9#$;{3R0zJv}#mr7@}e+5-(`{C?^vEE#xb7uBY=X#_1v+@~@l?W@Zaq+Yo9bpu&
        zR<0us_T`(Q6qp1xYb)Rq;tJ|aTZ&y5xqx<_j-|>1$SEi@3!A||
        z9YH<3ub_#ai=2WG_V9iQ!NU8mB|$4ZK3Gr>_s15;6W-XV-*##3TjwoMP&yb
        zq!L{!sQoUn<_ZWb)BbzloM2Zs1tb=+FBn*$!EQmp3Ml#oe;g0);^XP&_osni`NR1A
        z0SL>FG{F)8;h%d#4-g0eK+%&0UD-=ghUr~yDQ?!lNE5tKiJ_rjY{@`Q1vjbVAFU;|?Qs;w|1hFx_
        z`*jR7rVAU>9*yRSpD1)#aOb!)@ak(5hk;guG$_9)=K8Ie^uOP<63|FjrX2UEcJw07
        zD5c?bxHD${?)1+CMgPg@0|kH>4NzJZO*;#rl-xA_8*SHCS}ygKZP7*uHbRtmaTE%n
        zp7Vt7QIt|IIN?)fyS#8IxKHO$?TeY{DpQl5^kyAd$HH^Aa)SJC+I0!ULR
        znF7*z6R6~{CCW6M^qKuU!N`I`>YB3i6toA7f7#3%T&$5&wm0nY{&d9(g)LB$%g9dX
        zf>HfjVn9;)rG-^=)tiGDd<5M4wDHPl@yEGU_whSh78l$%S*WCqjvj^Xt?_VKp0T{pQGU!F;?_^4EMT$__$E
        zH0hMGQlo@W2p^_tPZsnirl@pGb<#0a^*g5ihYtSzKKx%Wg;i4h8B_c6Z+PPWM!I%g
        zOr-dLp|0@RV@@&InVrwRJfPT~ZY840gT$Jl4)HP^qcTUWE~1&}C2wS3Sv9pJWiRva
        zyK}a9ilnrYe7SB$bu~GF&GM`D1h@ukNsJY|Yt>|?q(4gzgSUuGwSIfsmlD)%J2V0@
        zTU&-58&x%P)-#Oev2~&}bv^wwRbD$?Enu(jJiuwM3shGOZ{$juY+RGk#m^`!p7+vO
        zAjWFn1{dq`T?N^TggHmN3~VGf^5?a_)R-cj5yfk-?V<|S)%uKn{YGL)7(~eAhWA56
        zj7ZS7amp#qQM;t>%6F)v{1S-Gq>88IPiL?2X9=q_r$vhc4{Pd3$WssBMbZaV2W
        zu&8||{U99-3!x+JudoA1KSAx^0qg$*YLr)FKtJ($lC@k)W?khPY!~B&3F~Xnxs_WH)b*(MC{~@>r={U4@A6+2p8il>0lojdT`r8~C>rA6;jw^lZK9gk<_y!v
        za(Rbclc{1;TFBtT`lr|YO0}|UXzh>FLsx6RQUq8=?V4{NR#=oxL2}kHb-ZAfuN
        Date: Fri, 10 Jan 2025 13:34:07 -0500
        Subject: [PATCH 075/201] Changes SPI dependency visibility
        
        Signed-off-by: Darshit Chanpura 
        ---
         build.gradle | 2 +-
         1 file changed, 1 insertion(+), 1 deletion(-)
        
        diff --git a/build.gradle b/build.gradle
        index 8e4148c23d..2124b0d9de 100644
        --- a/build.gradle
        +++ b/build.gradle
        @@ -574,7 +574,7 @@ tasks.integrationTest.finalizedBy(jacocoTestReport) // report is always generate
         check.dependsOn integrationTest
         
         dependencies {
        -    compileOnly project(path: ":opensearch-resource-sharing-spi")
        +    implementation project(path: ":opensearch-resource-sharing-spi")
             implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}"
             implementation "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}"
             implementation "org.apache.httpcomponents.client5:httpclient5-cache:${versions.httpclient5}"
        
        From 289659fcb4d7c3310c811db45415e954ef5c4087 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Fri, 10 Jan 2025 14:03:20 -0500
        Subject: [PATCH 076/201] Fixes CI errors
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../security/OpenSearchSecurityPlugin.java    |  1 +
         .../configuration/DlsFlsValveImpl.java        |  7 ++++++-
         .../resources/ResourceAccessHandler.java      | 21 ++++++++++++++++---
         3 files changed, 25 insertions(+), 4 deletions(-)
        
        diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        index bc8de0a6c3..211adc9319 100644
        --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        @@ -1284,6 +1284,7 @@ public Collection createComponents(
                 components.add(dcf);
                 components.add(userService);
                 components.add(passwordHasher);
        +        components.add(resourceAccessHandler);
         
                 components.add(sslSettingsManager);
                 if (isSslCertReloadEnabled(settings) && sslCertificatesHotReloadEnabled(settings)) {
        diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        index f9897baae0..c603f4c02f 100644
        --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        @@ -71,7 +71,12 @@
         import org.opensearch.security.privileges.DocumentAllowList;
         import org.opensearch.security.privileges.PrivilegesEvaluationContext;
         import org.opensearch.security.privileges.PrivilegesEvaluationException;
        -import org.opensearch.security.privileges.dlsfls.*;
        +import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext;
        +import org.opensearch.security.privileges.dlsfls.DlsFlsLegacyHeaders;
        +import org.opensearch.security.privileges.dlsfls.DlsFlsProcessedConfig;
        +import org.opensearch.security.privileges.dlsfls.DlsRestriction;
        +import org.opensearch.security.privileges.dlsfls.FieldMasking;
        +import org.opensearch.security.privileges.dlsfls.IndexToRuleMap;
         import org.opensearch.security.resolver.IndexResolverReplacer;
         import org.opensearch.security.resources.ResourceAccessHandler;
         import org.opensearch.security.securityconf.DynamicConfigFactory;
        diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        index 03e1cfc13e..38b689bfad 100644
        --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        @@ -12,7 +12,11 @@
         package org.opensearch.security.resources;
         
         import java.io.IOException;
        -import java.util.*;
        +import java.util.Collections;
        +import java.util.HashSet;
        +import java.util.List;
        +import java.util.Map;
        +import java.util.Set;
         
         import com.fasterxml.jackson.core.JsonProcessingException;
         import org.apache.logging.log4j.LogManager;
        @@ -21,7 +25,11 @@
         
         import org.opensearch.common.util.concurrent.ThreadContext;
         import org.opensearch.core.xcontent.NamedXContentRegistry;
        -import org.opensearch.index.query.*;
        +import org.opensearch.index.query.BoolQueryBuilder;
        +import org.opensearch.index.query.ConstantScoreQueryBuilder;
        +import org.opensearch.index.query.QueryBuilder;
        +import org.opensearch.index.query.QueryBuilders;
        +import org.opensearch.index.query.QueryShardContext;
         import org.opensearch.security.DefaultObjectMapper;
         import org.opensearch.security.OpenSearchSecurityPlugin;
         import org.opensearch.security.configuration.AdminDNs;
        @@ -51,7 +59,6 @@ public ResourceAccessHandler(
                 final ResourceSharingIndexHandler resourceSharingIndexHandler,
                 AdminDNs adminDns
             ) {
        -        super();
                 this.threadContext = threadPool.getThreadContext();
                 this.resourceSharingIndexHandler = resourceSharingIndexHandler;
                 this.adminDNs = adminDns;
        @@ -380,6 +387,14 @@ public Query createResourceDLSQuery(Set resourceIds, QueryShardContext q
                 return builder.toQuery(queryShardContext);
             }
         
        +    /**
        +     * Creates a DLS restriction for the given resource IDs.
        +     * @param resourceIds The resource IDs to create the restriction for.
        +     * @param xContentRegistry The named XContent registry.
        +     * @return The DLS restriction.
        +     * @throws JsonProcessingException If an error occurs while processing JSON.
        +     * @throws PrivilegesConfigurationValidationException If the privileges configuration is invalid.
        +     */
             public DlsRestriction createResourceDLSRestriction(Set resourceIds, NamedXContentRegistry xContentRegistry)
                 throws JsonProcessingException, PrivilegesConfigurationValidationException {
                 String jsonQuery = String.format(
        
        From 512c1a867fe93ea644fb66ed0243733f76312b69 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Fri, 10 Jan 2025 14:44:06 -0500
        Subject: [PATCH 077/201] Fixes tests running in SSL only mode
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../org/opensearch/security/OpenSearchSecurityPlugin.java  | 7 +++++--
         1 file changed, 5 insertions(+), 2 deletions(-)
        
        diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        index 211adc9319..68a1399cad 100644
        --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        @@ -2163,8 +2163,11 @@ public void onNodeStarted(DiscoveryNode localNode) {
                     cr.initOnNodeStart();
                 }
         
        -        // create resource sharing index if absent
        -        rmr.createResourceSharingIndexIfAbsent();
        +        // rmr will be null when sec plugin is disabled or is in SSLOnly mode, hence rmr will not be instantiated
        +        if (rmr != null) {
        +            // create resource sharing index if absent
        +            rmr.createResourceSharingIndexIfAbsent();
        +        }
         
                 final Set securityModules = ReflectionHelper.getModulesLoaded();
                 log.info("{} OpenSearch Security modules loaded so far: {}", securityModules.size(), securityModules);
        
        From 85c4556e52d7137ae1fe1a3da04b26a311708836 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Fri, 10 Jan 2025 15:33:57 -0500
        Subject: [PATCH 078/201] Fixes checkstyle errors
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../security/resources/CreatedByTests.java    |  5 +++--
         .../resources/RecipientTypeRegistryTests.java | 10 +++++----
         .../security/resources/ShareWithTests.java    | 21 +++++++++++--------
         3 files changed, 21 insertions(+), 15 deletions(-)
        
        diff --git a/src/test/java/org/opensearch/security/resources/CreatedByTests.java b/src/test/java/org/opensearch/security/resources/CreatedByTests.java
        index 6b183ccbc7..346a949444 100644
        --- a/src/test/java/org/opensearch/security/resources/CreatedByTests.java
        +++ b/src/test/java/org/opensearch/security/resources/CreatedByTests.java
        @@ -19,17 +19,18 @@
         import org.opensearch.core.common.io.stream.StreamOutput;
         import org.opensearch.core.xcontent.XContentBuilder;
         import org.opensearch.core.xcontent.XContentParser;
        -import org.opensearch.test.OpenSearchTestCase;
        +import org.opensearch.security.test.SingleClusterTest;
         
         import static org.hamcrest.Matchers.equalTo;
         import static org.hamcrest.Matchers.greaterThan;
         import static org.hamcrest.Matchers.is;
         import static org.hamcrest.Matchers.notNullValue;
         import static org.hamcrest.Matchers.nullValue;
        +import static org.junit.Assert.assertThrows;
         import static org.mockito.Mockito.mock;
         import static org.mockito.Mockito.when;
         
        -public class CreatedByTests extends OpenSearchTestCase {
        +public class CreatedByTests extends SingleClusterTest {
         
             private static final String CREATOR_TYPE = "user";
         
        diff --git a/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java b/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java
        index 394bae608e..47151898d1 100644
        --- a/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java
        +++ b/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java
        @@ -10,12 +10,14 @@
         
         import org.hamcrest.MatcherAssert;
         
        -import org.opensearch.test.OpenSearchTestCase;
        +import org.opensearch.security.test.SingleClusterTest;
         
         import static org.hamcrest.Matchers.equalTo;
         import static org.hamcrest.Matchers.is;
        +import static org.hamcrest.Matchers.notNullValue;
        +import static org.junit.Assert.assertThrows;
         
        -public class RecipientTypeRegistryTests extends OpenSearchTestCase {
        +public class RecipientTypeRegistryTests extends SingleClusterTest {
         
             public void testFromValue() {
                 RecipientTypeRegistry.registerRecipientType("ble1", new RecipientType("ble1"));
        @@ -23,8 +25,8 @@ public void testFromValue() {
         
                 // Valid Value
                 RecipientType type = RecipientTypeRegistry.fromValue("ble1");
        -        assertNotNull(type);
        -        assertEquals("ble1", type.getType());
        +        MatcherAssert.assertThat(type, notNullValue());
        +        MatcherAssert.assertThat(type.getType(), is(equalTo("ble1")));
         
                 // Invalid Value
                 IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> RecipientTypeRegistry.fromValue("bleble"));
        diff --git a/src/test/java/org/opensearch/security/resources/ShareWithTests.java b/src/test/java/org/opensearch/security/resources/ShareWithTests.java
        index 43b2b6f502..cec50a8198 100644
        --- a/src/test/java/org/opensearch/security/resources/ShareWithTests.java
        +++ b/src/test/java/org/opensearch/security/resources/ShareWithTests.java
        @@ -21,11 +21,12 @@
         import org.opensearch.common.xcontent.XContentType;
         import org.opensearch.common.xcontent.json.JsonXContent;
         import org.opensearch.core.common.io.stream.StreamOutput;
        +import org.opensearch.core.xcontent.NamedXContentRegistry;
         import org.opensearch.core.xcontent.ToXContent;
         import org.opensearch.core.xcontent.XContentBuilder;
         import org.opensearch.core.xcontent.XContentParser;
         import org.opensearch.security.spi.resources.ResourceAccessScope;
        -import org.opensearch.test.OpenSearchTestCase;
        +import org.opensearch.security.test.SingleClusterTest;
         
         import org.mockito.Mockito;
         
        @@ -33,6 +34,8 @@
         import static org.hamcrest.Matchers.empty;
         import static org.hamcrest.Matchers.equalTo;
         import static org.hamcrest.Matchers.is;
        +import static org.hamcrest.Matchers.notNullValue;
        +import static org.junit.Assert.assertThrows;
         import static org.mockito.Mockito.doThrow;
         import static org.mockito.Mockito.eq;
         import static org.mockito.Mockito.mock;
        @@ -40,7 +43,7 @@
         import static org.mockito.Mockito.verify;
         import static org.mockito.Mockito.when;
         
        -public class ShareWithTests extends OpenSearchTestCase {
        +public class ShareWithTests extends SingleClusterTest {
         
             @Before
             public void setupResourceRecipientTypes() {
        @@ -55,16 +58,16 @@ public void testFromXContentWhenCurrentTokenIsNotStartObject() throws IOExceptio
         
                 ShareWith shareWith = ShareWith.fromXContent(parser);
         
        -        assertNotNull(shareWith);
        +        MatcherAssert.assertThat(shareWith, notNullValue());
                 Set sharedWithScopes = shareWith.getSharedWithScopes();
        -        assertNotNull(sharedWithScopes);
        +        MatcherAssert.assertThat(sharedWithScopes, notNullValue());
                 MatcherAssert.assertThat(1, equalTo(sharedWithScopes.size()));
         
                 SharedWithScope scope = sharedWithScopes.iterator().next();
                 MatcherAssert.assertThat("read_only", equalTo(scope.getScope()));
         
                 SharedWithScope.ScopeRecipients scopeRecipients = scope.getSharedWithPerScope();
        -        assertNotNull(scopeRecipients);
        +        MatcherAssert.assertThat(scopeRecipients, notNullValue());
                 Map> recipients = scopeRecipients.getRecipients();
                 MatcherAssert.assertThat(recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.USERS.getName())).size(), is(1));
                 MatcherAssert.assertThat(recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.USERS.getName())), contains("user1"));
        @@ -77,11 +80,11 @@ public void testFromXContentWhenCurrentTokenIsNotStartObject() throws IOExceptio
         
             public void testFromXContentWithEmptyInput() throws IOException {
                 String emptyJson = "{}";
        -        XContentParser parser = XContentType.JSON.xContent().createParser(xContentRegistry(), null, emptyJson);
        +        XContentParser parser = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, null, emptyJson);
         
                 ShareWith result = ShareWith.fromXContent(parser);
         
        -        assertNotNull(result);
        +        MatcherAssert.assertThat(result, notNullValue());
                 MatcherAssert.assertThat(result.getSharedWithScopes(), is(empty()));
             }
         
        @@ -108,7 +111,7 @@ public void testFromXContentWithStartObject() throws IOException {
         
                 ShareWith shareWith = ShareWith.fromXContent(parser);
         
        -        assertNotNull(shareWith);
        +        MatcherAssert.assertThat(shareWith, notNullValue());
                 Set scopes = shareWith.getSharedWithScopes();
                 MatcherAssert.assertThat(scopes.size(), equalTo(2));
         
        @@ -152,7 +155,7 @@ public void testFromXContentWithUnexpectedEndOfInput() throws IOException {
         
                 ShareWith result = ShareWith.fromXContent(mockParser);
         
        -        assertNotNull(result);
        +        MatcherAssert.assertThat(result, notNullValue());
                 MatcherAssert.assertThat(result.getSharedWithScopes(), is(empty()));
             }
         
        
        From 3e6531d89d5f4d34ab54820d472a1820697d46a4 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Mon, 13 Jan 2025 11:46:10 -0500
        Subject: [PATCH 079/201] Adds featureFlag for resource sharing
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../security/SearchOperationTest.java         |   2 +
         .../security/OpenSearchSecurityPlugin.java    |  60 ++++--
         .../configuration/DlsFlsValveImpl.java        |  10 +-
         .../SecurityFlsDlsIndexSearcherWrapper.java   |  20 +-
         .../resources/ResourceAccessHandler.java      |   7 +
         .../ResourceSharingIndexHandler.java          |   8 +-
         ...ourceSharingIndexManagementRepository.java |  25 ++-
         .../security/support/ConfigConstants.java     | 185 +++++++++---------
         .../security/SlowIntegrationTests.java        |   1 +
         9 files changed, 194 insertions(+), 124 deletions(-)
        
        diff --git a/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java b/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java
        index cbb5ec11f0..adcd32f224 100644
        --- a/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java
        +++ b/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java
        @@ -143,6 +143,7 @@
         import static org.opensearch.security.Song.TITLE_POISON;
         import static org.opensearch.security.Song.TITLE_SONG_1_PLUS_1;
         import static org.opensearch.security.auditlog.impl.AuditCategory.INDEX_EVENT;
        +import static org.opensearch.security.support.ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED;
         import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
         import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;
         import static org.opensearch.test.framework.audit.AuditMessagePredicate.auditPredicate;
        @@ -383,6 +384,7 @@ public class SearchOperationTest {
                     new AuditConfiguration(true).compliance(new AuditCompliance().enabled(true))
                         .filters(new AuditFilters().enabledRest(true).enabledTransport(true))
                 )
        +        .nodeSettings(Map.of(OPENSEARCH_RESOURCE_SHARING_ENABLED, false))
                 .build();
         
             @Rule
        diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        index 68a1399cad..a65cc15f7d 100644
        --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        @@ -696,14 +696,19 @@ public List getRestHandlers(
                         );
         
                         // Adds rest handlers for resource-access-control actions
        -                handlers.addAll(
        -                    List.of(
        -                        new RestShareResourceAction(),
        -                        new RestRevokeResourceAccessAction(),
        -                        new RestListAccessibleResourcesAction(),
        -                        new RestVerifyResourceAccessAction()
        -                    )
        -                );
        +                if (settings.getAsBoolean(
        +                    ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED,
        +                    ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT
        +                )) {
        +                    handlers.addAll(
        +                        List.of(
        +                            new RestShareResourceAction(),
        +                            new RestRevokeResourceAccessAction(),
        +                            new RestListAccessibleResourcesAction(),
        +                            new RestVerifyResourceAccessAction()
        +                        )
        +                    );
        +                }
                         log.debug("Added {} rest handler(s)", handlers.size());
                     }
                 }
        @@ -733,14 +738,19 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre
                     actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class));
         
                     // Resource-access-control related actions
        -            actions.addAll(
        -                List.of(
        -                    new ActionHandler<>(ShareResourceAction.INSTANCE, TransportShareResourceAction.class),
        -                    new ActionHandler<>(RevokeResourceAccessAction.INSTANCE, TransportRevokeResourceAccessAction.class),
        -                    new ActionHandler<>(ListAccessibleResourcesAction.INSTANCE, TransportListAccessibleResourcesAction.class),
        -                    new ActionHandler<>(VerifyResourceAccessAction.INSTANCE, TransportVerifyResourceAccessAction.class)
        -                )
        -            );
        +            if (settings.getAsBoolean(
        +                ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED,
        +                ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT
        +            )) {
        +                actions.addAll(
        +                    List.of(
        +                        new ActionHandler<>(ShareResourceAction.INSTANCE, TransportShareResourceAction.class),
        +                        new ActionHandler<>(RevokeResourceAccessAction.INSTANCE, TransportRevokeResourceAccessAction.class),
        +                        new ActionHandler<>(ListAccessibleResourcesAction.INSTANCE, TransportListAccessibleResourcesAction.class),
        +                        new ActionHandler<>(VerifyResourceAccessAction.INSTANCE, TransportVerifyResourceAccessAction.class)
        +                    )
        +                );
        +            }
                 }
                 return actions;
             }
        @@ -1271,8 +1281,12 @@ public Collection createComponents(
                 );
                 resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns);
                 resourceAccessHandler.initializeRecipientTypes();
        -
        -        rmr = ResourceSharingIndexManagementRepository.create(rsIndexHandler);
        +        // Resource Sharing index is enabled by default
        +        boolean isResourceSharingEnabled = settings.getAsBoolean(
        +            ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED,
        +            ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT
        +        );
        +        rmr = ResourceSharingIndexManagementRepository.create(rsIndexHandler, isResourceSharingEnabled);
         
                 components.add(adminDns);
                 components.add(cr);
        @@ -2139,6 +2153,16 @@ public List> getSettings() {
         
                     // Privileges evaluation
                     settings.add(ActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE);
        +
        +            // Resource Sharing
        +            settings.add(
        +                Setting.boolSetting(
        +                    ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED,
        +                    ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT,
        +                    Property.NodeScope,
        +                    Property.Filtered
        +                )
        +            );
                 }
         
                 return settings;
        diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        index c603f4c02f..22a05edcd0 100644
        --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        @@ -101,6 +101,7 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve {
             private final FieldMasking.Config fieldMaskingConfig;
             private final Settings settings;
             private final ResourceAccessHandler resourceAccessHandler;
        +    private final boolean isResourceSharingEnabled;
         
             public DlsFlsValveImpl(
                 Settings settings,
        @@ -123,6 +124,10 @@ public DlsFlsValveImpl(
                 this.dlsFlsBaseContext = dlsFlsBaseContext;
                 this.settings = settings;
                 this.resourceAccessHandler = resourceAccessHandler;
        +        this.isResourceSharingEnabled = settings.getAsBoolean(
        +            ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED,
        +            ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT
        +        );
         
                 clusterService.addListener(event -> {
                     DlsFlsProcessedConfig config = dlsFlsProcessedConfig.get();
        @@ -390,11 +395,8 @@ public void handleSearchContext(SearchContext searchContext, ThreadPool threadPo
                     DlsRestriction dlsRestriction;
         
                     Set resourceIds;
        -            if (OpenSearchSecurityPlugin.getResourceIndices().contains(index)) {
        +            if (this.isResourceSharingEnabled && OpenSearchSecurityPlugin.getResourceIndices().contains(index)) {
                         resourceIds = this.resourceAccessHandler.getAccessibleResourceIdsForCurrentUser(index);
        -                if (resourceIds.isEmpty()) {
        -                    return;
        -                }
                         // Create a DLS restriction to filter search results with accessible resources only
                         dlsRestriction = this.resourceAccessHandler.createResourceDLSRestriction(resourceIds, namedXContentRegistry);
         
        diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java
        index e76fba0b56..662476928d 100644
        --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java
        +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java
        @@ -65,6 +65,7 @@ public class SecurityFlsDlsIndexSearcherWrapper extends SystemIndexSearcherWrapp
             private final Supplier dlsFlsProcessedConfigSupplier;
             private final DlsFlsBaseContext dlsFlsBaseContext;
             private final ResourceAccessHandler resourceAccessHandler;
        +    private final boolean isResourceSharingEnabled;
         
             public SecurityFlsDlsIndexSearcherWrapper(
                 final IndexService indexService,
        @@ -109,6 +110,10 @@ public SecurityFlsDlsIndexSearcherWrapper(
                 this.dlsFlsProcessedConfigSupplier = dlsFlsProcessedConfigSupplier;
                 this.dlsFlsBaseContext = dlsFlsBaseContext;
                 this.resourceAccessHandler = resourceAccessHandler;
        +        this.isResourceSharingEnabled = settings.getAsBoolean(
        +            ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED,
        +            ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT
        +        );
             }
         
             @SuppressWarnings("unchecked")
        @@ -123,9 +128,14 @@ protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdm
                 }
         
                 String indexName = shardId != null ? shardId.getIndexName() : null;
        -        Set resourceIds = null;
        -        if (!Strings.isNullOrEmpty(indexName) && OpenSearchSecurityPlugin.getResourceIndices().contains(indexName)) {
        +        Set resourceIds;
        +        if (this.isResourceSharingEnabled
        +            && !Strings.isNullOrEmpty(indexName)
        +            && OpenSearchSecurityPlugin.getResourceIndices().contains(indexName)) {
                     resourceIds = this.resourceAccessHandler.getAccessibleResourceIdsForCurrentUser(indexName);
        +            // resourceIds.isEmpty() indicates that the index is a resource index but the user does not have access to any resource under
        +            // the
        +            // index
                     if (resourceIds.isEmpty()) {
                         return new EmptyFilterLeafReader.EmptyDirectoryReader(reader);
                     }
        @@ -148,10 +158,7 @@ protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdm
                     );
                 }
         
        -        // resourceIds == null indicates that the index is not a resource index
        -        // resourceIds.isEmpty() indicates that the index is a resource index but the user does not have access to any resource under the
        -        // index
        -        if (isAdmin || privilegesEvaluationContext == null || resourceIds == null) {
        +        if (isAdmin || privilegesEvaluationContext == null) {
                     return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader(
                         reader,
                         FieldPrivileges.FlsRule.ALLOW_ALL,
        @@ -167,7 +174,6 @@ protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdm
                 }
         
                 try {
        -
                     DlsFlsProcessedConfig config = this.dlsFlsProcessedConfigSupplier.get();
                     DlsRestriction dlsRestriction;
         
        diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        index 38b689bfad..47eaf65791 100644
        --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        @@ -397,6 +397,13 @@ public Query createResourceDLSQuery(Set resourceIds, QueryShardContext q
              */
             public DlsRestriction createResourceDLSRestriction(Set resourceIds, NamedXContentRegistry xContentRegistry)
                 throws JsonProcessingException, PrivilegesConfigurationValidationException {
        +
        +        // resourceIds.isEmpty() is true when user doesn't have access to any resources
        +        if (resourceIds.isEmpty()) {
        +            LOGGER.debug("No resources found for user");
        +            return DlsRestriction.FULL;
        +        }
        +
                 String jsonQuery = String.format(
                     "{ \"bool\": { \"filter\": [ { \"terms\": { \"_id\": %s } } ] } }",
                     DefaultObjectMapper.writeValueAsString(resourceIds, true)
        diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java
        index 7d4a55b8ca..1847a6f3d1 100644
        --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java
        +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java
        @@ -123,12 +123,16 @@ public void createResourceSharingIndexIfAbsent(Callable callable) {
                     CreateIndexRequest cir = new CreateIndexRequest(resourceSharingIndex).settings(INDEX_SETTINGS).waitForActiveShards(1);
                     ActionListener cirListener = ActionListener.wrap(response -> {
                         LOGGER.info("Resource sharing index {} created.", resourceSharingIndex);
        -                callable.call();
        +                if (callable != null) {
        +                    callable.call();
        +                }
                     }, (failResponse) -> {
                         /* Index already exists, ignore and continue */
                         LOGGER.info("Index {} already exists.", resourceSharingIndex);
                         try {
        -                    callable.call();
        +                    if (callable != null) {
        +                        callable.call();
        +                    }
                         } catch (Exception e) {
                             throw new RuntimeException(e);
                         }
        diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java
        index 17f57269be..9ad7e18975 100644
        --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java
        +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java
        @@ -11,17 +11,29 @@
         
         package org.opensearch.security.resources;
         
        +import org.apache.logging.log4j.LogManager;
        +import org.apache.logging.log4j.Logger;
        +
         public class ResourceSharingIndexManagementRepository {
         
        +    private static final Logger log = LogManager.getLogger(ResourceSharingIndexManagementRepository.class);
        +
             private final ResourceSharingIndexHandler resourceSharingIndexHandler;
        +    private final boolean resourceSharingEnabled;
         
        -    protected ResourceSharingIndexManagementRepository(final ResourceSharingIndexHandler resourceSharingIndexHandler) {
        +    protected ResourceSharingIndexManagementRepository(
        +        final ResourceSharingIndexHandler resourceSharingIndexHandler,
        +        boolean isResourceSharingEnabled
        +    ) {
                 this.resourceSharingIndexHandler = resourceSharingIndexHandler;
        +        this.resourceSharingEnabled = isResourceSharingEnabled;
             }
         
        -    public static ResourceSharingIndexManagementRepository create(ResourceSharingIndexHandler resourceSharingIndexHandler) {
        -
        -        return new ResourceSharingIndexManagementRepository(resourceSharingIndexHandler);
        +    public static ResourceSharingIndexManagementRepository create(
        +        ResourceSharingIndexHandler resourceSharingIndexHandler,
        +        boolean isResourceSharingEnabled
        +    ) {
        +        return new ResourceSharingIndexManagementRepository(resourceSharingIndexHandler, isResourceSharingEnabled);
             }
         
             /**
        @@ -32,7 +44,10 @@ public static ResourceSharingIndexManagementRepository create(ResourceSharingInd
              */
             public void createResourceSharingIndexIfAbsent() {
                 // TODO check if this should be wrapped in an atomic completable future
        +        if (resourceSharingEnabled) {
        +            log.info("Attempting to create Resource Sharing index");
        +            this.resourceSharingIndexHandler.createResourceSharingIndexIfAbsent(() -> null);
        +        }
         
        -        this.resourceSharingIndexHandler.createResourceSharingIndexIfAbsent(() -> null);
             }
         }
        diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java
        index a510b79aed..dc0d68fea9 100644
        --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java
        +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java
        @@ -43,6 +43,7 @@
         public class ConfigConstants {
         
             public static final String OPENDISTRO_SECURITY_CONFIG_PREFIX = "_opendistro_security_";
        +    public static final String SECURITY_SETTINGS_PREFIX = "plugins.security.";
         
             public static final String OPENDISTRO_SECURITY_CHANNEL_TYPE = OPENDISTRO_SECURITY_CONFIG_PREFIX + "channel_type";
         
        @@ -126,11 +127,11 @@ public class ConfigConstants {
         
             public static final String OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX = ".opendistro_security";
         
        -    public static final String SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE = "plugins.security.enable_snapshot_restore_privilege";
        +    public static final String SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE = SECURITY_SETTINGS_PREFIX + "enable_snapshot_restore_privilege";
             public static final boolean SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE = true;
         
        -    public static final String SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES =
        -        "plugins.security.check_snapshot_restore_write_privileges";
        +    public static final String SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES = SECURITY_SETTINGS_PREFIX
        +        + "check_snapshot_restore_write_privileges";
             public static final boolean SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES = true;
             public static final Set SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES = Collections.unmodifiableSet(
                 new HashSet(Arrays.asList("indices:admin/create", "indices:data/write/index"
        @@ -138,37 +139,39 @@ public class ConfigConstants {
                 ))
             );
         
        -    public static final String SECURITY_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = "plugins.security.cert.intercluster_request_evaluator_class";
        +    public static final String SECURITY_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = SECURITY_SETTINGS_PREFIX
        +        + "cert.intercluster_request_evaluator_class";
             public static final String OPENDISTRO_SECURITY_ACTION_NAME = OPENDISTRO_SECURITY_CONFIG_PREFIX + "action_name";
         
        -    public static final String SECURITY_AUTHCZ_ADMIN_DN = "plugins.security.authcz.admin_dn";
        -    public static final String SECURITY_CONFIG_INDEX_NAME = "plugins.security.config_index_name";
        -    public static final String SECURITY_AUTHCZ_IMPERSONATION_DN = "plugins.security.authcz.impersonation_dn";
        -    public static final String SECURITY_AUTHCZ_REST_IMPERSONATION_USERS = "plugins.security.authcz.rest_impersonation_user";
        +    public static final String SECURITY_AUTHCZ_ADMIN_DN = SECURITY_SETTINGS_PREFIX + "authcz.admin_dn";
        +    public static final String SECURITY_CONFIG_INDEX_NAME = SECURITY_SETTINGS_PREFIX + "config_index_name";
        +    public static final String SECURITY_AUTHCZ_IMPERSONATION_DN = SECURITY_SETTINGS_PREFIX + "authcz.impersonation_dn";
        +    public static final String SECURITY_AUTHCZ_REST_IMPERSONATION_USERS = SECURITY_SETTINGS_PREFIX + "authcz.rest_impersonation_user";
         
             public static final String BCRYPT = "bcrypt";
             public static final String PBKDF2 = "pbkdf2";
         
        -    public static final String SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS = "plugins.security.password.hashing.bcrypt.rounds";
        +    public static final String SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS = SECURITY_SETTINGS_PREFIX + "password.hashing.bcrypt.rounds";
             public static final int SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS_DEFAULT = 12;
        -    public static final String SECURITY_PASSWORD_HASHING_BCRYPT_MINOR = "plugins.security.password.hashing.bcrypt.minor";
        +    public static final String SECURITY_PASSWORD_HASHING_BCRYPT_MINOR = SECURITY_SETTINGS_PREFIX + "password.hashing.bcrypt.minor";
             public static final String SECURITY_PASSWORD_HASHING_BCRYPT_MINOR_DEFAULT = "Y";
         
        -    public static final String SECURITY_PASSWORD_HASHING_ALGORITHM = "plugins.security.password.hashing.algorithm";
        +    public static final String SECURITY_PASSWORD_HASHING_ALGORITHM = SECURITY_SETTINGS_PREFIX + "password.hashing.algorithm";
             public static final String SECURITY_PASSWORD_HASHING_ALGORITHM_DEFAULT = BCRYPT;
        -    public static final String SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS = "plugins.security.password.hashing.pbkdf2.iterations";
        +    public static final String SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS = SECURITY_SETTINGS_PREFIX
        +        + "password.hashing.pbkdf2.iterations";
             public static final int SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS_DEFAULT = 600_000;
        -    public static final String SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH = "plugins.security.password.hashing.pbkdf2.length";
        +    public static final String SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH = SECURITY_SETTINGS_PREFIX + "password.hashing.pbkdf2.length";
             public static final int SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH_DEFAULT = 256;
        -    public static final String SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION = "plugins.security.password.hashing.pbkdf2.function";
        +    public static final String SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION = SECURITY_SETTINGS_PREFIX + "password.hashing.pbkdf2.function";
             public static final String SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION_DEFAULT = Hmac.SHA256.name();
         
        -    public static final String SECURITY_AUDIT_TYPE_DEFAULT = "plugins.security.audit.type";
        -    public static final String SECURITY_AUDIT_CONFIG_DEFAULT = "plugins.security.audit.config";
        -    public static final String SECURITY_AUDIT_CONFIG_ROUTES = "plugins.security.audit.routes";
        -    public static final String SECURITY_AUDIT_CONFIG_ENDPOINTS = "plugins.security.audit.endpoints";
        -    public static final String SECURITY_AUDIT_THREADPOOL_SIZE = "plugins.security.audit.threadpool.size";
        -    public static final String SECURITY_AUDIT_THREADPOOL_MAX_QUEUE_LEN = "plugins.security.audit.threadpool.max_queue_len";
        +    public static final String SECURITY_AUDIT_TYPE_DEFAULT = SECURITY_SETTINGS_PREFIX + "audit.type";
        +    public static final String SECURITY_AUDIT_CONFIG_DEFAULT = SECURITY_SETTINGS_PREFIX + "audit.config";
        +    public static final String SECURITY_AUDIT_CONFIG_ROUTES = SECURITY_SETTINGS_PREFIX + "audit.routes";
        +    public static final String SECURITY_AUDIT_CONFIG_ENDPOINTS = SECURITY_SETTINGS_PREFIX + "audit.endpoints";
        +    public static final String SECURITY_AUDIT_THREADPOOL_SIZE = SECURITY_SETTINGS_PREFIX + "audit.threadpool.size";
        +    public static final String SECURITY_AUDIT_THREADPOOL_MAX_QUEUE_LEN = SECURITY_SETTINGS_PREFIX + "audit.threadpool.max_queue_len";
             public static final String OPENDISTRO_SECURITY_AUDIT_LOG_REQUEST_BODY = "opendistro_security.audit.log_request_body";
             public static final String OPENDISTRO_SECURITY_AUDIT_RESOLVE_INDICES = "opendistro_security.audit.resolve_indices";
             public static final String OPENDISTRO_SECURITY_AUDIT_ENABLE_REST = "opendistro_security.audit.enable_rest";
        @@ -183,13 +186,13 @@ public class ConfigConstants {
             );
             public static final String OPENDISTRO_SECURITY_AUDIT_IGNORE_USERS = "opendistro_security.audit.ignore_users";
             public static final String OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS = "opendistro_security.audit.ignore_requests";
        -    public static final String SECURITY_AUDIT_IGNORE_HEADERS = "plugins.security.audit.ignore_headers";
        +    public static final String SECURITY_AUDIT_IGNORE_HEADERS = SECURITY_SETTINGS_PREFIX + "audit.ignore_headers";
             public static final String OPENDISTRO_SECURITY_AUDIT_RESOLVE_BULK_REQUESTS = "opendistro_security.audit.resolve_bulk_requests";
             public static final boolean OPENDISTRO_SECURITY_AUDIT_SSL_VERIFY_HOSTNAMES_DEFAULT = true;
             public static final boolean OPENDISTRO_SECURITY_AUDIT_SSL_ENABLE_SSL_CLIENT_AUTH_DEFAULT = false;
             public static final String OPENDISTRO_SECURITY_AUDIT_EXCLUDE_SENSITIVE_HEADERS = "opendistro_security.audit.exclude_sensitive_headers";
         
        -    public static final String SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX = "plugins.security.audit.config.";
        +    public static final String SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX = SECURITY_SETTINGS_PREFIX + "audit.config.";
         
             // Internal Opensearch data_stream
             public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_NAME = "data_stream.name";
        @@ -232,31 +235,31 @@ public class ConfigConstants {
             public static final String SECURITY_AUDIT_LOG4J_LEVEL = "log4j.level";
         
             // retry
        -    public static final String SECURITY_AUDIT_RETRY_COUNT = "plugins.security.audit.config.retry_count";
        -    public static final String SECURITY_AUDIT_RETRY_DELAY_MS = "plugins.security.audit.config.retry_delay_ms";
        +    public static final String SECURITY_AUDIT_RETRY_COUNT = SECURITY_SETTINGS_PREFIX + "audit.config.retry_count";
        +    public static final String SECURITY_AUDIT_RETRY_DELAY_MS = SECURITY_SETTINGS_PREFIX + "audit.config.retry_delay_ms";
         
        -    public static final String SECURITY_KERBEROS_KRB5_FILEPATH = "plugins.security.kerberos.krb5_filepath";
        -    public static final String SECURITY_KERBEROS_ACCEPTOR_KEYTAB_FILEPATH = "plugins.security.kerberos.acceptor_keytab_filepath";
        -    public static final String SECURITY_KERBEROS_ACCEPTOR_PRINCIPAL = "plugins.security.kerberos.acceptor_principal";
        -    public static final String SECURITY_CERT_OID = "plugins.security.cert.oid";
        -    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_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_KERBEROS_KRB5_FILEPATH = SECURITY_SETTINGS_PREFIX + "kerberos.krb5_filepath";
        +    public static final String SECURITY_KERBEROS_ACCEPTOR_KEYTAB_FILEPATH = SECURITY_SETTINGS_PREFIX + "kerberos.acceptor_keytab_filepath";
        +    public static final String SECURITY_KERBEROS_ACCEPTOR_PRINCIPAL = SECURITY_SETTINGS_PREFIX + "kerberos.acceptor_principal";
        +    public static final String SECURITY_CERT_OID = SECURITY_SETTINGS_PREFIX + "cert.oid";
        +    public static final String SECURITY_CERT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = SECURITY_SETTINGS_PREFIX
        +        + "cert.intercluster_request_evaluator_class";
        +    public static final String SECURITY_ADVANCED_MODULES_ENABLED = SECURITY_SETTINGS_PREFIX + "advanced_modules_enabled";
        +    public static final String SECURITY_NODES_DN = SECURITY_SETTINGS_PREFIX + "nodes_dn";
        +    public static final String SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED = SECURITY_SETTINGS_PREFIX + "nodes_dn_dynamic_config_enabled";
        +    public static final String SECURITY_DISABLED = SECURITY_SETTINGS_PREFIX + "disabled";
         
        -    public static final String SECURITY_CACHE_TTL_MINUTES = "plugins.security.cache.ttl_minutes";
        -    public static final String SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES = "plugins.security.allow_unsafe_democertificates";
        -    public static final String SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX = "plugins.security.allow_default_init_securityindex";
        +    public static final String SECURITY_CACHE_TTL_MINUTES = SECURITY_SETTINGS_PREFIX + "cache.ttl_minutes";
        +    public static final String SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES = SECURITY_SETTINGS_PREFIX + "allow_unsafe_democertificates";
        +    public static final String SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX = SECURITY_SETTINGS_PREFIX + "allow_default_init_securityindex";
         
        -    public static final String SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE =
        -        "plugins.security.allow_default_init_securityindex.use_cluster_state";
        +    public static final String SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE = SECURITY_SETTINGS_PREFIX
        +        + "allow_default_init_securityindex.use_cluster_state";
         
        -    public static final String SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST =
        -        "plugins.security.background_init_if_securityindex_not_exist";
        +    public static final String SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST = SECURITY_SETTINGS_PREFIX
        +        + "background_init_if_securityindex_not_exist";
         
        -    public static final String SECURITY_ROLES_MAPPING_RESOLUTION = "plugins.security.roles_mapping_resolution";
        +    public static final String SECURITY_ROLES_MAPPING_RESOLUTION = SECURITY_SETTINGS_PREFIX + "roles_mapping_resolution";
         
             public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_METADATA_ONLY =
                 "opendistro_security.compliance.history.write.metadata_only";
        @@ -275,21 +278,22 @@ public class ConfigConstants {
             public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED =
                 "opendistro_security.compliance.history.external_config_enabled";
             public static final String OPENDISTRO_SECURITY_SOURCE_FIELD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "source_field_context";
        -    public static final String SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION =
        -        "plugins.security.compliance.disable_anonymous_authentication";
        -    public static final String SECURITY_COMPLIANCE_IMMUTABLE_INDICES = "plugins.security.compliance.immutable_indices";
        -    public static final String SECURITY_COMPLIANCE_SALT = "plugins.security.compliance.salt";
        +    public static final String SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION = SECURITY_SETTINGS_PREFIX
        +        + "compliance.disable_anonymous_authentication";
        +    public static final String SECURITY_COMPLIANCE_IMMUTABLE_INDICES = SECURITY_SETTINGS_PREFIX + "compliance.immutable_indices";
        +    public static final String SECURITY_COMPLIANCE_SALT = SECURITY_SETTINGS_PREFIX + "compliance.salt";
             public static final String SECURITY_COMPLIANCE_SALT_DEFAULT = "e1ukloTsQlOgPquJ";// 16 chars
             public static final String SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED =
                 "opendistro_security.compliance.history.internal_config_enabled";
        -    public static final String SECURITY_SSL_ONLY = "plugins.security.ssl_only";
        +    public static final String SECURITY_SSL_ONLY = SECURITY_SETTINGS_PREFIX + "ssl_only";
             public static final String SECURITY_CONFIG_SSL_DUAL_MODE_ENABLED = "plugins.security_config.ssl_dual_mode_enabled";
             public static final String SECURITY_SSL_DUAL_MODE_SKIP_SECURITY = OPENDISTRO_SECURITY_CONFIG_PREFIX + "passive_security";
             public static final String LEGACY_OPENDISTRO_SECURITY_CONFIG_SSL_DUAL_MODE_ENABLED = "opendistro_security_config.ssl_dual_mode_enabled";
        -    public static final String SECURITY_SSL_CERT_RELOAD_ENABLED = "plugins.security.ssl_cert_reload_enabled";
        -    public static final String SECURITY_SSL_CERTIFICATES_HOT_RELOAD_ENABLED = "plugins.security.ssl.certificates_hot_reload.enabled";
        -    public static final String SECURITY_DISABLE_ENVVAR_REPLACEMENT = "plugins.security.disable_envvar_replacement";
        -    public static final String SECURITY_DFM_EMPTY_OVERRIDES_ALL = "plugins.security.dfm_empty_overrides_all";
        +    public static final String SECURITY_SSL_CERT_RELOAD_ENABLED = SECURITY_SETTINGS_PREFIX + "ssl_cert_reload_enabled";
        +    public static final String SECURITY_SSL_CERTIFICATES_HOT_RELOAD_ENABLED = SECURITY_SETTINGS_PREFIX
        +        + "ssl.certificates_hot_reload.enabled";
        +    public static final String SECURITY_DISABLE_ENVVAR_REPLACEMENT = SECURITY_SETTINGS_PREFIX + "disable_envvar_replacement";
        +    public static final String SECURITY_DFM_EMPTY_OVERRIDES_ALL = SECURITY_SETTINGS_PREFIX + "dfm_empty_overrides_all";
         
             public enum RolesMappingResolution {
                 MAPPING_ONLY,
        @@ -297,44 +301,46 @@ public enum RolesMappingResolution {
                 BOTH
             }
         
        -    public static final String SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS = "plugins.security.filter_securityindex_from_all_requests";
        -    public static final String SECURITY_DLS_MODE = "plugins.security.dls.mode";
        +    public static final String SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS = SECURITY_SETTINGS_PREFIX
        +        + "filter_securityindex_from_all_requests";
        +    public static final String SECURITY_DLS_MODE = SECURITY_SETTINGS_PREFIX + "dls.mode";
             // REST API
        -    public static final String SECURITY_RESTAPI_ROLES_ENABLED = "plugins.security.restapi.roles_enabled";
        -    public static final String SECURITY_RESTAPI_ADMIN_ENABLED = "plugins.security.restapi.admin.enabled";
        -    public static final String SECURITY_RESTAPI_ENDPOINTS_DISABLED = "plugins.security.restapi.endpoints_disabled";
        -    public static final String SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX = "plugins.security.restapi.password_validation_regex";
        -    public static final String SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE =
        -        "plugins.security.restapi.password_validation_error_message";
        -    public static final String SECURITY_RESTAPI_PASSWORD_MIN_LENGTH = "plugins.security.restapi.password_min_length";
        -    public static final String SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH =
        -        "plugins.security.restapi.password_score_based_validation_strength";
        +    public static final String SECURITY_RESTAPI_ROLES_ENABLED = SECURITY_SETTINGS_PREFIX + "restapi.roles_enabled";
        +    public static final String SECURITY_RESTAPI_ADMIN_ENABLED = SECURITY_SETTINGS_PREFIX + "restapi.admin.enabled";
        +    public static final String SECURITY_RESTAPI_ENDPOINTS_DISABLED = SECURITY_SETTINGS_PREFIX + "restapi.endpoints_disabled";
        +    public static final String SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX = SECURITY_SETTINGS_PREFIX + "restapi.password_validation_regex";
        +    public static final String SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE = SECURITY_SETTINGS_PREFIX
        +        + "restapi.password_validation_error_message";
        +    public static final String SECURITY_RESTAPI_PASSWORD_MIN_LENGTH = SECURITY_SETTINGS_PREFIX + "restapi.password_min_length";
        +    public static final String SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH = SECURITY_SETTINGS_PREFIX
        +        + "restapi.password_score_based_validation_strength";
             // Illegal Opcodes from here on
        -    public static final String SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY =
        -        "plugins.security.unsupported.disable_rest_auth_initially";
        -    public static final String SECURITY_UNSUPPORTED_DELAY_INITIALIZATION_SECONDS =
        -        "plugins.security.unsupported.delay_initialization_seconds";
        -    public static final String SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY =
        -        "plugins.security.unsupported.disable_intertransport_auth_initially";
        -    public static final String SECURITY_UNSUPPORTED_PASSIVE_INTERTRANSPORT_AUTH_INITIALLY =
        -        "plugins.security.unsupported.passive_intertransport_auth_initially";
        -    public static final String SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED =
        -        "plugins.security.unsupported.restore.securityindex.enabled";
        -    public static final String SECURITY_UNSUPPORTED_INJECT_USER_ENABLED = "plugins.security.unsupported.inject_user.enabled";
        -    public static final String SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED = "plugins.security.unsupported.inject_user.admin.enabled";
        -    public static final String SECURITY_UNSUPPORTED_ALLOW_NOW_IN_DLS = "plugins.security.unsupported.allow_now_in_dls";
        -
        -    public static final String SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION =
        -        "plugins.security.unsupported.restapi.allow_securityconfig_modification";
        -    public static final String SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES = "plugins.security.unsupported.load_static_resources";
        -    public static final String SECURITY_UNSUPPORTED_ACCEPT_INVALID_CONFIG = "plugins.security.unsupported.accept_invalid_config";
        +    public static final String SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY = SECURITY_SETTINGS_PREFIX
        +        + "unsupported.disable_rest_auth_initially";
        +    public static final String SECURITY_UNSUPPORTED_DELAY_INITIALIZATION_SECONDS = SECURITY_SETTINGS_PREFIX
        +        + "unsupported.delay_initialization_seconds";
        +    public static final String SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY = SECURITY_SETTINGS_PREFIX
        +        + "unsupported.disable_intertransport_auth_initially";
        +    public static final String SECURITY_UNSUPPORTED_PASSIVE_INTERTRANSPORT_AUTH_INITIALLY = SECURITY_SETTINGS_PREFIX
        +        + "unsupported.passive_intertransport_auth_initially";
        +    public static final String SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED = SECURITY_SETTINGS_PREFIX
        +        + "unsupported.restore.securityindex.enabled";
        +    public static final String SECURITY_UNSUPPORTED_INJECT_USER_ENABLED = SECURITY_SETTINGS_PREFIX + "unsupported.inject_user.enabled";
        +    public static final String SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED = SECURITY_SETTINGS_PREFIX
        +        + "unsupported.inject_user.admin.enabled";
        +    public static final String SECURITY_UNSUPPORTED_ALLOW_NOW_IN_DLS = SECURITY_SETTINGS_PREFIX + "unsupported.allow_now_in_dls";
        +
        +    public static final String SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION = SECURITY_SETTINGS_PREFIX
        +        + "unsupported.restapi.allow_securityconfig_modification";
        +    public static final String SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES = SECURITY_SETTINGS_PREFIX + "unsupported.load_static_resources";
        +    public static final String SECURITY_UNSUPPORTED_ACCEPT_INVALID_CONFIG = SECURITY_SETTINGS_PREFIX + "unsupported.accept_invalid_config";
         
             // Protected indices settings. Marked for deprecation, after all config indices move to System indices.
        -    public static final String SECURITY_PROTECTED_INDICES_ENABLED_KEY = "plugins.security.protected_indices.enabled";
        +    public static final String SECURITY_PROTECTED_INDICES_ENABLED_KEY = SECURITY_SETTINGS_PREFIX + "protected_indices.enabled";
             public static final Boolean SECURITY_PROTECTED_INDICES_ENABLED_DEFAULT = false;
        -    public static final String SECURITY_PROTECTED_INDICES_KEY = "plugins.security.protected_indices.indices";
        +    public static final String SECURITY_PROTECTED_INDICES_KEY = SECURITY_SETTINGS_PREFIX + "protected_indices.indices";
             public static final List SECURITY_PROTECTED_INDICES_DEFAULT = Collections.emptyList();
        -    public static final String SECURITY_PROTECTED_INDICES_ROLES_KEY = "plugins.security.protected_indices.roles";
        +    public static final String SECURITY_PROTECTED_INDICES_ROLES_KEY = SECURITY_SETTINGS_PREFIX + "protected_indices.roles";
             public static final List SECURITY_PROTECTED_INDICES_ROLES_DEFAULT = Collections.emptyList();
         
             // Roles injection for plugins
        @@ -348,19 +354,20 @@ public enum RolesMappingResolution {
         
             // System indices settings
             public static final String SYSTEM_INDEX_PERMISSION = "system:admin/system_index";
        -    public static final String SECURITY_SYSTEM_INDICES_ENABLED_KEY = "plugins.security.system_indices.enabled";
        +    public static final String SECURITY_SYSTEM_INDICES_ENABLED_KEY = SECURITY_SETTINGS_PREFIX + "system_indices.enabled";
             public static final Boolean SECURITY_SYSTEM_INDICES_ENABLED_DEFAULT = false;
        -    public static final String SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY = "plugins.security.system_indices.permission.enabled";
        +    public static final String SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY = SECURITY_SETTINGS_PREFIX
        +        + "system_indices.permission.enabled";
             public static final Boolean SECURITY_SYSTEM_INDICES_PERMISSIONS_DEFAULT = false;
        -    public static final String SECURITY_SYSTEM_INDICES_KEY = "plugins.security.system_indices.indices";
        +    public static final String SECURITY_SYSTEM_INDICES_KEY = SECURITY_SETTINGS_PREFIX + "system_indices.indices";
             public static final List SECURITY_SYSTEM_INDICES_DEFAULT = Collections.emptyList();
        -    public static final String SECURITY_MASKED_FIELDS_ALGORITHM_DEFAULT = "plugins.security.masked_fields.algorithm.default";
        +    public static final String SECURITY_MASKED_FIELDS_ALGORITHM_DEFAULT = SECURITY_SETTINGS_PREFIX + "masked_fields.algorithm.default";
         
             public static final String TENANCY_PRIVATE_TENANT_NAME = "private";
             public static final String TENANCY_GLOBAL_TENANT_NAME = "global";
             public static final String TENANCY_GLOBAL_TENANT_DEFAULT_NAME = "";
         
        -    public static final String USE_JDK_SERIALIZATION = "plugins.security.use_jdk_serialization";
        +    public static final String USE_JDK_SERIALIZATION = SECURITY_SETTINGS_PREFIX + "use_jdk_serialization";
         
             // On-behalf-of endpoints settings
             // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings
        @@ -373,6 +380,8 @@ public enum RolesMappingResolution {
         
             // Resource sharing index
             public static final String OPENSEARCH_RESOURCE_SHARING_INDEX = ".opensearch_resource_sharing";
        +    public static final String OPENSEARCH_RESOURCE_SHARING_ENABLED = SECURITY_SETTINGS_PREFIX + "resource_sharing.enabled";
        +    public static final boolean OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT = false;
         
             public static Set getSettingAsSet(
                 final Settings settings,
        diff --git a/src/test/java/org/opensearch/security/SlowIntegrationTests.java b/src/test/java/org/opensearch/security/SlowIntegrationTests.java
        index 74e3bfa9e4..99ac9fb1b9 100644
        --- a/src/test/java/org/opensearch/security/SlowIntegrationTests.java
        +++ b/src/test/java/org/opensearch/security/SlowIntegrationTests.java
        @@ -66,6 +66,7 @@ public void testCustomInterclusterRequestEvaluator() throws Exception {
                         ConfigConstants.SECURITY_INTERCLUSTER_REQUEST_EVALUATOR_CLASS,
                         "org.opensearch.security.AlwaysFalseInterClusterRequestEvaluator"
                     )
        +            .put(ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, false)
                     .put("discovery.initial_state_timeout", "8s")
                     .build();
                 setup(Settings.EMPTY, null, settings, false, ClusterConfiguration.DEFAULT, 5, 1);
        
        From 5cab8af6ea51234c51784ffd8282900634de296d Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Mon, 13 Jan 2025 15:04:07 -0500
        Subject: [PATCH 080/201] Fixes spotbugsintegtest error
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../security/DoNotFailOnForbiddenTests.java      | 16 ++++++++++------
         1 file changed, 10 insertions(+), 6 deletions(-)
        
        diff --git a/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java b/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java
        index 456d1ebada..4aa6005beb 100644
        --- a/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java
        +++ b/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java
        @@ -12,6 +12,7 @@
         import java.io.BufferedReader;
         import java.io.IOException;
         import java.io.InputStreamReader;
        +import java.nio.charset.StandardCharsets;
         import java.util.List;
         import java.util.stream.Collectors;
         
        @@ -462,8 +463,9 @@ public void shouldPerformCatIndices_positive() throws IOException {
                     Request getIndicesRequest = new Request("GET", "/_cat/indices");
                     // High level client doesn't support _cat/_indices API
                     Response getIndicesResponse = restHighLevelClient.getLowLevelClient().performRequest(getIndicesRequest);
        -            List indexes = new BufferedReader(new InputStreamReader(getIndicesResponse.getEntity().getContent())).lines()
        -                .collect(Collectors.toList());
        +            List indexes = new BufferedReader(
        +                new InputStreamReader(getIndicesResponse.getEntity().getContent(), StandardCharsets.UTF_8)
        +            ).lines().collect(Collectors.toList());
         
                     assertThat(indexes.size(), equalTo(1));
                     assertThat(indexes.get(0), containsString("marvelous_songs"));
        @@ -476,8 +478,9 @@ public void shouldPerformCatAliases_positive() throws IOException {
                 try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) {
                     Request getAliasesRequest = new Request("GET", "/_cat/aliases");
                     Response getAliasesResponse = restHighLevelClient.getLowLevelClient().performRequest(getAliasesRequest);
        -            List aliases = new BufferedReader(new InputStreamReader(getAliasesResponse.getEntity().getContent())).lines()
        -                .collect(Collectors.toList());
        +            List aliases = new BufferedReader(
        +                new InputStreamReader(getAliasesResponse.getEntity().getContent(), StandardCharsets.UTF_8)
        +            ).lines().collect(Collectors.toList());
         
                     // Does not fail on forbidden, but alias response only contains index which user has access to
                     assertThat(getAliasesResponse.getStatusLine().getStatusCode(), equalTo(200));
        @@ -490,8 +493,9 @@ public void shouldPerformCatAliases_positive() throws IOException {
                 try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) {
                     Request getAliasesRequest = new Request("GET", "/_cat/aliases");
                     Response getAliasesResponse = restHighLevelClient.getLowLevelClient().performRequest(getAliasesRequest);
        -            List aliases = new BufferedReader(new InputStreamReader(getAliasesResponse.getEntity().getContent())).lines()
        -                .collect(Collectors.toList());
        +            List aliases = new BufferedReader(
        +                new InputStreamReader(getAliasesResponse.getEntity().getContent(), StandardCharsets.UTF_8)
        +            ).lines().collect(Collectors.toList());
         
                     // Admin has access to all
                     assertThat(getAliasesResponse.getStatusLine().getStatusCode(), equalTo(200));
        
        From 8366a059cdf181cc8b59ebdce50d37589c2a5199 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Mon, 13 Jan 2025 15:47:40 -0500
        Subject: [PATCH 081/201] Fixes SafeSerializationUtils test
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../security/support/SafeSerializationUtilsTest.java        | 6 ++++++
         1 file changed, 6 insertions(+)
        
        diff --git a/src/test/java/org/opensearch/security/support/SafeSerializationUtilsTest.java b/src/test/java/org/opensearch/security/support/SafeSerializationUtilsTest.java
        index f69d4e0291..187fd8b372 100644
        --- a/src/test/java/org/opensearch/security/support/SafeSerializationUtilsTest.java
        +++ b/src/test/java/org/opensearch/security/support/SafeSerializationUtilsTest.java
        @@ -17,6 +17,7 @@
         import java.util.HashMap;
         import java.util.regex.Pattern;
         
        +import org.junit.After;
         import org.junit.Test;
         
         import org.opensearch.security.auth.UserInjector;
        @@ -35,6 +36,11 @@
         
         public class SafeSerializationUtilsTest {
         
        +    @After
        +    public void clearCache() {
        +        SafeSerializationUtils.safeClassCache.clear();
        +    }
        +
             @Test
             public void testSafeClasses() {
                 assertTrue(SafeSerializationUtils.isSafeClass(String.class));
        
        From 534838f235fe9456410f1800aa85bb6009cdfb12 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Mon, 13 Jan 2025 16:36:10 -0500
        Subject: [PATCH 082/201] Fix CI workflow that called assemble instead of
         :assemble and adds SPI to maven publish task and updates SPI readme
        
        Signed-off-by: Darshit Chanpura 
        ---
         .github/actions/create-bwc-build/action.yaml |   2 +-
         .github/workflows/ci.yml                     |  10 +-
         .github/workflows/maven-publish.yml          |   3 +-
         .github/workflows/plugin_install.yml         |   2 +-
         spi/README.md                                | 145 +------------------
         spi/build.gradle                             |   9 +-
         6 files changed, 19 insertions(+), 152 deletions(-)
        
        diff --git a/.github/actions/create-bwc-build/action.yaml b/.github/actions/create-bwc-build/action.yaml
        index 8960849333..0f9e373b16 100644
        --- a/.github/actions/create-bwc-build/action.yaml
        +++ b/.github/actions/create-bwc-build/action.yaml
        @@ -42,7 +42,7 @@ runs:
               uses: gradle/gradle-build-action@v2
               with:
                 cache-disabled: true
        -        arguments: assemble
        +        arguments: :assemble
                 build-root-directory: ${{ inputs.plugin-branch }}
         
             - id: get-opensearch-version
        diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
        index 5a41062883..9919075cc6 100644
        --- a/.github/workflows/ci.yml
        +++ b/.github/workflows/ci.yml
        @@ -208,7 +208,7 @@ jobs:
             - uses: github/codeql-action/init@v3
               with:
                 languages: java
        -    - run: ./gradlew clean assemble
        +    - run: ./gradlew clean :assemble
             - uses: github/codeql-action/analyze@v3
         
           build-artifact-names:
        @@ -238,13 +238,13 @@ jobs:
                 echo ${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}
                 echo ${{ env.TEST_QUALIFIER }}
         
        -    - run: ./gradlew clean assemble && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip
        +    - run: ./gradlew clean :assemble && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip
         
        -    - run: ./gradlew clean assemble -Dbuild.snapshot=false && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_NO_SNAPSHOT }}.zip
        +    - run: ./gradlew clean :assemble -Dbuild.snapshot=false && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_NO_SNAPSHOT }}.zip
         
        -    - run: ./gradlew clean assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}.zip
        +    - run: ./gradlew clean :assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}.zip
         
        -    - run: ./gradlew clean assemble -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}-SNAPSHOT.zip
        +    - run: ./gradlew clean :assemble -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}-SNAPSHOT.zip
         
             - run: ./gradlew clean publishPluginZipPublicationToZipStagingRepository && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.pom
         
        diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml
        index d10fd67beb..2d4e7e1df0 100644
        --- a/.github/workflows/maven-publish.yml
        +++ b/.github/workflows/maven-publish.yml
        @@ -32,4 +32,5 @@ jobs:
                   export SONATYPE_PASSWORD=$(aws secretsmanager get-secret-value --secret-id maven-snapshots-password --query SecretString --output text)
                   echo "::add-mask::$SONATYPE_USERNAME"
                   echo "::add-mask::$SONATYPE_PASSWORD"
        -          ./gradlew publishPluginZipPublicationToSnapshotsRepository
        +          ./gradlew --no-daemon publishPluginZipPublicationToSnapshotsRepository
        +          ./gradlew --no-daemon :opensearch-resource-sharing-spi:publishMavenJavaPublicationToSnapshotsRepository
        diff --git a/.github/workflows/plugin_install.yml b/.github/workflows/plugin_install.yml
        index 3f8d61795c..c427b160c4 100644
        --- a/.github/workflows/plugin_install.yml
        +++ b/.github/workflows/plugin_install.yml
        @@ -32,7 +32,7 @@ jobs:
                 uses: gradle/gradle-build-action@v3
                 with:
                   cache-disabled: true
        -          arguments: assemble
        +          arguments: :assemble
         
               # Move and rename the plugin for installation
               - name: Move and rename the plugin for installation
        diff --git a/spi/README.md b/spi/README.md
        index ccd73db983..38efb1cf85 100644
        --- a/spi/README.md
        +++ b/spi/README.md
        @@ -1,147 +1,6 @@
        -# Resource Sharing and Access Control Plugin
        +# Resource Sharing and Access Control SPI
         
        -This plugin demonstrates resource sharing and access control functionality, providing APIs to create, manage, and verify access to resources. The plugin enables fine-grained permissions for sharing and accessing resources, making it suitable for systems requiring robust security and collaboration.
        -
        -## Features
        -
        -- Create and delete resources.
        -- Share resources with specific users, roles and/or backend_roles with specific scope(s).
        -- Revoke access to shared resources for a list of or all scopes.
        -- Verify access permissions for a given user within a given scope.
        -- List all resources accessible to current user.
        -
        -## API Endpoints
        -
        -The plugin exposes the following six API endpoints:
        -
        -### 1. Create Resource
        -- **Endpoint:** `POST /_plugins/sample_resource_sharing/create`
        -- **Description:** Creates a new resource. Also creates a resource sharing entry if security plugin is enabled.
        -- **Request Body:**
        -  ```json
        -  {
        -    "name": ""
        -  }
        -  ```
        -- **Response:**
        -  ```json
        -  {
        -    "message": "Resource  created successfully."
        -  }
        -  ```
        -
        -### 2. Delete Resource
        -- **Endpoint:** `DELETE /_plugins/sample_resource_sharing/{resource_id}`
        -- **Description:** Deletes a specified resource owned by the requesting user.
        -- **Response:**
        -  ```json
        -  {
        -    "message": "Resource  deleted successfully."
        -  }
        -  ```
        -
        -### 3. Share Resource
        -- **Endpoint:** `POST /_plugins/sample_resource_sharing/share`
        -- **Description:** Shares a resource with specified users or roles with defined scope.
        -- **Request Body:**
        -  ```json
        -    {
        -      "resource_id" :  "{{ADMIN_RESOURCE_ID}}",
        -      "share_with" : {
        -        "SAMPLE_FULL_ACCESS": {
        -            "users": ["test"],
        -            "roles": ["test_role"],
        -            "backend_roles": ["test_backend_role"]
        -        },
        -        "READ_ONLY": {
        -            "users": ["test"],
        -            "roles": ["test_role"],
        -            "backend_roles": ["test_backend_role"]
        -        },
        -        "READ_WRITE": {
        -            "users": ["test"],
        -            "roles": ["test_role"],
        -            "backend_roles": ["test_backend_role"]
        -        }
        -      }
        -    }
        -  ```
        -- **Response:**
        -  ```json
        -    {
        -    "message": "Resource  shared successfully."
        -    }
        -  ```
        -
        -### 4. Revoke Access
        -- **Endpoint:** `POST /_plugins/sample_resource_sharing/revoke`
        -- **Description:** Revokes access to a resource for specified users or roles.
        -- **Request Body:**
        -  ```json
        -    {
        -      "resource_id" :  "",
        -      "entities" : {
        -            "users": ["test", "admin"],
        -            "roles": ["test_role", "all_access"],
        -            "backend_roles": ["test_backend_role", "admin"]
        -      },
        -      "scopes": ["SAMPLE_FULL_ACCESS", "READ_ONLY", "READ_WRITE"]
        -    }
        -  ```
        -- **Response:**
        -  ```json
        -    {
        -      "message": "Resource  access revoked successfully."
        -    }
        -  ```
        -
        -### 5. Verify Access
        -- **Endpoint:** `GET /_plugins/sample_resource_sharing/verify_resource_access`
        -- **Description:** Verifies if a user or role has access to a specific resource with a specific scope.
        -- **Request Body:**
        -    ```json
        -    {
        -      "resource_id": "",
        -      "scope": "SAMPLE_FULL_ACCESS"
        -    }
        -    ```
        -- **Response:**
        -  ```json
        -  {
        -    "message": "User has requested scope SAMPLE_FULL_ACCESS access to "
        -  }
        -  ```
        -
        -### 6. List Accessible Resources
        -- **Endpoint:** `GET /_plugins/sample_resource_sharing/list`
        -- **Description:** Lists all resources accessible to the requesting user or role.
        -- **Response:**
        -  ```json
        -  {
        -    "resource-ids": [
        -        "",
        -        ""
        -    ]
        -  }
        -  ```
        -
        -## Installation
        -
        -1. Clone the repository:
        -   ```bash
        -   git clone git@github.com:opensearch-project/security.git
        -   ```
        -
        -2. Navigate to the project directory:
        -   ```bash
        -   cd sample-resource-plugin
        -   ```
        -
        -3. Build and deploy the plugin:
        -   ```bash
        -   $ ./gradlew clean build -x test -x integrationTest -x spotbugsIntegrationTest
        -   $ ./bin/opensearch-plugin install file: /sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-3.0.0.0-SNAPSHOT.zip
        -   ```
        +This SPI provides interfaces to implement Resource Sharing and Access Control.
         
         ## License
         
        diff --git a/spi/build.gradle b/spi/build.gradle
        index 2cfe1a0d21..2e7c7edb87 100644
        --- a/spi/build.gradle
        +++ b/spi/build.gradle
        @@ -69,6 +69,13 @@ publishing {
                 }
             }
             repositories {
        -        mavenLocal()
        +        maven {
        +            name = "Snapshots" //  optional target repository name
        +            url = "https://aws.oss.sonatype.org/content/repositories/snapshots"
        +            credentials {
        +                username "$System.env.SONATYPE_USERNAME"
        +                password "$System.env.SONATYPE_PASSWORD"
        +            }
        +        }
             }
         }
        
        From e1e876f86e72f22aff01e6e210925e95d944c58a Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Tue, 14 Jan 2025 10:13:15 -0500
        Subject: [PATCH 083/201] Removes Guice reference from Sample plugin and
         changes warn log to info
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../sample/SampleResourcePlugin.java          | 48 -------------------
         .../security/OpenSearchSecurityPlugin.java    |  2 +-
         2 files changed, 1 insertion(+), 49 deletions(-)
        
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java
        index 6c68ef81ab..11cbbcb308 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java
        @@ -8,7 +8,6 @@
          */
         package org.opensearch.sample;
         
        -import java.util.ArrayList;
         import java.util.Collection;
         import java.util.Collections;
         import java.util.List;
        @@ -22,10 +21,6 @@
         import org.opensearch.cluster.metadata.IndexNameExpressionResolver;
         import org.opensearch.cluster.node.DiscoveryNodes;
         import org.opensearch.cluster.service.ClusterService;
        -import org.opensearch.common.inject.Inject;
        -import org.opensearch.common.lifecycle.Lifecycle;
        -import org.opensearch.common.lifecycle.LifecycleComponent;
        -import org.opensearch.common.lifecycle.LifecycleListener;
         import org.opensearch.common.settings.ClusterSettings;
         import org.opensearch.common.settings.IndexScopedSettings;
         import org.opensearch.common.settings.Settings;
        @@ -50,7 +45,6 @@
         import org.opensearch.sample.resource.actions.transport.DeleteResourceTransportAction;
         import org.opensearch.script.ScriptService;
         import org.opensearch.security.spi.resources.ResourceParser;
        -import org.opensearch.security.spi.resources.ResourceService;
         import org.opensearch.security.spi.resources.ResourceSharingExtension;
         import org.opensearch.threadpool.ThreadPool;
         import org.opensearch.watcher.ResourceWatcherService;
        @@ -124,46 +118,4 @@ public String getResourceIndex() {
             public ResourceParser getResourceParser() {
                 return new SampleResourceParser();
             }
        -
        -    @Override
        -    public Collection> getGuiceServiceClasses() {
        -        final List> services = new ArrayList<>(1);
        -        services.add(GuiceHolder.class);
        -        return services;
        -    }
        -
        -    public static class GuiceHolder implements LifecycleComponent {
        -
        -        private static ResourceService resourceService;
        -
        -        @Inject
        -        public GuiceHolder(final ResourceService resourceService) {
        -            GuiceHolder.resourceService = resourceService;
        -        }
        -
        -        public static ResourceService getResourceService() {
        -            return resourceService;
        -        }
        -
        -        @Override
        -        public void close() {}
        -
        -        @Override
        -        public Lifecycle.State lifecycleState() {
        -            return null;
        -        }
        -
        -        @Override
        -        public void addLifecycleListener(LifecycleListener listener) {}
        -
        -        @Override
        -        public void removeLifecycleListener(LifecycleListener listener) {}
        -
        -        @Override
        -        public void start() {}
        -
        -        @Override
        -        public void stop() {}
        -
        -    }
         }
        diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        index a65cc15f7d..96190f9f23 100644
        --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        @@ -785,7 +785,7 @@ public void onIndexModule(IndexModule indexModule) {
                     resourceSharingIndexListener.initialize(threadPool, localClient, auditLog);
                     if (RESOURCE_INDICES.contains(indexModule.getIndex().getName())) {
                         indexModule.addIndexOperationListener(resourceSharingIndexListener);
        -                log.warn("Security plugin started listening to operations on resource-index {}", indexModule.getIndex().getName());
        +                log.info("Security plugin started listening to operations on resource-index {}", indexModule.getIndex().getName());
                     }
         
                     indexModule.forceQueryCacheProvider((indexSettings, nodeCache) -> new QueryCache() {
        
        From 45e09606ba0308c5ca1530fe05409b717064106b Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Tue, 14 Jan 2025 20:21:25 -0500
        Subject: [PATCH 084/201] Separates integration test dependencies and updates
         build.gradle files
        
        Signed-off-by: Darshit Chanpura 
        ---
         build.gradle                        |  78 +++++++++----
         sample-resource-plugin/build.gradle | 172 +++++++---------------------
         spi/build.gradle                    |   1 -
         3 files changed, 94 insertions(+), 157 deletions(-)
        
        diff --git a/build.gradle b/build.gradle
        index af07730ab5..b8c9288a60 100644
        --- a/build.gradle
        +++ b/build.gradle
        @@ -499,6 +499,10 @@ configurations {
                     force "org.checkerframework:checker-qual:3.48.4"
                     force "ch.qos.logback:logback-classic:1.5.16"
                     force "commons-io:commons-io:2.18.0"
        +            force "com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.2"
        +            force "org.hamcrest:hamcrest:2.2"
        +            force "org.mockito:mockito-core:5.15.2"
        +            force "net.bytebuddy:byte-buddy:1.15.11"
                 }
             }
         
        @@ -506,6 +510,55 @@ configurations {
             integrationTestRuntimeOnly.extendsFrom runtimeOnly
         }
         
        +allprojects {
        +    configurations {
        +        integrationTestImplementation.extendsFrom implementation
        +    }
        +    dependencies {
        +        //integration test framework:
        +        integrationTestImplementation('com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.2') {
        +            exclude(group: 'junit', module: 'junit')
        +        }
        +        integrationTestImplementation 'junit:junit:4.13.2'
        +        integrationTestImplementation("org.opensearch.plugin:reindex-client:${opensearch_version}"){
        +            exclude(group: 'org.slf4j', module: 'slf4j-api')
        +        }
        +        integrationTestImplementation "org.opensearch.plugin:percolator-client:${opensearch_version}"
        +        integrationTestImplementation 'commons-io:commons-io:2.18.0'
        +        integrationTestImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}"
        +        integrationTestImplementation "org.apache.logging.log4j:log4j-jul:${versions.log4j}"
        +        integrationTestImplementation 'org.hamcrest:hamcrest:2.2'
        +        integrationTestImplementation "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}"
        +        integrationTestImplementation "org.bouncycastle:bcutil-jdk18on:${versions.bouncycastle}"
        +        integrationTestImplementation('org.awaitility:awaitility:4.2.2') {
        +            exclude(group: 'org.hamcrest', module: 'hamcrest')
        +        }
        +        integrationTestImplementation 'com.unboundid:unboundid-ldapsdk:4.0.14'
        +        integrationTestImplementation "org.opensearch.plugin:mapper-size:${opensearch_version}"
        +        integrationTestImplementation "org.apache.httpcomponents:httpclient-cache:4.5.14"
        +        integrationTestImplementation "org.apache.httpcomponents:httpclient:4.5.14"
        +        integrationTestImplementation "org.apache.httpcomponents:fluent-hc:4.5.14"
        +        integrationTestImplementation "org.apache.httpcomponents:httpcore:4.4.16"
        +        integrationTestImplementation "org.apache.httpcomponents:httpasyncclient:4.1.5"
        +        integrationTestImplementation "org.mockito:mockito-core:5.15.2"
        +        integrationTestImplementation "org.passay:passay:1.6.6"
        +        integrationTestImplementation "org.opensearch:opensearch:${opensearch_version}"
        +        integrationTestImplementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}"
        +        integrationTestImplementation "org.opensearch.plugin:aggs-matrix-stats-client:${opensearch_version}"
        +        integrationTestImplementation "org.opensearch.plugin:parent-join-client:${opensearch_version}"
        +        integrationTestImplementation 'com.password4j:password4j:1.8.2'
        +        integrationTestImplementation "com.google.guava:guava:${guava_version}"
        +        integrationTestImplementation "org.apache.commons:commons-lang3:${versions.commonslang}"
        +        integrationTestImplementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}"
        +        integrationTestImplementation 'org.greenrobot:eventbus-java:3.3.1'
        +        integrationTestImplementation('com.flipkart.zjsonpatch:zjsonpatch:0.4.16'){
        +            exclude(group:'com.fasterxml.jackson.core')
        +        }
        +        integrationTestImplementation 'org.slf4j:slf4j-api:2.0.12'
        +        integrationTestImplementation 'com.selectivem.collections:special-collections-complete:1.4.0'
        +    }
        +}
        +
         //create source set 'integrationTest'
         //add classes from the main source set to the compilation and runtime classpaths of the integrationTest
         sourceSets {
        @@ -724,31 +777,6 @@ dependencies {
         
             compileOnly "org.opensearch:opensearch:${opensearch_version}"
         
        -    //integration test framework:
        -    integrationTestImplementation('com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.2') {
        -        exclude(group: 'junit', module: 'junit')
        -    }
        -    integrationTestImplementation 'junit:junit:4.13.2'
        -    integrationTestImplementation "org.opensearch.plugin:reindex-client:${opensearch_version}"
        -    integrationTestImplementation "org.opensearch.plugin:percolator-client:${opensearch_version}"
        -    integrationTestImplementation 'commons-io:commons-io:2.18.0'
        -    integrationTestImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}"
        -    integrationTestImplementation "org.apache.logging.log4j:log4j-jul:${versions.log4j}"
        -    integrationTestImplementation 'org.hamcrest:hamcrest:2.2'
        -    integrationTestImplementation "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}"
        -    integrationTestImplementation "org.bouncycastle:bcutil-jdk18on:${versions.bouncycastle}"
        -    integrationTestImplementation('org.awaitility:awaitility:4.2.2') {
        -        exclude(group: 'org.hamcrest', module: 'hamcrest')
        -    }
        -    integrationTestImplementation 'com.unboundid:unboundid-ldapsdk:4.0.14'
        -    integrationTestImplementation "org.opensearch.plugin:mapper-size:${opensearch_version}"
        -    integrationTestImplementation "org.apache.httpcomponents:httpclient-cache:4.5.14"
        -    integrationTestImplementation "org.apache.httpcomponents:httpclient:4.5.14"
        -    integrationTestImplementation "org.apache.httpcomponents:fluent-hc:4.5.14"
        -    integrationTestImplementation "org.apache.httpcomponents:httpcore:4.4.16"
        -    integrationTestImplementation "org.apache.httpcomponents:httpasyncclient:4.1.5"
        -    integrationTestImplementation "org.mockito:mockito-core:5.15.2"
        -
             //spotless
             implementation('com.google.googlejavaformat:google-java-format:1.25.2') {
                 exclude group: 'com.google.guava'
        diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle
        index efdf700599..797fc78ac4 100644
        --- a/sample-resource-plugin/build.gradle
        +++ b/sample-resource-plugin/build.gradle
        @@ -5,9 +5,6 @@
         
         apply plugin: 'opensearch.opensearchplugin'
         apply plugin: 'opensearch.testclusters'
        -apply plugin: 'opensearch.java-rest-test'
        -
        -import org.opensearch.gradle.test.RestIntegTestTask
         
         java {
             sourceCompatibility = JavaVersion.VERSION_21
        @@ -20,8 +17,13 @@ opensearchplugin {
             classname 'org.opensearch.sample.SampleResourcePlugin'
         }
         
        +dependencyLicenses.enabled = false
        +thirdPartyAudit.enabled = false
        +loggerUsageCheck.enabled = false
        +tasks.test.enabled=false
        +validateNebulaPom.enabled=false
        +
         ext {
        -    projectSubstitutions = [:]
             licenseFile = rootProject.file('LICENSE.txt')
             noticeFile = rootProject.file('NOTICE.txt')
             opensearch_version = System.getProperty("opensearch.version", "3.0.0-SNAPSHOT")
        @@ -31,7 +33,6 @@ ext {
             version_tokens = opensearch_version.tokenize('-')
             opensearch_build = version_tokens[0] + '.0'
         
        -
             if (buildVersionQualifier) {
                 opensearch_build += "-${buildVersionQualifier}"
             }
        @@ -46,144 +47,53 @@ repositories {
             maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" }
         }
         
        +configurations.all {
        +    resolutionStrategy {
        +        force 'com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.2',
        +                'org.hamcrest:hamcrest:2.2',
        +                'org.apache.httpcomponents:httpclient:4.5.14',
        +                'org.apache.httpcomponents:httpcore:4.4.16',
        +                'org.mockito:mockito-core:5.15.2',
        +                'net.bytebuddy:byte-buddy:1.15.11',
        +                'commons-codec:commons-codec:1.16.1',
        +                'com.fasterxml.jackson.core:jackson-databind:2.18.2',
        +                'com.fasterxml.jackson.core:jackson-databind:2.18.2'
        +    }
        +}
        +
         dependencies {
        +    // Main implementation dependencies
             implementation "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}"
             implementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}"
        -}
        -
        -dependencyLicenses.enabled = false
        -thirdPartyAudit.enabled = false
        -
        -def es_tmp_dir = rootProject.file('build/private/es_tmp').absoluteFile
        -es_tmp_dir.mkdirs()
        -
        -File repo = file("$buildDir/testclusters/repo")
        -def _numNodes = findProperty('numNodes') as Integer ?: 1
        -
        -licenseHeaders.enabled = true
        -validateNebulaPom.enabled = false
        -testingConventions.enabled = false
        -loggerUsageCheck.enabled = false
         
        -javaRestTest.dependsOn(rootProject.assemble)
        -javaRestTest {
        -    systemProperty 'tests.security.manager', 'false'
        +    // Integration test dependencies
        +    integrationTestImplementation rootProject.sourceSets.integrationTest.output
        +    integrationTestImplementation rootProject.sourceSets.main.output
         }
        -testClusters.javaRestTest {
        -    testDistribution = 'INTEG_TEST'
        -}
        -
        -task integTest(type: RestIntegTestTask) {
        -    description = "Run tests against a cluster"
        -    testClassesDirs = sourceSets.test.output.classesDirs
        -    classpath = sourceSets.test.runtimeClasspath
        -}
        -tasks.named("check").configure { dependsOn(integTest) }
        -
        -integTest {
        -    if (project.hasProperty('excludeTests')) {
        -        project.properties['excludeTests']?.replaceAll('\\s', '')?.split('[,;]')?.each {
        -            exclude "${it}"
        -        }
        -    }
        -    systemProperty 'tests.security.manager', 'false'
        -    systemProperty 'java.io.tmpdir', es_tmp_dir.absolutePath
        -
        -    systemProperty "https", System.getProperty("https")
        -    systemProperty "user", System.getProperty("user")
        -    systemProperty "password", System.getProperty("password")
        -    // Tell the test JVM if the cluster JVM is running under a debugger so that tests can use longer timeouts for
        -    // requests. The 'doFirst' delays reading the debug setting on the cluster till execution time.
        -    doFirst {
        -        // Tell the test JVM if the cluster JVM is running under a debugger so that tests can
        -        // use longer timeouts for requests.
        -        def isDebuggingCluster = getDebug() || System.getProperty("test.debug") != null
        -        systemProperty 'cluster.debug', isDebuggingCluster
        -        // Set number of nodes system property to be used in tests
        -        systemProperty 'cluster.number_of_nodes', "${_numNodes}"
        -        // There seems to be an issue when running multi node run or integ tasks with unicast_hosts
        -        // not being written, the waitForAllConditions ensures it's written
        -        getClusters().forEach { cluster ->
        -            cluster.waitForAllConditions()
        -        }
        -    }
         
        -    // The -Dcluster.debug option makes the cluster debuggable; this makes the tests debuggable
        -    if (System.getProperty("test.debug") != null) {
        -        jvmArgs '-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=8000'
        -    }
        -    if (System.getProperty("tests.rest.bwcsuite") == null) {
        -        filter {
        -            excludeTestsMatching "org.opensearch.security.sampleextension.bwc.*IT"
        +sourceSets {
        +    integrationTest {
        +        java {
        +            srcDir file('src/integrationTest/java')
        +            compileClasspath += sourceSets.main.output
        +            runtimeClasspath += sourceSets.main.output
                 }
        -    }
        -}
        -project.getTasks().getByName('bundlePlugin').dependsOn(rootProject.tasks.getByName('build'))
        -Zip bundle = (Zip) project.getTasks().getByName("bundlePlugin");
        -Zip rootBundle = (Zip) rootProject.getTasks().getByName("bundlePlugin");
        -integTest.dependsOn(bundle)
        -integTest.getClusters().forEach{c -> {
        -    c.plugin(rootProject.getObjects().fileProperty().value(rootBundle.getArchiveFile()))
        -    c.plugin(project.getObjects().fileProperty().value(bundle.getArchiveFile()))
        -}}
        -
        -testClusters.integTest {
        -    testDistribution = 'INTEG_TEST'
        -
        -    // Cluster shrink exception thrown if we try to set numberOfNodes to 1, so only apply if > 1
        -    if (_numNodes > 1) numberOfNodes = _numNodes
        -    // When running integration tests it doesn't forward the --debug-jvm to the cluster anymore
        -    // i.e. we have to use a custom property to flag when we want to debug OpenSearch JVM
        -    // since we also support multi node integration tests we increase debugPort per node
        -    if (System.getProperty("cluster.debug") != null) {
        -        def debugPort = 5005
        -        nodes.forEach { node ->
        -            node.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=*:${debugPort}")
        -            debugPort += 1
        +        resources {
        +            srcDir file('src/integrationTest/resources')
                 }
             }
        -    setting 'path.repo', repo.absolutePath
         }
         
        -afterEvaluate {
        -    testClusters.integTest.nodes.each { node ->
        -        def plugins = node.plugins
        -        def firstPlugin = plugins.get(0)
        -        if (firstPlugin.provider == project.bundlePlugin.archiveFile) {
        -            plugins.remove(0)
        -            plugins.add(firstPlugin)
        -        }
        +tasks.register("integrationTest", Test) {
        +    description = 'Run integration tests for the subproject.'
        +    group = 'verification'
        +
        +    testClassesDirs = sourceSets.integrationTest.output.classesDirs
        +    classpath = sourceSets.integrationTest.runtimeClasspath
         
        -        node.extraConfigFile("kirk.pem", file("src/test/resources/security/kirk.pem"))
        -        node.extraConfigFile("kirk-key.pem", file("src/test/resources/security/kirk-key.pem"))
        -        node.extraConfigFile("esnode.pem", file("src/test/resources/security/esnode.pem"))
        -        node.extraConfigFile("esnode-key.pem", file("src/test/resources/security/esnode-key.pem"))
        -        node.extraConfigFile("root-ca.pem", file("src/test/resources/security/root-ca.pem"))
        -        node.setting("plugins.security.ssl.transport.pemcert_filepath", "esnode.pem")
        -        node.setting("plugins.security.ssl.transport.pemkey_filepath", "esnode-key.pem")
        -        node.setting("plugins.security.ssl.transport.pemtrustedcas_filepath", "root-ca.pem")
        -        node.setting("plugins.security.ssl.transport.enforce_hostname_verification", "false")
        -        node.setting("plugins.security.ssl.http.enabled", "true")
        -        node.setting("plugins.security.ssl.http.pemcert_filepath", "esnode.pem")
        -        node.setting("plugins.security.ssl.http.pemkey_filepath", "esnode-key.pem")
        -        node.setting("plugins.security.ssl.http.pemtrustedcas_filepath", "root-ca.pem")
        -        node.setting("plugins.security.allow_unsafe_democertificates", "true")
        -        node.setting("plugins.security.allow_default_init_securityindex", "true")
        -        node.setting("plugins.security.authcz.admin_dn", "\n - CN=kirk,OU=client,O=client,L=test,C=de")
        -        node.setting("plugins.security.audit.type", "internal_opensearch")
        -        node.setting("plugins.security.enable_snapshot_restore_privilege", "true")
        -        node.setting("plugins.security.check_snapshot_restore_write_privileges", "true")
        -        node.setting("plugins.security.restapi.roles_enabled", "[\"all_access\", \"security_rest_api_access\"]")
        -    }
         }
         
        -run {
        -    doFirst {
        -        // There seems to be an issue when running multi node run or integ tasks with unicast_hosts
        -        // not being written, the waitForAllConditions ensures it's written
        -        getClusters().forEach { cluster ->
        -            cluster.waitForAllConditions()
        -        }
        -    }
        -    useCluster testClusters.integTest
        +// Ensure integrationTest task depends on the root project's compile task
        +tasks.named("integrationTest").configure {
        +    dependsOn rootProject.tasks.named("compileIntegrationTestJava")
         }
        diff --git a/spi/build.gradle b/spi/build.gradle
        index 2e7c7edb87..b2db11979f 100644
        --- a/spi/build.gradle
        +++ b/spi/build.gradle
        @@ -20,7 +20,6 @@ repositories {
         
         dependencies {
             compileOnly "org.opensearch:opensearch:${opensearch_version}"
        -    testImplementation "org.opensearch.test:framework:${opensearch_version}"
         }
         
         java {
        
        From fb0f66fea703543a79bd4fb84bb4f024e830df22 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Tue, 14 Jan 2025 23:08:28 -0500
        Subject: [PATCH 085/201] Moves unconcerned changes to a new PR
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../security/DoNotFailOnForbiddenTests.java      | 16 ++++++----------
         .../support/SafeSerializationUtilsTest.java      |  6 ------
         2 files changed, 6 insertions(+), 16 deletions(-)
        
        diff --git a/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java b/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java
        index 4aa6005beb..456d1ebada 100644
        --- a/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java
        +++ b/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java
        @@ -12,7 +12,6 @@
         import java.io.BufferedReader;
         import java.io.IOException;
         import java.io.InputStreamReader;
        -import java.nio.charset.StandardCharsets;
         import java.util.List;
         import java.util.stream.Collectors;
         
        @@ -463,9 +462,8 @@ public void shouldPerformCatIndices_positive() throws IOException {
                     Request getIndicesRequest = new Request("GET", "/_cat/indices");
                     // High level client doesn't support _cat/_indices API
                     Response getIndicesResponse = restHighLevelClient.getLowLevelClient().performRequest(getIndicesRequest);
        -            List indexes = new BufferedReader(
        -                new InputStreamReader(getIndicesResponse.getEntity().getContent(), StandardCharsets.UTF_8)
        -            ).lines().collect(Collectors.toList());
        +            List indexes = new BufferedReader(new InputStreamReader(getIndicesResponse.getEntity().getContent())).lines()
        +                .collect(Collectors.toList());
         
                     assertThat(indexes.size(), equalTo(1));
                     assertThat(indexes.get(0), containsString("marvelous_songs"));
        @@ -478,9 +476,8 @@ public void shouldPerformCatAliases_positive() throws IOException {
                 try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) {
                     Request getAliasesRequest = new Request("GET", "/_cat/aliases");
                     Response getAliasesResponse = restHighLevelClient.getLowLevelClient().performRequest(getAliasesRequest);
        -            List aliases = new BufferedReader(
        -                new InputStreamReader(getAliasesResponse.getEntity().getContent(), StandardCharsets.UTF_8)
        -            ).lines().collect(Collectors.toList());
        +            List aliases = new BufferedReader(new InputStreamReader(getAliasesResponse.getEntity().getContent())).lines()
        +                .collect(Collectors.toList());
         
                     // Does not fail on forbidden, but alias response only contains index which user has access to
                     assertThat(getAliasesResponse.getStatusLine().getStatusCode(), equalTo(200));
        @@ -493,9 +490,8 @@ public void shouldPerformCatAliases_positive() throws IOException {
                 try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) {
                     Request getAliasesRequest = new Request("GET", "/_cat/aliases");
                     Response getAliasesResponse = restHighLevelClient.getLowLevelClient().performRequest(getAliasesRequest);
        -            List aliases = new BufferedReader(
        -                new InputStreamReader(getAliasesResponse.getEntity().getContent(), StandardCharsets.UTF_8)
        -            ).lines().collect(Collectors.toList());
        +            List aliases = new BufferedReader(new InputStreamReader(getAliasesResponse.getEntity().getContent())).lines()
        +                .collect(Collectors.toList());
         
                     // Admin has access to all
                     assertThat(getAliasesResponse.getStatusLine().getStatusCode(), equalTo(200));
        diff --git a/src/test/java/org/opensearch/security/support/SafeSerializationUtilsTest.java b/src/test/java/org/opensearch/security/support/SafeSerializationUtilsTest.java
        index 187fd8b372..f69d4e0291 100644
        --- a/src/test/java/org/opensearch/security/support/SafeSerializationUtilsTest.java
        +++ b/src/test/java/org/opensearch/security/support/SafeSerializationUtilsTest.java
        @@ -17,7 +17,6 @@
         import java.util.HashMap;
         import java.util.regex.Pattern;
         
        -import org.junit.After;
         import org.junit.Test;
         
         import org.opensearch.security.auth.UserInjector;
        @@ -36,11 +35,6 @@
         
         public class SafeSerializationUtilsTest {
         
        -    @After
        -    public void clearCache() {
        -        SafeSerializationUtils.safeClassCache.clear();
        -    }
        -
             @Test
             public void testSafeClasses() {
                 assertTrue(SafeSerializationUtils.isSafeClass(String.class));
        
        From 4b8f236fab6c3307f01756c79bb6465996136e70 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Tue, 14 Jan 2025 23:25:46 -0500
        Subject: [PATCH 086/201] Fixes build.gradle and adds a new update APIs
        
        Signed-off-by: Darshit Chanpura 
        ---
         sample-resource-plugin/build.gradle                 | 13 +++++++++++--
         .../rest/create/CreateResourceRestAction.java       |  7 +++++--
         2 files changed, 16 insertions(+), 4 deletions(-)
        
        diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle
        index 797fc78ac4..99f8e4d74c 100644
        --- a/sample-resource-plugin/build.gradle
        +++ b/sample-resource-plugin/build.gradle
        @@ -15,15 +15,22 @@ opensearchplugin {
             name 'opensearch-sample-resource-plugin'
             description 'Sample plugin that extends OpenSearch Resource Plugin'
             classname 'org.opensearch.sample.SampleResourcePlugin'
        +    extendedPlugins = ['opensearch-security;optional=true']
         }
         
         dependencyLicenses.enabled = false
         thirdPartyAudit.enabled = false
         loggerUsageCheck.enabled = false
        -tasks.test.enabled=false
        -validateNebulaPom.enabled=false
        +validateNebulaPom.enabled = false
        +testingConventions.enabled = false
        +tasks.configureEach { task ->
        +    if(task.name.contains("forbiddenApisIntegrationTest")) {
        +        task.enabled = false
        +    }
        +}
         
         ext {
        +    projectSubstitutions = [:]
             licenseFile = rootProject.file('LICENSE.txt')
             noticeFile = rootProject.file('NOTICE.txt')
             opensearch_version = System.getProperty("opensearch.version", "3.0.0-SNAPSHOT")
        @@ -97,3 +104,5 @@ tasks.register("integrationTest", Test) {
         tasks.named("integrationTest").configure {
             dependsOn rootProject.tasks.named("compileIntegrationTestJava")
         }
        +
        +project.getTasks().getByName('bundlePlugin').dependsOn(rootProject.tasks.getByName('build'))
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java
        index bcfa0ae9df..e990cc8a1d 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java
        @@ -19,8 +19,8 @@
         import org.opensearch.rest.action.RestToXContentListener;
         import org.opensearch.sample.SampleResource;
         
        -import static java.util.Collections.singletonList;
         import static org.opensearch.rest.RestRequest.Method.POST;
        +import static org.opensearch.rest.RestRequest.Method.PUT;
         
         public class CreateResourceRestAction extends BaseRestHandler {
         
        @@ -28,7 +28,10 @@ public CreateResourceRestAction() {}
         
             @Override
             public List routes() {
        -        return singletonList(new Route(POST, "/_plugins/sample_resource_sharing/create"));
        +        return List.of(
        +            new Route(PUT, "/_plugins/sample_resource_sharing/create"),
        +            new Route(POST, "/_plugins/sample_resource_sharing/update")
        +        );
             }
         
             @Override
        
        From 9f9dd0811572ed75520231f6d6a614afd1121ace Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Tue, 14 Jan 2025 23:26:31 -0500
        Subject: [PATCH 087/201] Cleans up SPI
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../ResourceAccessControlPlugin.java          |  21 --
         .../spi/resources/ResourceService.java        |  54 -----
         .../DefaultResourceAccessControlPlugin.java   |  28 ---
         .../spi/resources/fallback/package-info.java  |  14 --
         ...faultResourceAccessControlPluginTests.java | 123 ----------
         .../spi/resources/ResourceServiceTests.java   | 220 ------------------
         .../security/OpenSearchSecurityPlugin.java    |  12 +-
         7 files changed, 2 insertions(+), 470 deletions(-)
         delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessControlPlugin.java
         delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceService.java
         delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/fallback/DefaultResourceAccessControlPlugin.java
         delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/fallback/package-info.java
         delete mode 100644 spi/src/tests/java/opensearch/security/spi/resources/DefaultResourceAccessControlPluginTests.java
         delete mode 100644 spi/src/tests/java/opensearch/security/spi/resources/ResourceServiceTests.java
        
        diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessControlPlugin.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessControlPlugin.java
        deleted file mode 100644
        index 5f9c2558c2..0000000000
        --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessControlPlugin.java
        +++ /dev/null
        @@ -1,21 +0,0 @@
        -/*
        - * SPDX-License-Identifier: Apache-2.0
        - *
        - * The OpenSearch Contributors require contributions made to
        - * this file be licensed under the Apache-2.0 license or a
        - * compatible open source license.
        - */
        -
        -package org.opensearch.security.spi.resources;
        -
        -/**
        - * This plugin allows to control access to resources. It is used by the ResourcePlugins to check whether a user has access to a resource defined by that plugin.
        - * It also defines java APIs to list, share or revoke resources with other users.
        - * User information will be fetched from the ThreadContext.
        - *
        - * @opensearch.experimental
        - */
        -public interface ResourceAccessControlPlugin {
        -
        -    boolean hasPermission(String resourceId, String resourceIndex, ResourceAccessScope> scope);
        -}
        diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceService.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceService.java
        deleted file mode 100644
        index 19d24b97e6..0000000000
        --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceService.java
        +++ /dev/null
        @@ -1,54 +0,0 @@
        -/*
        - * SPDX-License-Identifier: Apache-2.0
        - *
        - * The OpenSearch Contributors require contributions made to
        - * this file be licensed under the Apache-2.0 license or a
        - * compatible open source license.
        - */
        -
        -package org.opensearch.security.spi.resources;
        -
        -import java.util.List;
        -import java.util.stream.Collectors;
        -
        -import org.apache.logging.log4j.LogManager;
        -import org.apache.logging.log4j.Logger;
        -
        -import org.opensearch.OpenSearchException;
        -import org.opensearch.common.inject.Inject;
        -import org.opensearch.security.spi.resources.fallback.DefaultResourceAccessControlPlugin;
        -
        -/**
        - * Service to get the current ResourceSharingExtension to perform authorization.
        - *
        - * @opensearch.experimental
        - */
        -public class ResourceService {
        -    private static final Logger log = LogManager.getLogger(ResourceService.class);
        -
        -    private final ResourceAccessControlPlugin resourceACPlugin;
        -
        -    @Inject
        -    public ResourceService(final List resourceACPlugins) {
        -
        -        if (resourceACPlugins.isEmpty()) {
        -            log.info("Security plugin disabled: Using DefaultResourceAccessControlPlugin");
        -            resourceACPlugin = new DefaultResourceAccessControlPlugin();
        -        } else if (resourceACPlugins.size() == 1) {
        -            log.info("Security plugin enabled: Using OpenSearchSecurityPlugin");
        -            resourceACPlugin = resourceACPlugins.get(0);
        -        } else {
        -            throw new OpenSearchException(
        -                "Multiple resource access control plugins are not supported, found: "
        -                    + resourceACPlugins.stream().map(Object::getClass).map(Class::getName).collect(Collectors.joining(","))
        -            );
        -        }
        -    }
        -
        -    /**
        -     * Gets the ResourceAccessControlPlugin in-effect to perform authorization
        -     */
        -    public ResourceAccessControlPlugin getResourceAccessControlPlugin() {
        -        return resourceACPlugin;
        -    }
        -}
        diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/fallback/DefaultResourceAccessControlPlugin.java b/spi/src/main/java/org/opensearch/security/spi/resources/fallback/DefaultResourceAccessControlPlugin.java
        deleted file mode 100644
        index 379aa15d5d..0000000000
        --- a/spi/src/main/java/org/opensearch/security/spi/resources/fallback/DefaultResourceAccessControlPlugin.java
        +++ /dev/null
        @@ -1,28 +0,0 @@
        -/*
        - * SPDX-License-Identifier: Apache-2.0
        - *
        - * The OpenSearch Contributors require contributions made to
        - * this file be licensed under the Apache-2.0 license or a
        - * compatible open source license.
        - */
        -
        -package org.opensearch.security.spi.resources.fallback;
        -
        -import org.opensearch.security.spi.resources.ResourceAccessControlPlugin;
        -import org.opensearch.security.spi.resources.ResourceAccessScope;
        -
        -/**
        - * A default plugin for resource access control
        - */
        -public class DefaultResourceAccessControlPlugin implements ResourceAccessControlPlugin {
        -    /**
        -     * @param resourceId    the resource on which access is to be checked
        -     * @param resourceIndex where the resource exists
        -     * @param scope         the scope being requested
        -     * @return true always since this is a passthrough implementation
        -     */
        -    @Override
        -    public boolean hasPermission(String resourceId, String resourceIndex, ResourceAccessScope> scope) {
        -        return true;
        -    }
        -}
        diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/fallback/package-info.java b/spi/src/main/java/org/opensearch/security/spi/resources/fallback/package-info.java
        deleted file mode 100644
        index 2dd2803b38..0000000000
        --- a/spi/src/main/java/org/opensearch/security/spi/resources/fallback/package-info.java
        +++ /dev/null
        @@ -1,14 +0,0 @@
        -/*
        - * SPDX-License-Identifier: Apache-2.0
        - *
        - * The OpenSearch Contributors require contributions made to
        - * this file be licensed under the Apache-2.0 license or a
        - * compatible open source license.
        - */
        -
        -/**
        - * This package defines a pass-through implementation of ResourceAccessControlPlugin.
        - *
        - * @opensearch.experimental
        - */
        -package main.java.org.opensearch.security.spi.resources.fallback;
        diff --git a/spi/src/tests/java/opensearch/security/spi/resources/DefaultResourceAccessControlPluginTests.java b/spi/src/tests/java/opensearch/security/spi/resources/DefaultResourceAccessControlPluginTests.java
        deleted file mode 100644
        index 686f8484b9..0000000000
        --- a/spi/src/tests/java/opensearch/security/spi/resources/DefaultResourceAccessControlPluginTests.java
        +++ /dev/null
        @@ -1,123 +0,0 @@
        -/*
        - * SPDX-License-Identifier: Apache-2.0
        - *
        - * The OpenSearch Contributors require contributions made to
        - * this file be licensed under the Apache-2.0 license or a
        - * compatible open source license.
        - */
        -
        -package tests.java.opensearch.security.spi.resources;
        -
        -public class DefaultResourceAccessControlPluginTests {
        -    // @Override
        -    // protected Collection> nodePlugins() {
        -    // return List.of(TestResourcePlugin.class);
        -    // }
        -    //
        -    // public void testGetResources() throws IOException {
        -    // final Client client = client();
        -    //
        -    // createIndex(SAMPLE_TEST_INDEX);
        -    // indexSampleDocuments();
        -    //
        -    // Set resources;
        -    // try (
        -    // DefaultResourceAccessControlExtension plugin = new DefaultResourceAccessControlExtension(
        -    // client,
        -    // internalCluster().getInstance(ThreadPool.class)
        -    // )
        -    // ) {
        -    // resources = plugin.getAccessibleResourcesForCurrentUser(SAMPLE_TEST_INDEX, TestResourcePlugin.TestResource.class);
        -    //
        -    // assertNotNull(resources);
        -    // MatcherAssert.assertThat(resources, hasSize(2));
        -    //
        -    // MatcherAssert.assertThat(resources, hasItem(hasProperty("id", is("1"))));
        -    // MatcherAssert.assertThat(resources, hasItem(hasProperty("id", is("2"))));
        -    // }
        -    // }
        -    //
        -    // public void testSampleResourcePluginListResources() throws IOException {
        -    // createIndex(SAMPLE_TEST_INDEX);
        -    // indexSampleDocuments();
        -    //
        -    // ResourceAccessControlPlugin racPlugin = TestResourcePlugin.GuiceHolder.getResourceService().getResourceAccessControlPlugin();
        -    // MatcherAssert.assertThat(racPlugin.getClass(), is(DefaultResourceAccessControlExtension.class));
        -    //
        -    // Set resources = racPlugin.getAccessibleResourcesForCurrentUser(
        -    // SAMPLE_TEST_INDEX,
        -    // TestResourcePlugin.TestResource.class
        -    // );
        -    //
        -    // assertNotNull(resources);
        -    // MatcherAssert.assertThat(resources, hasSize(2));
        -    // MatcherAssert.assertThat(resources, hasItem(hasProperty("id", is("1"))));
        -    // MatcherAssert.assertThat(resources, hasItem(hasProperty("id", is("2"))));
        -    // }
        -    //
        -    // public void testSampleResourcePluginCallsHasPermission() {
        -    //
        -    // ResourceAccessControlPlugin racPlugin = TestResourcePlugin.GuiceHolder.getResourceService().getResourceAccessControlPlugin();
        -    // MatcherAssert.assertThat(racPlugin.getClass(), is(DefaultResourceAccessControlExtension.class));
        -    //
        -    // boolean canAccess = racPlugin.hasPermission("1", SAMPLE_TEST_INDEX, null);
        -    //
        -    // MatcherAssert.assertThat(canAccess, is(true));
        -    //
        -    // }
        -    //
        -    // public void testSampleResourcePluginCallsShareWith() {
        -    //
        -    // ResourceAccessControlPlugin racPlugin = TestResourcePlugin.GuiceHolder.getResourceService().getResourceAccessControlPlugin();
        -    // MatcherAssert.assertThat(racPlugin.getClass(), is(DefaultResourceAccessControlExtension.class));
        -    //
        -    // ResourceSharing sharingInfo = racPlugin.shareWith("1", SAMPLE_TEST_INDEX, new ShareWith(Set.of()));
        -    //
        -    // MatcherAssert.assertThat(sharingInfo, is(nullValue()));
        -    // }
        -    //
        -    // public void testSampleResourcePluginCallsRevokeAccess() {
        -    //
        -    // ResourceAccessControlPlugin racPlugin = TestResourcePlugin.GuiceHolder.getResourceService().getResourceAccessControlPlugin();
        -    // MatcherAssert.assertThat(racPlugin.getClass(), is(DefaultResourceAccessControlExtension.class));
        -    //
        -    // ResourceSharing sharingInfo = racPlugin.revokeAccess("1", SAMPLE_TEST_INDEX, Map.of(), Set.of("some_scope"));
        -    //
        -    // MatcherAssert.assertThat(sharingInfo, is(nullValue()));
        -    // }
        -    //
        -    // public void testSampleResourcePluginCallsDeleteResourceSharingRecord() {
        -    // ResourceAccessControlPlugin racPlugin = TestResourcePlugin.GuiceHolder.getResourceService().getResourceAccessControlPlugin();
        -    // MatcherAssert.assertThat(racPlugin.getClass(), is(DefaultResourceAccessControlExtension.class));
        -    //
        -    // boolean recordDeleted = racPlugin.deleteResourceSharingRecord("1", SAMPLE_TEST_INDEX);
        -    //
        -    // // no record to delete
        -    // MatcherAssert.assertThat(recordDeleted, is(false));
        -    // }
        -    //
        -    // public void testSampleResourcePluginCallsDeleteAllResourceSharingRecordsForCurrentUser() {
        -    // ResourceAccessControlPlugin racPlugin = TestResourcePlugin.GuiceHolder.getResourceService().getResourceAccessControlPlugin();
        -    // MatcherAssert.assertThat(racPlugin.getClass(), is(DefaultResourceAccessControlExtension.class));
        -    //
        -    // boolean recordDeleted = racPlugin.deleteAllResourceSharingRecordsForCurrentUser();
        -    //
        -    // // no records to delete
        -    // MatcherAssert.assertThat(recordDeleted, is(false));
        -    // }
        -    //
        -    // private void indexSampleDocuments() throws IOException {
        -    // XContentBuilder doc1 = jsonBuilder().startObject().field("id", "1").field("name", "Test Document 1").endObject();
        -    //
        -    // XContentBuilder doc2 = jsonBuilder().startObject().field("id", "2").field("name", "Test Document 2").endObject();
        -    //
        -    // try (Client client = client()) {
        -    //
        -    // client.prepareIndex(SAMPLE_TEST_INDEX).setId("1").setSource(doc1).get();
        -    //
        -    // client.prepareIndex(SAMPLE_TEST_INDEX).setId("2").setSource(doc2).get();
        -    //
        -    // client.admin().indices().prepareRefresh(SAMPLE_TEST_INDEX).get();
        -    // }
        -    // }
        -}
        diff --git a/spi/src/tests/java/opensearch/security/spi/resources/ResourceServiceTests.java b/spi/src/tests/java/opensearch/security/spi/resources/ResourceServiceTests.java
        deleted file mode 100644
        index e537dc1697..0000000000
        --- a/spi/src/tests/java/opensearch/security/spi/resources/ResourceServiceTests.java
        +++ /dev/null
        @@ -1,220 +0,0 @@
        -/// *
        -// * SPDX-License-Identifier: Apache-2.0
        -// *
        -// * The OpenSearch Contributors require contributions made to
        -// * this file be licensed under the Apache-2.0 license or a
        -// * compatible open source license.
        -// */
        -//
        -// package tests.java.opensearch.security.spi.resources;
        -//
        -// import org.hamcrest.MatcherAssert;
        -// import org.mockito.Mock;
        -// import org.mockito.MockitoAnnotations;
        -// import org.opensearch.OpenSearchException;
        -// import org.opensearch.accesscontrol.resources.fallback.DefaultResourceAccessControlExtension;
        -// import org.opensearch.client.Client;
        -// import org.opensearch.plugins.ResourceAccessControlPlugin;
        -// import org.opensearch.plugins.ResourceSharingExtension;
        -// import org.opensearch.test.OpenSearchTestCase;
        -// import org.opensearch.threadpool.ThreadPool;
        -//
        -// import java.util.ArrayList;
        -// import java.util.Arrays;
        -// import java.util.Collections;
        -// import java.util.List;
        -//
        -// import static org.hamcrest.Matchers.*;
        -// import static org.mockito.Mockito.mock;
        -//
        -// public class ResourceServiceTests extends OpenSearchTestCase {
        -//
        -// @Mock
        -// private Client client;
        -//
        -// @Mock
        -// private ThreadPool threadPool;
        -//
        -// public void setup() {
        -// MockitoAnnotations.openMocks(this);
        -// }
        -//
        -// public void testGetResourceAccessControlPluginReturnsInitializedPlugin() {
        -// setup();
        -// Client mockClient = mock(Client.class);
        -// ThreadPool mockThreadPool = mock(ThreadPool.class);
        -//
        -// ResourceAccessControlPlugin mockPlugin = mock(ResourceAccessControlPlugin.class);
        -// List plugins = new ArrayList<>();
        -// plugins.add(mockPlugin);
        -//
        -// List resourcePlugins = new ArrayList<>();
        -//
        -// ResourceService resourceService = new ResourceService(plugins, resourcePlugins, mockClient, mockThreadPool);
        -//
        -// ResourceAccessControlPlugin result = resourceService.getResourceAccessControlPlugin();
        -//
        -// MatcherAssert.assertThat(mockPlugin, equalTo(result));
        -// }
        -//
        -// public void testGetResourceAccessControlPlugin_NoPlugins() {
        -// setup();
        -// List emptyPlugins = new ArrayList<>();
        -// List resourcePlugins = new ArrayList<>();
        -//
        -// ResourceService resourceService = new ResourceService(emptyPlugins, resourcePlugins, client, threadPool);
        -//
        -// ResourceAccessControlPlugin result = resourceService.getResourceAccessControlPlugin();
        -//
        -// assertNotNull(result);
        -// MatcherAssert.assertThat(result, instanceOf(DefaultResourceAccessControlExtension.class));
        -// }
        -//
        -// public void testGetResourceAccessControlPlugin_SinglePlugin() {
        -// setup();
        -// ResourceAccessControlPlugin mockPlugin = mock(ResourceAccessControlPlugin.class);
        -// List singlePlugin = Arrays.asList(mockPlugin);
        -// List resourcePlugins = new ArrayList<>();
        -//
        -// ResourceService resourceService = new ResourceService(singlePlugin, resourcePlugins, client, threadPool);
        -//
        -// ResourceAccessControlPlugin result = resourceService.getResourceAccessControlPlugin();
        -//
        -// assertNotNull(result);
        -// assertSame(mockPlugin, result);
        -// }
        -//
        -// public void testListResourcePluginsReturnsPluginList() {
        -// setup();
        -// List resourceACPlugins = new ArrayList<>();
        -// List expectedResourcePlugins = new ArrayList<>();
        -// expectedResourcePlugins.add(mock(ResourceSharingExtension.class));
        -// expectedResourcePlugins.add(mock(ResourceSharingExtension.class));
        -//
        -// ResourceService resourceService = new ResourceService(resourceACPlugins, expectedResourcePlugins, client, threadPool);
        -//
        -// List actualResourcePlugins = resourceService.listResourcePlugins();
        -//
        -// MatcherAssert.assertThat(expectedResourcePlugins, equalTo(actualResourcePlugins));
        -// }
        -//
        -// public void testListResourcePlugins_concurrentModification() {
        -// setup();
        -// List emptyACPlugins = Collections.emptyList();
        -// List resourcePlugins = new ArrayList<>();
        -// resourcePlugins.add(mock(ResourceSharingExtension.class));
        -//
        -// ResourceService resourceService = new ResourceService(emptyACPlugins, resourcePlugins, client, threadPool);
        -//
        -// Thread modifierThread = new Thread(() -> { resourcePlugins.add(mock(ResourceSharingExtension.class)); });
        -//
        -// modifierThread.start();
        -//
        -// List result = resourceService.listResourcePlugins();
        -//
        -// assertNotNull(result);
        -// // The size could be either 1 or 2 depending on the timing of the concurrent modification
        -// assertTrue(result.size() == 1 || result.size() == 2);
        -// }
        -//
        -// public void testListResourcePlugins_emptyList() {
        -// setup();
        -// List emptyACPlugins = Collections.emptyList();
        -// List emptyResourcePlugins = Collections.emptyList();
        -//
        -// ResourceService resourceService = new ResourceService(emptyACPlugins, emptyResourcePlugins, client, threadPool);
        -//
        -// List result = resourceService.listResourcePlugins();
        -//
        -// assertNotNull(result);
        -// MatcherAssert.assertThat(result, is(empty()));
        -// }
        -//
        -// public void testListResourcePlugins_immutability() {
        -// setup();
        -// List emptyACPlugins = Collections.emptyList();
        -// List resourcePlugins = new ArrayList<>();
        -// resourcePlugins.add(mock(ResourceSharingExtension.class));
        -//
        -// ResourceService resourceService = new ResourceService(emptyACPlugins, resourcePlugins, client, threadPool);
        -//
        -// List result = resourceService.listResourcePlugins();
        -//
        -// assertThrows(UnsupportedOperationException.class, () -> { result.add(mock(ResourceSharingExtension.class)); });
        -// }
        -//
        -// public void testResourceServiceConstructorWithMultiplePlugins() {
        -// setup();
        -// ResourceAccessControlPlugin plugin1 = mock(ResourceAccessControlPlugin.class);
        -// ResourceAccessControlPlugin plugin2 = mock(ResourceAccessControlPlugin.class);
        -// List resourceACPlugins = Arrays.asList(plugin1, plugin2);
        -// List resourcePlugins = Arrays.asList(mock(ResourceSharingExtension.class));
        -//
        -// assertThrows(OpenSearchException.class, () -> { new ResourceService(resourceACPlugins, resourcePlugins, client, threadPool); });
        -// }
        -//
        -// public void testResourceServiceConstructor_MultiplePlugins() {
        -// setup();
        -// ResourceAccessControlPlugin mockPlugin1 = mock(ResourceAccessControlPlugin.class);
        -// ResourceAccessControlPlugin mockPlugin2 = mock(ResourceAccessControlPlugin.class);
        -// List multiplePlugins = Arrays.asList(mockPlugin1, mockPlugin2);
        -// List resourcePlugins = new ArrayList<>();
        -//
        -// assertThrows(
        -// org.opensearch.OpenSearchException.class,
        -// () -> { new ResourceService(multiplePlugins, resourcePlugins, client, threadPool); }
        -// );
        -// }
        -//
        -// public void testResourceServiceWithMultipleResourceACPlugins() {
        -// setup();
        -// List multipleResourceACPlugins = Arrays.asList(
        -// mock(ResourceAccessControlPlugin.class),
        -// mock(ResourceAccessControlPlugin.class)
        -// );
        -// List resourcePlugins = new ArrayList<>();
        -//
        -// assertThrows(
        -// OpenSearchException.class,
        -// () -> { new ResourceService(multipleResourceACPlugins, resourcePlugins, client, threadPool); }
        -// );
        -// }
        -//
        -// public void testResourceServiceWithNoAccessControlPlugin() {
        -// setup();
        -// List resourceACPlugins = new ArrayList<>();
        -// List resourcePlugins = new ArrayList<>();
        -// Client client = mock(Client.class);
        -// ThreadPool threadPool = mock(ThreadPool.class);
        -//
        -// ResourceService resourceService = new ResourceService(resourceACPlugins, resourcePlugins, client, threadPool);
        -//
        -// MatcherAssert.assertThat(resourceService.getResourceAccessControlPlugin(), instanceOf(DefaultResourceAccessControlExtension.class));
        -// MatcherAssert.assertThat(resourcePlugins, equalTo(resourceService.listResourcePlugins()));
        -// }
        -//
        -// public void testResourceServiceWithNoResourceACPlugins() {
        -// setup();
        -// List emptyResourceACPlugins = new ArrayList<>();
        -// List resourcePlugins = new ArrayList<>();
        -//
        -// ResourceService resourceService = new ResourceService(emptyResourceACPlugins, resourcePlugins, client, threadPool);
        -//
        -// assertNotNull(resourceService.getResourceAccessControlPlugin());
        -// }
        -//
        -// public void testResourceServiceWithSingleResourceAccessControlPlugin() {
        -// setup();
        -// List resourceACPlugins = new ArrayList<>();
        -// ResourceAccessControlPlugin mockPlugin = mock(ResourceAccessControlPlugin.class);
        -// resourceACPlugins.add(mockPlugin);
        -//
        -// List resourcePlugins = new ArrayList<>();
        -//
        -// ResourceService resourceService = new ResourceService(resourceACPlugins, resourcePlugins, client, threadPool);
        -//
        -// assertNotNull(resourceService);
        -// MatcherAssert.assertThat(mockPlugin, equalTo(resourceService.getResourceAccessControlPlugin()));
        -// MatcherAssert.assertThat(resourcePlugins, equalTo(resourceService.listResourcePlugins()));
        -// }
        -// }
        diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        index 96190f9f23..72c78ef530 100644
        --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        @@ -200,8 +200,6 @@
         import org.opensearch.security.setting.OpensearchDynamicSetting;
         import org.opensearch.security.setting.TransportPassiveAuthSetting;
         import org.opensearch.security.spi.resources.Resource;
        -import org.opensearch.security.spi.resources.ResourceAccessControlPlugin;
        -import org.opensearch.security.spi.resources.ResourceAccessScope;
         import org.opensearch.security.spi.resources.ResourceParser;
         import org.opensearch.security.spi.resources.ResourceProvider;
         import org.opensearch.security.spi.resources.ResourceSharingExtension;
        @@ -260,7 +258,6 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin
                 ClusterPlugin,
                 MapperPlugin,
                 IdentityPlugin,
        -        ResourceAccessControlPlugin,
                 // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings
                 ExtensionAwarePlugin,
                 ExtensiblePlugin
        @@ -301,7 +298,6 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin
             private volatile DlsFlsBaseContext dlsFlsBaseContext;
             private ResourceSharingIndexManagementRepository rmr;
             private ResourceAccessHandler resourceAccessHandler;
        -    private final Set indicesToListen = new HashSet<>();
             private static final Map RESOURCE_PROVIDERS = new HashMap<>();
             private static final Set RESOURCE_INDICES = new HashSet<>();
         
        @@ -2306,15 +2302,11 @@ public static Set getResourceIndices() {
                 return ImmutableSet.copyOf(RESOURCE_INDICES);
             }
         
        -    @Override
        -    public boolean hasPermission(String resourceId, String resourceIndex, ResourceAccessScope> scope) {
        -        return this.resourceAccessHandler.hasPermission(resourceId, resourceIndex, scope.value());
        -    }
        -
             // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings
             @Override
             public void loadExtensions(ExtensiblePlugin.ExtensionLoader loader) {
         
        +        log.info("Loading extensions");
                 for (ResourceSharingExtension extension : loader.loadExtensions(ResourceSharingExtension.class)) {
                     String resourceType = extension.getResourceType();
                     String resourceIndexName = extension.getResourceIndex();
        @@ -2324,7 +2316,7 @@ public void loadExtensions(ExtensiblePlugin.ExtensionLoader loader) {
         
                     ResourceProvider resourceProvider = new ResourceProvider(resourceType, resourceIndexName, resourceParser);
                     RESOURCE_PROVIDERS.put(resourceIndexName, resourceProvider);
        -            log.info("Loaded resource provider extension: {}, index: {}", resourceType, resourceIndexName);
        +            log.info("Loaded resource sharing extension: {}, index: {}", resourceType, resourceIndexName);
                 }
             }
             // CS-ENFORCE-SINGLE
        
        From ac3ef42d489d1e6b173a525a11788feb81337973 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Wed, 15 Jan 2025 16:28:20 -0500
        Subject: [PATCH 088/201] Refactors build gradle and renames meta-inf services
         and updates the feature flag usage
        
        Signed-off-by: Darshit Chanpura 
        ---
         sample-resource-plugin/build.gradle           | 12 +++++--
         sample-resource-plugin/plugin-security.policy | 16 ++++++++++
         ...ty.spi.resources.ResourceSharingExtension} |  0
         .../security/OpenSearchSecurityPlugin.java    | 32 ++++++++++++-------
         .../resources/ResourceAccessHandler.java      |  7 ++--
         .../ResourceSharingIndexHandler.java          |  3 --
         .../security/support/ConfigConstants.java     |  2 +-
         7 files changed, 53 insertions(+), 19 deletions(-)
         create mode 100644 sample-resource-plugin/plugin-security.policy
         rename sample-resource-plugin/src/main/resources/META-INF/services/{org.opensearch.plugins.ResourcePlugin => org.opensearch.security.spi.resources.ResourceSharingExtension} (100%)
        
        diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle
        index 99f8e4d74c..e2152dfeb1 100644
        --- a/sample-resource-plugin/build.gradle
        +++ b/sample-resource-plugin/build.gradle
        @@ -70,12 +70,13 @@ configurations.all {
         
         dependencies {
             // Main implementation dependencies
        -    implementation "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}"
        -    implementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}"
        +    compileOnly "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}"
        +    compileOnly "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}"
         
             // Integration test dependencies
             integrationTestImplementation rootProject.sourceSets.integrationTest.output
             integrationTestImplementation rootProject.sourceSets.main.output
        +    integrationTestImplementation "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}"
         }
         
         sourceSets {
        @@ -91,6 +92,13 @@ sourceSets {
             }
         }
         
        +tasks.named("bundlePlugin") {
        +    from("$projectDir/plugin-security.policy") {
        +        into ''
        +    }
        +    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
        +}
        +
         tasks.register("integrationTest", Test) {
             description = 'Run integration tests for the subproject.'
             group = 'verification'
        diff --git a/sample-resource-plugin/plugin-security.policy b/sample-resource-plugin/plugin-security.policy
        new file mode 100644
        index 0000000000..9bb63a8402
        --- /dev/null
        +++ b/sample-resource-plugin/plugin-security.policy
        @@ -0,0 +1,16 @@
        + /*
        + * 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.
        + */
        +
        +grant {
        +  permission java.lang.RuntimePermission "getClassLoader";
        +  permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
        +  permission java.lang.RuntimePermission "accessDeclaredMembers";
        +};
        \ No newline at end of file
        diff --git a/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin b/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.security.spi.resources.ResourceSharingExtension
        similarity index 100%
        rename from sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.plugins.ResourcePlugin
        rename to sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.security.spi.resources.ResourceSharingExtension
        diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        index 72c78ef530..b7c6fce01b 100644
        --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        @@ -779,7 +779,10 @@ public void onIndexModule(IndexModule indexModule) {
                     // Listening on POST and DELETE operations in resource indices
                     ResourceSharingIndexListener resourceSharingIndexListener = ResourceSharingIndexListener.getInstance();
                     resourceSharingIndexListener.initialize(threadPool, localClient, auditLog);
        -            if (RESOURCE_INDICES.contains(indexModule.getIndex().getName())) {
        +            if (settings.getAsBoolean(
        +                ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED,
        +                ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT
        +            ) && RESOURCE_INDICES.contains(indexModule.getIndex().getName())) {
                         indexModule.addIndexOperationListener(resourceSharingIndexListener);
                         log.info("Security plugin started listening to operations on resource-index {}", indexModule.getIndex().getName());
                     }
        @@ -2184,7 +2187,10 @@ public void onNodeStarted(DiscoveryNode localNode) {
                 }
         
                 // rmr will be null when sec plugin is disabled or is in SSLOnly mode, hence rmr will not be instantiated
        -        if (rmr != null) {
        +        if (settings.getAsBoolean(
        +            ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED,
        +            ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT
        +        ) && rmr != null) {
                     // create resource sharing index if absent
                     rmr.createResourceSharingIndexIfAbsent();
                 }
        @@ -2306,17 +2312,21 @@ public static Set getResourceIndices() {
             @Override
             public void loadExtensions(ExtensiblePlugin.ExtensionLoader loader) {
         
        -        log.info("Loading extensions");
        -        for (ResourceSharingExtension extension : loader.loadExtensions(ResourceSharingExtension.class)) {
        -            String resourceType = extension.getResourceType();
        -            String resourceIndexName = extension.getResourceIndex();
        -            ResourceParser resourceParser = extension.getResourceParser();
        +        if (settings.getAsBoolean(
        +            ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED,
        +            ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT
        +        )) {
        +            for (ResourceSharingExtension extension : loader.loadExtensions(ResourceSharingExtension.class)) {
        +                String resourceType = extension.getResourceType();
        +                String resourceIndexName = extension.getResourceIndex();
        +                ResourceParser resourceParser = extension.getResourceParser();
         
        -            RESOURCE_INDICES.add(resourceIndexName);
        +                RESOURCE_INDICES.add(resourceIndexName);
         
        -            ResourceProvider resourceProvider = new ResourceProvider(resourceType, resourceIndexName, resourceParser);
        -            RESOURCE_PROVIDERS.put(resourceIndexName, resourceProvider);
        -            log.info("Loaded resource sharing extension: {}, index: {}", resourceType, resourceIndexName);
        +                ResourceProvider resourceProvider = new ResourceProvider(resourceType, resourceIndexName, resourceParser);
        +                RESOURCE_PROVIDERS.put(resourceIndexName, resourceProvider);
        +                log.info("Loaded resource sharing extension: {}, index: {}", resourceType, resourceIndexName);
        +            }
                 }
             }
             // CS-ENFORCE-SINGLE
        diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        index 47eaf65791..37a85dbcbd 100644
        --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        @@ -275,7 +275,7 @@ private Set loadOwnResources(String resourceIndex, String userName) {
             }
         
             /**
        -     * Loads resources shared with the specified entities within the given resource index.
        +     * Loads resources shared with the specified entities within the given resource index, including public resources.
              *
              * @param resourceIndex The resource index to load resources from.
              * @param entities The set of entities to check for shared resources.
        @@ -283,7 +283,10 @@ private Set loadOwnResources(String resourceIndex, String userName) {
              * @return A set of resource IDs shared with the specified entities.
              */
             private Set loadSharedWithResources(String resourceIndex, Set entities, String RecipientType) {
        -        return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entities, RecipientType);
        +        Set entitiesCopy = new HashSet<>(entities);
        +        // To allow "public" resources to be matched for any user, role, backend_role
        +        entitiesCopy.add("*");
        +        return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entitiesCopy, RecipientType);
             }
         
             /**
        diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java
        index 1847a6f3d1..c339fa4d20 100644
        --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java
        +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java
        @@ -412,9 +412,6 @@ public Set fetchDocumentsForAGivenScope(String pluginIndex, Set
                     entities
                 );
         
        -        // To allow "public" resources to be matched for any user, role, backend_role
        -        entities.add("*");
        -
                 Set resourceIds = new HashSet<>();
                 final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L));
         
        diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java
        index dc0d68fea9..cd6fc011c0 100644
        --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java
        +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java
        @@ -381,7 +381,7 @@ public enum RolesMappingResolution {
             // Resource sharing index
             public static final String OPENSEARCH_RESOURCE_SHARING_INDEX = ".opensearch_resource_sharing";
             public static final String OPENSEARCH_RESOURCE_SHARING_ENABLED = SECURITY_SETTINGS_PREFIX + "resource_sharing.enabled";
        -    public static final boolean OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT = false;
        +    public static final boolean OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT = true;
         
             public static Set getSettingAsSet(
                 final Settings settings,
        
        From fcb5c9b9f7a6a98e7f9d6a0a30ed44d4b2789be3 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Thu, 16 Jan 2025 11:43:28 -0500
        Subject: [PATCH 089/201] Fetches the user from newly stored persistent header
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../security/resources/ResourceAccessHandler.java    | 12 ++++++------
         1 file changed, 6 insertions(+), 6 deletions(-)
        
        diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        index 37a85dbcbd..d1b19b7712 100644
        --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        @@ -83,7 +83,7 @@ public void initializeRecipientTypes() {
              * @return A set of accessible resource IDs.
              */
             public Set getAccessibleResourceIdsForCurrentUser(String resourceIndex) {
        -        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER);
        +        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER);
                 if (user == null) {
                     LOGGER.info("Unable to fetch user details ");
                     return Collections.emptySet();
        @@ -143,7 +143,7 @@ public  Set getAccessibleResourcesForCurrentUser(String r
             public boolean hasPermission(String resourceId, String resourceIndex, String scope) {
                 validateArguments(resourceId, resourceIndex, scope);
         
        -        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER);
        +        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER);
         
                 LOGGER.info("Checking if {} has {} permission to resource {}", user.getName(), scope, resourceId);
         
        @@ -184,7 +184,7 @@ public boolean hasPermission(String resourceId, String resourceIndex, String sco
             public ResourceSharing shareWith(String resourceId, String resourceIndex, ShareWith shareWith) {
                 validateArguments(resourceId, resourceIndex, shareWith);
         
        -        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER);
        +        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER);
                 LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith.toString());
         
                 // check if user is admin, if yes the user has permission
        @@ -208,7 +208,7 @@ public ResourceSharing revokeAccess(
                 Set scopes
             ) {
                 validateArguments(resourceId, resourceIndex, revokeAccess, scopes);
        -        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER);
        +        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER);
                 LOGGER.info("User {} revoking access to resource {} for {} for scopes {} ", user.getName(), resourceId, revokeAccess, scopes);
         
                 // check if user is admin, if yes the user has permission
        @@ -226,7 +226,7 @@ public ResourceSharing revokeAccess(
             public boolean deleteResourceSharingRecord(String resourceId, String resourceIndex) {
                 validateArguments(resourceId, resourceIndex);
         
        -        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER);
        +        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER);
                 LOGGER.info("Deleting resource sharing record for resource {} in {} created by {}", resourceId, resourceIndex, user.getName());
         
                 ResourceSharing document = this.resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId);
        @@ -247,7 +247,7 @@ public boolean deleteResourceSharingRecord(String resourceId, String resourceInd
              */
             public boolean deleteAllResourceSharingRecordsForCurrentUser() {
         
        -        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER);
        +        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER);
                 LOGGER.info("Deleting all resource sharing records for resource {}", user.getName());
         
                 return this.resourceSharingIndexHandler.deleteAllRecordsForUser(user.getName());
        
        From 8bf699365c887bb2a4db0eaf521f3c1a78c36d99 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Thu, 16 Jan 2025 12:14:31 -0500
        Subject: [PATCH 090/201] Updates parser method to use XContentParser when
         contructing a resource from response
        
        Signed-off-by: Darshit Chanpura 
        ---
         sample-resource-plugin/build.gradle           |  7 ----
         sample-resource-plugin/plugin-security.policy | 16 ---------
         .../org/opensearch/sample/SampleResource.java | 33 +++++++++++++++++++
         .../sample/SampleResourceParser.java          | 23 ++-----------
         .../spi/resources/ResourceParser.java         |  8 +++--
         .../ResourceSharingIndexHandler.java          | 13 ++++++--
         6 files changed, 51 insertions(+), 49 deletions(-)
         delete mode 100644 sample-resource-plugin/plugin-security.policy
        
        diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle
        index e2152dfeb1..22c70d8389 100644
        --- a/sample-resource-plugin/build.gradle
        +++ b/sample-resource-plugin/build.gradle
        @@ -92,13 +92,6 @@ sourceSets {
             }
         }
         
        -tasks.named("bundlePlugin") {
        -    from("$projectDir/plugin-security.policy") {
        -        into ''
        -    }
        -    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
        -}
        -
         tasks.register("integrationTest", Test) {
             description = 'Run integration tests for the subproject.'
             group = 'verification'
        diff --git a/sample-resource-plugin/plugin-security.policy b/sample-resource-plugin/plugin-security.policy
        deleted file mode 100644
        index 9bb63a8402..0000000000
        --- a/sample-resource-plugin/plugin-security.policy
        +++ /dev/null
        @@ -1,16 +0,0 @@
        - /*
        - * SPDX-License-Identifier: Apache-2.0
        - *
        - * The OpenSearch Contributors require contributions made to
        - * this file be licensed under the Apache-2.0 license or a
        - * compatible open source license.
        - *
        - * Modifications Copyright OpenSearch Contributors. See
        - * GitHub history for details.
        - */
        -
        -grant {
        -  permission java.lang.RuntimePermission "getClassLoader";
        -  permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
        -  permission java.lang.RuntimePermission "accessDeclaredMembers";
        -};
        \ No newline at end of file
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java
        index 508d8e7597..ce123380b4 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java
        @@ -14,11 +14,16 @@
         import java.io.IOException;
         import java.util.Map;
         
        +import org.opensearch.core.ParseField;
         import org.opensearch.core.common.io.stream.StreamInput;
         import org.opensearch.core.common.io.stream.StreamOutput;
        +import org.opensearch.core.xcontent.ConstructingObjectParser;
         import org.opensearch.core.xcontent.XContentBuilder;
        +import org.opensearch.core.xcontent.XContentParser;
         import org.opensearch.security.spi.resources.Resource;
         
        +import static org.opensearch.core.xcontent.ConstructingObjectParser.constructorArg;
        +
         public class SampleResource extends Resource {
         
             private String name;
        @@ -36,6 +41,34 @@ public SampleResource(StreamInput in) throws IOException {
                 this.attributes = in.readMap(StreamInput::readString, StreamInput::readString);
             }
         
        +    @SuppressWarnings("unchecked")
        +    private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(
        +        "sample_resource",
        +        true,
        +        a -> {
        +            SampleResource s;
        +            try {
        +                s = new SampleResource();
        +            } catch (IOException e) {
        +                throw new RuntimeException(e);
        +            }
        +            s.setName((String) a[0]);
        +            s.setDescription((String) a[1]);
        +            s.setAttributes((Map) a[2]);
        +            return s;
        +        }
        +    );
        +
        +    static {
        +        PARSER.declareString(constructorArg(), new ParseField("name"));
        +        PARSER.declareString(constructorArg(), new ParseField("description"));
        +        PARSER.declareObjectOrNull(constructorArg(), (p, c) -> p.mapStrings(), null, new ParseField("attributes"));
        +    }
        +
        +    public static SampleResource fromXContent(XContentParser parser) throws IOException {
        +        return PARSER.parse(parser, null);
        +    }
        +
             @Override
             public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
                 return builder.startObject().field("name", name).field("description", description).field("attributes", attributes).endObject();
        diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceParser.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceParser.java
        index 4bb80fe0e4..42fb2582e2 100644
        --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceParser.java
        +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceParser.java
        @@ -12,30 +12,13 @@
         package org.opensearch.sample;
         
         import java.io.IOException;
        -import java.security.AccessController;
        -import java.security.PrivilegedActionException;
        -import java.security.PrivilegedExceptionAction;
         
        -import com.fasterxml.jackson.databind.ObjectMapper;
        -
        -import org.opensearch.SpecialPermission;
        +import org.opensearch.core.xcontent.XContentParser;
         import org.opensearch.security.spi.resources.ResourceParser;
         
        -@SuppressWarnings("removal")
         public class SampleResourceParser implements ResourceParser {
             @Override
        -    public SampleResource parse(String s) throws IOException {
        -        ObjectMapper obj = new ObjectMapper();
        -        final SecurityManager sm = System.getSecurityManager();
        -
        -        if (sm != null) {
        -            sm.checkPermission(new SpecialPermission());
        -        }
        -
        -        try {
        -            return AccessController.doPrivileged((PrivilegedExceptionAction) () -> obj.readValue(s, SampleResource.class));
        -        } catch (final PrivilegedActionException e) {
        -            throw (IOException) e.getCause();
        -        }
        +    public SampleResource parseXContent(XContentParser parser) throws IOException {
        +        return SampleResource.fromXContent(parser);
             }
         }
        diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java
        index b3c2d0079d..be57200da4 100644
        --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java
        +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java
        @@ -10,12 +10,14 @@
         
         import java.io.IOException;
         
        +import org.opensearch.core.xcontent.XContentParser;
        +
         public interface ResourceParser {
             /**
        -     * Parse stringified json input to a desired Resource type
        -     * @param source the stringified json input
        +     * Parse source bytes supplied by the parser to a desired Resource type
        +     * @param parser to parser bytes-ref json input
              * @return the parsed object of Resource type
              * @throws IOException if something went wrong while parsing
              */
        -    T parse(String source) throws IOException;
        +    T parseXContent(XContentParser parser) throws IOException;
         }
        diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java
        index c339fa4d20..da8376244f 100644
        --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java
        +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java
        @@ -43,8 +43,10 @@
         import org.opensearch.common.unit.TimeValue;
         import org.opensearch.common.util.concurrent.ThreadContext;
         import org.opensearch.common.xcontent.LoggingDeprecationHandler;
        +import org.opensearch.common.xcontent.XContentHelper;
         import org.opensearch.common.xcontent.XContentType;
         import org.opensearch.core.action.ActionListener;
        +import org.opensearch.core.common.bytes.BytesReference;
         import org.opensearch.core.xcontent.NamedXContentRegistry;
         import org.opensearch.core.xcontent.ToXContent;
         import org.opensearch.core.xcontent.XContentBuilder;
        @@ -1187,9 +1189,14 @@ public  Set getResourceDocumentsFromIds(
         
                     for (MultiGetItemResponse itemResponse : response.getResponses()) {
                         if (!itemResponse.isFailed() && itemResponse.getResponse().isExists()) {
        -                    String sourceAsString = itemResponse.getResponse().getSourceAsString();
        -                    // T resource = DefaultObjectMapper.readValue(sourceAsString, clazz);
        -                    T resource = parser.parse(sourceAsString);
        +                    BytesReference sourceAsString = itemResponse.getResponse().getSourceAsBytesRef();
        +                    XContentParser xContentParser = XContentHelper.createParser(
        +                        NamedXContentRegistry.EMPTY,
        +                        LoggingDeprecationHandler.INSTANCE,
        +                        sourceAsString,
        +                        XContentType.JSON
        +                    );
        +                    T resource = parser.parseXContent(xContentParser);
                             result.add(resource);
                         }
                     }
        
        From 8534158a3978a6677a27efb6f4aae960bb8fd5ce Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Thu, 16 Jan 2025 15:32:24 -0500
        Subject: [PATCH 091/201] Revert :assemble change and updates build.gradle for
         sample-plugin
        
        Signed-off-by: Darshit Chanpura 
        ---
         .github/actions/create-bwc-build/action.yaml |  2 +-
         .github/workflows/ci.yml                     | 10 +++++-----
         .github/workflows/plugin_install.yml         |  2 +-
         sample-resource-plugin/build.gradle          |  2 --
         4 files changed, 7 insertions(+), 9 deletions(-)
        
        diff --git a/.github/actions/create-bwc-build/action.yaml b/.github/actions/create-bwc-build/action.yaml
        index 0f9e373b16..8960849333 100644
        --- a/.github/actions/create-bwc-build/action.yaml
        +++ b/.github/actions/create-bwc-build/action.yaml
        @@ -42,7 +42,7 @@ runs:
               uses: gradle/gradle-build-action@v2
               with:
                 cache-disabled: true
        -        arguments: :assemble
        +        arguments: assemble
                 build-root-directory: ${{ inputs.plugin-branch }}
         
             - id: get-opensearch-version
        diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
        index 9919075cc6..5a41062883 100644
        --- a/.github/workflows/ci.yml
        +++ b/.github/workflows/ci.yml
        @@ -208,7 +208,7 @@ jobs:
             - uses: github/codeql-action/init@v3
               with:
                 languages: java
        -    - run: ./gradlew clean :assemble
        +    - run: ./gradlew clean assemble
             - uses: github/codeql-action/analyze@v3
         
           build-artifact-names:
        @@ -238,13 +238,13 @@ jobs:
                 echo ${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}
                 echo ${{ env.TEST_QUALIFIER }}
         
        -    - run: ./gradlew clean :assemble && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip
        +    - run: ./gradlew clean assemble && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip
         
        -    - run: ./gradlew clean :assemble -Dbuild.snapshot=false && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_NO_SNAPSHOT }}.zip
        +    - run: ./gradlew clean assemble -Dbuild.snapshot=false && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_NO_SNAPSHOT }}.zip
         
        -    - run: ./gradlew clean :assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}.zip
        +    - run: ./gradlew clean assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}.zip
         
        -    - run: ./gradlew clean :assemble -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}-SNAPSHOT.zip
        +    - run: ./gradlew clean assemble -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}-SNAPSHOT.zip
         
             - run: ./gradlew clean publishPluginZipPublicationToZipStagingRepository && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.pom
         
        diff --git a/.github/workflows/plugin_install.yml b/.github/workflows/plugin_install.yml
        index c427b160c4..3f8d61795c 100644
        --- a/.github/workflows/plugin_install.yml
        +++ b/.github/workflows/plugin_install.yml
        @@ -32,7 +32,7 @@ jobs:
                 uses: gradle/gradle-build-action@v3
                 with:
                   cache-disabled: true
        -          arguments: :assemble
        +          arguments: assemble
         
               # Move and rename the plugin for installation
               - name: Move and rename the plugin for installation
        diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle
        index 22c70d8389..6ceca2704c 100644
        --- a/sample-resource-plugin/build.gradle
        +++ b/sample-resource-plugin/build.gradle
        @@ -105,5 +105,3 @@ tasks.register("integrationTest", Test) {
         tasks.named("integrationTest").configure {
             dependsOn rootProject.tasks.named("compileIntegrationTestJava")
         }
        -
        -project.getTasks().getByName('bundlePlugin').dependsOn(rootProject.tasks.getByName('build'))
        
        From 982612607401389f5cc88f2b2af7ebd98d75a546 Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Fri, 17 Jan 2025 16:05:54 -0500
        Subject: [PATCH 092/201] Adds a new type of exception for SPI
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../resources/ResourceSharingException.java   | 28 +++++++++++++++++++
         1 file changed, 28 insertions(+)
         create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingException.java
        
        diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingException.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingException.java
        new file mode 100644
        index 0000000000..e669341726
        --- /dev/null
        +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingException.java
        @@ -0,0 +1,28 @@
        +package org.opensearch.security.spi.resources;
        +
        +import java.io.IOException;
        +
        +import org.opensearch.OpenSearchException;
        +import org.opensearch.core.common.io.stream.StreamInput;
        +
        +/**
        + * This class represents an exception that occurs during resource sharing operations.
        + * It extends the OpenSearchException class.
        + */
        +public class ResourceSharingException extends OpenSearchException {
        +    public ResourceSharingException(Throwable cause) {
        +        super(cause);
        +    }
        +
        +    public ResourceSharingException(String msg, Object... args) {
        +        super(msg, args);
        +    }
        +
        +    public ResourceSharingException(String msg, Throwable cause, Object... args) {
        +        super(msg, cause, args);
        +    }
        +
        +    public ResourceSharingException(StreamInput in) throws IOException {
        +        super(in);
        +    }
        +}
        
        From fe0539270f136ba9e026107ffa0ebb56c90a975f Mon Sep 17 00:00:00 2001
        From: Darshit Chanpura 
        Date: Fri, 17 Jan 2025 16:06:34 -0500
        Subject: [PATCH 093/201] Changes actionGet calls to action listeners
        
        Signed-off-by: Darshit Chanpura 
        ---
         .../security/OpenSearchSecurityPlugin.java    |   7 +
         .../security/auth/UserSubjectImpl.java        |   4 +
         .../configuration/DlsFlsValveImpl.java        | 116 +--
         .../SecurityFlsDlsIndexSearcherWrapper.java   | 144 ++--
         .../security/resources/CreatedBy.java         |  16 +-
         .../resources/ResourceAccessHandler.java      | 360 ++++++---
         .../ResourceSharingIndexHandler.java          | 701 +++++++++++-------
         .../ResourceSharingIndexListener.java         |   2 +-
         .../RestListAccessibleResourcesAction.java    |  11 +-
         ...ransportListAccessibleResourcesAction.java |  23 +-
         .../TransportRevokeResourceAccessAction.java  |  39 +-
         .../access/TransportShareResourceAction.java  |  32 +-
         .../TransportVerifyResourceAccessAction.java  |  35 +-
         .../security/resources/CreatedByTests.java    |   4 +-
         14 files changed, 972 insertions(+), 522 deletions(-)
        
        diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        index 70fa1a6c3d..27340523d6 100644
        --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java
        @@ -58,6 +58,7 @@
         import java.util.stream.Collectors;
         import java.util.stream.Stream;
         
        +import com.google.common.annotations.VisibleForTesting;
         import com.google.common.collect.ImmutableMap;
         import com.google.common.collect.ImmutableSet;
         import com.google.common.collect.Lists;
        @@ -2308,6 +2309,12 @@ public static Map getResourceProviders() {
                 return ImmutableMap.copyOf(RESOURCE_PROVIDERS);
             }
         
        +    // TODO following should be removed once core test framework allows loading extensions
        +    @VisibleForTesting
        +    public static Map getResourceProvidersMutable() {
        +        return RESOURCE_PROVIDERS;
        +    }
        +
             public static Set getResourceIndices() {
                 return ImmutableSet.copyOf(RESOURCE_INDICES);
             }
        diff --git a/src/main/java/org/opensearch/security/auth/UserSubjectImpl.java b/src/main/java/org/opensearch/security/auth/UserSubjectImpl.java
        index 63adc559e3..a28ed8dd63 100644
        --- a/src/main/java/org/opensearch/security/auth/UserSubjectImpl.java
        +++ b/src/main/java/org/opensearch/security/auth/UserSubjectImpl.java
        @@ -48,4 +48,8 @@ public  T runAs(Callable callable) throws Exception {
                     return callable.call();
                 }
             }
        +
        +    public User getUser() {
        +        return user;
        +    }
         }
        diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        index 22a05edcd0..b776284af5 100644
        --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java
        @@ -17,7 +17,6 @@
         import java.util.Comparator;
         import java.util.List;
         import java.util.Objects;
        -import java.util.Set;
         import java.util.concurrent.atomic.AtomicReference;
         import java.util.function.Consumer;
         import java.util.stream.StreamSupport;
        @@ -392,61 +391,71 @@ public void handleSearchContext(SearchContext searchContext, ThreadPool threadPo
         
                     DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get();
         
        -            DlsRestriction dlsRestriction;
        -
        -            Set resourceIds;
                     if (this.isResourceSharingEnabled && OpenSearchSecurityPlugin.getResourceIndices().contains(index)) {
        -                resourceIds = this.resourceAccessHandler.getAccessibleResourceIdsForCurrentUser(index);
        -                // Create a DLS restriction to filter search results with accessible resources only
        -                dlsRestriction = this.resourceAccessHandler.createResourceDLSRestriction(resourceIds, namedXContentRegistry);
        +                this.resourceAccessHandler.getAccessibleResourceIdsForCurrentUser(index, ActionListener.wrap(resourceIds -> {
         
        +                    log.info("Creating a DLS restriction for resource IDs: {}", resourceIds);
        +                    // Create a DLS restriction to filter search results with accessible resources only
        +                    DlsRestriction dlsRestriction = this.resourceAccessHandler.createResourceDLSRestriction(
        +                        resourceIds,
        +                        namedXContentRegistry
        +                    );
        +                    applyDlsRestrictionToSearchContext(dlsRestriction, index, searchContext, mode);
        +                }, exception -> {
        +                    log.error("Failed to fetch resource IDs for index '{}': {}", index, exception.getMessage());
        +                    applyDlsRestrictionToSearchContext(DlsRestriction.FULL, index, searchContext, mode);
        +                }));
                     } else {
        -                dlsRestriction = config.getDocumentPrivileges().getRestriction(privilegesEvaluationContext, index);
        +                DlsRestriction dlsRestriction = config.getDocumentPrivileges().getRestriction(privilegesEvaluationContext, index);
        +                applyDlsRestrictionToSearchContext(dlsRestriction, index, searchContext, mode);
                     }
         
        -            if (log.isTraceEnabled()) {
        -                log.trace("handleSearchContext(); index: {}; dlsRestriction: {}", index, dlsRestriction);
        -            }
        +        } catch (Exception e) {
        +            log.error("Error in handleSearchContext()", e);
        +            throw new RuntimeException("Error evaluating dls for a search query: " + e, e);
        +        }
        +    }
         
        -            DocumentAllowList documentAllowList = DocumentAllowList.get(threadContext);
        +    private void applyDlsRestrictionToSearchContext(DlsRestriction dlsRestriction, String index, SearchContext searchContext, Mode mode) {
        +        if (log.isTraceEnabled()) {
        +            log.trace("handleSearchContext(); index: {}; dlsRestriction: {}", index, dlsRestriction);
        +        }
         
        -            if (documentAllowList.isEntryForIndexPresent(index)) {
        -                // The documentAllowList is needed for two cases:
        -                // - DLS rules which use "term lookup queries" and thus need to access indices for which no privileges are present
        -                // - Dashboards multi tenancy which can redirect index accesses to indices for which no normal index privileges are present
        +        DocumentAllowList documentAllowList = DocumentAllowList.get(threadContext);
         
        -                if (!dlsRestriction.isUnrestricted() && documentAllowList.isAllowed(index, "*")) {
        -                    dlsRestriction = DlsRestriction.NONE;
        -                    log.debug("Lifting DLS for {} due to present document allowlist", index);
        -                }
        +        if (documentAllowList.isEntryForIndexPresent(index)) {
        +            // The documentAllowList is needed for two cases:
        +            // - DLS rules which use "term lookup queries" and thus need to access indices for which no privileges are present
        +            // - Dashboards multi tenancy which can redirect index accesses to indices for which no normal index privileges are present
        +
        +            if (!dlsRestriction.isUnrestricted() && documentAllowList.isAllowed(index, "*")) {
        +                dlsRestriction = DlsRestriction.NONE;
        +                log.debug("Lifting DLS for {} due to present document allowlist", index);
                     }
        +        }
         
        -            if (!dlsRestriction.isUnrestricted()) {
        -                if (mode == Mode.ADAPTIVE && dlsRestriction.containsTermLookupQuery()) {
        -                    // Special case for scroll operations:
        -                    // Normally, the check dlsFlsBaseContext.isDlsDoneOnFilterLevel() already aborts early if DLS filter level mode
        -                    // has been activated. However, this is not the case for scroll operations, as these lose the thread context value
        -                    // on which dlsFlsBaseContext.isDlsDoneOnFilterLevel() is based on. Thus, we need to check here again the deeper
        -                    // conditions.
        -                    log.trace("DlsRestriction: contains TLQ.");
        -                    return;
        -                }
        +        if (!dlsRestriction.isUnrestricted()) {
        +            if (mode == Mode.ADAPTIVE && dlsRestriction.containsTermLookupQuery()) {
        +                // Special case for scroll operations:
        +                // Normally, the check dlsFlsBaseContext.isDlsDoneOnFilterLevel() already aborts early if DLS filter level mode
        +                // has been activated. However, this is not the case for scroll operations, as these lose the thread context value
        +                // on which dlsFlsBaseContext.isDlsDoneOnFilterLevel() is based on. Thus, we need to check here again the deeper
        +                // conditions.
        +                log.trace("DlsRestriction: contains TLQ.");
        +                return;
        +            }
         
        -                assert searchContext.parsedQuery() != null;
        +            assert searchContext.parsedQuery() != null;
         
        -                BooleanQuery.Builder queryBuilder = dlsRestriction.toBooleanQueryBuilder(
        -                    searchContext.getQueryShardContext(),
        -                    (q) -> new ConstantScoreQuery(q)
        -                );
        +            BooleanQuery.Builder queryBuilder = dlsRestriction.toBooleanQueryBuilder(
        +                searchContext.getQueryShardContext(),
        +                (q) -> new ConstantScoreQuery(q)
        +            );
         
        -                queryBuilder.add(searchContext.parsedQuery().query(), Occur.MUST);
        +            queryBuilder.add(searchContext.parsedQuery().query(), Occur.MUST);
         
        -                searchContext.parsedQuery(new ParsedQuery(queryBuilder.build()));
        -                searchContext.preProcess(true);
        -            }
        -        } catch (Exception e) {
        -            log.error("Error in handleSearchContext()", e);
        -            throw new RuntimeException("Error evaluating dls for a search query: " + e, e);
        +            searchContext.parsedQuery(new ParsedQuery(queryBuilder.build()));
        +            searchContext.preProcess(true);
                 }
             }
         
        @@ -515,10 +524,7 @@ private static InternalAggregation aggregateBuckets(InternalAggregation aggregat
                 return aggregation;
             }
         
        -    private static List mergeBuckets(
        -        List buckets,
        -        Comparator comparator
        -    ) {
        +    private static List mergeBuckets(List buckets, Comparator comparator) {
                 if (log.isDebugEnabled()) {
                     log.debug("Merging buckets: {}", buckets.stream().map(b -> b.getKeyAsString()).collect(ImmutableList.toImmutableList()));
                 }
        @@ -562,12 +568,12 @@ private Mode getDlsModeHeader() {
         
             private static class BucketMerger implements Consumer {
                 private Comparator comparator;
        -        private StringTerms.Bucket bucket = null;
        +        private Bucket bucket = null;
                 private int mergeCount;
                 private long mergedDocCount;
                 private long mergedDocCountError;
                 private boolean showDocCountError = true;
        -        private final ImmutableList.Builder builder;
        +        private final ImmutableList.Builder builder;
         
                 BucketMerger(Comparator comparator, int size) {
                     this.comparator = Objects.requireNonNull(comparator);
        @@ -579,7 +585,7 @@ private void finalizeBucket() {
                         builder.add(this.bucket);
                     } else {
                         builder.add(
        -                    new StringTerms.Bucket(
        +                    new Bucket(
                                 StringTermsGetter.getTerm(bucket),
                                 mergedDocCount,
                                 (InternalAggregations) bucket.getAggregations(),
        @@ -591,7 +597,7 @@ private void finalizeBucket() {
                     }
                 }
         
        -        private void merge(StringTerms.Bucket bucket) {
        +        private void merge(Bucket bucket) {
                     if (this.bucket != null && (bucket == null || comparator.compare(this.bucket, bucket) != 0)) {
                         finalizeBucket();
                         this.bucket = null;
        @@ -602,13 +608,13 @@ private void merge(StringTerms.Bucket bucket) {
                     }
                 }
         
        -        public List getBuckets() {
        +        public List getBuckets() {
                     merge(null);
                     return builder.build();
                 }
         
                 @Override
        -        public void accept(StringTerms.Bucket bucket) {
        +        public void accept(Bucket bucket) {
                     merge(bucket);
                     mergeCount++;
                     mergedDocCount += bucket.getDocCount();
        @@ -625,7 +631,7 @@ public void accept(StringTerms.Bucket bucket) {
         
             private static class StringTermsGetter {
                 private static final Field REDUCE_ORDER = getField(InternalTerms.class, "reduceOrder");
        -        private static final Field TERM_BYTES = getField(StringTerms.Bucket.class, "termBytes");
        +        private static final Field TERM_BYTES = getField(Bucket.class, "termBytes");
                 private static final Field FORMAT = getField(InternalTerms.Bucket.class, "format");
         
                 private StringTermsGetter() {}
        @@ -669,11 +675,11 @@ public static BucketOrder getReduceOrder(StringTerms stringTerms) {
                     return getFieldValue(REDUCE_ORDER, stringTerms);
                 }
         
        -        public static BytesRef getTerm(StringTerms.Bucket bucket) {
        +        public static BytesRef getTerm(Bucket bucket) {
                     return getFieldValue(TERM_BYTES, bucket);
                 }
         
        -        public static DocValueFormat getDocValueFormat(StringTerms.Bucket bucket) {
        +        public static DocValueFormat getDocValueFormat(Bucket bucket) {
                     return getFieldValue(FORMAT, bucket);
                 }
             }
        diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java
        index 662476928d..af0a1a9282 100644
        --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java
        +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java
        @@ -16,6 +16,8 @@
         import java.util.Collections;
         import java.util.HashSet;
         import java.util.Set;
        +import java.util.concurrent.CountDownLatch;
        +import java.util.concurrent.atomic.AtomicReference;
         import java.util.function.LongSupplier;
         import java.util.function.Supplier;
         
        @@ -29,6 +31,7 @@
         import org.opensearch.cluster.metadata.IndexMetadata;
         import org.opensearch.cluster.service.ClusterService;
         import org.opensearch.common.settings.Settings;
        +import org.opensearch.core.action.ActionListener;
         import org.opensearch.core.common.Strings;
         import org.opensearch.core.index.shard.ShardId;
         import org.opensearch.index.IndexService;
        @@ -48,6 +51,7 @@
         import org.opensearch.security.privileges.dlsfls.FieldMasking;
         import org.opensearch.security.privileges.dlsfls.FieldPrivileges;
         import org.opensearch.security.resources.ResourceAccessHandler;
        +import org.opensearch.security.spi.resources.ResourceSharingException;
         import org.opensearch.security.support.ConfigConstants;
         
         public class SecurityFlsDlsIndexSearcherWrapper extends SystemIndexSearcherWrapper {
        @@ -119,60 +123,113 @@ public SecurityFlsDlsIndexSearcherWrapper(
             @SuppressWarnings("unchecked")
             @Override
             protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdmin) throws IOException {
        -
                 final ShardId shardId = ShardUtils.extractShardId(reader);
                 PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext();
        +        final String indexName = (shardId != null) ? shardId.getIndexName() : null;
         
                 if (log.isTraceEnabled()) {
        -            log.trace("dlsFlsWrap(); index: {}; privilegeEvaluationContext: {}", index.getName(), privilegesEvaluationContext);
        +            log.trace("dlsFlsWrap(); index: {}; isAdmin: {}", indexName, isAdmin);
                 }
         
        -        String indexName = shardId != null ? shardId.getIndexName() : null;
        -        Set resourceIds;
        -        if (this.isResourceSharingEnabled
        -            && !Strings.isNullOrEmpty(indexName)
        -            && OpenSearchSecurityPlugin.getResourceIndices().contains(indexName)) {
        -            resourceIds = this.resourceAccessHandler.getAccessibleResourceIdsForCurrentUser(indexName);
        -            // resourceIds.isEmpty() indicates that the index is a resource index but the user does not have access to any resource under
        -            // the
        -            // index
        -            if (resourceIds.isEmpty()) {
        -                return new EmptyFilterLeafReader.EmptyDirectoryReader(reader);
        -            }
        -            // Create a resource DLS query for the current user
        -            QueryShardContext queryShardContext = this.indexService.newQueryShardContext(shardId.getId(), null, nowInMillis, null);
        -            Query resourceQuery = this.resourceAccessHandler.createResourceDLSQuery(resourceIds, queryShardContext);
        +        // 1. If user is admin, or we have no shard/index info, just wrap with default logic (no doc-level restriction).
        +        if (isAdmin || Strings.isNullOrEmpty(indexName)) {
        +            return wrapWithDefaultDlsFls(reader, shardId);
        +        }
         
        -            // TODO the FlsRule must still be checked
        -            return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader(
        -                reader,
        -                FieldPrivileges.FlsRule.ALLOW_ALL,
        -                resourceQuery,
        -                indexService,
        -                threadContext,
        -                clusterService,
        -                auditlog,
        -                FieldMasking.FieldMaskingRule.ALLOW_ALL,
        -                shardId,
        -                metaFields
        -            );
        +        // 2. If resource sharing is disabled or this is not a resource index, fallback to standard DLS/FLS logic.
        +        if (!this.isResourceSharingEnabled || !OpenSearchSecurityPlugin.getResourceIndices().contains(indexName)) {
        +            return wrapStandardDlsFls(privilegesEvaluationContext, reader, shardId, indexName, isAdmin);
                 }
         
        -        if (isAdmin || privilegesEvaluationContext == null) {
        -            return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader(
        -                reader,
        -                FieldPrivileges.FlsRule.ALLOW_ALL,
        -                null,
        -                indexService,
        -                threadContext,
        -                clusterService,
        -                auditlog,
        -                FieldMasking.FieldMaskingRule.ALLOW_ALL,
        -                shardId,
        -                metaFields
        -            );
        +        // TODO see if steps 3,4,5 can be changed to be completely asynchronous
        +        // 3.Since we need DirectoryReader *now*, we'll block the thread using a CountDownLatch until the async call completes.
        +        final AtomicReference> resourceIdsRef = new AtomicReference<>(Collections.emptySet());
        +        final AtomicReference exceptionRef = new AtomicReference<>(null);
        +        final CountDownLatch latch = new CountDownLatch(1);
        +
        +        // 4. Perform the async call to fetch resource IDs
        +        this.resourceAccessHandler.getAccessibleResourceIdsForCurrentUser(indexName, ActionListener.wrap(resourceIds -> {
        +            log.debug("Fetched resource IDs for index '{}': {}", indexName, resourceIds);
        +            resourceIdsRef.set(resourceIds);
        +            latch.countDown();
        +        }, ex -> {
        +            log.error("Failed to fetch resource IDs for index '{}': {}", indexName, ex.getMessage(), ex);
        +            exceptionRef.set(ex);
        +            latch.countDown();
        +        }));
        +
        +        // 5. Block until the async call completes
        +        try {
        +            latch.await();
        +        } catch (InterruptedException e) {
        +            Thread.currentThread().interrupt();
        +            throw new IOException("Interrupted while waiting for resource IDs", e);
        +        }
        +
        +        // 6. Throw any errors
        +        if (exceptionRef.get() != null) {
        +            throw new ResourceSharingException("Failed to get resource IDs for index: " + indexName, exceptionRef.get());
        +        }
        +
        +        // 7. If the user has no accessible resources, produce a reader that yields zero documents
        +        final Set resourceIds = resourceIdsRef.get();
        +        if (resourceIds.isEmpty()) {
        +            log.debug("User has no accessible resources in index '{}'; returning EmptyDirectoryReader.", indexName);
        +            return new EmptyFilterLeafReader.EmptyDirectoryReader(reader);
                 }
         
        +        // 8. Build the resource-based query to restrict docs
        +        final QueryShardContext queryShardContext = this.indexService.newQueryShardContext(shardId.getId(), null, nowInMillis, null);
        +        final Query resourceQuery = this.resourceAccessHandler.createResourceDLSQuery(resourceIds, queryShardContext);
        +
        +        log.debug("Applying resource-based DLS query for index '{}'", indexName);
        +
        +        // 9. Wrap with a DLS/FLS DirectoryReader that includes doc-level restriction (resourceQuery),
        +        // with FLS (ALLOW_ALL) since we don't need field-level restrictions here.
        +        return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader(
        +            reader,
        +            FieldPrivileges.FlsRule.ALLOW_ALL,
        +            resourceQuery,
        +            indexService,
        +            threadContext,
        +            clusterService,
        +            auditlog,
        +            FieldMasking.FieldMaskingRule.ALLOW_ALL,
        +            shardId,
        +            metaFields
        +        );
        +    }
        +
        +    /**
        +     * Wrap the reader with an "ALLOW_ALL" doc-level filter and field privileges,
        +     * i.e., no doc-level or field-level restrictions.
        +     */
        +    private DirectoryReader wrapWithDefaultDlsFls(DirectoryReader reader, ShardId shardId) throws IOException {
        +        return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader(
        +            reader,
        +            FieldPrivileges.FlsRule.ALLOW_ALL,
        +            null,  // no doc-level restriction
        +            indexService,
        +            threadContext,
        +            clusterService,
        +            auditlog,
        +            FieldMasking.FieldMaskingRule.ALLOW_ALL,
        +            shardId,
        +            metaFields
        +        );
        +    }
        +
        +    /**
        +     * Fallback to your existing logic to handle DLS/FLS if the index is not a resource index,
        +     * or if other conditions apply (like dlsFlsBaseContext usage, etc.).
        +     */
        +    private DirectoryReader wrapStandardDlsFls(
        +        PrivilegesEvaluationContext privilegesEvaluationContext,
        +        DirectoryReader reader,
        +        ShardId shardId,
        +        String indexName,
        +        boolean isAdmin
        +    ) throws IOException {
                 try {
                     DlsFlsProcessedConfig config = this.dlsFlsProcessedConfigSupplier.get();
                     DlsRestriction dlsRestriction;
        @@ -244,4 +301,5 @@ protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdm
                     throw new OpenSearchException("Error while evaluating DLS/FLS", e);
                 }
             }
        +
         }
        diff --git a/src/main/java/org/opensearch/security/resources/CreatedBy.java b/src/main/java/org/opensearch/security/resources/CreatedBy.java
        index 3790d56a72..69af99719e 100644
        --- a/src/main/java/org/opensearch/security/resources/CreatedBy.java
        +++ b/src/main/java/org/opensearch/security/resources/CreatedBy.java
        @@ -25,16 +25,16 @@
          */
         public class CreatedBy implements ToXContentFragment, NamedWriteable {
         
        -    private final String creatorType;
        +    private final Enum creatorType;
             private final String creator;
         
        -    public CreatedBy(String creatorType, String creator) {
        +    public CreatedBy(Enum creatorType, String creator) {
                 this.creatorType = creatorType;
                 this.creator = creator;
             }
         
             public CreatedBy(StreamInput in) throws IOException {
        -        this.creatorType = in.readString();
        +        this.creatorType = in.readEnum(Creator.class);
                 this.creator = in.readString();
             }
         
        @@ -42,7 +42,7 @@ public String getCreator() {
                 return creator;
             }
         
        -    public String getCreatorType() {
        +    public Enum getCreatorType() {
                 return creatorType;
             }
         
        @@ -58,23 +58,23 @@ public String getWriteableName() {
         
             @Override
             public void writeTo(StreamOutput out) throws IOException {
        -        out.writeString(creatorType);
        +        out.writeEnum(Creator.valueOf(creatorType.name()));
                 out.writeString(creator);
             }
         
             @Override
             public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
        -        return builder.startObject().field(creatorType, creator).endObject();
        +        return builder.startObject().field(String.valueOf(creatorType), creator).endObject();
             }
         
             public static CreatedBy fromXContent(XContentParser parser) throws IOException {
                 String creator = null;
        -        String creatorType = null;
        +        Enum creatorType = null;
                 XContentParser.Token token;
         
                 while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
                     if (token == XContentParser.Token.FIELD_NAME) {
        -                creatorType = parser.currentName();
        +                creatorType = Creator.valueOf(parser.currentName());
                     } else if (token == XContentParser.Token.VALUE_STRING) {
                         creator = parser.text();
                     }
        diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        index d1b19b7712..b1387e712c 100644
        --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java
        @@ -23,7 +23,10 @@
         import org.apache.logging.log4j.Logger;
         import org.apache.lucene.search.Query;
         
        +import org.opensearch.OpenSearchException;
        +import org.opensearch.action.StepListener;
         import org.opensearch.common.util.concurrent.ThreadContext;
        +import org.opensearch.core.action.ActionListener;
         import org.opensearch.core.xcontent.NamedXContentRegistry;
         import org.opensearch.index.query.BoolQueryBuilder;
         import org.opensearch.index.query.ConstantScoreQueryBuilder;
        @@ -32,12 +35,14 @@
         import org.opensearch.index.query.QueryShardContext;
         import org.opensearch.security.DefaultObjectMapper;
         import org.opensearch.security.OpenSearchSecurityPlugin;
        +import org.opensearch.security.auth.UserSubjectImpl;
         import org.opensearch.security.configuration.AdminDNs;
         import org.opensearch.security.privileges.PrivilegesConfigurationValidationException;
         import org.opensearch.security.privileges.dlsfls.DlsRestriction;
         import org.opensearch.security.privileges.dlsfls.DocumentPrivileges;
         import org.opensearch.security.spi.resources.Resource;
         import org.opensearch.security.spi.resources.ResourceParser;
        +import org.opensearch.security.spi.resources.ResourceSharingException;
         import org.opensearch.security.support.ConfigConstants;
         import org.opensearch.security.user.User;
         import org.opensearch.threadpool.ThreadPool;
        @@ -82,54 +87,113 @@ public void initializeRecipientTypes() {
              * @param resourceIndex The resource index to check for accessible resources.
              * @return A set of accessible resource IDs.
              */
        -    public Set getAccessibleResourceIdsForCurrentUser(String resourceIndex) {
        -        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER);
        +    public void getAccessibleResourceIdsForCurrentUser(String resourceIndex, ActionListener> listener) {
        +        final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent(
        +            ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER
        +        );
        +        final User user = (userSubject == null) ? null : userSubject.getUser();
        +
        +        // If no user is authenticated, return an empty set
                 if (user == null) {
        -            LOGGER.info("Unable to fetch user details ");
        -            return Collections.emptySet();
        +            LOGGER.info("Unable to fetch user details.");
        +            listener.onResponse(Collections.emptySet());
        +            return;
                 }
         
        -        LOGGER.info("Listing accessible resources within a resource index {} for : {}", resourceIndex, user.getName());
        -
        -        Set resourceIds = new HashSet<>();
        +        LOGGER.info("Listing accessible resources within the resource index {} for user: {}", resourceIndex, user.getName());
         
        -        // check if user is admin, if yes all resources should be accessible
        +        // 2. If the user is admin, simply fetch all resources
                 if (adminDNs.isAdmin(user)) {
        -            resourceIds.addAll(loadAllResources(resourceIndex));
        -            return resourceIds;
        +            loadAllResources(resourceIndex, new ActionListener<>() {
        +                @Override
        +                public void onResponse(Set allResources) {
        +                    listener.onResponse(allResources);
        +                }
        +
        +                @Override
        +                public void onFailure(Exception e) {
        +                    listener.onFailure(e);
        +                }
        +            });
        +            return;
                 }
         
        -        // 0. Own resources
        -        resourceIds.addAll(loadOwnResources(resourceIndex, user.getName()));
        +        // StepListener for the user’s "own" resources
        +        StepListener> ownResourcesListener = new StepListener<>();
        +
        +        // StepListener for resources shared with the user’s name
        +        StepListener> userNameResourcesListener = new StepListener<>();
        +
        +        // StepListener for resources shared with the user’s roles
        +        StepListener> rolesResourcesListener = new StepListener<>();
        +
        +        // StepListener for resources shared with the user’s backend roles
        +        StepListener> backendRolesResourcesListener = new StepListener<>();
         
        -        // 1. By username
        -        resourceIds.addAll(loadSharedWithResources(resourceIndex, Set.of(user.getName()), Recipient.USERS.toString()));
        +        // Load own resources for the user.
        +        loadOwnResources(resourceIndex, user.getName(), ownResourcesListener);
         
        -        // 2. By roles
        -        Set roles = user.getSecurityRoles();
        -        resourceIds.addAll(loadSharedWithResources(resourceIndex, roles, Recipient.ROLES.toString()));
        +        // Load resources shared with the user by its name.
        +        ownResourcesListener.whenComplete(ownResources -> {
        +            loadSharedWithResources(resourceIndex, Set.of(user.getName()), Recipient.USERS.toString(), userNameResourcesListener);
        +        }, listener::onFailure);
         
        -        // 3. By backend_roles
        -        Set backendRoles = user.getRoles();
        -        resourceIds.addAll(loadSharedWithResources(resourceIndex, backendRoles, Recipient.BACKEND_ROLES.toString()));
        +        // Load resources shared with the user’s roles.
        +        userNameResourcesListener.whenComplete(userNameResources -> {
        +            loadSharedWithResources(resourceIndex, user.getSecurityRoles(), Recipient.ROLES.toString(), rolesResourcesListener);
        +        }, listener::onFailure);
         
        -        return resourceIds;
        +        // Load resources shared with the user’s backend roles.
        +        rolesResourcesListener.whenComplete(rolesResources -> {
        +            loadSharedWithResources(resourceIndex, user.getRoles(), Recipient.BACKEND_ROLES.toString(), backendRolesResourcesListener);
        +        }, listener::onFailure);
        +
        +        // Combine all results and pass them back to the original listener.
        +        backendRolesResourcesListener.whenComplete(backendRolesResources -> {
        +            Set allResources = new HashSet<>();
        +
        +            // Retrieve results from each StepListener
        +            allResources.addAll(ownResourcesListener.result());
        +            allResources.addAll(userNameResourcesListener.result());
        +            allResources.addAll(rolesResourcesListener.result());
        +            allResources.addAll(backendRolesResourcesListener.result());
        +
        +            LOGGER.debug("Found {} accessible resources for user {}", allResources.size(), user.getName());
        +            listener.onResponse(allResources);
        +        }, listener::onFailure);
             }
         
             /**
              * Returns a set of accessible resources for the current user within the specified resource index.
              *
              * @param resourceIndex The resource index to check for accessible resources.
        -     * @return A set of accessible resource IDs.
              */
             @SuppressWarnings("unchecked")
        -    public  Set getAccessibleResourcesForCurrentUser(String resourceIndex) {
        -        validateArguments(resourceIndex);
        -        ResourceParser parser = OpenSearchSecurityPlugin.getResourceProviders().get(resourceIndex).getResourceParser();
        -        Set resourceIds = getAccessibleResourceIdsForCurrentUser(resourceIndex);
        -        return resourceIds.isEmpty()
        -            ? Set.of()
        -            : this.resourceSharingIndexHandler.getResourceDocumentsFromIds(resourceIds, resourceIndex, parser);
        +    public  void getAccessibleResourcesForCurrentUser(String resourceIndex, ActionListener> listener) {
        +        try {
        +            validateArguments(resourceIndex);
        +            ResourceParser parser = OpenSearchSecurityPlugin.getResourceProviders().get(resourceIndex).getResourceParser();
        +            Set resourceIds = getAccessibleResourceIdsForCurrentUser(resourceIndex);
        +
        +            if (resourceIds.isEmpty()) {
        +                listener.onResponse(Set.of());
        +                return;
        +            }
        +
        +            this.resourceSharingIndexHandler.getResourceDocumentsFromIds(
        +                resourceIds,
        +                resourceIndex,
        +                parser,
        +                ActionListener.wrap(
        +                    listener::onResponse,
        +                    exception -> listener.onFailure(
        +                        new ResourceSharingException("Failed to get accessible resources: " + exception.getMessage(), exception)
        +                    )
        +                )
        +            );
        +        } catch (Exception e) {
        +            listener.onFailure(new ResourceSharingException("Failed to process accessible resources request: " + e.getMessage(), e));
        +        }
             }
         
             /**
        @@ -138,40 +202,60 @@ public  Set getAccessibleResourcesForCurrentUser(String r
              * @param resourceId      The resource ID to check access for.
              * @param resourceIndex   The resource index containing the resource.
              * @param scope           The permission scope to check.
        -     * @return True if the user has the specified permission, false otherwise.
              */
        -    public boolean hasPermission(String resourceId, String resourceIndex, String scope) {
        +    public void hasPermission(String resourceId, String resourceIndex, String scope, ActionListener listener) {
                 validateArguments(resourceId, resourceIndex, scope);
         
        -        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER);
        +        final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent(
        +            ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER
        +        );
        +        final User user = (userSubject == null) ? null : userSubject.getUser();
        +
        +        if (user == null) {
        +            LOGGER.warn("No authenticated user found in ThreadContext");
        +            listener.onResponse(false);
        +            return;
        +        }
         
        -        LOGGER.info("Checking if {} has {} permission to resource {}", user.getName(), scope, resourceId);
        +        LOGGER.info("Checking if user '{}' has '{}' permission to resource '{}'", user.getName(), scope, resourceId);
         
        -        // check if user is admin, if yes the user has permission
                 if (adminDNs.isAdmin(user)) {
        -            return true;
        +            LOGGER.info("User '{}' is admin, automatically granted '{}' permission on '{}'", user.getName(), scope, resourceId);
        +            listener.onResponse(true);
        +            return;
                 }
         
                 Set userRoles = user.getSecurityRoles();
                 Set userBackendRoles = user.getRoles();
         
        -        ResourceSharing document = this.resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId);
        -        if (document == null) {
        -            LOGGER.warn("Resource {} not found in index {}", resourceId, resourceIndex);
        -            return false;  // If the document doesn't exist, no permissions can be granted
        -        }
        -
        -        if (isSharedWithEveryone(document)
        -            || isOwnerOfResource(document, user.getName())
        -            || isSharedWithEntity(document, Recipient.USERS, Set.of(user.getName()), scope)
        -            || isSharedWithEntity(document, Recipient.ROLES, userRoles, scope)
        -            || isSharedWithEntity(document, Recipient.BACKEND_ROLES, userBackendRoles, scope)) {
        -            LOGGER.info("User {} has {} access to {}", user.getName(), scope, resourceId);
        -            return true;
        -        }
        +        this.resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId, ActionListener.wrap(document -> {
        +            if (document == null) {
        +                LOGGER.warn("Resource '{}' not found in index '{}'", resourceId, resourceIndex);
        +                listener.onResponse(false);
        +                return;
        +            }
         
        -        LOGGER.info("User {} does not have {} access to {} ", user.getName(), scope, resourceId);
        -        return false;
        +            if (isSharedWithEveryone(document)
        +                || isOwnerOfResource(document, user.getName())
        +                || isSharedWithEntity(document, Recipient.USERS, Set.of(user.getName()), scope)
        +                || isSharedWithEntity(document, Recipient.ROLES, userRoles, scope)
        +                || isSharedWithEntity(document, Recipient.BACKEND_ROLES, userBackendRoles, scope)) {
        +
        +                LOGGER.info("User '{}' has '{}' permission to resource '{}'", user.getName(), scope, resourceId);
        +                listener.onResponse(true);
        +            } else {
        +                LOGGER.info("User '{}' does not have '{}' permission to resource '{}'", user.getName(), scope, resourceId);
        +                listener.onResponse(false);
        +            }
        +        }, exception -> {
        +            LOGGER.error(
        +                "Failed to fetch resource sharing document for resource '{}' in index '{}': {}",
        +                resourceId,
        +                resourceIndex,
        +                exception.getMessage()
        +            );
        +            listener.onFailure(exception);
        +        }));
             }
         
             /**
        @@ -179,18 +263,44 @@ public boolean hasPermission(String resourceId, String resourceIndex, String sco
              * @param resourceId The resource ID to share.
              * @param resourceIndex  The index where resource is store
              * @param shareWith The users, roles, and backend roles as well as scope to share the resource with.
        -     * @return The updated ResourceSharing document.
              */
        -    public ResourceSharing shareWith(String resourceId, String resourceIndex, ShareWith shareWith) {
        +    public void shareWith(String resourceId, String resourceIndex, ShareWith shareWith, ActionListener listener) {
                 validateArguments(resourceId, resourceIndex, shareWith);
         
        -        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER);
        +        final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent(
        +            ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER
        +        );
        +        final User user = (userSubject == null) ? null : userSubject.getUser();
        +
        +        if (user == null) {
        +            LOGGER.warn("No authenticated user found in the ThreadContext.");
        +            listener.onFailure(new OpenSearchException("No authenticated user found."));
        +            return;
        +        }
        +
                 LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith.toString());
         
        -        // check if user is admin, if yes the user has permission
                 boolean isAdmin = adminDNs.isAdmin(user);
         
        -        return this.resourceSharingIndexHandler.updateResourceSharingInfo(resourceId, resourceIndex, user.getName(), shareWith, isAdmin);
        +        this.resourceSharingIndexHandler.updateResourceSharingInfo(
        +            resourceId,
        +            resourceIndex,
        +            user.getName(),
        +            shareWith,
        +            isAdmin,
        +            ActionListener.wrap(
        +                // On success, return the updated ResourceSharing
        +                updatedResourceSharing -> {
        +                    LOGGER.info("Successfully shared resource {} with {}", resourceId, shareWith.toString());
        +                    listener.onResponse(updatedResourceSharing);
        +                },
        +                // On failure, log and pass the exception along
        +                e -> {
        +                    LOGGER.error("Failed to share resource {} with {}: {}", resourceId, shareWith.toString(), e.getMessage());
        +                    listener.onFailure(e);
        +                }
        +            )
        +        );
             }
         
             /**
        @@ -201,44 +311,114 @@ public ResourceSharing shareWith(String resourceId, String resourceIndex, ShareW
              * @param scopes The permission scopes to revoke access for.
              * @return The updated ResourceSharing document.
              */
        -    public ResourceSharing revokeAccess(
        +    public void revokeAccess(
                 String resourceId,
                 String resourceIndex,
                 Map> revokeAccess,
        -        Set scopes
        +        Set scopes,
        +        ActionListener listener
             ) {
        +        // Validate input
                 validateArguments(resourceId, resourceIndex, revokeAccess, scopes);
        -        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER);
        -        LOGGER.info("User {} revoking access to resource {} for {} for scopes {} ", user.getName(), resourceId, revokeAccess, scopes);
         
        -        // check if user is admin, if yes the user has permission
        -        boolean isAdmin = adminDNs.isAdmin(user);
        +        // Retrieve user
        +        final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent(
        +            ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER
        +        );
        +        final User user = (userSubject == null) ? null : userSubject.getUser();
        +
        +        if (user != null) {
        +            LOGGER.info("User {} revoking access to resource {} for {} for scopes {} ", user.getName(), resourceId, revokeAccess, scopes);
        +        } else {
        +            listener.onFailure(
        +                new ResourceSharingException(
        +                    "Failed to revoke access to resource {} for {} for scopes {} with no authenticated user",
        +                    resourceId,
        +                    revokeAccess,
        +                    scopes
        +                )
        +            );
        +        }
         
        -        return this.resourceSharingIndexHandler.revokeAccess(resourceId, resourceIndex, revokeAccess, scopes, user.getName(), isAdmin);
        +        boolean isAdmin = (user != null) && adminDNs.isAdmin(user);
        +
        +        this.resourceSharingIndexHandler.revokeAccess(
        +            resourceId,
        +            resourceIndex,
        +            revokeAccess,
        +            scopes,
        +            (user != null ? user.getName() : null),
        +            isAdmin,
        +            ActionListener.wrap(listener::onResponse, exception -> {
        +                LOGGER.error("Failed to revoke access to resource {} in index {}: {}", resourceId, resourceIndex, exception.getMessage());
        +                listener.onFailure(exception);
        +            })
        +        );
             }
         
             /**
              * Deletes a resource sharing record by its ID and the resource index it belongs to.
              * @param resourceId The resource ID to delete.
              * @param resourceIndex The resource index containing the resource.
        -     * @return True if the record was successfully deleted, false otherwise.
              */
        -    public boolean deleteResourceSharingRecord(String resourceId, String resourceIndex) {
        -        validateArguments(resourceId, resourceIndex);
        -
        -        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER);
        -        LOGGER.info("Deleting resource sharing record for resource {} in {} created by {}", resourceId, resourceIndex, user.getName());
        +    public void deleteResourceSharingRecord(String resourceId, String resourceIndex, ActionListener listener) {
        +        try {
        +            validateArguments(resourceId, resourceIndex);
        +
        +            final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent(
        +                ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER
        +            );
        +            final User user = (userSubject == null) ? null : userSubject.getUser();
        +
        +            if (user != null) {
        +                LOGGER.info(
        +                    "Deleting resource sharing record for resource {} in {} created by {}",
        +                    resourceId,
        +                    resourceIndex,
        +                    user.getName()
        +                );
        +            } else {
        +                LOGGER.info("Deleting resource sharing record for resource {} in {} with no authenticated user", resourceId, resourceIndex);
        +            }
         
        -        ResourceSharing document = this.resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId);
        -        if (document == null) {
        -            LOGGER.info("Document {} does not exist in index {}", resourceId, resourceIndex);
        -            return false;
        +            resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId, ActionListener.wrap(document -> {
        +                if (document == null) {
        +                    LOGGER.info("Document {} does not exist in index {}", resourceId, resourceIndex);
        +                    listener.onResponse(false);
        +                    return;
        +                }
        +
        +                // Check if the user is allowed to delete
        +                boolean isAdmin = (user != null && adminDNs.isAdmin(user));
        +                boolean isOwner = (user != null && isOwnerOfResource(document, user.getName()));
        +
        +                if (!isAdmin && !isOwner) {
        +                    LOGGER.info(
        +                        "User {} does not have access to delete the record {}",
        +                        (user == null ? "UNKNOWN" : user.getName()),
        +                        resourceId
        +                    );
        +                    listener.onResponse(false);
        +                    return;
        +                }
        +
        +                // Finally, perform the actual record deletion (assuming it's a synchronous call)
        +                boolean result = resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex);
        +                listener.onResponse(result);
        +            }, exception -> {
        +                // If an error happens while fetching
        +                LOGGER.error(
        +                    "Failed to fetch resource sharing document for resource {} in index {}. Error: {}",
        +                    resourceId,
        +                    resourceIndex,
        +                    exception.getMessage()
        +                );
        +                listener.onFailure(exception);
        +            }));
        +        } catch (Exception e) {
        +            LOGGER.error("Failed to delete resource sharing record for resource {}", resourceId, e);
        +            listener.onFailure(e);
                 }
        -        if (!(adminDNs.isAdmin(user) || isOwnerOfResource(document, user.getName()))) {
        -            LOGGER.info("User {} does not have access to delete the record {} ", user.getName(), resourceId);
        -            return false;
        -        }
        -        return this.resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex);
             }
         
             /**
        @@ -247,7 +427,10 @@ public boolean deleteResourceSharingRecord(String resourceId, String resourceInd
              */
             public boolean deleteAllResourceSharingRecordsForCurrentUser() {
         
        -        final User user = (User) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER);
        +        final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent(
        +            ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER
        +        );
        +        User user = userSubject == null ? null : userSubject.getUser();
                 LOGGER.info("Deleting all resource sharing records for resource {}", user.getName());
         
                 return this.resourceSharingIndexHandler.deleteAllRecordsForUser(user.getName());
        @@ -259,8 +442,8 @@ public boolean deleteAllResourceSharingRecordsForCurrentUser() {
              * @param resourceIndex The resource index to load resources from.
              * @return A set of resource IDs.
              */
        -    private Set loadAllResources(String resourceIndex) {
        -        return this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex);
        +    private void loadAllResources(String resourceIndex, ActionListener> listener) {
        +        this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex, listener);
             }
         
             /**
        @@ -270,8 +453,8 @@ private Set loadAllResources(String resourceIndex) {
              * @param userName The username of the owner.
              * @return A set of resource IDs owned by the user.
              */
        -    private Set loadOwnResources(String resourceIndex, String userName) {
        -        return this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName);
        +    private void loadOwnResources(String resourceIndex, String userName, ActionListener> listener) {
        +        this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName, listener);
             }
         
             /**
        @@ -279,14 +462,19 @@ private Set loadOwnResources(String resourceIndex, String userName) {
              *
              * @param resourceIndex The resource index to load resources from.
              * @param entities The set of entities to check for shared resources.
        -     * @param RecipientType The type of entity (e.g., users, roles, backend_roles).
        +     * @param recipientType The type of entity (e.g., users, roles, backend_roles).
              * @return A set of resource IDs shared with the specified entities.
              */
        -    private Set loadSharedWithResources(String resourceIndex, Set entities, String RecipientType) {
        +    private void loadSharedWithResources(
        +        String resourceIndex,
        +        Set entities,
        +        String recipientType,
        +        ActionListener> listener
        +    ) {
                 Set entitiesCopy = new HashSet<>(entities);
                 // To allow "public" resources to be matched for any user, role, backend_role
                 entitiesCopy.add("*");
        -        return this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entitiesCopy, RecipientType);
        +        this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entitiesCopy, recipientType, listener);
             }
         
             /**
        diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java
        index da8376244f..576a146d3b 100644
        --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java
        +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java
        @@ -26,23 +26,23 @@
         
         import org.opensearch.OpenSearchException;
         import org.opensearch.action.DocWriteRequest;
        +import org.opensearch.action.StepListener;
         import org.opensearch.action.admin.indices.create.CreateIndexRequest;
         import org.opensearch.action.admin.indices.create.CreateIndexResponse;
         import org.opensearch.action.get.MultiGetItemResponse;
         import org.opensearch.action.get.MultiGetRequest;
        -import org.opensearch.action.get.MultiGetResponse;
         import org.opensearch.action.index.IndexRequest;
         import org.opensearch.action.index.IndexResponse;
         import org.opensearch.action.search.ClearScrollRequest;
         import org.opensearch.action.search.SearchRequest;
         import org.opensearch.action.search.SearchResponse;
        -import org.opensearch.action.search.SearchScrollAction;
         import org.opensearch.action.search.SearchScrollRequest;
         import org.opensearch.action.support.WriteRequest;
         import org.opensearch.client.Client;
         import org.opensearch.common.unit.TimeValue;
         import org.opensearch.common.util.concurrent.ThreadContext;
         import org.opensearch.common.xcontent.LoggingDeprecationHandler;
        +import org.opensearch.common.xcontent.XContentFactory;
         import org.opensearch.common.xcontent.XContentHelper;
         import org.opensearch.common.xcontent.XContentType;
         import org.opensearch.core.action.ActionListener;
        @@ -216,14 +216,8 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn
             * 
             *
             * @param pluginIndex The source index to match against the source_idx field
        -    * @return Set containing resource IDs that belong to the specified system index.
        -    *         Returns an empty list if:
        -    *         
          - *
        • No matching documents are found
        • - *
        • An error occurs during the search operation
        • - *
        • The system index parameter is invalid
        • - *
        - * + * @param listener The listener to be notified when the operation completes. + * The listener receives a set of resource IDs as a result. * @apiNote This method: *
          *
        • Uses source filtering for optimal performance
        • @@ -231,40 +225,54 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn *
        • Returns an empty list instead of throwing exceptions
        • *
        */ - public Set fetchAllDocuments(String pluginIndex) { - LOGGER.debug("Fetching all documents from {} where source_idx = {}", resourceSharingIndex, pluginIndex); + public void fetchAllDocuments(String pluginIndex, ActionListener> listener) { + LOGGER.debug("Fetching all documents asynchronously from {} where source_idx = {}", resourceSharingIndex, pluginIndex); - // TODO: Once stashContext is replaced with switchContext this call will have to be modified - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + try (final ThreadContext.StoredContext storedContext = this.threadPool.getThreadContext().stashContext();) { SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); - - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)); - searchSourceBuilder.size(10000); // TODO check what size should be set here. - - searchSourceBuilder.fetchSource(new String[] { "resource_id" }, null); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query( + QueryBuilders.termQuery("source_idx.keyword", pluginIndex) + ).size(10000).fetchSource(new String[] { "resource_id" }, null); searchRequest.source(searchSourceBuilder); - SearchResponse searchResponse = client.search(searchRequest).actionGet(); - - Set resourceIds = new HashSet<>(); + client.search(searchRequest, new ActionListener<>() { + @Override + public void onResponse(SearchResponse searchResponse) { + try { + Set resourceIds = new HashSet<>(); + + SearchHit[] hits = searchResponse.getHits().getHits(); + for (SearchHit hit : hits) { + Map sourceAsMap = hit.getSourceAsMap(); + if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { + resourceIds.add(sourceAsMap.get("resource_id").toString()); + } + } - SearchHit[] hits = searchResponse.getHits().getHits(); - for (SearchHit hit : hits) { - Map sourceAsMap = hit.getSourceAsMap(); - if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { - resourceIds.add(sourceAsMap.get("resource_id").toString()); + LOGGER.debug("Found {} documents in {} for source_idx: {}", resourceIds.size(), resourceSharingIndex, pluginIndex); + + listener.onResponse(resourceIds); + } catch (Exception e) { + LOGGER.error( + "Error while processing search response from {} for source_idx: {}", + resourceSharingIndex, + pluginIndex, + e + ); + listener.onFailure(e); + } } - } - - LOGGER.debug("Found {} documents in {} for source_idx: {}", resourceIds.size(), resourceSharingIndex, pluginIndex); - - return resourceIds; + @Override + public void onFailure(Exception e) { + LOGGER.error("Failed to fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); + listener.onFailure(e); + } + }); } catch (Exception e) { - LOGGER.error("Failed to fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); - return Set.of(); + LOGGER.error("Failed to initiate fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); + listener.onFailure(e); } } @@ -313,15 +321,14 @@ public Set fetchAllDocuments(String pluginIndex) { * * @param pluginIndex The source index to match against the source_idx field * @param entities Set of values to match in the specified RecipientType field - * @param RecipientType The type of association with the resource. Must be one of: + * @param recipientType The type of association with the resource. Must be one of: *
          *
        • "users" - for user-based access
        • *
        • "roles" - for role-based access
        • *
        • "backend_roles" - for backend role-based access
        • *
        - * @return Set List of resource IDs that match the criteria. The list may be empty - * if no matches are found - * + * @param listener The listener to be notified when the operation completes. + * The listener receives a set of resource IDs as a result. * @throws RuntimeException if the search operation fails * * @apiNote This method: @@ -334,9 +341,14 @@ public Set fetchAllDocuments(String pluginIndex) { * */ - public Set fetchDocumentsForAllScopes(String pluginIndex, Set entities, String RecipientType) { + public void fetchDocumentsForAllScopes( + String pluginIndex, + Set entities, + String recipientType, + ActionListener> listener + ) { // "*" must match all scopes - return fetchDocumentsForAGivenScope(pluginIndex, entities, RecipientType, "*"); + fetchDocumentsForAGivenScope(pluginIndex, entities, recipientType, "*", listener); } /** @@ -384,16 +396,15 @@ public Set fetchDocumentsForAllScopes(String pluginIndex, Set en * * @param pluginIndex The source index to match against the source_idx field * @param entities Set of values to match in the specified RecipientType field - * @param RecipientType The type of association with the resource. Must be one of: + * @param recipientType The type of association with the resource. Must be one of: *
          *
        • "users" - for user-based access
        • *
        • "roles" - for role-based access
        • *
        • "backend_roles" - for backend role-based access
        • *
        * @param scope The scope of the access. Should be implementation of {@link ResourceAccessScope} - * @return Set List of resource IDs that match the criteria. The list may be empty - * if no matches are found - * + * @param listener The listener to be notified when the operation completes. + * The listener receives a set of resource IDs as a result. * @throws RuntimeException if the search operation fails * * @apiNote This method: @@ -405,20 +416,25 @@ public Set fetchDocumentsForAllScopes(String pluginIndex, Set en *
      27. Properly cleans up scroll context after use
      28. * */ - public Set fetchDocumentsForAGivenScope(String pluginIndex, Set entities, String RecipientType, String scope) { + public void fetchDocumentsForAGivenScope( + String pluginIndex, + Set entities, + String recipientType, + String scope, + ActionListener> listener + ) { LOGGER.debug( - "Fetching documents from index: {}, where share_with.{}.{} contains any of {}", + "Fetching documents asynchronously from index: {}, where share_with.{}.{} contains any of {}", pluginIndex, scope, - RecipientType, + recipientType, entities ); - Set resourceIds = new HashSet<>(); + final Set resourceIds = new HashSet<>(); final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); - // TODO: Once stashContext is replaced with switchContext this call will have to be modified - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + try (ThreadContext.StoredContext storedContext = this.threadPool.getThreadContext().stashContext()) { SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); searchRequest.scroll(scroll); @@ -428,36 +444,54 @@ public Set fetchDocumentsForAGivenScope(String pluginIndex, Set if ("*".equals(scope)) { for (String entity : entities) { shouldQuery.should( - QueryBuilders.multiMatchQuery(entity, "share_with.*." + RecipientType + ".keyword") + QueryBuilders.multiMatchQuery(entity, "share_with.*." + recipientType + ".keyword") .type(MultiMatchQueryBuilder.Type.BEST_FIELDS) ); } } else { for (String entity : entities) { - shouldQuery.should(QueryBuilders.termQuery("share_with." + scope + "." + RecipientType + ".keyword", entity)); + shouldQuery.should(QueryBuilders.termQuery("share_with." + scope + "." + recipientType + ".keyword", entity)); } } shouldQuery.minimumShouldMatch(1); boolQuery.must(QueryBuilders.existsQuery("share_with")).must(shouldQuery); - executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery); - - LOGGER.debug("Found {} documents matching the criteria in {}", resourceIds.size(), resourceSharingIndex); - - return resourceIds; - + executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery, ActionListener.wrap(success -> { + try { + // If 'success' indicates the search completed, log and return the results + LOGGER.debug("Found {} documents matching the criteria in {}", resourceIds.size(), resourceSharingIndex); + listener.onResponse(resourceIds); + } finally { + // Always close the stashed context + storedContext.close(); + } + }, exception -> { + try { + LOGGER.error( + "Search failed for pluginIndex={}, scope={}, recipientType={}, entities={}", + pluginIndex, + scope, + recipientType, + entities, + exception + ); + listener.onFailure(exception); + } finally { + storedContext.close(); + } + })); } catch (Exception e) { LOGGER.error( - "Failed to fetch documents from {} for criteria - pluginIndex: {}, scope: {}, RecipientType: {}, entities: {}", + "Failed to initiate fetch from {} for criteria - pluginIndex: {}, scope: {}, RecipientType: {}, entities: {}", resourceSharingIndex, pluginIndex, scope, - RecipientType, + recipientType, entities, e ); - throw new RuntimeException("Failed to fetch documents: " + e.getMessage(), e); + listener.onFailure(new RuntimeException("Failed to fetch documents: " + e.getMessage(), e)); } } @@ -494,8 +528,8 @@ public Set fetchDocumentsForAGivenScope(String pluginIndex, Set * @param pluginIndex The source index to match against the source_idx field * @param field The field name to search in. Must be a valid field in the index mapping * @param value The value to match for the specified field. Performs exact term matching - * @return Set List of resource IDs that match the criteria. Returns an empty list - * if no matches are found + * @param listener The listener to be notified when the operation completes. + * The listener receives a set of resource IDs as a result. * * @throws IllegalArgumentException if any parameter is null or empty * @throws RuntimeException if the search operation fails, wrapping the underlying exception @@ -514,9 +548,10 @@ public Set fetchDocumentsForAGivenScope(String pluginIndex, Set * Set resources = fetchDocumentsByField("myIndex", "status", "active"); * */ - public Set fetchDocumentsByField(String pluginIndex, String field, String value) { + public void fetchDocumentsByField(String pluginIndex, String field, String value, ActionListener> listener) { if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(field) || StringUtils.isBlank(value)) { - throw new IllegalArgumentException("pluginIndex, field, and value must not be null or empty"); + listener.onFailure(new IllegalArgumentException("pluginIndex, field, and value must not be null or empty")); + return; } LOGGER.debug("Fetching documents from index: {}, where {} = {}", pluginIndex, field, value); @@ -533,15 +568,18 @@ public Set fetchDocumentsByField(String pluginIndex, String field, Strin .must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)) .must(QueryBuilders.termQuery(field + ".keyword", value)); - executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery); - - LOGGER.info("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); - - return resourceIds; + executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery, ActionListener.wrap(success -> { + LOGGER.info("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); + listener.onResponse(resourceIds); + }, exception -> { + LOGGER.error("Failed to fetch documents from {} where {} = {}", resourceSharingIndex, field, value, exception); + listener.onFailure(new RuntimeException("Failed to fetch documents: " + exception.getMessage(), exception)); + })); } catch (Exception e) { - LOGGER.error("Failed to fetch documents from {} where {} = {}", resourceSharingIndex, field, value, e); - throw new RuntimeException("Failed to fetch documents: " + e.getMessage(), e); + LOGGER.error("Failed to initiate fetch from {} where {} = {}", resourceSharingIndex, field, value, e); + listener.onFailure(new RuntimeException("Failed to initiate fetch: " + e.getMessage(), e)); } + } /** @@ -574,8 +612,8 @@ public Set fetchDocumentsByField(String pluginIndex, String field, Strin * * @param pluginIndex The source index to match against the source_idx field * @param resourceId The resource ID to fetch. Must exactly match the resource_id field - * @return ResourceSharing object if a matching document is found, null if no document - * matches the criteria + * @param listener The listener to be notified when the operation completes. + * The listener receives the parsed ResourceSharing object or null if not found * * @throws IllegalArgumentException if pluginIndexName or resourceId is null or empty * @throws RuntimeException if the search operation fails or parsing errors occur, @@ -598,53 +636,72 @@ public Set fetchDocumentsByField(String pluginIndex, String field, Strin * } * */ - - public ResourceSharing fetchDocumentById(String pluginIndex, String resourceId) { + public void fetchDocumentById(String pluginIndex, String resourceId, ActionListener listener) { if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(resourceId)) { - throw new IllegalArgumentException("pluginIndexName and resourceId must not be null or empty"); + listener.onFailure(new IllegalArgumentException("pluginIndex and resourceId must not be null or empty")); + return; } + LOGGER.debug("Fetching document from index: {}, resourceId: {}", pluginIndex, resourceId); - LOGGER.debug("Fetching document from index: {}, with resourceId: {}", pluginIndex, resourceId); - - // TODO: Once stashContext is replaced with switchContext this call will have to be modified - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); - + try (ThreadContext.StoredContext storedContext = this.threadPool.getThreadContext().stashContext()) { BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() .must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)) .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery).size(1); // We only need one document since - // a resource must have only one - // sharing entry - searchRequest.source(searchSourceBuilder); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery).size(1); // There is only one document for + // a single resource - SearchResponse searchResponse = client.search(searchRequest).actionGet(); + SearchRequest searchRequest = new SearchRequest(resourceSharingIndex).source(searchSourceBuilder); - SearchHit[] hits = searchResponse.getHits().getHits(); - if (hits.length == 0) { - LOGGER.debug("No document found for resourceId: {} in index: {}", resourceId, pluginIndex); - return null; - } + client.search(searchRequest, new ActionListener<>() { + @Override + public void onResponse(SearchResponse searchResponse) { + try { + SearchHit[] hits = searchResponse.getHits().getHits(); + if (hits.length == 0) { + LOGGER.debug("No document found for resourceId: {} in index: {}", resourceId, pluginIndex); + listener.onResponse(null); + return; + } - SearchHit hit = hits[0]; - try ( - XContentParser parser = XContentType.JSON.xContent() - .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, hit.getSourceAsString()) - ) { + SearchHit hit = hits[0]; + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, hit.getSourceAsString()) + ) { + parser.nextToken(); + ResourceSharing resourceSharing = ResourceSharing.fromXContent(parser); - parser.nextToken(); + LOGGER.debug("Successfully fetched document for resourceId: {} from index: {}", resourceId, pluginIndex); - ResourceSharing resourceSharing = ResourceSharing.fromXContent(parser); + listener.onResponse(resourceSharing); + } + } catch (Exception e) { + LOGGER.error("Failed to parse document for resourceId: {} from index: {}", resourceId, pluginIndex, e); + listener.onFailure( + new OpenSearchException( + "Failed to parse document for resourceId: " + resourceId + " from index: " + pluginIndex, + e + ) + ); + } + } - LOGGER.debug("Successfully fetched document for resourceId: {} from index: {}", resourceId, pluginIndex); + @Override + public void onFailure(Exception e) { - return resourceSharing; - } + LOGGER.error("Failed to fetch document for resourceId: {} from index: {}", resourceId, pluginIndex, e); + listener.onFailure( + new OpenSearchException("Failed to fetch document for resourceId: " + resourceId + " from index: " + pluginIndex, e) + ); + } + }); } catch (Exception e) { LOGGER.error("Failed to fetch document for resourceId: {} from index: {}", resourceId, pluginIndex, e); - throw new OpenSearchException("Failed to fetch document for resourceId: " + resourceId + " from index: " + pluginIndex, e); + listener.onFailure( + new OpenSearchException("Failed to fetch document for resourceId: " + resourceId + " from index: " + pluginIndex, e) + ); } } @@ -654,41 +711,100 @@ public ResourceSharing fetchDocumentById(String pluginIndex, String resourceId) * @param scroll Search Scroll * @param searchRequest Request to execute * @param boolQuery Query to execute with the request + * @param listener Listener to be notified when the operation completes */ - private void executeSearchRequest(Set resourceIds, Scroll scroll, SearchRequest searchRequest, BoolQueryBuilder boolQuery) { + private void executeSearchRequest( + Set resourceIds, + Scroll scroll, + SearchRequest searchRequest, + BoolQueryBuilder boolQuery, + ActionListener listener + ) { SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery) .size(1000) .fetchSource(new String[] { "resource_id" }, null); searchRequest.source(searchSourceBuilder); - SearchResponse searchResponse = client.search(searchRequest).actionGet(); - String scrollId = searchResponse.getScrollId(); - SearchHit[] hits = searchResponse.getHits().getHits(); + StepListener searchStep = new StepListener<>(); - while (hits != null && hits.length > 0) { - for (SearchHit hit : hits) { - Map sourceAsMap = hit.getSourceAsMap(); - if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { - resourceIds.add(sourceAsMap.get("resource_id").toString()); - } + client.search(searchRequest, searchStep); + + searchStep.whenComplete(initialResponse -> { + String scrollId = initialResponse.getScrollId(); + processScrollResults(resourceIds, scroll, scrollId, initialResponse.getHits().getHits(), listener); + }, listener::onFailure); + } + + /** + * Helper method to process scroll results recursively. + * @param resourceIds List to collect resource IDs + * @param scroll Search Scroll + * @param scrollId Scroll ID + * @param hits Search hits + * @param listener Listener to be notified when the operation completes + */ + private void processScrollResults( + Set resourceIds, + Scroll scroll, + String scrollId, + SearchHit[] hits, + ActionListener listener + ) { + // If no hits, clean up and complete + if (hits == null || hits.length == 0) { + clearScroll(scrollId, listener); + return; + } + + // Process current batch of hits + for (SearchHit hit : hits) { + Map sourceAsMap = hit.getSourceAsMap(); + if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { + resourceIds.add(sourceAsMap.get("resource_id").toString()); } + } + + // Prepare next scroll request + SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); + scrollRequest.scroll(scroll); + + // Execute next scroll + client.searchScroll(scrollRequest, ActionListener.wrap(scrollResponse -> { + // Process next batch recursively + processScrollResults(resourceIds, scroll, scrollResponse.getScrollId(), scrollResponse.getHits().getHits(), listener); + }, e -> { + // Clean up scroll context on failure + clearScroll(scrollId, ActionListener.wrap(r -> listener.onFailure(e), ex -> { + e.addSuppressed(ex); + listener.onFailure(e); + })); + })); + } - SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); - scrollRequest.scroll(scroll); - searchResponse = client.execute(SearchScrollAction.INSTANCE, scrollRequest).actionGet(); - scrollId = searchResponse.getScrollId(); - hits = searchResponse.getHits().getHits(); + /** + * Helper method to clear scroll context. + * @param scrollId Scroll ID + * @param listener Listener to be notified when the operation completes + */ + private void clearScroll(String scrollId, ActionListener listener) { + if (scrollId == null) { + listener.onResponse(null); + return; } ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); clearScrollRequest.addScrollId(scrollId); - client.clearScroll(clearScrollRequest).actionGet(); + + client.clearScroll(clearScrollRequest, ActionListener.wrap(r -> listener.onResponse(null), e -> { + LOGGER.warn("Failed to clear scroll context", e); + listener.onResponse(null); + })); } /** * Updates the sharing configuration for an existing resource in the resource sharing index. - * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map, Set, String, boolean)} + * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map, Set, String, boolean, ActionListener)} * This method modifies the sharing permissions for a specific resource identified by its * resource ID and source index. * @@ -704,93 +820,105 @@ private void executeSearchRequest(Set resourceIds, Scroll scroll, Search * } * } * @param isAdmin Boolean indicating whether the user requesting to share is an admin or not - * @return ResourceSharing Returns resourceSharing object if the update was successful, null otherwise + * @param listener Listener to be notified when the operation completes + * * @throws RuntimeException if there's an error during the update operation */ - public ResourceSharing updateResourceSharingInfo( + public void updateResourceSharingInfo( String resourceId, String sourceIdx, String requestUserName, ShareWith shareWith, - boolean isAdmin + boolean isAdmin, + ActionListener listener ) { XContentBuilder builder; Map shareWithMap; try { - builder = jsonBuilder(); + builder = XContentFactory.jsonBuilder(); shareWith.toXContent(builder, ToXContent.EMPTY_PARAMS); String json = builder.toString(); shareWithMap = DefaultObjectMapper.readValue(json, new TypeReference<>() { }); - } catch (IOException e) { LOGGER.error("Failed to build json content", e); - throw new OpenSearchException("Failed to build json content", e); + listener.onFailure(new OpenSearchException("Failed to build json content", e)); + return; } - // Check if the user requesting to share is the owner of the resource - // TODO Add a way for users who are not creators to be able to share the resource - ResourceSharing currentSharingInfo = fetchDocumentById(sourceIdx, resourceId); - if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { - LOGGER.error("User {} is not authorized to share resource {}", requestUserName, resourceId); - throw new OpenSearchException("User " + requestUserName + " is not authorized to share resource " + resourceId); - } + StepListener fetchDocListener = new StepListener<>(); + StepListener updateScriptListener = new StepListener<>(); + StepListener updatedSharingListener = new StepListener<>(); - CreatedBy createdBy; - if (currentSharingInfo == null) { - createdBy = new CreatedBy(Creator.USER.getName(), requestUserName); - } else { - createdBy = currentSharingInfo.getCreatedBy(); - } + // Fetch resource sharing doc + fetchDocumentById(sourceIdx, resourceId, fetchDocListener); - // Atomic operation - Script updateScript = new Script(ScriptType.INLINE, "painless", """ - if (ctx._source.share_with == null) { - ctx._source.share_with = [:]; + // build update script + fetchDocListener.whenComplete(currentSharingInfo -> { + // Check if user can share. At present only the resource creator and admin is allowed to share the resource + if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { + + LOGGER.error("User {} is not authorized to share resource {}", requestUserName, resourceId); + throw new OpenSearchException("User " + requestUserName + " is not authorized to share resource " + resourceId); } - for (def entry : params.shareWith.entrySet()) { - def scopeName = entry.getKey(); - def newScope = entry.getValue(); + Script updateScript = new Script(ScriptType.INLINE, "painless", """ + if (ctx._source.share_with == null) { + ctx._source.share_with = [:]; + } - if (!ctx._source.share_with.containsKey(scopeName)) { - def newScopeEntry = [:]; - for (def field : newScope.entrySet()) { - if (field.getValue() != null && !field.getValue().isEmpty()) { - newScopeEntry[field.getKey()] = new HashSet(field.getValue()); + for (def entry : params.shareWith.entrySet()) { + def scopeName = entry.getKey(); + def newScope = entry.getValue(); + + if (!ctx._source.share_with.containsKey(scopeName)) { + def newScopeEntry = [:]; + for (def field : newScope.entrySet()) { + if (field.getValue() != null && !field.getValue().isEmpty()) { + newScopeEntry[field.getKey()] = new HashSet(field.getValue()); + } } - } - ctx._source.share_with[scopeName] = newScopeEntry; - } else { - def existingScope = ctx._source.share_with[scopeName]; + ctx._source.share_with[scopeName] = newScopeEntry; + } else { + def existingScope = ctx._source.share_with[scopeName]; - for (def field : newScope.entrySet()) { - def fieldName = field.getKey(); - def newValues = field.getValue(); + for (def field : newScope.entrySet()) { + def fieldName = field.getKey(); + def newValues = field.getValue(); - if (newValues != null && !newValues.isEmpty()) { - if (!existingScope.containsKey(fieldName)) { - existingScope[fieldName] = new HashSet(); - } + if (newValues != null && !newValues.isEmpty()) { + if (!existingScope.containsKey(fieldName)) { + existingScope[fieldName] = new HashSet(); + } - for (def value : newValues) { - if (!existingScope[fieldName].contains(value)) { - existingScope[fieldName].add(value); + for (def value : newValues) { + if (!existingScope[fieldName].contains(value)) { + existingScope[fieldName].add(value); + } } } } } } - } - """, Collections.singletonMap("shareWith", shareWithMap)); + """, Collections.singletonMap("shareWith", shareWithMap)); - boolean success = updateByQueryResourceSharing(sourceIdx, resourceId, updateScript); - if (!success) { - LOGGER.error("Failed to update resource sharing info for resource {}", resourceId); - throw new OpenSearchException("Failed to update resource sharing info for resource " + resourceId); - } + updateByQueryResourceSharing(sourceIdx, resourceId, updateScript, updateScriptListener); + + }, listener::onFailure); - return new ResourceSharing(resourceId, sourceIdx, createdBy, shareWith); + // Build & return the updated ResourceSharing + updateScriptListener.whenComplete(success -> { + if (!success) { + LOGGER.error("Failed to update resource sharing info for resource {}", resourceId); + listener.onResponse(null); + return; + } + // TODO check if this should be replaced by Java in-memory computation (current intuition is that it will be more memory + // intensive to do it in java) + fetchDocumentById(sourceIdx, resourceId, updatedSharingListener); + }, listener::onFailure); + + updatedSharingListener.whenComplete(listener::onResponse, listener::onFailure); } /** @@ -821,8 +949,7 @@ public ResourceSharing updateResourceSharingInfo( * @param resourceId The resource ID to match in the query (exact match) * @param updateScript The script containing the update operations to be performed. * This script defines how the matching documents should be modified - * @return boolean true if at least one document was updated, false if no documents - * were found or update failed + * @param listener Listener to be notified when the operation completes * * @apiNote This method: *
          @@ -846,8 +973,7 @@ public ResourceSharing updateResourceSharingInfo( * } * */ - private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId, Script updateScript) { - // TODO: Once stashContext is replaced with switchContext this call will have to be modified + private void updateByQueryResourceSharing(String sourceIdx, String resourceId, Script updateScript, ActionListener listener) { try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { BoolQueryBuilder query = QueryBuilders.boolQuery() .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) @@ -857,24 +983,36 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId .setScript(updateScript) .setRefresh(true); - BulkByScrollResponse response = client.execute(UpdateByQueryAction.INSTANCE, ubq).actionGet(); + client.execute(UpdateByQueryAction.INSTANCE, ubq, new ActionListener<>() { + @Override + public void onResponse(BulkByScrollResponse response) { + long updated = response.getUpdated(); + if (updated > 0) { + LOGGER.info("Successfully updated {} documents in {}.", updated, resourceSharingIndex); + listener.onResponse(true); + } else { + LOGGER.info( + "No documents found to update in {} for source_idx: {} and resource_id: {}", + resourceSharingIndex, + sourceIdx, + resourceId + ); + listener.onResponse(false); + } - if (response.getUpdated() > 0) { - LOGGER.info("Successfully updated {} documents in {}.", response.getUpdated(), resourceSharingIndex); - return true; - } else { - LOGGER.info( - "No documents found to update in {} for source_idx: {} and resource_id: {}", - resourceSharingIndex, - sourceIdx, - resourceId - ); - return false; - } + } + + @Override + public void onFailure(Exception e) { + LOGGER.error("Failed to update documents in {}.", resourceSharingIndex, e); + listener.onFailure(e); + + } + }); } catch (Exception e) { - LOGGER.error("Failed to update documents in {}.", resourceSharingIndex, e); - return false; + LOGGER.error("Failed to update documents in {} before request submission.", resourceSharingIndex, e); + listener.onFailure(e); } } @@ -913,7 +1051,7 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId * @param scopes A list of scopes to revoke access from. If null or empty, access is revoked from all scopes * @param requestUserName The user trying to revoke the accesses * @param isAdmin Boolean indicating whether the user is an admin or not - * @return The updated ResourceSharing object after revoking access, or null if the document doesn't exist + * @param listener Listener to be notified when the operation completes * @throws IllegalArgumentException if resourceId, sourceIdx is null/empty, or if revokeAccess is null/empty * @throws RuntimeException if the update operation fails or encounters an error * @@ -930,76 +1068,102 @@ private boolean updateByQueryResourceSharing(String sourceIdx, String resourceId * ResourceSharing updated = revokeAccess("resourceId", "pluginIndex", revokeAccess); * */ - public ResourceSharing revokeAccess( + public void revokeAccess( String resourceId, String sourceIdx, Map> revokeAccess, Set scopes, String requestUserName, - boolean isAdmin + boolean isAdmin, + ActionListener listener ) { if (StringUtils.isBlank(resourceId) || StringUtils.isBlank(sourceIdx) || revokeAccess == null || revokeAccess.isEmpty()) { - throw new IllegalArgumentException("resourceId, sourceIdx, and revokeAccess must not be null or empty"); + listener.onFailure(new IllegalArgumentException("resourceId, sourceIdx, and revokeAccess must not be null or empty")); + return; } - // TODO Check if access can be revoked by non-creator - ResourceSharing currentSharingInfo = fetchDocumentById(sourceIdx, resourceId); - if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { - LOGGER.error("User {} is not authorized to revoke access to resource {}", requestUserName, resourceId); - throw new OpenSearchException("User " + requestUserName + " is not authorized to revoke access to resource " + resourceId); - } + try (ThreadContext.StoredContext storedContext = this.threadPool.getThreadContext().stashContext()) { - LOGGER.debug("Revoking access for resource {} in {} for entities: {} and scopes: {}", resourceId, sourceIdx, revokeAccess, scopes); + LOGGER.debug( + "Revoking access for resource {} in {} for entities: {} and scopes: {}", + resourceId, + sourceIdx, + revokeAccess, + scopes + ); - // TODO: Once stashContext is replaced with switchContext this call will have to be modified - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - Map revoke = new HashMap<>(); - for (Map.Entry> entry : revokeAccess.entrySet()) { - revoke.put(entry.getKey().getType().toLowerCase(), new ArrayList<>(entry.getValue())); - } + StepListener currentSharingListener = new StepListener<>(); + StepListener revokeUpdateListener = new StepListener<>(); + StepListener updatedSharingListener = new StepListener<>(); - List scopesToUse = scopes != null ? new ArrayList<>(scopes) : new ArrayList<>(); + // Fetch the current ResourceSharing document + fetchDocumentById(sourceIdx, resourceId, currentSharingListener); - Script revokeScript = new Script(ScriptType.INLINE, "painless", """ - if (ctx._source.share_with != null) { - Set scopesToProcess = new HashSet(params.scopes.isEmpty() ? ctx._source.share_with.keySet() : params.scopes); + // Check permissions & build revoke script + currentSharingListener.whenComplete(currentSharingInfo -> { + // Only admin or the creator of the resource is currently allowed to revoke access + if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { + throw new OpenSearchException( + "User " + requestUserName + " is not authorized to revoke access to resource " + resourceId + ); + } - for (def scopeName : scopesToProcess) { - if (ctx._source.share_with.containsKey(scopeName)) { - def existingScope = ctx._source.share_with.get(scopeName); + Map revoke = new HashMap<>(); + for (Map.Entry> entry : revokeAccess.entrySet()) { + revoke.put(entry.getKey().getType().toLowerCase(), new ArrayList<>(entry.getValue())); + } + List scopesToUse = (scopes != null) ? new ArrayList<>(scopes) : new ArrayList<>(); - for (def entry : params.revokeAccess.entrySet()) { - def RecipientType = entry.getKey(); - def entitiesToRemove = entry.getValue(); + // Build the revoke script + Script revokeScript = new Script(ScriptType.INLINE, "painless", """ + if (ctx._source.share_with != null) { + Set scopesToProcess = new HashSet(params.scopes.isEmpty() ? ctx._source.share_with.keySet() : params.scopes); - if (existingScope.containsKey(RecipientType) && existingScope[RecipientType] != null) { - if (!(existingScope[RecipientType] instanceof HashSet)) { - existingScope[RecipientType] = new HashSet(existingScope[RecipientType]); - } + for (def scopeName : scopesToProcess) { + if (ctx._source.share_with.containsKey(scopeName)) { + def existingScope = ctx._source.share_with.get(scopeName); - existingScope[RecipientType].removeAll(entitiesToRemove); + for (def entry : params.revokeAccess.entrySet()) { + def RecipientType = entry.getKey(); + def entitiesToRemove = entry.getValue(); - if (existingScope[RecipientType].isEmpty()) { - existingScope.remove(RecipientType); + if (existingScope.containsKey(RecipientType) && existingScope[RecipientType] != null) { + if (!(existingScope[RecipientType] instanceof HashSet)) { + existingScope[RecipientType] = new HashSet(existingScope[RecipientType]); + } + + existingScope[RecipientType].removeAll(entitiesToRemove); + + if (existingScope[RecipientType].isEmpty()) { + existingScope.remove(RecipientType); + } } } - } - if (existingScope.isEmpty()) { - ctx._source.share_with.remove(scopeName); + if (existingScope.isEmpty()) { + ctx._source.share_with.remove(scopeName); + } } } } - } - """, Map.of("revokeAccess", revoke, "scopes", scopesToUse)); + """, Map.of("revokeAccess", revoke, "scopes", scopesToUse)); + updateByQueryResourceSharing(sourceIdx, resourceId, revokeScript, revokeUpdateListener); - boolean success = updateByQueryResourceSharing(sourceIdx, resourceId, revokeScript); + }, listener::onFailure); - return success ? fetchDocumentById(sourceIdx, resourceId) : null; + // Return doc or null based on successful result, fail otherwise + revokeUpdateListener.whenComplete(success -> { + if (!success) { + LOGGER.error("Failed to revoke access for resource {} in index {} (no docs updated).", resourceId, sourceIdx); + listener.onResponse(null); + return; + } + // TODO check if this should be replaced by Java in-memory computation (current intuition is that it will be more memory + // intensive to do it in java) + fetchDocumentById(sourceIdx, resourceId, updatedSharingListener); + }, listener::onFailure); - } catch (Exception e) { - LOGGER.error("Failed to revoke access for resource: {} in index: {}", resourceId, sourceIdx, e); - throw new RuntimeException("Failed to revoke access: " + e.getMessage(), e); + updatedSharingListener.whenComplete(listener::onResponse, listener::onFailure); } } @@ -1167,47 +1331,54 @@ public boolean deleteAllRecordsForUser(String name) { * @param parser The class to deserialize the documents into a specified type defined by the parser. * @return A set of deserialized documents. */ - public Set getResourceDocumentsFromIds( + public void getResourceDocumentsFromIds( Set resourceIds, String resourceIndex, - ResourceParser parser + ResourceParser parser, + ActionListener> listener ) { - Set result = new HashSet<>(); if (resourceIds.isEmpty()) { - return result; + listener.onResponse(new HashSet<>()); + return; } // stashing Context to avoid permission issues in-case resourceIndex is a system index - // TODO: Once stashContext is replaced with switchContext this call will have to be modified try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { MultiGetRequest request = new MultiGetRequest(); for (String id : resourceIds) { request.add(new MultiGetRequest.Item(resourceIndex, id)); } - MultiGetResponse response = client.multiGet(request).actionGet(); - - for (MultiGetItemResponse itemResponse : response.getResponses()) { - if (!itemResponse.isFailed() && itemResponse.getResponse().isExists()) { - BytesReference sourceAsString = itemResponse.getResponse().getSourceAsBytesRef(); - XContentParser xContentParser = XContentHelper.createParser( - NamedXContentRegistry.EMPTY, - LoggingDeprecationHandler.INSTANCE, - sourceAsString, - XContentType.JSON - ); - T resource = parser.parseXContent(xContentParser); - result.add(resource); + client.multiGet(request, ActionListener.wrap(response -> { + Set result = new HashSet<>(); + try { + for (MultiGetItemResponse itemResponse : response.getResponses()) { + if (!itemResponse.isFailed() && itemResponse.getResponse().isExists()) { + BytesReference sourceAsString = itemResponse.getResponse().getSourceAsBytesRef(); + XContentParser xContentParser = XContentHelper.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + sourceAsString, + XContentType.JSON + ); + T resource = parser.parseXContent(xContentParser); + result.add(resource); + } + } + listener.onResponse(result); + } catch (Exception e) { + listener.onFailure(new OpenSearchException("Failed to parse resources: " + e.getMessage(), e)); } - } - } catch (IndexNotFoundException e) { - LOGGER.error("Index {} does not exist", resourceIndex, e); - throw e; - } catch (Exception e) { - LOGGER.error("Failed to fetch resources with ids {} from index {}", resourceIds, resourceIndex, e); - throw new OpenSearchException("Failed to fetch resources: " + e.getMessage(), e); + }, e -> { + if (e instanceof IndexNotFoundException) { + LOGGER.error("Index {} does not exist", resourceIndex, e); + listener.onFailure(e); + } else { + LOGGER.error("Failed to fetch resources with ids {} from index {}", resourceIds, resourceIndex, e); + listener.onFailure(new OpenSearchException("Failed to fetch resources: " + e.getMessage(), e)); + } + })); } - - return result; } + } diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java index 649a21dfb1..140e0eca33 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -92,7 +92,7 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re ResourceSharing sharing = this.resourceSharingIndexHandler.indexResourceSharing( resourceId, resourceIndex, - new CreatedBy(Creator.USER.getName(), user.getName()), + new CreatedBy(Creator.USER, user.getName()), null ); log.info("Successfully created a resource sharing entry {}", sharing); diff --git a/src/main/java/org/opensearch/security/rest/resources/access/list/RestListAccessibleResourcesAction.java b/src/main/java/org/opensearch/security/rest/resources/access/list/RestListAccessibleResourcesAction.java index 61935ee709..85fb04554b 100644 --- a/src/main/java/org/opensearch/security/rest/resources/access/list/RestListAccessibleResourcesAction.java +++ b/src/main/java/org/opensearch/security/rest/resources/access/list/RestListAccessibleResourcesAction.java @@ -10,12 +10,10 @@ import java.io.IOException; import java.util.List; -import java.util.Map; import com.google.common.collect.ImmutableList; import org.opensearch.client.node.NodeClient; -import org.opensearch.core.xcontent.XContentParser; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.RestRequest; import org.opensearch.rest.action.RestToXContentListener; @@ -30,7 +28,7 @@ public RestListAccessibleResourcesAction() {} @Override public List routes() { - return addRoutesPrefix(ImmutableList.of(new Route(GET, "/resources/list")), PLUGIN_ROUTE_PREFIX); + return addRoutesPrefix(ImmutableList.of(new Route(GET, "/resources/list/{resourceIndex}")), PLUGIN_ROUTE_PREFIX); } @Override @@ -40,12 +38,7 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - - String resourceIndex = (String) source.get("resource_index"); + String resourceIndex = request.param("resourceIndex", ""); final ListAccessibleResourcesRequest listAccessibleResourcesRequest = new ListAccessibleResourcesRequest(resourceIndex); return channel -> client.executeLocally( ListAccessibleResourcesAction.INSTANCE, diff --git a/src/main/java/org/opensearch/security/transport/resources/access/TransportListAccessibleResourcesAction.java b/src/main/java/org/opensearch/security/transport/resources/access/TransportListAccessibleResourcesAction.java index e165b65436..25c727de67 100644 --- a/src/main/java/org/opensearch/security/transport/resources/access/TransportListAccessibleResourcesAction.java +++ b/src/main/java/org/opensearch/security/transport/resources/access/TransportListAccessibleResourcesAction.java @@ -8,8 +8,6 @@ package org.opensearch.security.transport.resources.access; -import java.util.Set; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -22,7 +20,6 @@ import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesAction; import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesRequest; import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesResponse; -import org.opensearch.security.spi.resources.Resource; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; @@ -45,14 +42,22 @@ public TransportListAccessibleResourcesAction( @Override protected void doExecute(Task task, ListAccessibleResourcesRequest request, ActionListener listener) { try { - Set resources = resourceAccessHandler.getAccessibleResourcesForCurrentUser(request.getResourceIndex()); - log.info("Successfully fetched accessible resources for current user : {}", resources); - String resourceType = OpenSearchSecurityPlugin.getResourceProviders().get(request.getResourceIndex()).getResourceType(); - listener.onResponse(new ListAccessibleResourcesResponse(resourceType, resources)); + resourceAccessHandler.getAccessibleResourcesForCurrentUser(request.getResourceIndex(), ActionListener.wrap(resources -> { + try { + log.info("Successfully fetched accessible resources for current user : {}", resources); + String resourceType = OpenSearchSecurityPlugin.getResourceProviders().get(request.getResourceIndex()).getResourceType(); + listener.onResponse(new ListAccessibleResourcesResponse(resourceType, resources)); + } catch (Exception e) { + log.error("Failed to process accessible resources response", e); + listener.onFailure(e); + } + }, e -> { + log.error("Failed to list accessible resources for current user", e); + listener.onFailure(e); + })); } catch (Exception e) { - log.info("Failed to list accessible resources for current user: ", e); + log.error("Failed to initiate accessible resources request", e); listener.onFailure(e); } - } } diff --git a/src/main/java/org/opensearch/security/transport/resources/access/TransportRevokeResourceAccessAction.java b/src/main/java/org/opensearch/security/transport/resources/access/TransportRevokeResourceAccessAction.java index 7a04e5d46f..97f139780d 100644 --- a/src/main/java/org/opensearch/security/transport/resources/access/TransportRevokeResourceAccessAction.java +++ b/src/main/java/org/opensearch/security/transport/resources/access/TransportRevokeResourceAccessAction.java @@ -11,16 +11,15 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.OpenSearchException; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; import org.opensearch.security.resources.ResourceAccessHandler; -import org.opensearch.security.resources.ResourceSharing; import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessAction; import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessRequest; import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessResponse; +import org.opensearch.security.spi.resources.ResourceSharingException; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; @@ -41,25 +40,29 @@ public TransportRevokeResourceAccessAction( @Override protected void doExecute(Task task, RevokeResourceAccessRequest request, ActionListener listener) { try { - ResourceSharing revoke = revokeAccess(request); - if (revoke == null) { - log.error("Failed to revoke access to resource {}", request.getResourceId()); - listener.onFailure(new OpenSearchException("Failed to revoke access to resource " + request.getResourceId())); - return; - } - log.info("Revoked resource access for resource: {} with {}", request.getResourceId(), revoke.toString()); - listener.onResponse(new RevokeResourceAccessResponse("Resource " + request.getResourceId() + " access revoked successfully.")); + this.resourceAccessHandler.revokeAccess( + request.getResourceId(), + request.getResourceIndex(), + request.getRevokeAccess(), + request.getScopes(), + ActionListener.wrap(resourceSharing -> { + if (resourceSharing == null) { + log.error("Failed to revoke access to resource {}", request.getResourceId()); + listener.onFailure(new ResourceSharingException("Failed to revoke access to resource " + request.getResourceId())); + } else { + log.info("Revoked resource access for resource: {} with {}", request.getResourceId(), resourceSharing.toString()); + listener.onResponse( + new RevokeResourceAccessResponse("Resource " + request.getResourceId() + " access revoked successfully.") + ); + } + }, e -> { + log.error("Exception while revoking access to resource {}: {}", request.getResourceId(), e.getMessage(), e); + listener.onFailure(e); + }) + ); } catch (Exception e) { listener.onFailure(e); } } - private ResourceSharing revokeAccess(RevokeResourceAccessRequest request) { - return this.resourceAccessHandler.revokeAccess( - request.getResourceId(), - request.getResourceIndex(), - request.getRevokeAccess(), - request.getScopes() - ); - } } diff --git a/src/main/java/org/opensearch/security/transport/resources/access/TransportShareResourceAction.java b/src/main/java/org/opensearch/security/transport/resources/access/TransportShareResourceAction.java index 4959de2ab2..0de7987dc4 100644 --- a/src/main/java/org/opensearch/security/transport/resources/access/TransportShareResourceAction.java +++ b/src/main/java/org/opensearch/security/transport/resources/access/TransportShareResourceAction.java @@ -17,7 +17,6 @@ import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; import org.opensearch.security.resources.ResourceAccessHandler; -import org.opensearch.security.resources.ResourceSharing; import org.opensearch.security.rest.resources.access.share.ShareResourceAction; import org.opensearch.security.rest.resources.access.share.ShareResourceRequest; import org.opensearch.security.rest.resources.access.share.ShareResourceResponse; @@ -40,22 +39,27 @@ public TransportShareResourceAction( @Override protected void doExecute(Task task, ShareResourceRequest request, ActionListener listener) { - ResourceSharing sharing = null; try { - sharing = shareResource(request); - if (sharing == null) { - log.error("Failed to share resource {}", request.getResourceId()); - listener.onFailure(new OpenSearchException("Failed to share resource " + request.getResourceId())); - return; - } - log.info("Shared resource : {} with {}", request.getResourceId(), sharing.toString()); - listener.onResponse(new ShareResourceResponse("Resource " + request.getResourceId() + " shared successfully.")); + this.resourceAccessHandler.shareWith( + request.getResourceId(), + request.getResourceIndex(), + request.getShareWith(), + ActionListener.wrap(resourceSharing -> { + if (resourceSharing == null) { + log.error("Failed to share resource {}", request.getResourceId()); + listener.onFailure(new OpenSearchException("Failed to share resource " + request.getResourceId())); + } else { + log.info("Shared resource : {} with {}", request.getResourceId(), resourceSharing.toString()); + listener.onResponse(new ShareResourceResponse("Resource " + request.getResourceId() + " shared successfully.")); + } + }, e -> { + log.error("Error while sharing resource {}: {}", request.getResourceId(), e.getMessage(), e); + listener.onFailure(e); + }) + ); } catch (Exception e) { + log.error("Exception while trying to share resource {}: {}", request.getResourceId(), e.getMessage(), e); listener.onFailure(e); } } - - private ResourceSharing shareResource(ShareResourceRequest request) throws Exception { - return this.resourceAccessHandler.shareWith(request.getResourceId(), request.getResourceIndex(), request.getShareWith()); - } } diff --git a/src/main/java/org/opensearch/security/transport/resources/access/TransportVerifyResourceAccessAction.java b/src/main/java/org/opensearch/security/transport/resources/access/TransportVerifyResourceAccessAction.java index 0b732a1cb1..93965f9f0b 100644 --- a/src/main/java/org/opensearch/security/transport/resources/access/TransportVerifyResourceAccessAction.java +++ b/src/main/java/org/opensearch/security/transport/resources/access/TransportVerifyResourceAccessAction.java @@ -41,22 +41,33 @@ public TransportVerifyResourceAccessAction( @Override protected void doExecute(Task task, VerifyResourceAccessRequest request, ActionListener listener) { try { - boolean hasRequestedScopeAccess = this.resourceAccessHandler.hasPermission( + resourceAccessHandler.hasPermission( request.getResourceId(), request.getResourceIndex(), - request.getScope() - ); + request.getScope(), + new ActionListener<>() { + @Override + public void onResponse(Boolean hasRequestedScopeAccess) { + StringBuilder sb = new StringBuilder(); + sb.append("User "); + sb.append(hasRequestedScopeAccess ? "has" : "does not have"); + sb.append(" requested scope "); + sb.append(request.getScope()); + sb.append(" access to "); + sb.append(request.getResourceId()); + + log.info(sb.toString()); - StringBuilder sb = new StringBuilder(); - sb.append("User "); - sb.append(hasRequestedScopeAccess ? "has" : "does not have"); - sb.append(" requested scope "); - sb.append(request.getScope()); - sb.append(" access to "); - sb.append(request.getResourceId()); + listener.onResponse(new VerifyResourceAccessResponse(sb.toString())); + } - log.info(sb.toString()); - listener.onResponse(new VerifyResourceAccessResponse(sb.toString())); + @Override + public void onFailure(Exception e) { + log.info("Failed to check user permissions for resource {}", request.getResourceId(), e); + listener.onFailure(e); + } + } + ); } catch (Exception e) { log.info("Failed to check user permissions for resource {}", request.getResourceId(), e); listener.onFailure(e); diff --git a/src/test/java/org/opensearch/security/resources/CreatedByTests.java b/src/test/java/org/opensearch/security/resources/CreatedByTests.java index 346a949444..0bc651b4d5 100644 --- a/src/test/java/org/opensearch/security/resources/CreatedByTests.java +++ b/src/test/java/org/opensearch/security/resources/CreatedByTests.java @@ -32,7 +32,7 @@ public class CreatedByTests extends SingleClusterTest { - private static final String CREATOR_TYPE = "user"; + private static final Enum CREATOR_TYPE = Creator.USER; public void testCreatedByConstructorWithValidUser() { String expectedUser = "testUser"; @@ -45,7 +45,7 @@ public void testCreatedByFromStreamInput() throws IOException { String expectedUser = "testUser"; try (BytesStreamOutput out = new BytesStreamOutput()) { - out.writeString(CREATOR_TYPE); + out.writeEnum(Creator.valueOf(CREATOR_TYPE.name())); out.writeString(expectedUser); StreamInput in = out.bytes().streamInput(); From b5f29619cd25b845026b52069f2b5ce624f14695 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 17 Jan 2025 16:06:59 -0500 Subject: [PATCH 094/201] Adds update sample request flow Signed-off-by: Darshit Chanpura --- .../sample/SampleResourcePlugin.java | 3 + .../rest/create/CreateResourceRestAction.java | 29 ++++++- .../rest/create/UpdateResourceAction.java | 29 +++++++ .../rest/create/UpdateResourceRequest.java | 58 ++++++++++++++ .../UpdateResourceTransportAction.java | 79 +++++++++++++++++++ 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/UpdateResourceAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/UpdateResourceRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index 11cbbcb308..6d386b85cb 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -39,10 +39,12 @@ import org.opensearch.rest.RestHandler; import org.opensearch.sample.resource.actions.rest.create.CreateResourceAction; import org.opensearch.sample.resource.actions.rest.create.CreateResourceRestAction; +import org.opensearch.sample.resource.actions.rest.create.UpdateResourceAction; import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceAction; import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceRestAction; import org.opensearch.sample.resource.actions.transport.CreateResourceTransportAction; import org.opensearch.sample.resource.actions.transport.DeleteResourceTransportAction; +import org.opensearch.sample.resource.actions.transport.UpdateResourceTransportAction; import org.opensearch.script.ScriptService; import org.opensearch.security.spi.resources.ResourceParser; import org.opensearch.security.spi.resources.ResourceSharingExtension; @@ -94,6 +96,7 @@ public List getRestHandlers( public List> getActions() { return List.of( new ActionHandler<>(CreateResourceAction.INSTANCE, CreateResourceTransportAction.class), + new ActionHandler<>(UpdateResourceAction.INSTANCE, UpdateResourceTransportAction.class), new ActionHandler<>(DeleteResourceAction.INSTANCE, DeleteResourceTransportAction.class) ); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java index e990cc8a1d..02b6527a53 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java @@ -30,7 +30,7 @@ public CreateResourceRestAction() {} public List routes() { return List.of( new Route(PUT, "/_plugins/sample_resource_sharing/create"), - new Route(POST, "/_plugins/sample_resource_sharing/update") + new Route(POST, "/_plugins/sample_resource_sharing/update/{resourceId}") ); } @@ -46,6 +46,33 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli source = parser.map(); } + switch (request.method()) { + case PUT: + return createResource(source, client); + case POST: + return updateResource(source, request.param("resourceId"), client); + default: + throw new IllegalArgumentException("Illegal method: " + request.method()); + } + } + + private RestChannelConsumer updateResource(Map source, String resourceId, NodeClient client) throws IOException { + String name = (String) source.get("name"); + String description = source.containsKey("description") ? (String) source.get("description") : null; + Map attributes = source.containsKey("attributes") ? (Map) source.get("attributes") : null; + SampleResource resource = new SampleResource(); + resource.setName(name); + resource.setDescription(description); + resource.setAttributes(attributes); + final UpdateResourceRequest updateResourceRequest = new UpdateResourceRequest(resourceId, resource); + return channel -> client.executeLocally( + UpdateResourceAction.INSTANCE, + updateResourceRequest, + new RestToXContentListener<>(channel) + ); + } + + private RestChannelConsumer createResource(Map source, NodeClient client) throws IOException { String name = (String) source.get("name"); String description = source.containsKey("description") ? (String) source.get("description") : null; Map attributes = source.containsKey("attributes") ? (Map) source.get("attributes") : null; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/UpdateResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/UpdateResourceAction.java new file mode 100644 index 0000000000..129c2d1546 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/UpdateResourceAction.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.create; + +import org.opensearch.action.ActionType; + +/** + * Action to update a sample resource + */ +public class UpdateResourceAction extends ActionType { + /** + * Create sample resource action instance + */ + public static final UpdateResourceAction INSTANCE = new UpdateResourceAction(); + /** + * Create sample resource action name + */ + public static final String NAME = "cluster:admin/sample-resource-plugin/update"; + + private UpdateResourceAction() { + super(NAME, CreateResourceResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/UpdateResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/UpdateResourceRequest.java new file mode 100644 index 0000000000..db74525a3a --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/UpdateResourceRequest.java @@ -0,0 +1,58 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.create; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.security.spi.resources.Resource; + +/** + * Request object for UpdateResource transport action + */ +public class UpdateResourceRequest extends ActionRequest { + + private final String resourceId; + private final Resource resource; + + /** + * Default constructor + */ + public UpdateResourceRequest(String resourceId, Resource resource) { + this.resourceId = resourceId; + this.resource = resource; + } + + public UpdateResourceRequest(StreamInput in) throws IOException { + this.resourceId = in.readString(); + this.resource = in.readNamedWriteable(Resource.class); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(resourceId); + resource.writeTo(out); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public Resource getResource() { + return this.resource; + } + + public String getResourceId() { + return this.resourceId; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java new file mode 100644 index 0000000000..e9ec0127dd --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java @@ -0,0 +1,79 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.transport; + +import java.io.IOException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.action.update.UpdateRequest; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.sample.resource.actions.rest.create.*; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +public class UpdateResourceTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(UpdateResourceTransportAction.class); + + private final TransportService transportService; + private final Client nodeClient; + + @Inject + public UpdateResourceTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { + super(UpdateResourceAction.NAME, transportService, actionFilters, UpdateResourceRequest::new); + this.transportService = transportService; + this.nodeClient = nodeClient; + } + + @Override + protected void doExecute(Task task, UpdateResourceRequest request, ActionListener listener) { + ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + updateResource(request, listener); + listener.onResponse( + new CreateResourceResponse("Resource " + request.getResource().getResourceName() + " updated successfully.") + ); + } catch (Exception e) { + log.info("Failed to update resource: {}", request.getResourceId(), e); + listener.onFailure(e); + } + } + + private void updateResource(UpdateResourceRequest request, ActionListener listener) { + String resourceId = request.getResourceId(); + Resource sample = request.getResource(); + try (XContentBuilder builder = jsonBuilder()) { + UpdateRequest ur = new UpdateRequest(RESOURCE_INDEX_NAME, resourceId).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .doc(sample.toXContent(builder, ToXContent.EMPTY_PARAMS)); + + log.info("Update Request: {}", ur.toString()); + + nodeClient.update( + ur, + ActionListener.wrap(updateResponse -> { log.info("Updated resource: {}", updateResponse.toString()); }, listener::onFailure) + ); + } catch (IOException e) { + listener.onFailure(new RuntimeException(e)); + } + + } +} From f8622230333cf6e12e8dc6bfaa9914e26a1bea15 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 17 Jan 2025 16:32:26 -0500 Subject: [PATCH 095/201] Updates final methods to be async calss Signed-off-by: Darshit Chanpura --- .../resources/ResourceAccessHandler.java | 143 +++++++++------ .../ResourceSharingIndexHandler.java | 165 ++++++++++-------- .../ResourceSharingIndexListener.java | 14 +- 3 files changed, 187 insertions(+), 135 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index b1387e712c..98d244906a 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -85,7 +85,8 @@ public void initializeRecipientTypes() { /** * Returns a set of accessible resource IDs for the current user within the specified resource index. * @param resourceIndex The resource index to check for accessible resources. - * @return A set of accessible resource IDs. + * @param listener The listener to be notified with the set of accessible resource IDs. + * */ public void getAccessibleResourceIdsForCurrentUser(String resourceIndex, ActionListener> listener) { final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( @@ -134,19 +135,37 @@ public void onFailure(Exception e) { loadOwnResources(resourceIndex, user.getName(), ownResourcesListener); // Load resources shared with the user by its name. - ownResourcesListener.whenComplete(ownResources -> { - loadSharedWithResources(resourceIndex, Set.of(user.getName()), Recipient.USERS.toString(), userNameResourcesListener); - }, listener::onFailure); + ownResourcesListener.whenComplete( + ownResources -> loadSharedWithResources( + resourceIndex, + Set.of(user.getName()), + Recipient.USERS.toString(), + userNameResourcesListener + ), + listener::onFailure + ); // Load resources shared with the user’s roles. - userNameResourcesListener.whenComplete(userNameResources -> { - loadSharedWithResources(resourceIndex, user.getSecurityRoles(), Recipient.ROLES.toString(), rolesResourcesListener); - }, listener::onFailure); + userNameResourcesListener.whenComplete( + userNameResources -> loadSharedWithResources( + resourceIndex, + user.getSecurityRoles(), + Recipient.ROLES.toString(), + rolesResourcesListener + ), + listener::onFailure + ); // Load resources shared with the user’s backend roles. - rolesResourcesListener.whenComplete(rolesResources -> { - loadSharedWithResources(resourceIndex, user.getRoles(), Recipient.BACKEND_ROLES.toString(), backendRolesResourcesListener); - }, listener::onFailure); + rolesResourcesListener.whenComplete( + rolesResources -> loadSharedWithResources( + resourceIndex, + user.getRoles(), + Recipient.BACKEND_ROLES.toString(), + backendRolesResourcesListener + ), + listener::onFailure + ); // Combine all results and pass them back to the original listener. backendRolesResourcesListener.whenComplete(backendRolesResources -> { @@ -167,29 +186,36 @@ public void onFailure(Exception e) { * Returns a set of accessible resources for the current user within the specified resource index. * * @param resourceIndex The resource index to check for accessible resources. + * @param listener The listener to be notified with the set of accessible resources. */ @SuppressWarnings("unchecked") public void getAccessibleResourcesForCurrentUser(String resourceIndex, ActionListener> listener) { try { validateArguments(resourceIndex); + ResourceParser parser = OpenSearchSecurityPlugin.getResourceProviders().get(resourceIndex).getResourceParser(); - Set resourceIds = getAccessibleResourceIdsForCurrentUser(resourceIndex); - if (resourceIds.isEmpty()) { - listener.onResponse(Set.of()); - return; - } + StepListener> resourceIdsListener = new StepListener<>(); + StepListener> resourcesListener = new StepListener<>(); - this.resourceSharingIndexHandler.getResourceDocumentsFromIds( - resourceIds, - resourceIndex, - parser, - ActionListener.wrap( - listener::onResponse, - exception -> listener.onFailure( - new ResourceSharingException("Failed to get accessible resources: " + exception.getMessage(), exception) - ) - ) + // Fetch resource IDs + getAccessibleResourceIdsForCurrentUser(resourceIndex, resourceIdsListener); + + // Fetch docs + resourceIdsListener.whenComplete(resourceIds -> { + if (resourceIds.isEmpty()) { + // No accessible resources => immediately respond with empty set + listener.onResponse(Collections.emptySet()); + } else { + // Fetch the resource documents asynchronously + this.resourceSharingIndexHandler.getResourceDocumentsFromIds(resourceIds, resourceIndex, parser, resourcesListener); + } + }, listener::onFailure); + + // Send final response + resourcesListener.whenComplete( + listener::onResponse, + ex -> listener.onFailure(new ResourceSharingException("Failed to get accessible resources: " + ex.getMessage(), ex)) ); } catch (Exception e) { listener.onFailure(new ResourceSharingException("Failed to process accessible resources request: " + e.getMessage(), e)); @@ -202,6 +228,7 @@ public void getAccessibleResourcesForCurrentUser(String res * @param resourceId The resource ID to check access for. * @param resourceIndex The resource index containing the resource. * @param scope The permission scope to check. + * @param listener The listener to be notified with the permission check result. */ public void hasPermission(String resourceId, String resourceIndex, String scope, ActionListener listener) { validateArguments(resourceId, resourceIndex, scope); @@ -263,6 +290,7 @@ public void hasPermission(String resourceId, String resourceIndex, String scope, * @param resourceId The resource ID to share. * @param resourceIndex The index where resource is store * @param shareWith The users, roles, and backend roles as well as scope to share the resource with. + * @param listener The listener to be notified with the updated ResourceSharing document. */ public void shareWith(String resourceId, String resourceIndex, ShareWith shareWith, ActionListener listener) { validateArguments(resourceId, resourceIndex, shareWith); @@ -274,7 +302,7 @@ public void shareWith(String resourceId, String resourceIndex, ShareWith shareWi if (user == null) { LOGGER.warn("No authenticated user found in the ThreadContext."); - listener.onFailure(new OpenSearchException("No authenticated user found.")); + listener.onFailure(new ResourceSharingException("No authenticated user found.")); return; } @@ -309,7 +337,7 @@ public void shareWith(String resourceId, String resourceIndex, ShareWith shareWi * @param resourceIndex The index where resource is store * @param revokeAccess The users, roles, and backend roles to revoke access for. * @param scopes The permission scopes to revoke access for. - * @return The updated ResourceSharing document. + * @param listener The listener to be notified with the updated ResourceSharing document. */ public void revokeAccess( String resourceId, @@ -360,6 +388,7 @@ public void revokeAccess( * Deletes a resource sharing record by its ID and the resource index it belongs to. * @param resourceId The resource ID to delete. * @param resourceIndex The resource index containing the resource. + * @param listener The listener to be notified with the deletion result. */ public void deleteResourceSharingRecord(String resourceId, String resourceIndex, ActionListener listener) { try { @@ -378,17 +407,21 @@ public void deleteResourceSharingRecord(String resourceId, String resourceIndex, user.getName() ); } else { - LOGGER.info("Deleting resource sharing record for resource {} in {} with no authenticated user", resourceId, resourceIndex); + listener.onFailure(new ResourceSharingException("No authenticated user available.")); } - resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId, ActionListener.wrap(document -> { + StepListener fetchDocListener = new StepListener<>(); + StepListener deleteDocListener = new StepListener<>(); + + resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId, fetchDocListener); + + fetchDocListener.whenComplete(document -> { if (document == null) { LOGGER.info("Document {} does not exist in index {}", resourceId, resourceIndex); listener.onResponse(false); return; } - // Check if the user is allowed to delete boolean isAdmin = (user != null && adminDNs.isAdmin(user)); boolean isOwner = (user != null && isOwnerOfResource(document, user.getName())); @@ -398,23 +431,14 @@ public void deleteResourceSharingRecord(String resourceId, String resourceIndex, (user == null ? "UNKNOWN" : user.getName()), resourceId ); + // Not allowed => no deletion listener.onResponse(false); - return; + } else { + resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex, deleteDocListener); } + }, listener::onFailure); - // Finally, perform the actual record deletion (assuming it's a synchronous call) - boolean result = resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex); - listener.onResponse(result); - }, exception -> { - // If an error happens while fetching - LOGGER.error( - "Failed to fetch resource sharing document for resource {} in index {}. Error: {}", - resourceId, - resourceIndex, - exception.getMessage() - ); - listener.onFailure(exception); - })); + deleteDocListener.whenComplete(listener::onResponse, listener::onFailure); } catch (Exception e) { LOGGER.error("Failed to delete resource sharing record for resource {}", resourceId, e); listener.onFailure(e); @@ -423,24 +447,37 @@ public void deleteResourceSharingRecord(String resourceId, String resourceIndex, /** * Deletes all resource sharing records for the current user. - * @return True if all records were successfully deleted, false otherwise. + * @param listener The listener to be notified with the deletion result. */ - public boolean deleteAllResourceSharingRecordsForCurrentUser() { - + public void deleteAllResourceSharingRecordsForCurrentUser(ActionListener listener) { final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER ); - User user = userSubject == null ? null : userSubject.getUser(); - LOGGER.info("Deleting all resource sharing records for resource {}", user.getName()); + final User user = (userSubject == null) ? null : userSubject.getUser(); - return this.resourceSharingIndexHandler.deleteAllRecordsForUser(user.getName()); + if (user == null) { + listener.onFailure(new OpenSearchException("No authenticated user available.")); + return; + } + + LOGGER.info("Deleting all resource sharing records for user {}", user.getName()); + + resourceSharingIndexHandler.deleteAllRecordsForUser(user.getName(), ActionListener.wrap(listener::onResponse, exception -> { + LOGGER.error( + "Failed to delete all resource sharing records for user {}: {}", + user.getName(), + exception.getMessage(), + exception + ); + listener.onFailure(exception); + })); } /** * Loads all resources within the specified resource index. * * @param resourceIndex The resource index to load resources from. - * @return A set of resource IDs. + * @param listener The listener to be notified with the set of resource IDs. */ private void loadAllResources(String resourceIndex, ActionListener> listener) { this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex, listener); @@ -451,7 +488,7 @@ private void loadAllResources(String resourceIndex, ActionListener> * * @param resourceIndex The resource index to load resources from. * @param userName The username of the owner. - * @return A set of resource IDs owned by the user. + * @param listener The listener to be notified with the set of resource IDs. */ private void loadOwnResources(String resourceIndex, String userName, ActionListener> listener) { this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName, listener); @@ -463,7 +500,7 @@ private void loadOwnResources(String resourceIndex, String userName, ActionListe * @param resourceIndex The resource index to load resources from. * @param entities The set of entities to check for shared resources. * @param recipientType The type of entity (e.g., users, roles, backend_roles). - * @return A set of resource IDs shared with the specified entities. + * @param listener The listener to be notified with the set of resource IDs. */ private void loadSharedWithResources( String resourceIndex, diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 576a146d3b..1f88799b39 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -176,12 +176,13 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn .setOpType(DocWriteRequest.OpType.CREATE) // only create if an entry doesn't exist .request(); - ActionListener irListener = ActionListener.wrap(idxResponse -> { - LOGGER.info("Successfully created {} entry.", resourceSharingIndex); - }, (failResponse) -> { - LOGGER.error(failResponse.getMessage()); - LOGGER.info("Failed to create {} entry.", resourceSharingIndex); - }); + ActionListener irListener = ActionListener.wrap( + idxResponse -> LOGGER.info("Successfully created {} entry.", resourceSharingIndex), + (failResponse) -> { + LOGGER.error(failResponse.getMessage()); + LOGGER.info("Failed to create {} entry.", resourceSharingIndex); + } + ); client.index(ir, irListener); return entry; } catch (Exception e) { @@ -228,7 +229,7 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn public void fetchAllDocuments(String pluginIndex, ActionListener> listener) { LOGGER.debug("Fetching all documents asynchronously from {} where source_idx = {}", resourceSharingIndex, pluginIndex); - try (final ThreadContext.StoredContext storedContext = this.threadPool.getThreadContext().stashContext();) { + try (final ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query( QueryBuilders.termQuery("source_idx.keyword", pluginIndex) @@ -434,7 +435,7 @@ public void fetchDocumentsForAGivenScope( final Set resourceIds = new HashSet<>(); final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); - try (ThreadContext.StoredContext storedContext = this.threadPool.getThreadContext().stashContext()) { + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); searchRequest.scroll(scroll); @@ -458,28 +459,20 @@ public void fetchDocumentsForAGivenScope( boolQuery.must(QueryBuilders.existsQuery("share_with")).must(shouldQuery); executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery, ActionListener.wrap(success -> { - try { - // If 'success' indicates the search completed, log and return the results - LOGGER.debug("Found {} documents matching the criteria in {}", resourceIds.size(), resourceSharingIndex); - listener.onResponse(resourceIds); - } finally { - // Always close the stashed context - storedContext.close(); - } + LOGGER.debug("Found {} documents matching the criteria in {}", resourceIds.size(), resourceSharingIndex); + listener.onResponse(resourceIds); + }, exception -> { - try { - LOGGER.error( - "Search failed for pluginIndex={}, scope={}, recipientType={}, entities={}", - pluginIndex, - scope, - recipientType, - entities, - exception - ); - listener.onFailure(exception); - } finally { - storedContext.close(); - } + LOGGER.error( + "Search failed for pluginIndex={}, scope={}, recipientType={}, entities={}", + pluginIndex, + scope, + recipientType, + entities, + exception + ); + listener.onFailure(exception); + })); } catch (Exception e) { LOGGER.error( @@ -643,7 +636,7 @@ public void fetchDocumentById(String pluginIndex, String resourceId, ActionListe } LOGGER.debug("Fetching document from index: {}, resourceId: {}", pluginIndex, resourceId); - try (ThreadContext.StoredContext storedContext = this.threadPool.getThreadContext().stashContext()) { + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() .must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)) .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); @@ -1082,7 +1075,7 @@ public void revokeAccess( return; } - try (ThreadContext.StoredContext storedContext = this.threadPool.getThreadContext().stashContext()) { + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { LOGGER.debug( "Revoking access for resource {} in {} for entities: {} and scopes: {}", @@ -1114,7 +1107,6 @@ public void revokeAccess( } List scopesToUse = (scopes != null) ? new ArrayList<>(scopes) : new ArrayList<>(); - // Build the revoke script Script revokeScript = new Script(ScriptType.INLINE, "painless", """ if (ctx._source.share_with != null) { Set scopesToProcess = new HashSet(params.scopes.isEmpty() ? ctx._source.share_with.keySet() : params.scopes); @@ -1192,7 +1184,9 @@ public void revokeAccess( * * @param sourceIdx The source index to match in the query (exact match) * @param resourceId The resource ID to match in the query (exact match) - * @return boolean true if at least one document was deleted, false if no documents were found or deletion failed + * @param listener The listener to be notified when the operation completes + * @throws IllegalArgumentException if sourceIdx or resourceId is null/empty + * @throws RuntimeException if the delete operation fails or encounters an error * * @implNote The delete operation uses a bool query with two must clauses to ensure exact matching: *
          @@ -1208,10 +1202,14 @@ public void revokeAccess(
                * }
                * 
          */ - public boolean deleteResourceSharingRecord(String resourceId, String sourceIdx) { - LOGGER.debug("Deleting documents from {} where source_idx = {} and resource_id = {}", resourceSharingIndex, sourceIdx, resourceId); + public void deleteResourceSharingRecord(String resourceId, String sourceIdx, ActionListener listener) { + LOGGER.debug( + "Deleting documents asynchronously from {} where source_idx = {} and resource_id = {}", + resourceSharingIndex, + sourceIdx, + resourceId + ); - // TODO: Once stashContext is replaced with switchContext this call will have to be modified try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { DeleteByQueryRequest dbq = new DeleteByQueryRequest(resourceSharingIndex).setQuery( QueryBuilders.boolQuery() @@ -1219,24 +1217,36 @@ public boolean deleteResourceSharingRecord(String resourceId, String sourceIdx) .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)) ).setRefresh(true); - BulkByScrollResponse response = client.execute(DeleteByQueryAction.INSTANCE, dbq).actionGet(); + client.execute(DeleteByQueryAction.INSTANCE, dbq, new ActionListener<>() { + @Override + public void onResponse(BulkByScrollResponse response) { - if (response.getDeleted() > 0) { - LOGGER.info("Successfully deleted {} documents from {}", response.getDeleted(), resourceSharingIndex); - return true; - } else { - LOGGER.info( - "No documents found to delete in {} for source_idx: {} and resource_id: {}", - resourceSharingIndex, - sourceIdx, - resourceId - ); - return false; - } + long deleted = response.getDeleted(); + if (deleted > 0) { + LOGGER.info("Successfully deleted {} documents from {}", deleted, resourceSharingIndex); + listener.onResponse(true); + } else { + LOGGER.info( + "No documents found to delete in {} for source_idx: {} and resource_id: {}", + resourceSharingIndex, + sourceIdx, + resourceId + ); + // No documents were deleted + listener.onResponse(false); + } + } + + @Override + public void onFailure(Exception e) { + LOGGER.error("Failed to delete documents from {}", resourceSharingIndex, e); + listener.onFailure(e); + } + }); } catch (Exception e) { - LOGGER.error("Failed to delete documents from {}", resourceSharingIndex, e); - return false; + LOGGER.error("Failed to delete documents from {} before request submission", resourceSharingIndex, e); + listener.onFailure(e); } } @@ -1265,13 +1275,7 @@ public boolean deleteResourceSharingRecord(String resourceId, String sourceIdx) * * * @param name The username to match against the created_by.user field - * @return boolean indicating whether the deletion was successful: - *
            - *
          • true - if one or more documents were deleted
          • - *
          • false - if no documents were found
          • - *
          • false - if the operation failed due to an error
          • - *
          - * + * @param listener The listener to be notified when the operation completes * @throws IllegalArgumentException if name is null or empty * * @@ -1293,34 +1297,42 @@ public boolean deleteResourceSharingRecord(String resourceId, String sourceIdx) * } * */ - public boolean deleteAllRecordsForUser(String name) { + public void deleteAllRecordsForUser(String name, ActionListener listener) { if (StringUtils.isBlank(name)) { - throw new IllegalArgumentException("Username must not be null or empty"); + listener.onFailure(new IllegalArgumentException("Username must not be null or empty")); + return; } - LOGGER.debug("Deleting all records for user {}", name); + LOGGER.debug("Deleting all records for user {} asynchronously", name); - // TODO: Once stashContext is replaced with switchContext this call will have to be modified try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { DeleteByQueryRequest deleteRequest = new DeleteByQueryRequest(resourceSharingIndex).setQuery( QueryBuilders.termQuery("created_by.user", name) ).setRefresh(true); - BulkByScrollResponse response = client.execute(DeleteByQueryAction.INSTANCE, deleteRequest).actionGet(); - - long deletedDocs = response.getDeleted(); - - if (deletedDocs > 0) { - LOGGER.info("Successfully deleted {} documents created by user {}", deletedDocs, name); - return true; - } else { - LOGGER.info("No documents found for user {}", name); - return false; - } + client.execute(DeleteByQueryAction.INSTANCE, deleteRequest, new ActionListener<>() { + @Override + public void onResponse(BulkByScrollResponse response) { + long deletedDocs = response.getDeleted(); + if (deletedDocs > 0) { + LOGGER.info("Successfully deleted {} documents created by user {}", deletedDocs, name); + listener.onResponse(true); + } else { + LOGGER.info("No documents found for user {}", name); + // No documents matched => success = false + listener.onResponse(false); + } + } + @Override + public void onFailure(Exception e) { + LOGGER.error("Failed to delete documents for user {}", name, e); + listener.onFailure(e); + } + }); } catch (Exception e) { - LOGGER.error("Failed to delete documents for user {}", name, e); - return false; + LOGGER.error("Failed to delete documents for user {} before request submission", name, e); + listener.onFailure(e); } } @@ -1329,7 +1341,8 @@ public boolean deleteAllRecordsForUser(String name) { * * @param resourceIndex The resource index to fetch documents from. * @param parser The class to deserialize the documents into a specified type defined by the parser. - * @return A set of deserialized documents. + * @param listener The listener to be notified with the set of deserialized documents. + * @param The type of the deserialized documents. */ public void getResourceDocumentsFromIds( Set resourceIds, diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java index 140e0eca33..c3ca68356f 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -14,6 +14,7 @@ import org.apache.logging.log4j.Logger; import org.opensearch.client.Client; +import org.opensearch.core.action.ActionListener; import org.opensearch.core.index.shard.ShardId; import org.opensearch.index.engine.Engine; import org.opensearch.index.shard.IndexingOperationListener; @@ -116,11 +117,12 @@ public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResul String resourceId = delete.id(); - boolean success = this.resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex); - if (success) { - log.info("Successfully deleted resource sharing entry for resource {}", resourceId); - } else { - log.info("Failed to delete resource sharing entry for resource {}", resourceId); - } + this.resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex, ActionListener.wrap(deleted -> { + if (deleted) { + log.info("Successfully deleted resource sharing entry for resource {}", resourceId); + } else { + log.info("No resource sharing entry found for resource {}", resourceId); + } + }, exception -> log.error("Failed to delete resource sharing entry for resource {}", resourceId, exception))); } } From fe94573dd0ded39777bdbb32f443a6a499b117d8 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 17 Jan 2025 16:38:51 -0500 Subject: [PATCH 096/201] Moves Resource sharing index constant to a new constants class Signed-off-by: Darshit Chanpura --- .../opensearch/security/OpenSearchSecurityPlugin.java | 9 +++------ .../security/resources/ResourceSharingConstants.java | 6 ++++++ .../security/resources/ResourceSharingIndexListener.java | 2 +- .../org/opensearch/security/support/ConfigConstants.java | 3 +-- 4 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 27340523d6..e6f88c00c1 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -178,10 +178,7 @@ import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.resources.ResourceAccessHandler; -import org.opensearch.security.resources.ResourceSharingIndexHandler; -import org.opensearch.security.resources.ResourceSharingIndexListener; -import org.opensearch.security.resources.ResourceSharingIndexManagementRepository; +import org.opensearch.security.resources.*; import org.opensearch.security.rest.DashboardsInfoAction; import org.opensearch.security.rest.SecurityConfigUpdateAction; import org.opensearch.security.rest.SecurityHealthAction; @@ -1273,7 +1270,7 @@ public Collection createComponents( e.subscribeForChanges(dcf); } - final var resourceSharingIndex = ConfigConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; + final var resourceSharingIndex = ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; ResourceSharingIndexHandler rsIndexHandler = new ResourceSharingIndexHandler( resourceSharingIndex, localClient, @@ -2243,7 +2240,7 @@ public Collection getSystemIndexDescriptors(Settings sett ); final SystemIndexDescriptor securityIndexDescriptor = new SystemIndexDescriptor(indexPattern, "Security index"); final SystemIndexDescriptor resourceSharingIndexDescriptor = new SystemIndexDescriptor( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_INDEX, + ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX, "Resource Sharing index" ); return List.of(securityIndexDescriptor, resourceSharingIndexDescriptor); diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java b/src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java new file mode 100644 index 0000000000..1c171cfd18 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java @@ -0,0 +1,6 @@ +package org.opensearch.security.resources; + +public class ResourceSharingConstants { + // Resource sharing index + public static final String OPENSEARCH_RESOURCE_SHARING_INDEX = ".opensearch_resource_sharing"; +} diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java index c3ca68356f..9b6f7f1832 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -60,7 +60,7 @@ public void initialize(ThreadPool threadPool, Client client, AuditLog auditLog) initialized = true; this.threadPool = threadPool; this.resourceSharingIndexHandler = new ResourceSharingIndexHandler( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_INDEX, + ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX, client, threadPool, auditLog diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 8069c291f5..9585995919 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -381,8 +381,7 @@ public enum RolesMappingResolution { // Variable for initial admin password support public static final String OPENSEARCH_INITIAL_ADMIN_PASSWORD = "OPENSEARCH_INITIAL_ADMIN_PASSWORD"; - // Resource sharing index - public static final String OPENSEARCH_RESOURCE_SHARING_INDEX = ".opensearch_resource_sharing"; + // Resource sharing feature-flag public static final String OPENSEARCH_RESOURCE_SHARING_ENABLED = SECURITY_SETTINGS_PREFIX + "resource_sharing.enabled"; public static final boolean OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT = true; From 2bf0ba177441c9272226fbef1b8a1c1ba8ce7a63 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 17 Jan 2025 16:41:21 -0500 Subject: [PATCH 097/201] Uses ResourceSharingException Signed-off-by: Darshit Chanpura --- .../resources/ResourceAccessHandler.java | 3 +-- .../ResourceSharingIndexHandler.java | 23 +++++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 98d244906a..c2b67ab8d2 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -23,7 +23,6 @@ import org.apache.logging.log4j.Logger; import org.apache.lucene.search.Query; -import org.opensearch.OpenSearchException; import org.opensearch.action.StepListener; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; @@ -456,7 +455,7 @@ public void deleteAllResourceSharingRecordsForCurrentUser(ActionListener { // Only admin or the creator of the resource is currently allowed to revoke access if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { - throw new OpenSearchException( + throw new ResourceSharingException( "User " + requestUserName + " is not authorized to revoke access to resource " + resourceId ); } @@ -1380,7 +1383,7 @@ public void getResourceDocumentsFromIds( } listener.onResponse(result); } catch (Exception e) { - listener.onFailure(new OpenSearchException("Failed to parse resources: " + e.getMessage(), e)); + listener.onFailure(new ResourceSharingException("Failed to parse resources: " + e.getMessage(), e)); } }, e -> { if (e instanceof IndexNotFoundException) { @@ -1388,7 +1391,7 @@ public void getResourceDocumentsFromIds( listener.onFailure(e); } else { LOGGER.error("Failed to fetch resources with ids {} from index {}", resourceIds, resourceIndex, e); - listener.onFailure(new OpenSearchException("Failed to fetch resources: " + e.getMessage(), e)); + listener.onFailure(new ResourceSharingException("Failed to fetch resources: " + e.getMessage(), e)); } })); } From 47cb89dba6a84cfb7e07ae13a06cc242ebd3cc9f Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 17 Jan 2025 16:49:43 -0500 Subject: [PATCH 098/201] Adds correct license header Signed-off-by: Darshit Chanpura --- .../security/resources/ResourceSharingConstants.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java b/src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java index 1c171cfd18..a6ed3f2b03 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java @@ -1,3 +1,13 @@ +/* + * 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.resources; public class ResourceSharingConstants { From cd49948cae6e4d6e99064057678a3d0198e52503 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 17 Jan 2025 17:01:49 -0500 Subject: [PATCH 099/201] Fixes checkStyle violations :/ Signed-off-by: Darshit Chanpura --- .../org/opensearch/security/OpenSearchSecurityPlugin.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index e6f88c00c1..fa0e93c323 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -178,7 +178,11 @@ import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.resources.*; +import org.opensearch.security.resources.ResourceAccessHandler; +import org.opensearch.security.resources.ResourceSharingConstants; +import org.opensearch.security.resources.ResourceSharingIndexHandler; +import org.opensearch.security.resources.ResourceSharingIndexListener; +import org.opensearch.security.resources.ResourceSharingIndexManagementRepository; import org.opensearch.security.rest.DashboardsInfoAction; import org.opensearch.security.rest.SecurityConfigUpdateAction; import org.opensearch.security.rest.SecurityHealthAction; @@ -2306,7 +2310,7 @@ public static Map getResourceProviders() { return ImmutableMap.copyOf(RESOURCE_PROVIDERS); } - // TODO following should be removed once core test framework allows loading extensions + // TODO following should be removed once core test framework allows loading extended classes @VisibleForTesting public static Map getResourceProvidersMutable() { return RESOURCE_PROVIDERS; From a215d077cd2958808b4404d915f77bfd1fcb9e8a Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 00:38:50 -0500 Subject: [PATCH 100/201] Updates route names to use constant prefix Signed-off-by: Darshit Chanpura --- .../actions/rest/create/CreateResourceRestAction.java | 5 +++-- .../actions/rest/delete/DeleteResourceRestAction.java | 3 ++- .../transport/CreateResourceTransportAction.java | 11 ++++------- .../java/org/opensearch/sample/utils/Constants.java | 3 +++ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java index 02b6527a53..f1805e1820 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java @@ -21,6 +21,7 @@ import static org.opensearch.rest.RestRequest.Method.POST; import static org.opensearch.rest.RestRequest.Method.PUT; +import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; public class CreateResourceRestAction extends BaseRestHandler { @@ -29,8 +30,8 @@ public CreateResourceRestAction() {} @Override public List routes() { return List.of( - new Route(PUT, "/_plugins/sample_resource_sharing/create"), - new Route(POST, "/_plugins/sample_resource_sharing/update/{resourceId}") + new Route(PUT, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/create"), + new Route(POST, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/update/{resourceId}") ); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java index 6c88fdbc4d..699b5e0303 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java @@ -18,6 +18,7 @@ import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.DELETE; +import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; public class DeleteResourceRestAction extends BaseRestHandler { @@ -25,7 +26,7 @@ public DeleteResourceRestAction() {} @Override public List routes() { - return singletonList(new Route(DELETE, "/_plugins/sample_resource_sharing/delete/{resource_id}")); + return singletonList(new Route(DELETE, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/delete/{resource_id}")); } @Override diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java index c20f492985..21c994f7fa 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java @@ -51,9 +51,6 @@ protected void doExecute(Task task, CreateResourceRequest request, ActionListene ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { createResource(request, listener); - listener.onResponse( - new CreateResourceResponse("Resource " + request.getResource().getResourceName() + " created successfully.") - ); } catch (Exception e) { log.info("Failed to create resource", e); listener.onFailure(e); @@ -71,10 +68,10 @@ private void createResource(CreateResourceRequest request, ActionListener { log.info("Created resource: {}", idxResponse.toString()); }, listener::onFailure) - ); + nodeClient.index(ir, ActionListener.wrap(idxResponse -> { + log.info("Created resource: {}", idxResponse.getId()); + listener.onResponse(new CreateResourceResponse("Created resource: " + idxResponse.getId())); + }, listener::onFailure)); } catch (IOException e) { listener.onFailure(new RuntimeException(e)); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Constants.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Constants.java index ff7404d2cd..3be49d033e 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Constants.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Constants.java @@ -10,4 +10,7 @@ public class Constants { public static final String RESOURCE_INDEX_NAME = ".sample_resource_sharing_plugin"; + + public static final String SAMPLE_RESOURCE_PLUGIN_PREFIX = "_plugins/sample_resource_sharing"; + public static final String SAMPLE_RESOURCE_PLUGIN_API_PREFIX = "/" + SAMPLE_RESOURCE_PLUGIN_PREFIX; } From 86d89d52b1f755ad82a25b76c02afbc064ab32a9 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 00:40:54 -0500 Subject: [PATCH 101/201] Updates dls rules and updates a failing test suite Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 59 +++++++++++-------- .../configuration/DlsFlsValveImpl.java | 42 +++++++++---- .../SecurityFlsDlsIndexSearcherWrapper.java | 3 +- .../resources/ResourceAccessHandler.java | 10 ++-- .../security/IndexIntegrationTests.java | 10 ++-- 5 files changed, 76 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index fa0e93c323..87a2fb4989 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -49,6 +49,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Function; @@ -58,7 +60,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; @@ -828,7 +829,10 @@ public Weight doCache(Weight weight, QueryCachingPolicy policy) { @Override public void onPreQueryPhase(SearchContext context) { - dlsFlsValve.handleSearchContext(context, threadPool, namedXContentRegistry.get()); + CompletableFuture.runAsync( + () -> { dlsFlsValve.handleSearchContext(context, threadPool, namedXContentRegistry.get()); }, + threadPool.generic() + ).orTimeout(5, TimeUnit.SECONDS).join(); } @Override @@ -1194,6 +1198,22 @@ public Collection createComponents( namedXContentRegistry.get() ); + final var resourceSharingIndex = ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; + ResourceSharingIndexHandler rsIndexHandler = new ResourceSharingIndexHandler( + resourceSharingIndex, + localClient, + threadPool, + auditLog + ); + resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns); + resourceAccessHandler.initializeRecipientTypes(); + // Resource Sharing index is enabled by default + boolean isResourceSharingEnabled = settings.getAsBoolean( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT + ); + rmr = ResourceSharingIndexManagementRepository.create(rsIndexHandler, isResourceSharingEnabled); + dlsFlsBaseContext = new DlsFlsBaseContext(evaluator, threadPool.getThreadContext(), adminDns); if (SSLConfig.isSslOnlyMode()) { @@ -1274,22 +1294,6 @@ public Collection createComponents( e.subscribeForChanges(dcf); } - final var resourceSharingIndex = ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; - ResourceSharingIndexHandler rsIndexHandler = new ResourceSharingIndexHandler( - resourceSharingIndex, - localClient, - threadPool, - auditLog - ); - resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns); - resourceAccessHandler.initializeRecipientTypes(); - // Resource Sharing index is enabled by default - boolean isResourceSharingEnabled = settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - ); - rmr = ResourceSharingIndexManagementRepository.create(rsIndexHandler, isResourceSharingEnabled); - components.add(adminDns); components.add(cr); components.add(xffResolver); @@ -2190,10 +2194,12 @@ public void onNodeStarted(DiscoveryNode localNode) { } // rmr will be null when sec plugin is disabled or is in SSLOnly mode, hence rmr will not be instantiated - if (settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - ) && rmr != null) { + if (settings != null + && settings.getAsBoolean( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT + ) + && rmr != null) { // create resource sharing index if absent rmr.createResourceSharingIndexIfAbsent(); } @@ -2310,14 +2316,17 @@ public static Map getResourceProviders() { return ImmutableMap.copyOf(RESOURCE_PROVIDERS); } + public static Set getResourceIndices() { + return ImmutableSet.copyOf(RESOURCE_INDICES); + } + // TODO following should be removed once core test framework allows loading extended classes - @VisibleForTesting public static Map getResourceProvidersMutable() { return RESOURCE_PROVIDERS; } - public static Set getResourceIndices() { - return ImmutableSet.copyOf(RESOURCE_INDICES); + public static Set getResourceIndicesMutable() { + return RESOURCE_INDICES; } // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index b776284af5..18e02fab30 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -17,6 +17,7 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.StreamSupport; @@ -385,27 +386,42 @@ public void handleSearchContext(SearchContext searchContext, ThreadPool threadPo PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); - if (privilegesEvaluationContext == null && !OpenSearchSecurityPlugin.getResourceIndices().contains(index)) { + if (privilegesEvaluationContext == null) { return; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); if (this.isResourceSharingEnabled && OpenSearchSecurityPlugin.getResourceIndices().contains(index)) { - this.resourceAccessHandler.getAccessibleResourceIdsForCurrentUser(index, ActionListener.wrap(resourceIds -> { - - log.info("Creating a DLS restriction for resource IDs: {}", resourceIds); - // Create a DLS restriction to filter search results with accessible resources only - DlsRestriction dlsRestriction = this.resourceAccessHandler.createResourceDLSRestriction( - resourceIds, - namedXContentRegistry + CountDownLatch latch = new CountDownLatch(1); + + threadPool.generic() + .submit( + () -> this.resourceAccessHandler.getAccessibleResourceIdsForCurrentUser(index, ActionListener.wrap(resourceIds -> { + try { + log.info("Creating a DLS restriction for resource IDs: {}", resourceIds); + // Create a DLS restriction and apply it + DlsRestriction dlsRestriction = this.resourceAccessHandler.createResourceDLSRestriction( + resourceIds, + namedXContentRegistry + ); + applyDlsRestrictionToSearchContext(dlsRestriction, index, searchContext, mode); + } catch (Exception e) { + log.error("Error while creating or applying DLS restriction for index '{}': {}", index, e.getMessage()); + applyDlsRestrictionToSearchContext(DlsRestriction.FULL, index, searchContext, mode); + } finally { + latch.countDown(); // Release the latch + } + }, exception -> { + log.error("Failed to fetch resource IDs for index '{}': {}", index, exception.getMessage()); + // Apply a default restriction on failure + applyDlsRestrictionToSearchContext(DlsRestriction.FULL, index, searchContext, mode); + latch.countDown(); + })) ); - applyDlsRestrictionToSearchContext(dlsRestriction, index, searchContext, mode); - }, exception -> { - log.error("Failed to fetch resource IDs for index '{}': {}", index, exception.getMessage()); - applyDlsRestrictionToSearchContext(DlsRestriction.FULL, index, searchContext, mode); - })); + } else { + // Synchronous path for non-resource-sharing-enabled cases DlsRestriction dlsRestriction = config.getDocumentPrivileges().getRestriction(privilegesEvaluationContext, index); applyDlsRestrictionToSearchContext(dlsRestriction, index, searchContext, mode); } diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java index af0a1a9282..9a05f4f50b 100644 --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java @@ -132,10 +132,11 @@ protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdm } // 1. If user is admin, or we have no shard/index info, just wrap with default logic (no doc-level restriction). - if (isAdmin || Strings.isNullOrEmpty(indexName)) { + if (isAdmin || privilegesEvaluationContext == null) { return wrapWithDefaultDlsFls(reader, shardId); } + assert !Strings.isNullOrEmpty(indexName); // 2. If resource sharing is disabled or this is not a resource index, fallback to standard DLS/FLS logic. if (!this.isResourceSharingEnabled || !OpenSearchSecurityPlugin.getResourceIndices().contains(indexName)) { return wrapStandardDlsFls(privilegesEvaluationContext, reader, shardId, indexName, isAdmin); diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index c2b67ab8d2..49653ca3b5 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.Future; import com.fasterxml.jackson.core.JsonProcessingException; import org.apache.logging.log4j.LogManager; @@ -87,7 +88,7 @@ public void initializeRecipientTypes() { * @param listener The listener to be notified with the set of accessible resource IDs. * */ - public void getAccessibleResourceIdsForCurrentUser(String resourceIndex, ActionListener> listener) { + public Future getAccessibleResourceIdsForCurrentUser(String resourceIndex, ActionListener> listener) { final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER ); @@ -97,7 +98,7 @@ public void getAccessibleResourceIdsForCurrentUser(String resourceIndex, ActionL if (user == null) { LOGGER.info("Unable to fetch user details."); listener.onResponse(Collections.emptySet()); - return; + return null; } LOGGER.info("Listing accessible resources within the resource index {} for user: {}", resourceIndex, user.getName()); @@ -115,7 +116,7 @@ public void onFailure(Exception e) { listener.onFailure(e); } }); - return; + return null; } // StepListener for the user’s "own" resources @@ -179,6 +180,7 @@ public void onFailure(Exception e) { LOGGER.debug("Found {} accessible resources for user {}", allResources.size(), user.getName()); listener.onResponse(allResources); }, listener::onFailure); + return null; } /** @@ -627,7 +629,7 @@ public DlsRestriction createResourceDLSRestriction(Set resourceIds, Name // resourceIds.isEmpty() is true when user doesn't have access to any resources if (resourceIds.isEmpty()) { - LOGGER.debug("No resources found for user"); + LOGGER.info("No resources found for user. Enforcing full restriction."); return DlsRestriction.FULL; } diff --git a/src/test/java/org/opensearch/security/IndexIntegrationTests.java b/src/test/java/org/opensearch/security/IndexIntegrationTests.java index 648a9b1ade..14b84614c4 100644 --- a/src/test/java/org/opensearch/security/IndexIntegrationTests.java +++ b/src/test/java/org/opensearch/security/IndexIntegrationTests.java @@ -846,19 +846,19 @@ public void testIndexResolveMinus() throws Exception { resc = rh.executeGetRequest("/*,-foo*/_search", encodeBasicHeader("foo_all", "nagilum")); assertThat(resc.getStatusCode(), is(HttpStatus.SC_FORBIDDEN)); - resc = rh.executeGetRequest("/*,-*security/_search", encodeBasicHeader("foo_all", "nagilum")); + resc = rh.executeGetRequest("/*,-*security,-*resource*/_search", encodeBasicHeader("foo_all", "nagilum")); assertThat(resc.getStatusCode(), is(HttpStatus.SC_OK)); - resc = rh.executeGetRequest("/*,-*security/_search", encodeBasicHeader("foo_all", "nagilum")); + resc = rh.executeGetRequest("/*,-*security,-*resource*/_search", encodeBasicHeader("foo_all", "nagilum")); assertThat(resc.getStatusCode(), is(HttpStatus.SC_OK)); - resc = rh.executeGetRequest("/*,-*security,-foo*/_search", encodeBasicHeader("foo_all", "nagilum")); + resc = rh.executeGetRequest("/*,-*security,-foo*,-*resource*/_search", encodeBasicHeader("foo_all", "nagilum")); assertThat(resc.getStatusCode(), is(HttpStatus.SC_OK)); - resc = rh.executeGetRequest("/_all,-*security/_search", encodeBasicHeader("foo_all", "nagilum")); + resc = rh.executeGetRequest("/_all,-*security,-*resource*/_search", encodeBasicHeader("foo_all", "nagilum")); assertThat(resc.getStatusCode(), is(HttpStatus.SC_FORBIDDEN)); - resc = rh.executeGetRequest("/_all,-*security/_search", encodeBasicHeader("nagilum", "nagilum")); + resc = rh.executeGetRequest("/_all,-*security,-*resource*/_search", encodeBasicHeader("nagilum", "nagilum")); assertThat(resc.getStatusCode(), is(HttpStatus.SC_BAD_REQUEST)); } From b20cf18023a11116216286498dbd36cfcb0c4eb2 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 03:59:37 -0500 Subject: [PATCH 102/201] Corrects enum calls Signed-off-by: Darshit Chanpura --- .../org/opensearch/security/resources/CreatedBy.java | 2 +- .../org/opensearch/security/resources/Creator.java | 9 +++++++++ .../opensearch/security/resources/RecipientType.java | 12 ++---------- .../security/resources/ResourceAccessHandler.java | 6 +++--- .../resources/ResourceSharingIndexHandler.java | 12 ++++++++---- .../security/resources/SharedWithScope.java | 4 ++-- .../access/revoke/RevokeResourceAccessRequest.java | 2 +- .../resources/RecipientTypeRegistryTests.java | 2 +- 8 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/CreatedBy.java b/src/main/java/org/opensearch/security/resources/CreatedBy.java index 69af99719e..af27001663 100644 --- a/src/main/java/org/opensearch/security/resources/CreatedBy.java +++ b/src/main/java/org/opensearch/security/resources/CreatedBy.java @@ -74,7 +74,7 @@ public static CreatedBy fromXContent(XContentParser parser) throws IOException { while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { - creatorType = Creator.valueOf(parser.currentName()); + creatorType = Creator.fromName(parser.currentName()); } else if (token == XContentParser.Token.VALUE_STRING) { creator = parser.text(); } diff --git a/src/main/java/org/opensearch/security/resources/Creator.java b/src/main/java/org/opensearch/security/resources/Creator.java index c7a913d4de..ee2e9de7ab 100644 --- a/src/main/java/org/opensearch/security/resources/Creator.java +++ b/src/main/java/org/opensearch/security/resources/Creator.java @@ -20,4 +20,13 @@ public enum Creator { public String getName() { return name; } + + public static Creator fromName(String name) { + for (Creator creator : values()) { + if (creator.name.equalsIgnoreCase(name)) { // Case-insensitive comparison + return creator; + } + } + throw new IllegalArgumentException("No enum constant for name: " + name); + } } diff --git a/src/main/java/org/opensearch/security/resources/RecipientType.java b/src/main/java/org/opensearch/security/resources/RecipientType.java index 6ed3004b7e..bfe2bfec12 100644 --- a/src/main/java/org/opensearch/security/resources/RecipientType.java +++ b/src/main/java/org/opensearch/security/resources/RecipientType.java @@ -12,18 +12,10 @@ * This class determines a type of recipient a resource can be shared with. * An example type would be a user or a role. * This class is used to determine the type of recipient a resource can be shared with. + * * @opensearch.experimental */ -public class RecipientType { - private final String type; - - public RecipientType(String type) { - this.type = type; - } - - public String getType() { - return type; - } +public record RecipientType(String type) { @Override public String toString() { diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 49653ca3b5..53ce446881 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -139,7 +139,7 @@ public void onFailure(Exception e) { ownResources -> loadSharedWithResources( resourceIndex, Set.of(user.getName()), - Recipient.USERS.toString(), + Recipient.USERS.getName(), userNameResourcesListener ), listener::onFailure @@ -150,7 +150,7 @@ public void onFailure(Exception e) { userNameResources -> loadSharedWithResources( resourceIndex, user.getSecurityRoles(), - Recipient.ROLES.toString(), + Recipient.ROLES.getName(), rolesResourcesListener ), listener::onFailure @@ -161,7 +161,7 @@ public void onFailure(Exception e) { rolesResources -> loadSharedWithResources( resourceIndex, user.getRoles(), - Recipient.BACKEND_ROLES.toString(), + Recipient.BACKEND_ROLES.getName(), backendRolesResourcesListener ), listener::onFailure diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 45f885349c..4ba251370a 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -855,7 +855,9 @@ public void updateResourceSharingInfo( if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { LOGGER.error("User {} is not authorized to share resource {}", requestUserName, resourceId); - throw new ResourceSharingException("User " + requestUserName + " is not authorized to share resource " + resourceId); + listener.onFailure( + new ResourceSharingException("User " + requestUserName + " is not authorized to share resource " + resourceId) + ); } Script updateScript = new Script(ScriptType.INLINE, "painless", """ @@ -1099,14 +1101,16 @@ public void revokeAccess( currentSharingListener.whenComplete(currentSharingInfo -> { // Only admin or the creator of the resource is currently allowed to revoke access if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { - throw new ResourceSharingException( - "User " + requestUserName + " is not authorized to revoke access to resource " + resourceId + listener.onFailure( + new ResourceSharingException( + "User " + requestUserName + " is not authorized to revoke access to resource " + resourceId + ) ); } Map revoke = new HashMap<>(); for (Map.Entry> entry : revokeAccess.entrySet()) { - revoke.put(entry.getKey().getType().toLowerCase(), new ArrayList<>(entry.getValue())); + revoke.put(entry.getKey().type().toLowerCase(), new ArrayList<>(entry.getValue())); } List scopesToUse = (scopes != null) ? new ArrayList<>(scopes) : new ArrayList<>(); diff --git a/src/main/java/org/opensearch/security/resources/SharedWithScope.java b/src/main/java/org/opensearch/security/resources/SharedWithScope.java index 02e3db854f..5a0bbc01b4 100644 --- a/src/main/java/org/opensearch/security/resources/SharedWithScope.java +++ b/src/main/java/org/opensearch/security/resources/SharedWithScope.java @@ -150,7 +150,7 @@ public static ScopeRecipients fromXContent(XContentParser parser) throws IOExcep public void writeTo(StreamOutput out) throws IOException { out.writeMap( recipients, - (streamOutput, recipientType) -> streamOutput.writeString(recipientType.getType()), + (streamOutput, recipientType) -> streamOutput.writeString(recipientType.type()), (streamOutput, strings) -> streamOutput.writeCollection(strings, StreamOutput::writeString) ); } @@ -161,7 +161,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } for (Map.Entry> entry : recipients.entrySet()) { - builder.array(entry.getKey().getType(), entry.getValue().toArray()); + builder.array(entry.getKey().type(), entry.getValue().toArray()); } return builder; } diff --git a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessRequest.java b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessRequest.java index 667f1670dd..355658cf4c 100644 --- a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessRequest.java +++ b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessRequest.java @@ -50,7 +50,7 @@ public void writeTo(final StreamOutput out) throws IOException { out.writeString(resourceIndex); out.writeMap( revokeAccess, - (streamOutput, recipientType) -> streamOutput.writeString(recipientType.getType()), + (streamOutput, recipientType) -> streamOutput.writeString(recipientType.type()), StreamOutput::writeStringCollection ); out.writeStringCollection(scopes); diff --git a/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java b/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java index 47151898d1..d1a6854c3e 100644 --- a/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java +++ b/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java @@ -26,7 +26,7 @@ public void testFromValue() { // Valid Value RecipientType type = RecipientTypeRegistry.fromValue("ble1"); MatcherAssert.assertThat(type, notNullValue()); - MatcherAssert.assertThat(type.getType(), is(equalTo("ble1"))); + MatcherAssert.assertThat(type.type(), is(equalTo("ble1"))); // Invalid Value IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> RecipientTypeRegistry.fromValue("bleble")); From 753d5fdc00387860879807e1f6288bec44e68fe1 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 04:01:30 -0500 Subject: [PATCH 103/201] Updates the parser to have description as optional for sample resource Signed-off-by: Darshit Chanpura --- .../src/main/java/org/opensearch/sample/SampleResource.java | 2 +- .../actions/transport/UpdateResourceTransportAction.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java index ce123380b4..c1c5a4b66d 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java @@ -61,7 +61,7 @@ public SampleResource(StreamInput in) throws IOException { static { PARSER.declareString(constructorArg(), new ParseField("name")); - PARSER.declareString(constructorArg(), new ParseField("description")); + PARSER.declareStringOrNull(constructorArg(), new ParseField("description")); PARSER.declareObjectOrNull(constructorArg(), (p, c) -> p.mapStrings(), null, new ParseField("attributes")); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java index e9ec0127dd..9dda0f4e4b 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java @@ -23,7 +23,9 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.sample.resource.actions.rest.create.*; +import org.opensearch.sample.resource.actions.rest.create.CreateResourceResponse; +import org.opensearch.sample.resource.actions.rest.create.UpdateResourceAction; +import org.opensearch.sample.resource.actions.rest.create.UpdateResourceRequest; import org.opensearch.security.spi.resources.Resource; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; From c6faed8b8acbd5a7905857ed55e12a116af815a0 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 04:03:11 -0500 Subject: [PATCH 104/201] Adds integTest for sample-resource-plugin Signed-off-by: Darshit Chanpura --- .../sample/SampleResourcePluginTests.java | 311 ++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java new file mode 100644 index 0000000000..9818382dfd --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -0,0 +1,311 @@ +package org.opensearch.sample; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.painless.PainlessModulePlugin; +import org.opensearch.security.OpenSearchSecurityPlugin; +import org.opensearch.security.spi.resources.ResourceAccessScope; +import org.opensearch.security.spi.resources.ResourceProvider; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_PREFIX; +import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; +import static org.opensearch.security.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +/** + * These tests run with security enabled + */ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class SampleResourcePluginTests { + + public final static TestSecurityConfig.User SHARED_WITH_USER = new TestSecurityConfig.User("resource_sharing_test_user").roles( + new TestSecurityConfig.Role("shared_role").indexPermissions("*").on("*").clusterPermissions("*") + ); + + private static final String SAMPLE_RESOURCE_CREATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/create"; + private static final String SAMPLE_RESOURCE_UPDATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/update"; + private static final String SAMPLE_RESOURCE_DELETE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/delete"; + private static final String SECURITY_RESOURCE_LIST_ENDPOINT = PLUGINS_PREFIX + "/resources/list"; + private static final String SECURITY_RESOURCE_SHARE_ENDPOINT = PLUGINS_PREFIX + "/resources/share"; + private static final String SECURITY_RESOURCE_VERIFY_ENDPOINT = PLUGINS_PREFIX + "/resources/verify_access"; + private static final String SECURITY_RESOURCE_REVOKE_ENDPOINT = PLUGINS_PREFIX + "/resources/revoke"; + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .plugin(SampleResourcePlugin.class, PainlessModulePlugin.class) + .anonymousAuth(true) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_ADMIN, SHARED_WITH_USER) + .build(); + + @Test + public void testPluginInstalledCorrectly() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse pluginsResponse = client.get("_cat/plugins"); + assertThat(pluginsResponse.getBody(), containsString("org.opensearch.security.OpenSearchSecurityPlugin")); + assertThat(pluginsResponse.getBody(), containsString("org.opensearch.sample.SampleResourcePlugin")); + } + } + + @Test + public void testCreateUpdateDeleteSampleResource() throws Exception { + String resourceId; + String resourceSharingDocId; + // create sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResource = "{\"name\":\"sample\"}"; + HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + response.assertStatusCode(HttpStatus.SC_OK); + + resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + Thread.sleep(2000); + } + + // Create an entry in resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually + String json = String.format( + "{" + + " \"source_idx\": \".sample_resource_sharing_plugin\"," + + " \"resource_id\": \"%s\"," + + " \"created_by\": {" + + " \"user\": \"admin\"" + + " }" + + "}", + resourceId + ); + HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); + assertThat(response.getStatusReason(), containsString("Created")); + resourceSharingDocId = response.bodyAsJsonNode().get("_id").asText(); + // Also update the in-memory map and list + OpenSearchSecurityPlugin.getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); + ResourceProvider provider = new ResourceProvider( + SampleResource.class.getCanonicalName(), + RESOURCE_INDEX_NAME, + new SampleResourceParser() + ); + OpenSearchSecurityPlugin.getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); + + Thread.sleep(1000); + response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sample")); + } + + // Update sample resource (admin should be able to update resource) + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); + updateResponse.assertStatusCode(HttpStatus.SC_OK); + } + + // resource should be visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + + HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // resource should no longer be visible to shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + + HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(0)); + } + + // shared_with_user should not be able to share admin's resource with itself + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + + String shareWithPayload = "{" + + "\"resource_id\":\"" + + resourceId + + "\"," + + "\"resource_index\":\"" + + RESOURCE_INDEX_NAME + + "\"," + + "\"share_with\":{" + + "\"" + + SampleResourceScope.PUBLIC.value() + + "\":{" + + "\"users\": [\"" + + SHARED_WITH_USER.getName() + + "\"]" + + "}" + + "}" + + "}"; + HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload); + response.assertStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); + assertThat(response.bodyAsJsonNode().toString(), containsString("User " + SHARED_WITH_USER.getName() + " is not authorized")); + // TODO these tests must check for unauthorized instead of internal-server-error + // response.assertStatusCode(HttpStatus.SC_UNAUTHORIZED); + // assertThat(response.bodyAsJsonNode().get("message").asText(), containsString("User is not authorized")); + } + + // share resource with shared_with user + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + String shareWithPayload = "{" + + "\"resource_id\":\"" + + resourceId + + "\"," + + "\"resource_index\":\"" + + RESOURCE_INDEX_NAME + + "\"," + + "\"share_with\":{" + + "\"" + + SampleResourceScope.PUBLIC.value() + + "\":{" + + "\"users\": [\"" + + SHARED_WITH_USER.getName() + + "\"]" + + "}" + + "}" + + "}"; + HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("message").asText(), containsString(resourceId)); + } + + // resource should now be visible to shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + Thread.sleep(3000); // allow changes to be reflected + HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // resource is still visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + + HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // verify access + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + Thread.sleep(1000); + String verifyAccessPayload = "{\"resource_id\":\"" + + resourceId + + "\",\"resource_index\":\"" + + RESOURCE_INDEX_NAME + + "\",\"scope\":\"" + + ResourceAccessScope.PUBLIC + + "\"}"; + HttpResponse response = client.getWithJsonBody(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("User has requested scope " + ResourceAccessScope.PUBLIC + " access")); + } + + // shared_with user should not be able to revoke access to admin's resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + Thread.sleep(1000); + String revokePayload = "{" + + "\"resource_id\": \"" + + resourceId + + "\"," + + "\"resource_index\": \"" + + RESOURCE_INDEX_NAME + + "\"," + + "\"entities\": {" + + "\"users\": [\"" + + SHARED_WITH_USER.getName() + + "\"]" + + "}," + + "\"scopes\": [\"" + + ResourceAccessScope.PUBLIC + + "\"]" + + "}"; + + HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokePayload); + response.assertStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); + assertThat(response.bodyAsJsonNode().toString(), containsString("User " + SHARED_WITH_USER.getName() + " is not authorized")); + // TODO these tests must check for unauthorized instead of internal-server-error + // response.assertStatusCode(HttpStatus.SC_UNAUTHORIZED); + // assertThat(response.bodyAsJsonNode().get("message").asText(), containsString("User is not authorized")); + } + + // revoke share_wit_user's access + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + String revokePayload = "{" + + "\"resource_id\": \"" + + resourceId + + "\"," + + "\"resource_index\": \"" + + RESOURCE_INDEX_NAME + + "\"," + + "\"entities\": {" + + "\"users\": [\"" + + SHARED_WITH_USER.getName() + + "\"]" + + "}," + + "\"scopes\": [\"" + + ResourceAccessScope.PUBLIC + + "\"]" + + "}"; + + HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokePayload); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().toString(), containsString("Resource " + resourceId + " access revoked successfully.")); + } + + // verify access - share_with_user should no longer have access to admin's resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + Thread.sleep(1000); + String verifyAccessPayload = "{\"resource_id\":\"" + + resourceId + + "\",\"resource_index\":\"" + + RESOURCE_INDEX_NAME + + "\",\"scope\":\"" + + ResourceAccessScope.PUBLIC + + "\"}"; + HttpResponse response = client.getWithJsonBody(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("User does not have requested scope " + ResourceAccessScope.PUBLIC + " access")); + } + + // delete sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + Thread.sleep(2000); + } + + // corresponding entry should be removed from resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to delete the resource sharing entry manually + HttpResponse response = client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc/" + resourceSharingDocId); + assertThat(response.getStatusReason(), containsString("OK")); + + Thread.sleep(1000); + response = client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search"); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("hits\":[]")); + } + } + + // TODO add test case for updating the resource directly +} From 724b15beb843e67e4151b538ebb3f8d134e9f99c Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 04:03:58 -0500 Subject: [PATCH 105/201] Fixes integrationTestRuntimeClasspath task dependency conflict Signed-off-by: Darshit Chanpura --- build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index ec6a28a50c..7c0d55dc95 100644 --- a/build.gradle +++ b/build.gradle @@ -402,6 +402,7 @@ opensearchplugin { name 'opensearch-security' description 'Provide access control related features for OpenSearch' classname 'org.opensearch.security.OpenSearchSecurityPlugin' + extendedPlugins = ['lang-painless'] } // This requires an additional Jar not published as part of build-tools @@ -503,6 +504,8 @@ configurations { force "org.hamcrest:hamcrest:2.2" force "org.mockito:mockito-core:5.15.2" force "net.bytebuddy:byte-buddy:1.15.11" + force "org.ow2.asm:asm:9.7.1" + force "com.google.j2objc:j2objc-annotations:3.0.0" } } @@ -556,6 +559,7 @@ allprojects { } integrationTestImplementation 'org.slf4j:slf4j-api:2.0.12' integrationTestImplementation 'com.selectivem.collections:special-collections-complete:1.4.0' + integrationTestImplementation "org.opensearch.plugin:lang-painless:${opensearch_version}" } } @@ -628,6 +632,7 @@ check.dependsOn integrationTest dependencies { implementation project(path: ":opensearch-resource-sharing-spi") + implementation "org.opensearch.plugin:lang-painless:${opensearch_version}" implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" implementation "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}" implementation "org.apache.httpcomponents.client5:httpclient5-cache:${versions.httpclient5}" From 2f4ad39f69739ec1ab25875a159d4a6a3d3e26a9 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 17:30:49 -0500 Subject: [PATCH 106/201] Finalizes integration test for SampleResourcePlugin Signed-off-by: Darshit Chanpura --- .../sample/SampleResourcePluginTests.java | 209 ++++++++++++------ .../org/opensearch/sample/SampleResource.java | 5 +- 2 files changed, 146 insertions(+), 68 deletions(-) diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index 9818382dfd..b5997cac62 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -2,6 +2,7 @@ import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; +import org.junit.After; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; @@ -53,6 +54,16 @@ public class SampleResourcePluginTests { .users(USER_ADMIN, SHARED_WITH_USER) .build(); + @After + public void clearIndices() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + client.delete(RESOURCE_INDEX_NAME); + client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX); + OpenSearchSecurityPlugin.getResourceIndicesMutable().remove(RESOURCE_INDEX_NAME); + OpenSearchSecurityPlugin.getResourceProvidersMutable().remove(RESOURCE_INDEX_NAME); + } + } + @Test public void testPluginInstalledCorrectly() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { @@ -73,7 +84,6 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { response.assertStatusCode(HttpStatus.SC_OK); resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); - Thread.sleep(2000); } // Create an entry in resource-sharing index @@ -101,16 +111,15 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { ); OpenSearchSecurityPlugin.getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); - Thread.sleep(1000); + Thread.sleep(3000); response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); assertThat(response.getBody(), containsString("sample")); } // Update sample resource (admin should be able to update resource) try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - Thread.sleep(1000); - String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); updateResponse.assertStatusCode(HttpStatus.SC_OK); @@ -164,31 +173,14 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { // share resource with shared_with user try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { Thread.sleep(1000); - String shareWithPayload = "{" - + "\"resource_id\":\"" - + resourceId - + "\"," - + "\"resource_index\":\"" - + RESOURCE_INDEX_NAME - + "\"," - + "\"share_with\":{" - + "\"" - + SampleResourceScope.PUBLIC.value() - + "\":{" - + "\"users\": [\"" - + SHARED_WITH_USER.getName() - + "\"]" - + "}" - + "}" - + "}"; - HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload); + + HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload(resourceId)); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.bodyAsJsonNode().get("message").asText(), containsString(resourceId)); } // resource should now be visible to shared_with_user try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - Thread.sleep(3000); // allow changes to be reflected HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); @@ -197,7 +189,6 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { // resource is still visible to super-admin try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); @@ -206,7 +197,6 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { // verify access try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - Thread.sleep(1000); String verifyAccessPayload = "{\"resource_id\":\"" + resourceId + "\",\"resource_index\":\"" @@ -221,25 +211,7 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { // shared_with user should not be able to revoke access to admin's resource try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - Thread.sleep(1000); - String revokePayload = "{" - + "\"resource_id\": \"" - + resourceId - + "\"," - + "\"resource_index\": \"" - + RESOURCE_INDEX_NAME - + "\"," - + "\"entities\": {" - + "\"users\": [\"" - + SHARED_WITH_USER.getName() - + "\"]" - + "}," - + "\"scopes\": [\"" - + ResourceAccessScope.PUBLIC - + "\"]" - + "}"; - - HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokePayload); + HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayload(resourceId)); response.assertStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); assertThat(response.bodyAsJsonNode().toString(), containsString("User " + SHARED_WITH_USER.getName() + " is not authorized")); // TODO these tests must check for unauthorized instead of internal-server-error @@ -247,34 +219,15 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { // assertThat(response.bodyAsJsonNode().get("message").asText(), containsString("User is not authorized")); } - // revoke share_wit_user's access + // revoke share_with_user's access try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - Thread.sleep(1000); - String revokePayload = "{" - + "\"resource_id\": \"" - + resourceId - + "\"," - + "\"resource_index\": \"" - + RESOURCE_INDEX_NAME - + "\"," - + "\"entities\": {" - + "\"users\": [\"" - + SHARED_WITH_USER.getName() - + "\"]" - + "}," - + "\"scopes\": [\"" - + ResourceAccessScope.PUBLIC - + "\"]" - + "}"; - - HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokePayload); + HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayload(resourceId)); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.bodyAsJsonNode().toString(), containsString("Resource " + resourceId + " access revoked successfully.")); } // verify access - share_with_user should no longer have access to admin's resource try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - Thread.sleep(1000); String verifyAccessPayload = "{\"resource_id\":\"" + resourceId + "\",\"resource_index\":\"" @@ -291,7 +244,6 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); response.assertStatusCode(HttpStatus.SC_OK); - Thread.sleep(2000); } // corresponding entry should be removed from resource-sharing index @@ -308,4 +260,129 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { } // TODO add test case for updating the resource directly + @Test + public void testDLSRestrictionForResourceByDirectlyUpdatingTheResourceIndex() throws Exception { + String resourceId; + // create sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResource = "{\"name\":\"sample\"}"; + HttpResponse response = client.postJson(RESOURCE_INDEX_NAME + "/_doc", sampleResource); + response.assertStatusCode(HttpStatus.SC_CREATED); + + resourceId = response.bodyAsJsonNode().get("_id").asText(); + } + + // Create an entry in resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually + String json = String.format( + "{" + + " \"source_idx\": \".sample_resource_sharing_plugin\"," + + " \"resource_id\": \"%s\"," + + " \"created_by\": {" + + " \"user\": \"admin\"" + + " }" + + "}", + resourceId + ); + HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); + assertThat(response.getStatusReason(), containsString("Created")); + // Also update the in-memory map and list + OpenSearchSecurityPlugin.getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); + ResourceProvider provider = new ResourceProvider( + SampleResource.class.getCanonicalName(), + RESOURCE_INDEX_NAME, + new SampleResourceParser() + ); + OpenSearchSecurityPlugin.getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); + + Thread.sleep(1000); + } + + // resource is still visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + + HttpResponse response = client.postJson(RESOURCE_INDEX_NAME + "/_search", "{\"query\" : {\"match_all\" : {}}}"); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sample")); + } + + String updatePayload = "{" + "\"doc\": {" + "\"name\": \"sampleUpdated\"" + "}" + "}"; + + // Update sample resource with shared_with user. This will fail since the resource has not been shared + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse updateResponse = client.postJson(RESOURCE_INDEX_NAME + "/_update/" + resourceId, updatePayload); + // it will show not found since the resource is not visible to shared_with_user + updateResponse.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } + + // Admin is still allowed to update its own resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse updateResponse = client.postJson(RESOURCE_INDEX_NAME + "/_update/" + resourceId, updatePayload); + // it will show not found since the resource is not visible to shared_with_user + updateResponse.assertStatusCode(HttpStatus.SC_OK); + assertThat(updateResponse.bodyAsJsonNode().get("_shards").get("successful").asInt(), equalTo(1)); + } + + // Verify that share_with user does not have access to the resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse getResponse = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + // it will show not found since the resource is not visible to shared_with_user + getResponse.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } + + // share the resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload(resourceId)); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // Verify that share_with user now has access to the resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse getResponse = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + // it will show not found since the resource is not visible to shared_with_user + getResponse.assertStatusCode(HttpStatus.SC_OK); + assertThat(getResponse.getBody(), containsString("sampleUpdated")); + } + } + + private static String shareWithPayload(String resourceId) { + return "{" + + "\"resource_id\":\"" + + resourceId + + "\"," + + "\"resource_index\":\"" + + RESOURCE_INDEX_NAME + + "\"," + + "\"share_with\":{" + + "\"" + + SampleResourceScope.PUBLIC.value() + + "\":{" + + "\"users\": [\"" + + SHARED_WITH_USER.getName() + + "\"]" + + "}" + + "}" + + "}"; + } + + private static String revokeAccessPayload(String resourceId) { + return "{" + + "\"resource_id\": \"" + + resourceId + + "\"," + + "\"resource_index\": \"" + + RESOURCE_INDEX_NAME + + "\"," + + "\"entities\": {" + + "\"users\": [\"" + + SHARED_WITH_USER.getName() + + "\"]" + + "}," + + "\"scopes\": [\"" + + ResourceAccessScope.PUBLIC + + "\"]" + + "}"; + } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java index c1c5a4b66d..23aae25d42 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java @@ -23,6 +23,7 @@ import org.opensearch.security.spi.resources.Resource; import static org.opensearch.core.xcontent.ConstructingObjectParser.constructorArg; +import static org.opensearch.core.xcontent.ConstructingObjectParser.optionalConstructorArg; public class SampleResource extends Resource { @@ -61,8 +62,8 @@ public SampleResource(StreamInput in) throws IOException { static { PARSER.declareString(constructorArg(), new ParseField("name")); - PARSER.declareStringOrNull(constructorArg(), new ParseField("description")); - PARSER.declareObjectOrNull(constructorArg(), (p, c) -> p.mapStrings(), null, new ParseField("attributes")); + PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("description")); + PARSER.declareObjectOrNull(optionalConstructorArg(), (p, c) -> p.mapStrings(), null, new ParseField("attributes")); } public static SampleResource fromXContent(XContentParser parser) throws IOException { From 41eb9517a0a9fbcd8118e74d4993a72676ed0f08 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 17:37:58 -0500 Subject: [PATCH 107/201] Adds test retry logic for sample-sharing-plugin Signed-off-by: Darshit Chanpura --- sample-resource-plugin/build.gradle | 13 +++++++++++++ .../sample/SampleResourcePluginTests.java | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle index 6ceca2704c..e3c057cf3f 100644 --- a/sample-resource-plugin/build.gradle +++ b/sample-resource-plugin/build.gradle @@ -3,6 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +plugins { + id "org.gradle.test-retry" version "1.6.0" +} apply plugin: 'opensearch.opensearchplugin' apply plugin: 'opensearch.testclusters' @@ -48,6 +51,7 @@ ext { } } + repositories { mavenLocal() mavenCentral() @@ -93,6 +97,15 @@ sourceSets { } tasks.register("integrationTest", Test) { + doFirst { + if (System.getenv('DISABLE_RETRY') != 'true') { + retry { + failOnPassedAfterRetry = false + maxRetries = 2 + maxFailures = 5 + } + } + } description = 'Run integration tests for the subproject.' group = 'verification' diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index b5997cac62..9f7a1f503f 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -111,7 +111,7 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { ); OpenSearchSecurityPlugin.getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); - Thread.sleep(3000); + Thread.sleep(1000); response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); @@ -221,6 +221,7 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { // revoke share_with_user's access try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayload(resourceId)); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.bodyAsJsonNode().toString(), containsString("Resource " + resourceId + " access revoked successfully.")); From 1d2642375a20bfa004f6d3d0bdd1f4846d4bbe1f Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 17:42:18 -0500 Subject: [PATCH 108/201] Adds CI workflow that runs sample-plugin integration tests Signed-off-by: Darshit Chanpura --- .../integration-tests-sample-plugin.yml | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/integration-tests-sample-plugin.yml diff --git a/.github/workflows/integration-tests-sample-plugin.yml b/.github/workflows/integration-tests-sample-plugin.yml new file mode 100644 index 0000000000..9ea0fb92f3 --- /dev/null +++ b/.github/workflows/integration-tests-sample-plugin.yml @@ -0,0 +1,35 @@ +name: Bulk Integration Test For Sample Resource Plugin + +on: [workflow_dispatch] + +env: + GRADLE_OPTS: -Dhttp.keepAlive=false + +jobs: + bulk-integration-test-run: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + jdk: [21] + + steps: + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ matrix.jdk }} + + - uses: actions/checkout@v4 + + - run: OPENDISTRO_SECURITY_TEST_OPENSSL_OPT=true ./gradlew :sample-resource-sharing-plugin:integrationTest + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ matrix.jdk }}-${{ matrix.test-file }}-sample-resource-sharing-reports + path: | + ./sample-resource-sharing-plugin/build/reports/ + + - name: check archive for debugging + if: always() + run: echo "Check the artifact ${{ matrix.jdk }}-${{ matrix.test-file }}-sample-resource-sharing-reports.zip for detailed test results" From 2b7239ae4cb1ba74032f9d1ce801e659320d164e Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 17:52:20 -0500 Subject: [PATCH 109/201] Change painless dependency to compileOnly Signed-off-by: Darshit Chanpura --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7c0d55dc95..1e686e0605 100644 --- a/build.gradle +++ b/build.gradle @@ -632,7 +632,7 @@ check.dependsOn integrationTest dependencies { implementation project(path: ":opensearch-resource-sharing-spi") - implementation "org.opensearch.plugin:lang-painless:${opensearch_version}" + compileOnly "org.opensearch.plugin:lang-painless:${opensearch_version}" implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" implementation "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}" implementation "org.apache.httpcomponents.client5:httpclient5-cache:${versions.httpclient5}" From 7418bcb7c44f72a2fabfa86f9da39e6b87def02c Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 20:48:52 -0500 Subject: [PATCH 110/201] Attempts to fix CI Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 47 +++++++++++++++---- .../integration-tests-sample-plugin.yml | 2 +- spi/build.gradle | 2 +- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a41062883..c8a442f2ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -211,16 +211,14 @@ jobs: - run: ./gradlew clean assemble - uses: github/codeql-action/analyze@v3 - build-artifact-names: + build-version-qualifier: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 with: - distribution: temurin # Temurin is a distribution of adoptium + distribution: temurin java-version: 21 - - run: | security_plugin_version=$(./gradlew properties -q | grep -E '^version:' | awk '{print $2}') security_plugin_version_no_snapshot=$(echo $security_plugin_version | sed 's/-SNAPSHOT//g') @@ -238,15 +236,46 @@ jobs: echo ${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }} echo ${{ env.TEST_QUALIFIER }} - - run: ./gradlew clean assemble && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip + publish-spi: + name: Publish SPI + runs-on: ubuntu-latest + steps: + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + + - name: Checkout SPI + uses: actions/checkout@v4 + + - run: ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal + + - run: ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false + + - run: ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} + + - run: ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} + + build-artifact-names: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin # Temurin is a distribution of adoptium + java-version: 21 + + - run: ./gradlew clean assemble && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip && test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-${{ env.SECURITY_PLUGIN_VERSION }}.zip - - run: ./gradlew clean assemble -Dbuild.snapshot=false && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_NO_SNAPSHOT }}.zip + - run: ./gradlew clean assemble -Dbuild.snapshot=false && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_NO_SNAPSHOT }}.zip && test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-${{ env.SECURITY_PLUGIN_VERSION_NO_SNAPSHOT }}.zip - - run: ./gradlew clean assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}.zip + - run: ./gradlew clean assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}.zip && && test -s ./build/distributions/opensearch-sample-resource-plugin-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}.zip - - run: ./gradlew clean assemble -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}-SNAPSHOT.zip + - run: ./gradlew clean assemble -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}-SNAPSHOT.zip && test -s ./build/distributions/opensearch-sample-resource-plugin-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}-SNAPSHOT.zip - - run: ./gradlew clean publishPluginZipPublicationToZipStagingRepository && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.pom + - run: ./gradlew clean publishPluginZipPublicationToZipStagingRepository && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.pom && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip && test -s ./build/distributions/opensearch-sample-resource-plugin-${{ env.SECURITY_PLUGIN_VERSION }}.pom - name: List files in the build directory if there was an error run: ls -al ./build/distributions/ diff --git a/.github/workflows/integration-tests-sample-plugin.yml b/.github/workflows/integration-tests-sample-plugin.yml index 9ea0fb92f3..e148c528de 100644 --- a/.github/workflows/integration-tests-sample-plugin.yml +++ b/.github/workflows/integration-tests-sample-plugin.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 - - run: OPENDISTRO_SECURITY_TEST_OPENSSL_OPT=true ./gradlew :sample-resource-sharing-plugin:integrationTest + - run: OPENDISTRO_SECURITY_TEST_OPENSSL_OPT=true ./gradlew :opensearch-sample-resource-plugin:integrationTest - uses: actions/upload-artifact@v4 if: always() diff --git a/spi/build.gradle b/spi/build.gradle index b2db11979f..f23861c448 100644 --- a/spi/build.gradle +++ b/spi/build.gradle @@ -69,7 +69,7 @@ publishing { } repositories { maven { - name = "Snapshots" // optional target repository name + name = "Snapshots" url = "https://aws.oss.sonatype.org/content/repositories/snapshots" credentials { username "$System.env.SONATYPE_USERNAME" From a9763e91854a74bbbcf67d7b9a82bfb6482e9d9b Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 21:46:52 -0500 Subject: [PATCH 111/201] Changes verify to post Signed-off-by: Darshit Chanpura --- .../sample/SampleResourcePluginTests.java | 18 ++---------------- .../verify/RestVerifyResourceAccessAction.java | 4 ++-- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index 9f7a1f503f..b2d1c68aac 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -11,7 +11,6 @@ import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.spi.resources.ResourceAccessScope; import org.opensearch.security.spi.resources.ResourceProvider; -import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -20,9 +19,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.sample.AbstractSampleResourcePluginTests.*; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; -import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_PREFIX; -import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; @@ -32,19 +30,7 @@ */ @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class SampleResourcePluginTests { - - public final static TestSecurityConfig.User SHARED_WITH_USER = new TestSecurityConfig.User("resource_sharing_test_user").roles( - new TestSecurityConfig.Role("shared_role").indexPermissions("*").on("*").clusterPermissions("*") - ); - - private static final String SAMPLE_RESOURCE_CREATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/create"; - private static final String SAMPLE_RESOURCE_UPDATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/update"; - private static final String SAMPLE_RESOURCE_DELETE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/delete"; - private static final String SECURITY_RESOURCE_LIST_ENDPOINT = PLUGINS_PREFIX + "/resources/list"; - private static final String SECURITY_RESOURCE_SHARE_ENDPOINT = PLUGINS_PREFIX + "/resources/share"; - private static final String SECURITY_RESOURCE_VERIFY_ENDPOINT = PLUGINS_PREFIX + "/resources/verify_access"; - private static final String SECURITY_RESOURCE_REVOKE_ENDPOINT = PLUGINS_PREFIX + "/resources/revoke"; +public class SampleResourcePluginWithSecurityTests { @ClassRule public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) diff --git a/src/main/java/org/opensearch/security/rest/resources/access/verify/RestVerifyResourceAccessAction.java b/src/main/java/org/opensearch/security/rest/resources/access/verify/RestVerifyResourceAccessAction.java index 3a7e713a83..d8678c7c19 100644 --- a/src/main/java/org/opensearch/security/rest/resources/access/verify/RestVerifyResourceAccessAction.java +++ b/src/main/java/org/opensearch/security/rest/resources/access/verify/RestVerifyResourceAccessAction.java @@ -20,7 +20,7 @@ import org.opensearch.rest.RestRequest; import org.opensearch.rest.action.RestToXContentListener; -import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.POST; import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; @@ -30,7 +30,7 @@ public RestVerifyResourceAccessAction() {} @Override public List routes() { - return addRoutesPrefix(ImmutableList.of(new Route(GET, "/resources/verify_access")), PLUGIN_ROUTE_PREFIX); + return addRoutesPrefix(ImmutableList.of(new Route(POST, "/resources/verify_access")), PLUGIN_ROUTE_PREFIX); } @Override From 6918b28e480213dcab803b871fe051716c38d071 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 21:47:24 -0500 Subject: [PATCH 112/201] Adds a noop integration test when resource-sharing feature is disabled Signed-off-by: Darshit Chanpura --- .../AbstractSampleResourcePluginTests.java | 70 +++++++++ ...rcePluginResourceSharingDisabledTests.java | 134 ++++++++++++++++++ .../sample/SampleResourcePluginTests.java | 66 +-------- 3 files changed, 209 insertions(+), 61 deletions(-) create mode 100644 sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java create mode 100644 sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java new file mode 100644 index 0000000000..255e27ba53 --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java @@ -0,0 +1,70 @@ +package org.opensearch.sample; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.runner.RunWith; + +import org.opensearch.security.spi.resources.ResourceAccessScope; +import org.opensearch.test.framework.TestSecurityConfig; + +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_PREFIX; +import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; + +/** + * These tests run with security enabled + */ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class AbstractSampleResourcePluginTests { + + final static TestSecurityConfig.User SHARED_WITH_USER = new TestSecurityConfig.User("resource_sharing_test_user").roles( + new TestSecurityConfig.Role("shared_role").indexPermissions("*").on("*").clusterPermissions("*") + ); + + static final String SAMPLE_RESOURCE_CREATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/create"; + static final String SAMPLE_RESOURCE_UPDATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/update"; + static final String SAMPLE_RESOURCE_DELETE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/delete"; + static final String SECURITY_RESOURCE_LIST_ENDPOINT = PLUGINS_PREFIX + "/resources/list"; + static final String SECURITY_RESOURCE_SHARE_ENDPOINT = PLUGINS_PREFIX + "/resources/share"; + static final String SECURITY_RESOURCE_VERIFY_ENDPOINT = PLUGINS_PREFIX + "/resources/verify_access"; + static final String SECURITY_RESOURCE_REVOKE_ENDPOINT = PLUGINS_PREFIX + "/resources/revoke"; + + static String shareWithPayload(String resourceId) { + return "{" + + "\"resource_id\":\"" + + resourceId + + "\"," + + "\"resource_index\":\"" + + RESOURCE_INDEX_NAME + + "\"," + + "\"share_with\":{" + + "\"" + + SampleResourceScope.PUBLIC.value() + + "\":{" + + "\"users\": [\"" + + SHARED_WITH_USER.getName() + + "\"]" + + "}" + + "}" + + "}"; + } + + static String revokeAccessPayload(String resourceId) { + return "{" + + "\"resource_id\": \"" + + resourceId + + "\"," + + "\"resource_index\": \"" + + RESOURCE_INDEX_NAME + + "\"," + + "\"entities\": {" + + "\"users\": [\"" + + SHARED_WITH_USER.getName() + + "\"]" + + "}," + + "\"scopes\": [\"" + + ResourceAccessScope.PUBLIC + + "\"]" + + "}"; + } +} diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java new file mode 100644 index 0000000000..fd7158f1e4 --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java @@ -0,0 +1,134 @@ +package org.opensearch.sample; + +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.junit.*; +import org.junit.runner.RunWith; + +import org.opensearch.painless.PainlessModulePlugin; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.security.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; +import static org.opensearch.security.support.ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +/** + * These tests run with security disabled + */ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class SampleResourcePluginResourceSharingDisabledTests extends AbstractSampleResourcePluginTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .plugin(SampleResourcePlugin.class, PainlessModulePlugin.class) + .anonymousAuth(true) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_ADMIN, SHARED_WITH_USER) + .nodeSettings(Map.of(OPENSEARCH_RESOURCE_SHARING_ENABLED, false)) + .build(); + + @After + public void clearIndices() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + client.delete(RESOURCE_INDEX_NAME); + } + } + + @Test + public void testPluginInstalledCorrectly() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse pluginsResponse = client.get("_cat/plugins"); + assertThat(pluginsResponse.getBody(), containsString("org.opensearch.security.OpenSearchSecurityPlugin")); + assertThat(pluginsResponse.getBody(), containsString("org.opensearch.sample.SampleResourcePlugin")); + } + } + + @Test + public void testNoResourceRestrictions() throws Exception { + String resourceId; + // create sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResource = "{\"name\":\"sample\"}"; + HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + response.assertStatusCode(HttpStatus.SC_OK); + + resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + } + + // assert that resource-sharing index doesn't exist and neither do resource-sharing APIs + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse response = client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search"); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + + response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_BAD_REQUEST); + assertThat(response.getBody(), containsString("no handler found for uri")); + + response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, "{}"); + response.assertStatusCode(HttpStatus.SC_BAD_REQUEST); + assertThat(response.getBody(), containsString("no handler found for uri")); + + response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, "{}"); + response.assertStatusCode(HttpStatus.SC_BAD_REQUEST); + assertThat(response.getBody(), containsString("no handler found for uri")); + + response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, "{}"); + response.assertStatusCode(HttpStatus.SC_BAD_REQUEST); + assertThat(response.getBody(), containsString("no handler found for uri")); + } + + // resource should be visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + + HttpResponse response = client.postJson(RESOURCE_INDEX_NAME + "/_search", "{\"query\" : {\"match_all\" : {}}}"); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sample")); + } + + // resource should be visible to shared_with_user since there is no restriction and this user has * permission + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.postJson(RESOURCE_INDEX_NAME + "/_search", "{\"query\" : {\"match_all\" : {}}}"); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(1)); + } + + // shared_with_user is able to update admin's resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + String updatePayload = "{" + "\"name\": \"sampleUpdated\"" + "}"; + HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, updatePayload); + updateResponse.assertStatusCode(HttpStatus.SC_OK); + } + + // admin can see updated value + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse getResponse = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + getResponse.assertStatusCode(HttpStatus.SC_OK); + assertThat(getResponse.getBody(), containsString("sampleUpdated")); + } + + // delete sample resource - share_with user delete admin user's resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // admin can no longer see the resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse getResponse = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + getResponse.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } + + } +} diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index b2d1c68aac..63e6ad09aa 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -26,11 +26,11 @@ import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** - * These tests run with security enabled + * These tests run with resource sharing enabled */ @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class SampleResourcePluginWithSecurityTests { +public class SampleResourcePluginTests extends AbstractSampleResourcePluginTests { @ClassRule public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) @@ -131,24 +131,7 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { // shared_with_user should not be able to share admin's resource with itself try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - String shareWithPayload = "{" - + "\"resource_id\":\"" - + resourceId - + "\"," - + "\"resource_index\":\"" - + RESOURCE_INDEX_NAME - + "\"," - + "\"share_with\":{" - + "\"" - + SampleResourceScope.PUBLIC.value() - + "\":{" - + "\"users\": [\"" - + SHARED_WITH_USER.getName() - + "\"]" - + "}" - + "}" - + "}"; - HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload); + HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload(resourceId)); response.assertStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); assertThat(response.bodyAsJsonNode().toString(), containsString("User " + SHARED_WITH_USER.getName() + " is not authorized")); // TODO these tests must check for unauthorized instead of internal-server-error @@ -190,7 +173,7 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { + "\",\"scope\":\"" + ResourceAccessScope.PUBLIC + "\"}"; - HttpResponse response = client.getWithJsonBody(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); + HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString("User has requested scope " + ResourceAccessScope.PUBLIC + " access")); } @@ -222,7 +205,7 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { + "\",\"scope\":\"" + ResourceAccessScope.PUBLIC + "\"}"; - HttpResponse response = client.getWithJsonBody(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); + HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString("User does not have requested scope " + ResourceAccessScope.PUBLIC + " access")); } @@ -246,7 +229,6 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { } } - // TODO add test case for updating the resource directly @Test public void testDLSRestrictionForResourceByDirectlyUpdatingTheResourceIndex() throws Exception { String resourceId; @@ -334,42 +316,4 @@ public void testDLSRestrictionForResourceByDirectlyUpdatingTheResourceIndex() th } } - private static String shareWithPayload(String resourceId) { - return "{" - + "\"resource_id\":\"" - + resourceId - + "\"," - + "\"resource_index\":\"" - + RESOURCE_INDEX_NAME - + "\"," - + "\"share_with\":{" - + "\"" - + SampleResourceScope.PUBLIC.value() - + "\":{" - + "\"users\": [\"" - + SHARED_WITH_USER.getName() - + "\"]" - + "}" - + "}" - + "}"; - } - - private static String revokeAccessPayload(String resourceId) { - return "{" - + "\"resource_id\": \"" - + resourceId - + "\"," - + "\"resource_index\": \"" - + RESOURCE_INDEX_NAME - + "\"," - + "\"entities\": {" - + "\"users\": [\"" - + SHARED_WITH_USER.getName() - + "\"]" - + "}," - + "\"scopes\": [\"" - + ResourceAccessScope.PUBLIC - + "\"]" - + "}"; - } } From 456418e535076d2658123a878c99b8bfaafe84a8 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 18 Jan 2025 22:19:58 -0500 Subject: [PATCH 113/201] Fixes Github workflow Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 137 +++++++++--------- .../integration-tests-sample-plugin.yml | 35 ----- build.gradle | 3 + 3 files changed, 75 insertions(+), 100 deletions(-) delete mode 100644 .github/workflows/integration-tests-sample-plugin.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8a442f2ee..6300c70f89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,13 +111,26 @@ jobs: - name: Checkout security uses: actions/checkout@v4 - - name: Build and Test + - name: Run Integration Tests uses: gradle/gradle-build-action@v3 with: cache-disabled: true arguments: | integrationTest -Dbuild.snapshot=false + - name: Publish SPI to Local Maven + uses: gradle/gradle-build-action@v3 + with: + cache-disabled: true + arguments: :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false + + - name: Run SampleResourcePlugin Integration Tests + uses: gradle/gradle-build-action@v3 + with: + cache-disabled: true + arguments: | + :opensearch-sample-resource-plugin:integrationTest -Dbuild.snapshot=false + - uses: actions/upload-artifact@v4 if: always() with: @@ -125,7 +138,6 @@ jobs: path: | ./build/reports/ - resource-tests: env: CI_ENVIRONMENT: resource-test @@ -146,7 +158,7 @@ jobs: - name: Checkout security uses: actions/checkout@v4 - - name: Build and Test + - name: Run Resource Tests uses: gradle/gradle-build-action@v3 with: cache-disabled: true @@ -211,72 +223,67 @@ jobs: - run: ./gradlew clean assemble - uses: github/codeql-action/analyze@v3 - build-version-qualifier: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 21 - - run: | - security_plugin_version=$(./gradlew properties -q | grep -E '^version:' | awk '{print $2}') - security_plugin_version_no_snapshot=$(echo $security_plugin_version | sed 's/-SNAPSHOT//g') - security_plugin_version_only_number=$(echo $security_plugin_version_no_snapshot | cut -d- -f1) - test_qualifier=alpha2 - - echo "SECURITY_PLUGIN_VERSION=$security_plugin_version" >> $GITHUB_ENV - echo "SECURITY_PLUGIN_VERSION_NO_SNAPSHOT=$security_plugin_version_no_snapshot" >> $GITHUB_ENV - echo "SECURITY_PLUGIN_VERSION_ONLY_NUMBER=$security_plugin_version_only_number" >> $GITHUB_ENV - echo "TEST_QUALIFIER=$test_qualifier" >> $GITHUB_ENV - - - run: | - echo ${{ env.SECURITY_PLUGIN_VERSION }} - echo ${{ env.SECURITY_PLUGIN_VERSION_NO_SNAPSHOT }} - echo ${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }} - echo ${{ env.TEST_QUALIFIER }} - - publish-spi: - name: Publish SPI + build-artifact-names: runs-on: ubuntu-latest steps: - - name: Set up JDK + - name: Setup Environment + uses: actions/checkout@v4 + + - name: Configure Java uses: actions/setup-java@v4 with: distribution: temurin java-version: 21 - - name: Checkout SPI - uses: actions/checkout@v4 - - - run: ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal - - - run: ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false - - - run: ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} - - - run: ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} - - build-artifact-names: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-java@v4 - with: - distribution: temurin # Temurin is a distribution of adoptium - java-version: 21 - - - run: ./gradlew clean assemble && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip && test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-${{ env.SECURITY_PLUGIN_VERSION }}.zip - - - run: ./gradlew clean assemble -Dbuild.snapshot=false && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_NO_SNAPSHOT }}.zip && test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-${{ env.SECURITY_PLUGIN_VERSION_NO_SNAPSHOT }}.zip - - - run: ./gradlew clean assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}.zip && && test -s ./build/distributions/opensearch-sample-resource-plugin-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}.zip - - - run: ./gradlew clean assemble -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}-SNAPSHOT.zip && test -s ./build/distributions/opensearch-sample-resource-plugin-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}-SNAPSHOT.zip - - - run: ./gradlew clean publishPluginZipPublicationToZipStagingRepository && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.pom && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip && test -s ./build/distributions/opensearch-sample-resource-plugin-${{ env.SECURITY_PLUGIN_VERSION }}.pom - - - name: List files in the build directory if there was an error - run: ls -al ./build/distributions/ - if: failure() + - name: Build and Test Artifacts + run: | + # Set version variables + security_plugin_version=$(./gradlew properties -q | grep -E '^version:' | awk '{print $2}') + security_plugin_version_no_snapshot=$(echo $security_plugin_version | sed 's/-SNAPSHOT//g') + security_plugin_version_only_number=$(echo $security_plugin_version_no_snapshot | cut -d- -f1) + test_qualifier=alpha2 + + # Export variables to GitHub Environment + echo "SECURITY_PLUGIN_VERSION=$security_plugin_version" >> $GITHUB_ENV + echo "SECURITY_PLUGIN_VERSION_NO_SNAPSHOT=$security_plugin_version_no_snapshot" >> $GITHUB_ENV + echo "SECURITY_PLUGIN_VERSION_ONLY_NUMBER=$security_plugin_version_only_number" >> $GITHUB_ENV + echo "TEST_QUALIFIER=$test_qualifier" >> $GITHUB_ENV + + # Debug print versions + echo "Versions:" + echo $security_plugin_version + echo $security_plugin_version_no_snapshot + echo $security_plugin_version_only_number + echo $test_qualifier + + # Publish SPI + ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal + ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false + ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier + ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier + + # Build artifacts + ./gradlew clean assemble && \ + test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ + test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version.zip + + ./gradlew clean assemble -Dbuild.snapshot=false && \ + test -s ./build/distributions/opensearch-security-$security_plugin_version_no_snapshot.zip && \ + test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_no_snapshot.zip + + ./gradlew clean assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && \ + test -s ./build/distributions/opensearch-security-$security_plugin_version_only_number-$test_qualifier.zip && \ + test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_only_number-$test_qualifier.zip + + ./gradlew clean assemble -Dbuild.version_qualifier=$test_qualifier && \ + test -s ./build/distributions/opensearch-security-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.zip && \ + test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.zip + + ./gradlew clean publishPluginZipPublicationToZipStagingRepository && \ + test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ + test -s ./build/distributions/opensearch-security-$security_plugin_version.pom && \ + test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version.pom + + - name: List files in build directory on failure + if: failure() + run: ls -al ./build/distributions/ diff --git a/.github/workflows/integration-tests-sample-plugin.yml b/.github/workflows/integration-tests-sample-plugin.yml deleted file mode 100644 index e148c528de..0000000000 --- a/.github/workflows/integration-tests-sample-plugin.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Bulk Integration Test For Sample Resource Plugin - -on: [workflow_dispatch] - -env: - GRADLE_OPTS: -Dhttp.keepAlive=false - -jobs: - bulk-integration-test-run: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - jdk: [21] - - steps: - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: ${{ matrix.jdk }} - - - uses: actions/checkout@v4 - - - run: OPENDISTRO_SECURITY_TEST_OPENSSL_OPT=true ./gradlew :opensearch-sample-resource-plugin:integrationTest - - - uses: actions/upload-artifact@v4 - if: always() - with: - name: ${{ matrix.jdk }}-${{ matrix.test-file }}-sample-resource-sharing-reports - path: | - ./sample-resource-sharing-plugin/build/reports/ - - - name: check archive for debugging - if: always() - run: echo "Check the artifact ${{ matrix.jdk }}-${{ matrix.test-file }}-sample-resource-sharing-reports.zip for detailed test results" diff --git a/build.gradle b/build.gradle index 1e686e0605..8319dd0f0b 100644 --- a/build.gradle +++ b/build.gradle @@ -583,6 +583,9 @@ sourceSets { //add new task that runs integration tests task integrationTest(type: Test) { + filter { + excludeTestsMatching 'org.opensearch.sample.*ResourcePlugin*' + } doFirst { // Only run resources tests on resource-test CI environments or locally if (System.getenv('CI_ENVIRONMENT') != 'resource-test' && System.getenv('CI_ENVIRONMENT') != null) { From ba6928a25c77e3a6af10be3a7491d77ad27289d9 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 19 Jan 2025 00:35:45 -0500 Subject: [PATCH 114/201] Consolidates rest actions to a single place Signed-off-by: Darshit Chanpura --- .../AbstractSampleResourcePluginTests.java | 11 +- ...rcePluginResourceSharingDisabledTests.java | 4 +- .../sample/SampleResourcePluginTests.java | 1 - .../security/OpenSearchSecurityPlugin.java | 14 +- .../security/dlic/rest/support/Utils.java | 2 + .../access/RestResourceAccessAction.java | 168 ++++++++++++++++++ .../RestListAccessibleResourcesAction.java | 49 ----- .../RestRevokeResourceAccessAction.java | 74 -------- .../access/share/RestShareResourceAction.java | 79 -------- .../RestVerifyResourceAccessAction.java | 59 ------ 10 files changed, 181 insertions(+), 280 deletions(-) create mode 100644 src/main/java/org/opensearch/security/rest/resources/access/RestResourceAccessAction.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/list/RestListAccessibleResourcesAction.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/revoke/RestRevokeResourceAccessAction.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/share/RestShareResourceAction.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/verify/RestVerifyResourceAccessAction.java diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java index 255e27ba53..6435c371ab 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java @@ -8,7 +8,7 @@ import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_PREFIX; -import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_RESOURCE_ROUTE_PREFIX; /** * These tests run with security enabled @@ -24,10 +24,11 @@ public class AbstractSampleResourcePluginTests { static final String SAMPLE_RESOURCE_CREATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/create"; static final String SAMPLE_RESOURCE_UPDATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/update"; static final String SAMPLE_RESOURCE_DELETE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/delete"; - static final String SECURITY_RESOURCE_LIST_ENDPOINT = PLUGINS_PREFIX + "/resources/list"; - static final String SECURITY_RESOURCE_SHARE_ENDPOINT = PLUGINS_PREFIX + "/resources/share"; - static final String SECURITY_RESOURCE_VERIFY_ENDPOINT = PLUGINS_PREFIX + "/resources/verify_access"; - static final String SECURITY_RESOURCE_REVOKE_ENDPOINT = PLUGINS_PREFIX + "/resources/revoke"; + private static final String PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH = PLUGIN_RESOURCE_ROUTE_PREFIX.replaceFirst("/", ""); + static final String SECURITY_RESOURCE_LIST_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/list"; + static final String SECURITY_RESOURCE_SHARE_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/share"; + static final String SECURITY_RESOURCE_VERIFY_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/verify_access"; + static final String SECURITY_RESOURCE_REVOKE_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/revoke"; static String shareWithPayload(String resourceId) { return "{" diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java index fd7158f1e4..62ba2e343c 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java @@ -4,7 +4,9 @@ import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; -import org.junit.*; +import org.junit.After; +import org.junit.ClassRule; +import org.junit.Test; import org.junit.runner.RunWith; import org.opensearch.painless.PainlessModulePlugin; diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index 63e6ad09aa..f34cf0c561 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -19,7 +19,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; -import static org.opensearch.sample.AbstractSampleResourcePluginTests.*; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; import static org.opensearch.security.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 87a2fb4989..5b3ebbd635 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -190,13 +190,10 @@ import org.opensearch.security.rest.SecurityInfoAction; import org.opensearch.security.rest.SecurityWhoAmIAction; import org.opensearch.security.rest.TenantInfoAction; +import org.opensearch.security.rest.resources.access.RestResourceAccessAction; import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesAction; -import org.opensearch.security.rest.resources.access.list.RestListAccessibleResourcesAction; -import org.opensearch.security.rest.resources.access.revoke.RestRevokeResourceAccessAction; import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessAction; -import org.opensearch.security.rest.resources.access.share.RestShareResourceAction; import org.opensearch.security.rest.resources.access.share.ShareResourceAction; -import org.opensearch.security.rest.resources.access.verify.RestVerifyResourceAccessAction; import org.opensearch.security.rest.resources.access.verify.VerifyResourceAccessAction; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.CType; @@ -700,14 +697,7 @@ public List getRestHandlers( ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT )) { - handlers.addAll( - List.of( - new RestShareResourceAction(), - new RestRevokeResourceAccessAction(), - new RestListAccessibleResourcesAction(), - new RestVerifyResourceAccessAction() - ) - ); + handlers.add(new RestResourceAccessAction()); } log.debug("Added {} rest handler(s)", handlers.size()); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index 2e900169db..ba8a5cda5b 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java +++ b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java @@ -64,6 +64,8 @@ public class Utils { public final static String LEGACY_PLUGIN_API_ROUTE_PREFIX = LEGACY_PLUGIN_ROUTE_PREFIX + "/api"; + public final static String PLUGIN_RESOURCE_ROUTE_PREFIX = PLUGIN_ROUTE_PREFIX + "/resources"; + private static final ObjectMapper internalMapper = new ObjectMapper(); public static Map convertJsonToxToStructuredMap(ToXContent jsonContent) { diff --git a/src/main/java/org/opensearch/security/rest/resources/access/RestResourceAccessAction.java b/src/main/java/org/opensearch/security/rest/resources/access/RestResourceAccessAction.java new file mode 100644 index 0000000000..787e92171c --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resources/access/RestResourceAccessAction.java @@ -0,0 +1,168 @@ +/* + * 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. + */ + +package org.opensearch.security.rest.resources.access; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableList; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.security.resources.RecipientType; +import org.opensearch.security.resources.RecipientTypeRegistry; +import org.opensearch.security.resources.ShareWith; +import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesAction; +import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesRequest; +import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessAction; +import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessRequest; +import org.opensearch.security.rest.resources.access.share.ShareResourceAction; +import org.opensearch.security.rest.resources.access.share.ShareResourceRequest; +import org.opensearch.security.rest.resources.access.verify.VerifyResourceAccessAction; +import org.opensearch.security.rest.resources.access.verify.VerifyResourceAccessRequest; + +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.api.Responses.badRequest; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_RESOURCE_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +public class RestResourceAccessAction extends BaseRestHandler { + + public RestResourceAccessAction() {} + + @Override + public List routes() { + return addRoutesPrefix( + ImmutableList.of( + new Route(GET, "/list/{resourceIndex}"), + new Route(POST, "/revoke"), + new Route(POST, "/share"), + new Route(POST, "/verify_access") + ), + PLUGIN_RESOURCE_ROUTE_PREFIX + ); + } + + @Override + public String getName() { + return "resource_access_action"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + consumeParams(request); // to avoid 400s + String path = request.path().split(PLUGIN_RESOURCE_ROUTE_PREFIX)[1].split("/")[1]; + return switch (path) { + case "list" -> channel -> handleListRequest(request, client, channel); + case "revoke" -> channel -> handleRevokeRequest(request, client, channel); + case "share" -> channel -> handleShareRequest(request, client, channel); + case "verify_access" -> channel -> handleVerifyRequest(request, client, channel); + default -> channel -> badRequest(channel, "Unknown route: " + path); + }; + } + + private void consumeParams(RestRequest request) { + request.param("resourceIndex", ""); + } + + public void handleListRequest(RestRequest request, NodeClient client, RestChannel channel) { + String resourceIndex = request.param("resourceIndex", ""); + final ListAccessibleResourcesRequest listAccessibleResourcesRequest = new ListAccessibleResourcesRequest(resourceIndex); + client.executeLocally( + ListAccessibleResourcesAction.INSTANCE, + listAccessibleResourcesRequest, + new RestToXContentListener<>(channel) + ); + + } + + public void handleRevokeRequest(RestRequest request, NodeClient client, RestChannel channel) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String resourceId = (String) source.get("resource_id"); + String resourceIndex = (String) source.get("resource_index"); + @SuppressWarnings("unchecked") + Map> revokeSource = (Map>) source.get("entities"); + Map> revoke = revokeSource.entrySet() + .stream() + .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); + @SuppressWarnings("unchecked") + Set scopes = new HashSet<>(source.containsKey("scopes") ? (List) source.get("scopes") : List.of()); + final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest( + resourceId, + resourceIndex, + revoke, + scopes + ); + client.executeLocally(RevokeResourceAccessAction.INSTANCE, revokeResourceAccessRequest, new RestToXContentListener<>(channel)); + } + + public void handleShareRequest(RestRequest request, NodeClient client, RestChannel channel) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String resourceId = (String) source.get("resource_id"); + String resourceIndex = (String) source.get("resource_index"); + + ShareWith shareWith = parseShareWith(source); + final ShareResourceRequest shareResourceRequest = new ShareResourceRequest(resourceId, resourceIndex, shareWith); + client.executeLocally(ShareResourceAction.INSTANCE, shareResourceRequest, new RestToXContentListener<>(channel)); + } + + public void handleVerifyRequest(RestRequest request, NodeClient client, RestChannel channel) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String resourceId = (String) source.get("resource_id"); + String resourceIndex = (String) source.get("resource_index"); + String scope = (String) source.get("scope"); + + final VerifyResourceAccessRequest verifyResourceAccessRequest = new VerifyResourceAccessRequest(resourceId, resourceIndex, scope); + client.executeLocally(VerifyResourceAccessAction.INSTANCE, verifyResourceAccessRequest, new RestToXContentListener<>(channel)); + } + + private ShareWith parseShareWith(Map source) throws IOException { + @SuppressWarnings("unchecked") + Map shareWithMap = (Map) source.get("share_with"); + if (shareWithMap == null || shareWithMap.isEmpty()) { + throw new IllegalArgumentException("share_with is required and cannot be empty"); + } + + String jsonString = XContentFactory.jsonBuilder().map(shareWithMap).toString(); + + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, jsonString) + ) { + return ShareWith.fromXContent(parser); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/list/RestListAccessibleResourcesAction.java b/src/main/java/org/opensearch/security/rest/resources/access/list/RestListAccessibleResourcesAction.java deleted file mode 100644 index 85fb04554b..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/list/RestListAccessibleResourcesAction.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.list; - -import java.io.IOException; -import java.util.List; - -import com.google.common.collect.ImmutableList; - -import org.opensearch.client.node.NodeClient; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; - -import static org.opensearch.rest.RestRequest.Method.GET; -import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; -import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; - -public class RestListAccessibleResourcesAction extends BaseRestHandler { - - public RestListAccessibleResourcesAction() {} - - @Override - public List routes() { - return addRoutesPrefix(ImmutableList.of(new Route(GET, "/resources/list/{resourceIndex}")), PLUGIN_ROUTE_PREFIX); - } - - @Override - public String getName() { - return "list_accessible_resources"; - } - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - String resourceIndex = request.param("resourceIndex", ""); - final ListAccessibleResourcesRequest listAccessibleResourcesRequest = new ListAccessibleResourcesRequest(resourceIndex); - return channel -> client.executeLocally( - ListAccessibleResourcesAction.INSTANCE, - listAccessibleResourcesRequest, - new RestToXContentListener<>(channel) - ); - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RestRevokeResourceAccessAction.java b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RestRevokeResourceAccessAction.java deleted file mode 100644 index 2bde557884..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RestRevokeResourceAccessAction.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.revoke; - -import java.io.IOException; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import com.google.common.collect.ImmutableList; - -import org.opensearch.client.node.NodeClient; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; -import org.opensearch.security.resources.RecipientType; -import org.opensearch.security.resources.RecipientTypeRegistry; - -import static org.opensearch.rest.RestRequest.Method.POST; -import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; -import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; - -public class RestRevokeResourceAccessAction extends BaseRestHandler { - - public RestRevokeResourceAccessAction() {} - - @Override - public List routes() { - return addRoutesPrefix(ImmutableList.of(new Route(POST, "/resources/revoke")), PLUGIN_ROUTE_PREFIX); - } - - @Override - public String getName() { - return "revoke_resources_access"; - } - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - - String resourceId = (String) source.get("resource_id"); - String resourceIndex = (String) source.get("resource_index"); - @SuppressWarnings("unchecked") - Map> revokeSource = (Map>) source.get("entities"); - Map> revoke = revokeSource.entrySet() - .stream() - .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); - @SuppressWarnings("unchecked") - Set scopes = new HashSet<>(source.containsKey("scopes") ? (List) source.get("scopes") : List.of()); - final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest( - resourceId, - resourceIndex, - revoke, - scopes - ); - return channel -> client.executeLocally( - RevokeResourceAccessAction.INSTANCE, - revokeResourceAccessRequest, - new RestToXContentListener<>(channel) - ); - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/share/RestShareResourceAction.java b/src/main/java/org/opensearch/security/rest/resources/access/share/RestShareResourceAction.java deleted file mode 100644 index 3559ced3aa..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/share/RestShareResourceAction.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.share; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import com.google.common.collect.ImmutableList; - -import org.opensearch.client.node.NodeClient; -import org.opensearch.common.xcontent.LoggingDeprecationHandler; -import org.opensearch.common.xcontent.XContentFactory; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; -import org.opensearch.security.resources.ShareWith; - -import static org.opensearch.rest.RestRequest.Method.POST; -import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; -import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; - -public class RestShareResourceAction extends BaseRestHandler { - - public RestShareResourceAction() {} - - @Override - public List routes() { - return addRoutesPrefix(ImmutableList.of(new Route(POST, "/resources/share")), PLUGIN_ROUTE_PREFIX); - } - - @Override - public String getName() { - return "share_resources"; - } - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - - String resourceId = (String) source.get("resource_id"); - String resourceIndex = (String) source.get("resource_index"); - - ShareWith shareWith = parseShareWith(source); - final ShareResourceRequest shareResourceRequest = new ShareResourceRequest(resourceId, resourceIndex, shareWith); - return channel -> client.executeLocally(ShareResourceAction.INSTANCE, shareResourceRequest, new RestToXContentListener<>(channel)); - } - - private ShareWith parseShareWith(Map source) throws IOException { - @SuppressWarnings("unchecked") - Map shareWithMap = (Map) source.get("share_with"); - if (shareWithMap == null || shareWithMap.isEmpty()) { - throw new IllegalArgumentException("share_with is required and cannot be empty"); - } - - String jsonString = XContentFactory.jsonBuilder().map(shareWithMap).toString(); - - try ( - XContentParser parser = XContentType.JSON.xContent() - .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, jsonString) - ) { - return ShareWith.fromXContent(parser); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); - } - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/verify/RestVerifyResourceAccessAction.java b/src/main/java/org/opensearch/security/rest/resources/access/verify/RestVerifyResourceAccessAction.java deleted file mode 100644 index d8678c7c19..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/verify/RestVerifyResourceAccessAction.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.verify; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import com.google.common.collect.ImmutableList; - -import org.opensearch.client.node.NodeClient; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; - -import static org.opensearch.rest.RestRequest.Method.POST; -import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; -import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; - -public class RestVerifyResourceAccessAction extends BaseRestHandler { - - public RestVerifyResourceAccessAction() {} - - @Override - public List routes() { - return addRoutesPrefix(ImmutableList.of(new Route(POST, "/resources/verify_access")), PLUGIN_ROUTE_PREFIX); - } - - @Override - public String getName() { - return "verify_resource_access"; - } - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - - String resourceId = (String) source.get("resource_id"); - String resourceIndex = (String) source.get("resource_index"); - String scope = (String) source.get("scope"); - - final VerifyResourceAccessRequest verifyResourceAccessRequest = new VerifyResourceAccessRequest(resourceId, resourceIndex, scope); - return channel -> client.executeLocally( - VerifyResourceAccessAction.INSTANCE, - verifyResourceAccessRequest, - new RestToXContentListener<>(channel) - ); - } -} From a51563c3e6f335b075486d450fb704c131c68273 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 19 Jan 2025 01:57:08 -0500 Subject: [PATCH 115/201] Simplifies rest actions for resource acess and updates sample plugin integration tests Signed-off-by: Darshit Chanpura --- .../sample/SampleResourcePluginTests.java | 34 ++--- .../security/OpenSearchSecurityPlugin.java | 27 +--- .../resources/ResourceAccessHandler.java | 12 +- ...cessAction.java => ResourceApiAction.java} | 125 +++++++++++------- .../list/ListAccessibleResourcesAction.java | 25 ---- .../list/ListAccessibleResourcesRequest.java | 51 ------- .../list/ListAccessibleResourcesResponse.java | 70 ---------- .../revoke/RevokeResourceAccessAction.java | 21 --- .../revoke/RevokeResourceAccessRequest.java | 79 ----------- .../revoke/RevokeResourceAccessResponse.java | 42 ------ .../access/share/ShareResourceAction.java | 25 ---- .../access/share/ShareResourceRequest.java | 61 --------- .../access/share/ShareResourceResponse.java | 42 ------ .../verify/VerifyResourceAccessAction.java | 25 ---- .../verify/VerifyResourceAccessRequest.java | 69 ---------- .../verify/VerifyResourceAccessResponse.java | 52 -------- ...ransportListAccessibleResourcesAction.java | 63 --------- .../TransportRevokeResourceAccessAction.java | 68 ---------- .../access/TransportShareResourceAction.java | 65 --------- .../TransportVerifyResourceAccessAction.java | 77 ----------- 20 files changed, 97 insertions(+), 936 deletions(-) rename src/main/java/org/opensearch/security/rest/resources/access/{RestResourceAccessAction.java => ResourceApiAction.java} (56%) delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesAction.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesRequest.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessAction.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessRequest.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessResponse.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceAction.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceRequest.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceResponse.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessAction.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessRequest.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessResponse.java delete mode 100644 src/main/java/org/opensearch/security/transport/resources/access/TransportListAccessibleResourcesAction.java delete mode 100644 src/main/java/org/opensearch/security/transport/resources/access/TransportRevokeResourceAccessAction.java delete mode 100644 src/main/java/org/opensearch/security/transport/resources/access/TransportShareResourceAction.java delete mode 100644 src/main/java/org/opensearch/security/transport/resources/access/TransportVerifyResourceAccessAction.java diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index f34cf0c561..beec8a8c10 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -17,8 +17,7 @@ import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.*; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; import static org.opensearch.security.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; @@ -131,11 +130,11 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload(resourceId)); - response.assertStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); - assertThat(response.bodyAsJsonNode().toString(), containsString("User " + SHARED_WITH_USER.getName() + " is not authorized")); - // TODO these tests must check for unauthorized instead of internal-server-error - // response.assertStatusCode(HttpStatus.SC_UNAUTHORIZED); - // assertThat(response.bodyAsJsonNode().get("message").asText(), containsString("User is not authorized")); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + assertThat( + response.bodyAsJsonNode().get("message").asText(), + containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") + ); } // share resource with shared_with user @@ -144,7 +143,10 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload(resourceId)); response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("message").asText(), containsString(resourceId)); + assertThat( + response.bodyAsJsonNode().get("share_with").get(SampleResourceScope.PUBLIC.value()).get("users").get(0).asText(), + containsString(SHARED_WITH_USER.getName()) + ); } // resource should now be visible to shared_with_user @@ -174,17 +176,17 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { + "\"}"; HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), containsString("User has requested scope " + ResourceAccessScope.PUBLIC + " access")); + assertThat(response.bodyAsJsonNode().get("has_permission").asBoolean(), equalTo(true)); } // shared_with user should not be able to revoke access to admin's resource try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayload(resourceId)); - response.assertStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); - assertThat(response.bodyAsJsonNode().toString(), containsString("User " + SHARED_WITH_USER.getName() + " is not authorized")); - // TODO these tests must check for unauthorized instead of internal-server-error - // response.assertStatusCode(HttpStatus.SC_UNAUTHORIZED); - // assertThat(response.bodyAsJsonNode().get("message").asText(), containsString("User is not authorized")); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + assertThat( + response.bodyAsJsonNode().get("message").asText(), + containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") + ); } // revoke share_with_user's access @@ -192,7 +194,7 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { Thread.sleep(1000); HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayload(resourceId)); response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().toString(), containsString("Resource " + resourceId + " access revoked successfully.")); + assertThat(response.bodyAsJsonNode().get("share_with"), nullValue()); } // verify access - share_with_user should no longer have access to admin's resource @@ -206,7 +208,7 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { + "\"}"; HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), containsString("User does not have requested scope " + ResourceAccessScope.PUBLIC + " access")); + assertThat(response.bodyAsJsonNode().get("has_permission").asBoolean(), equalTo(false)); } // delete sample resource diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 5b3ebbd635..6b1f435256 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -190,11 +190,7 @@ import org.opensearch.security.rest.SecurityInfoAction; import org.opensearch.security.rest.SecurityWhoAmIAction; import org.opensearch.security.rest.TenantInfoAction; -import org.opensearch.security.rest.resources.access.RestResourceAccessAction; -import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesAction; -import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessAction; -import org.opensearch.security.rest.resources.access.share.ShareResourceAction; -import org.opensearch.security.rest.resources.access.verify.VerifyResourceAccessAction; +import org.opensearch.security.rest.resources.access.ResourceApiAction; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.setting.OpensearchDynamicSetting; @@ -220,10 +216,6 @@ import org.opensearch.security.transport.DefaultInterClusterRequestEvaluator; import org.opensearch.security.transport.InterClusterRequestEvaluator; import org.opensearch.security.transport.SecurityInterceptor; -import org.opensearch.security.transport.resources.access.TransportListAccessibleResourcesAction; -import org.opensearch.security.transport.resources.access.TransportRevokeResourceAccessAction; -import org.opensearch.security.transport.resources.access.TransportShareResourceAction; -import org.opensearch.security.transport.resources.access.TransportVerifyResourceAccessAction; import org.opensearch.security.user.User; import org.opensearch.security.user.UserService; import org.opensearch.tasks.Task; @@ -697,7 +689,7 @@ public List getRestHandlers( ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT )) { - handlers.add(new RestResourceAccessAction()); + handlers.add(new ResourceApiAction(resourceAccessHandler)); } log.debug("Added {} rest handler(s)", handlers.size()); } @@ -726,21 +718,6 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre actions.add(new ActionHandler<>(CertificatesActionType.INSTANCE, TransportCertificatesInfoNodesAction.class)); } actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class)); - - // Resource-access-control related actions - if (settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - )) { - actions.addAll( - List.of( - new ActionHandler<>(ShareResourceAction.INSTANCE, TransportShareResourceAction.class), - new ActionHandler<>(RevokeResourceAccessAction.INSTANCE, TransportRevokeResourceAccessAction.class), - new ActionHandler<>(ListAccessibleResourcesAction.INSTANCE, TransportListAccessibleResourcesAction.class), - new ActionHandler<>(VerifyResourceAccessAction.INSTANCE, TransportVerifyResourceAccessAction.class) - ) - ); - } } return actions; } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 53ce446881..01deb71d66 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -105,17 +105,7 @@ public Future getAccessibleResourceIdsForCurrentUser(String resourceIndex, // 2. If the user is admin, simply fetch all resources if (adminDNs.isAdmin(user)) { - loadAllResources(resourceIndex, new ActionListener<>() { - @Override - public void onResponse(Set allResources) { - listener.onResponse(allResources); - } - - @Override - public void onFailure(Exception e) { - listener.onFailure(e); - } - }); + loadAllResources(resourceIndex, ActionListener.wrap(listener::onResponse, listener::onFailure)); return null; } diff --git a/src/main/java/org/opensearch/security/rest/resources/access/RestResourceAccessAction.java b/src/main/java/org/opensearch/security/rest/resources/access/ResourceApiAction.java similarity index 56% rename from src/main/java/org/opensearch/security/rest/resources/access/RestResourceAccessAction.java rename to src/main/java/org/opensearch/security/rest/resources/access/ResourceApiAction.java index 787e92171c..07a29de897 100644 --- a/src/main/java/org/opensearch/security/rest/resources/access/RestResourceAccessAction.java +++ b/src/main/java/org/opensearch/security/rest/resources/access/ResourceApiAction.java @@ -16,38 +16,38 @@ import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.opensearch.client.node.NodeClient; import org.opensearch.common.xcontent.LoggingDeprecationHandler; import org.opensearch.common.xcontent.XContentFactory; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; -import org.opensearch.security.resources.RecipientType; -import org.opensearch.security.resources.RecipientTypeRegistry; -import org.opensearch.security.resources.ShareWith; -import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesAction; -import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesRequest; -import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessAction; -import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessRequest; -import org.opensearch.security.rest.resources.access.share.ShareResourceAction; -import org.opensearch.security.rest.resources.access.share.ShareResourceRequest; -import org.opensearch.security.rest.resources.access.verify.VerifyResourceAccessAction; -import org.opensearch.security.rest.resources.access.verify.VerifyResourceAccessRequest; +import org.opensearch.security.resources.*; +import org.opensearch.security.spi.resources.Resource; import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; -import static org.opensearch.security.dlic.rest.api.Responses.badRequest; +import static org.opensearch.security.dlic.rest.api.Responses.*; import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_RESOURCE_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; -public class RestResourceAccessAction extends BaseRestHandler { +public class ResourceApiAction extends BaseRestHandler { + private static final Logger LOGGER = LogManager.getLogger(ResourceApiAction.class); - public RestResourceAccessAction() {} + private final ResourceAccessHandler resourceAccessHandler; + + public ResourceApiAction(ResourceAccessHandler resourceAccessHandler) { + this.resourceAccessHandler = resourceAccessHandler; + } @Override public List routes() { @@ -64,18 +64,18 @@ public List routes() { @Override public String getName() { - return "resource_access_action"; + return "resource_api_action"; } @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - consumeParams(request); // to avoid 400s + consumeParams(request); // early consume params to avoid 400s String path = request.path().split(PLUGIN_RESOURCE_ROUTE_PREFIX)[1].split("/")[1]; return switch (path) { - case "list" -> channel -> handleListRequest(request, client, channel); - case "revoke" -> channel -> handleRevokeRequest(request, client, channel); - case "share" -> channel -> handleShareRequest(request, client, channel); - case "verify_access" -> channel -> handleVerifyRequest(request, client, channel); + case "list" -> channel -> handleListResources(request, channel); + case "revoke" -> channel -> handleRevokeResource(request, channel); + case "share" -> channel -> handleShareResource(request, channel); + case "verify_access" -> channel -> handleVerifyRequest(request, channel); default -> channel -> badRequest(channel, "Unknown route: " + path); }; } @@ -84,42 +84,33 @@ private void consumeParams(RestRequest request) { request.param("resourceIndex", ""); } - public void handleListRequest(RestRequest request, NodeClient client, RestChannel channel) { + private void handleListResources(RestRequest request, RestChannel channel) { String resourceIndex = request.param("resourceIndex", ""); - final ListAccessibleResourcesRequest listAccessibleResourcesRequest = new ListAccessibleResourcesRequest(resourceIndex); - client.executeLocally( - ListAccessibleResourcesAction.INSTANCE, - listAccessibleResourcesRequest, - new RestToXContentListener<>(channel) + resourceAccessHandler.getAccessibleResourcesForCurrentUser( + resourceIndex, + ActionListener.wrap(resources -> sendResponse(channel, resources), e -> handleError(channel, e.getMessage(), e)) ); - } - public void handleRevokeRequest(RestRequest request, NodeClient client, RestChannel channel) throws IOException { + private void handleShareResource(RestRequest request, RestChannel channel) throws IOException { Map source; try (XContentParser parser = request.contentParser()) { source = parser.map(); } - String resourceId = (String) source.get("resource_id"); String resourceIndex = (String) source.get("resource_index"); - @SuppressWarnings("unchecked") - Map> revokeSource = (Map>) source.get("entities"); - Map> revoke = revokeSource.entrySet() - .stream() - .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); - @SuppressWarnings("unchecked") - Set scopes = new HashSet<>(source.containsKey("scopes") ? (List) source.get("scopes") : List.of()); - final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest( + + ShareWith shareWith = parseShareWith(source); + resourceAccessHandler.shareWith( resourceId, resourceIndex, - revoke, - scopes + shareWith, + ActionListener.wrap(response -> sendResponse(channel, response), e -> handleError(channel, e.getMessage(), e)) ); - client.executeLocally(RevokeResourceAccessAction.INSTANCE, revokeResourceAccessRequest, new RestToXContentListener<>(channel)); } - public void handleShareRequest(RestRequest request, NodeClient client, RestChannel channel) throws IOException { + @SuppressWarnings("unchecked") + private void handleRevokeResource(RestRequest request, RestChannel channel) throws IOException { Map source; try (XContentParser parser = request.contentParser()) { source = parser.map(); @@ -128,12 +119,21 @@ public void handleShareRequest(RestRequest request, NodeClient client, RestChann String resourceId = (String) source.get("resource_id"); String resourceIndex = (String) source.get("resource_index"); - ShareWith shareWith = parseShareWith(source); - final ShareResourceRequest shareResourceRequest = new ShareResourceRequest(resourceId, resourceIndex, shareWith); - client.executeLocally(ShareResourceAction.INSTANCE, shareResourceRequest, new RestToXContentListener<>(channel)); + Map> revokeSource = (Map>) source.get("entities"); + Map> revoke = revokeSource.entrySet() + .stream() + .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); + Set scopes = new HashSet<>(source.containsKey("scopes") ? (List) source.get("scopes") : List.of()); + resourceAccessHandler.revokeAccess( + resourceId, + resourceIndex, + revoke, + scopes, + ActionListener.wrap(response -> sendResponse(channel, response), e -> handleError(channel, e.getMessage(), e)) + ); } - public void handleVerifyRequest(RestRequest request, NodeClient client, RestChannel channel) throws IOException { + private void handleVerifyRequest(RestRequest request, RestChannel channel) throws IOException { Map source; try (XContentParser parser = request.contentParser()) { source = parser.map(); @@ -143,12 +143,17 @@ public void handleVerifyRequest(RestRequest request, NodeClient client, RestChan String resourceIndex = (String) source.get("resource_index"); String scope = (String) source.get("scope"); - final VerifyResourceAccessRequest verifyResourceAccessRequest = new VerifyResourceAccessRequest(resourceId, resourceIndex, scope); - client.executeLocally(VerifyResourceAccessAction.INSTANCE, verifyResourceAccessRequest, new RestToXContentListener<>(channel)); + resourceAccessHandler.hasPermission( + resourceId, + resourceIndex, + scope, + ActionListener.wrap(response -> sendResponse(channel, response), e -> handleError(channel, e.getMessage(), e)) + ); } + @SuppressWarnings("unchecked") private ShareWith parseShareWith(Map source) throws IOException { - @SuppressWarnings("unchecked") + // Parse request body into ShareWith object Map shareWithMap = (Map) source.get("share_with"); if (shareWithMap == null || shareWithMap.isEmpty()) { throw new IllegalArgumentException("share_with is required and cannot be empty"); @@ -165,4 +170,26 @@ private ShareWith parseShareWith(Map source) throws IOException throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); } } + + @SuppressWarnings("unchecked") + private void sendResponse(RestChannel channel, Object response) throws IOException { + if (response instanceof Set) { + Set resources = (Set) response; + ok(channel, (builder, params) -> builder.startObject().field("resources", resources).endObject()); + } else if (response instanceof ResourceSharing resourceSharing) { + ok(channel, (resourceSharing::toXContent)); + } else if (response instanceof Boolean) { + ok(channel, (builder, params) -> builder.startObject().field("has_permission", String.valueOf(response)).endObject()); + } + } + + private void handleError(RestChannel channel, String message, Exception e) { + LOGGER.error(message, e); + if (message.contains("not authorized")) { + forbidden(channel, message); + } else if (message.contains("no authenticated")) { + unauthorized(channel); + } + channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, message)); + } } diff --git a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesAction.java b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesAction.java deleted file mode 100644 index 3a8aa6ae59..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesAction.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.list; - -import org.opensearch.action.ActionType; - -/** - * Action to list resources - */ -public class ListAccessibleResourcesAction extends ActionType { - - public static final ListAccessibleResourcesAction INSTANCE = new ListAccessibleResourcesAction(); - - public static final String NAME = "cluster:admin/security/resources/list"; - - private ListAccessibleResourcesAction() { - super(NAME, ListAccessibleResourcesResponse::new); - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesRequest.java b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesRequest.java deleted file mode 100644 index 414e25e305..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesRequest.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.list; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; - -/** - * Request object for ListSampleResource transport action - */ -public class ListAccessibleResourcesRequest extends ActionRequest { - - private final String resourceIndex; - - public ListAccessibleResourcesRequest(String resourceIndex) { - this.resourceIndex = resourceIndex; - } - - /** - * Constructor with stream input - * @param in the stream input - * @throws IOException IOException - */ - public ListAccessibleResourcesRequest(final StreamInput in) throws IOException { - this.resourceIndex = in.readString(); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeString(this.resourceIndex); - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - public String getResourceIndex() { - return resourceIndex; - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java b/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java deleted file mode 100644 index 8bb1f0ea02..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/list/ListAccessibleResourcesResponse.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.list; - -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.util.Set; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.security.spi.resources.Resource; - -/** - * Response to a ListAccessibleResourcesRequest - */ -public class ListAccessibleResourcesResponse extends ActionResponse implements ToXContentObject { - private final Set resources; - private final String resourceClass; - - public ListAccessibleResourcesResponse(String resourceClass, Set resources) { - this.resourceClass = resourceClass; - this.resources = resources; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(resourceClass); - out.writeCollection(resources); - } - - public ListAccessibleResourcesResponse(StreamInput in) throws IOException { - this.resourceClass = in.readString(); - this.resources = readResourcesFromStream(in); - } - - private Set readResourcesFromStream(StreamInput in) { - try { - // TODO check if there is a better way to handle this - Class clazz = Class.forName(this.resourceClass); - @SuppressWarnings("unchecked") - Class resourceClass = (Class) clazz; - return in.readSet(i -> { - try { - return resourceClass.getDeclaredConstructor(StreamInput.class).newInstance(i); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new RuntimeException(e); - } - }); - } catch (ClassNotFoundException | IOException e) { - return Set.of(); - } - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("resources", resources); - builder.endObject(); - return builder; - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessAction.java b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessAction.java deleted file mode 100644 index e27ce05a2b..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessAction.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.revoke; - -import org.opensearch.action.ActionType; - -public class RevokeResourceAccessAction extends ActionType { - public static final RevokeResourceAccessAction INSTANCE = new RevokeResourceAccessAction(); - - public static final String NAME = "cluster:admin/security/resources/revoke"; - - private RevokeResourceAccessAction() { - super(NAME, RevokeResourceAccessResponse::new); - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessRequest.java b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessRequest.java deleted file mode 100644 index 355658cf4c..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessRequest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.revoke; - -import java.io.IOException; -import java.util.Map; -import java.util.Set; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.security.resources.RecipientType; - -public class RevokeResourceAccessRequest extends ActionRequest { - - private final String resourceId; - private final String resourceIndex; - private final Map> revokeAccess; - private final Set scopes; - - public RevokeResourceAccessRequest( - String resourceId, - String resourceIndex, - Map> revokeAccess, - Set scopes - ) { - this.resourceId = resourceId; - this.resourceIndex = resourceIndex; - this.revokeAccess = revokeAccess; - this.scopes = scopes; - } - - public RevokeResourceAccessRequest(StreamInput in) throws IOException { - this.resourceId = in.readString(); - this.resourceIndex = in.readString(); - this.revokeAccess = in.readMap(input -> new RecipientType(input.readString()), input -> input.readSet(StreamInput::readString)); - this.scopes = in.readSet(StreamInput::readString); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeString(resourceId); - out.writeString(resourceIndex); - out.writeMap( - revokeAccess, - (streamOutput, recipientType) -> streamOutput.writeString(recipientType.type()), - StreamOutput::writeStringCollection - ); - out.writeStringCollection(scopes); - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - public String getResourceId() { - return resourceId; - } - - public String getResourceIndex() { - return resourceIndex; - } - - public Map> getRevokeAccess() { - return revokeAccess; - } - - public Set getScopes() { - return scopes; - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessResponse.java b/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessResponse.java deleted file mode 100644 index 090dfb54d0..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/revoke/RevokeResourceAccessResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.revoke; - -import java.io.IOException; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; - -public class RevokeResourceAccessResponse extends ActionResponse implements ToXContentObject { - private final String message; - - public RevokeResourceAccessResponse(String message) { - this.message = message; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(message); - } - - public RevokeResourceAccessResponse(final StreamInput in) throws IOException { - message = in.readString(); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("message", message); - builder.endObject(); - return builder; - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceAction.java b/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceAction.java deleted file mode 100644 index a112108bf1..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceAction.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.share; - -import org.opensearch.action.ActionType; - -/** - * Share resource - */ -public class ShareResourceAction extends ActionType { - - public static final ShareResourceAction INSTANCE = new ShareResourceAction(); - - public static final String NAME = "cluster:admin/security/resources/share"; - - private ShareResourceAction() { - super(NAME, ShareResourceResponse::new); - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceRequest.java b/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceRequest.java deleted file mode 100644 index 560e2967ba..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceRequest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.share; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.security.resources.ShareWith; - -public class ShareResourceRequest extends ActionRequest { - - private final String resourceId; - private final String resourceIndex; - private final ShareWith shareWith; - - public ShareResourceRequest(String resourceId, String resourceIndex, ShareWith shareWith) { - this.resourceId = resourceId; - this.resourceIndex = resourceIndex; - this.shareWith = shareWith; - } - - public ShareResourceRequest(StreamInput in) throws IOException { - this.resourceId = in.readString(); - this.resourceIndex = in.readString(); - this.shareWith = in.readNamedWriteable(ShareWith.class); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeString(resourceId); - out.writeString(resourceIndex); - out.writeNamedWriteable(shareWith); - } - - @Override - public ActionRequestValidationException validate() { - - return null; - } - - public String getResourceId() { - return resourceId; - } - - public String getResourceIndex() { - return resourceIndex; - } - - public ShareWith getShareWith() { - return shareWith; - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceResponse.java b/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceResponse.java deleted file mode 100644 index 15b83c8d6f..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/share/ShareResourceResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.share; - -import java.io.IOException; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; - -public class ShareResourceResponse extends ActionResponse implements ToXContentObject { - private final String message; - - public ShareResourceResponse(String message) { - this.message = message; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(message); - } - - public ShareResourceResponse(final StreamInput in) throws IOException { - message = in.readString(); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("message", message); - builder.endObject(); - return builder; - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessAction.java b/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessAction.java deleted file mode 100644 index 1f1f189ee1..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessAction.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.verify; - -import org.opensearch.action.ActionType; - -/** - * Action to verify resource access for current user - */ -public class VerifyResourceAccessAction extends ActionType { - - public static final VerifyResourceAccessAction INSTANCE = new VerifyResourceAccessAction(); - - public static final String NAME = "cluster:admin/security/resources/verify_access"; - - private VerifyResourceAccessAction() { - super(NAME, VerifyResourceAccessResponse::new); - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessRequest.java b/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessRequest.java deleted file mode 100644 index 529db51830..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessRequest.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.verify; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; - -public class VerifyResourceAccessRequest extends ActionRequest { - - private final String resourceId; - - private final String resourceIndex; - - private final String scope; - - /** - * Default constructor - */ - public VerifyResourceAccessRequest(String resourceId, String resourceIndex, String scope) { - this.resourceId = resourceId; - this.resourceIndex = resourceIndex; - this.scope = scope; - } - - /** - * Constructor with stream input - * @param in the stream input - * @throws IOException IOException - */ - public VerifyResourceAccessRequest(final StreamInput in) throws IOException { - this.resourceId = in.readString(); - this.resourceIndex = in.readString(); - this.scope = in.readString(); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeString(resourceId); - out.writeString(resourceIndex); - out.writeString(scope); - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - public String getResourceId() { - return resourceId; - } - - public String getResourceIndex() { - return resourceIndex; - } - - public String getScope() { - return scope; - } -} diff --git a/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessResponse.java b/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessResponse.java deleted file mode 100644 index a7fa7a2de4..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/verify/VerifyResourceAccessResponse.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access.verify; - -import java.io.IOException; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; - -public class VerifyResourceAccessResponse extends ActionResponse implements ToXContentObject { - private final String message; - - /** - * Default constructor - * - * @param message The message - */ - public VerifyResourceAccessResponse(String message) { - this.message = message; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(message); - } - - /** - * Constructor with StreamInput - * - * @param in the stream input - */ - public VerifyResourceAccessResponse(final StreamInput in) throws IOException { - message = in.readString(); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("message", message); - builder.endObject(); - return builder; - } -} diff --git a/src/main/java/org/opensearch/security/transport/resources/access/TransportListAccessibleResourcesAction.java b/src/main/java/org/opensearch/security/transport/resources/access/TransportListAccessibleResourcesAction.java deleted file mode 100644 index 25c727de67..0000000000 --- a/src/main/java/org/opensearch/security/transport/resources/access/TransportListAccessibleResourcesAction.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.transport.resources.access; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.common.inject.Inject; -import org.opensearch.core.action.ActionListener; -import org.opensearch.security.OpenSearchSecurityPlugin; -import org.opensearch.security.resources.ResourceAccessHandler; -import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesAction; -import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesRequest; -import org.opensearch.security.rest.resources.access.list.ListAccessibleResourcesResponse; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -public class TransportListAccessibleResourcesAction extends HandledTransportAction< - ListAccessibleResourcesRequest, - ListAccessibleResourcesResponse> { - private static final Logger log = LogManager.getLogger(TransportListAccessibleResourcesAction.class); - private final ResourceAccessHandler resourceAccessHandler; - - @Inject - public TransportListAccessibleResourcesAction( - TransportService transportService, - ActionFilters actionFilters, - ResourceAccessHandler resourceAccessHandler - ) { - super(ListAccessibleResourcesAction.NAME, transportService, actionFilters, ListAccessibleResourcesRequest::new); - this.resourceAccessHandler = resourceAccessHandler; - } - - @Override - protected void doExecute(Task task, ListAccessibleResourcesRequest request, ActionListener listener) { - try { - resourceAccessHandler.getAccessibleResourcesForCurrentUser(request.getResourceIndex(), ActionListener.wrap(resources -> { - try { - log.info("Successfully fetched accessible resources for current user : {}", resources); - String resourceType = OpenSearchSecurityPlugin.getResourceProviders().get(request.getResourceIndex()).getResourceType(); - listener.onResponse(new ListAccessibleResourcesResponse(resourceType, resources)); - } catch (Exception e) { - log.error("Failed to process accessible resources response", e); - listener.onFailure(e); - } - }, e -> { - log.error("Failed to list accessible resources for current user", e); - listener.onFailure(e); - })); - } catch (Exception e) { - log.error("Failed to initiate accessible resources request", e); - listener.onFailure(e); - } - } -} diff --git a/src/main/java/org/opensearch/security/transport/resources/access/TransportRevokeResourceAccessAction.java b/src/main/java/org/opensearch/security/transport/resources/access/TransportRevokeResourceAccessAction.java deleted file mode 100644 index 97f139780d..0000000000 --- a/src/main/java/org/opensearch/security/transport/resources/access/TransportRevokeResourceAccessAction.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.transport.resources.access; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.common.inject.Inject; -import org.opensearch.core.action.ActionListener; -import org.opensearch.security.resources.ResourceAccessHandler; -import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessAction; -import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessRequest; -import org.opensearch.security.rest.resources.access.revoke.RevokeResourceAccessResponse; -import org.opensearch.security.spi.resources.ResourceSharingException; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -public class TransportRevokeResourceAccessAction extends HandledTransportAction { - private static final Logger log = LogManager.getLogger(TransportRevokeResourceAccessAction.class); - private final ResourceAccessHandler resourceAccessHandler; - - @Inject - public TransportRevokeResourceAccessAction( - TransportService transportService, - ActionFilters actionFilters, - ResourceAccessHandler resourceAccessHandler - ) { - super(RevokeResourceAccessAction.NAME, transportService, actionFilters, RevokeResourceAccessRequest::new); - this.resourceAccessHandler = resourceAccessHandler; - } - - @Override - protected void doExecute(Task task, RevokeResourceAccessRequest request, ActionListener listener) { - try { - this.resourceAccessHandler.revokeAccess( - request.getResourceId(), - request.getResourceIndex(), - request.getRevokeAccess(), - request.getScopes(), - ActionListener.wrap(resourceSharing -> { - if (resourceSharing == null) { - log.error("Failed to revoke access to resource {}", request.getResourceId()); - listener.onFailure(new ResourceSharingException("Failed to revoke access to resource " + request.getResourceId())); - } else { - log.info("Revoked resource access for resource: {} with {}", request.getResourceId(), resourceSharing.toString()); - listener.onResponse( - new RevokeResourceAccessResponse("Resource " + request.getResourceId() + " access revoked successfully.") - ); - } - }, e -> { - log.error("Exception while revoking access to resource {}: {}", request.getResourceId(), e.getMessage(), e); - listener.onFailure(e); - }) - ); - } catch (Exception e) { - listener.onFailure(e); - } - } - -} diff --git a/src/main/java/org/opensearch/security/transport/resources/access/TransportShareResourceAction.java b/src/main/java/org/opensearch/security/transport/resources/access/TransportShareResourceAction.java deleted file mode 100644 index 0de7987dc4..0000000000 --- a/src/main/java/org/opensearch/security/transport/resources/access/TransportShareResourceAction.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.transport.resources.access; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.OpenSearchException; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.common.inject.Inject; -import org.opensearch.core.action.ActionListener; -import org.opensearch.security.resources.ResourceAccessHandler; -import org.opensearch.security.rest.resources.access.share.ShareResourceAction; -import org.opensearch.security.rest.resources.access.share.ShareResourceRequest; -import org.opensearch.security.rest.resources.access.share.ShareResourceResponse; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -public class TransportShareResourceAction extends HandledTransportAction { - private static final Logger log = LogManager.getLogger(TransportShareResourceAction.class); - private final ResourceAccessHandler resourceAccessHandler; - - @Inject - public TransportShareResourceAction( - TransportService transportService, - ActionFilters actionFilters, - ResourceAccessHandler resourceAccessHandler - ) { - super(ShareResourceAction.NAME, transportService, actionFilters, ShareResourceRequest::new); - this.resourceAccessHandler = resourceAccessHandler; - } - - @Override - protected void doExecute(Task task, ShareResourceRequest request, ActionListener listener) { - try { - this.resourceAccessHandler.shareWith( - request.getResourceId(), - request.getResourceIndex(), - request.getShareWith(), - ActionListener.wrap(resourceSharing -> { - if (resourceSharing == null) { - log.error("Failed to share resource {}", request.getResourceId()); - listener.onFailure(new OpenSearchException("Failed to share resource " + request.getResourceId())); - } else { - log.info("Shared resource : {} with {}", request.getResourceId(), resourceSharing.toString()); - listener.onResponse(new ShareResourceResponse("Resource " + request.getResourceId() + " shared successfully.")); - } - }, e -> { - log.error("Error while sharing resource {}: {}", request.getResourceId(), e.getMessage(), e); - listener.onFailure(e); - }) - ); - } catch (Exception e) { - log.error("Exception while trying to share resource {}: {}", request.getResourceId(), e.getMessage(), e); - listener.onFailure(e); - } - } -} diff --git a/src/main/java/org/opensearch/security/transport/resources/access/TransportVerifyResourceAccessAction.java b/src/main/java/org/opensearch/security/transport/resources/access/TransportVerifyResourceAccessAction.java deleted file mode 100644 index 93965f9f0b..0000000000 --- a/src/main/java/org/opensearch/security/transport/resources/access/TransportVerifyResourceAccessAction.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.transport.resources.access; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.client.Client; -import org.opensearch.common.inject.Inject; -import org.opensearch.core.action.ActionListener; -import org.opensearch.security.resources.ResourceAccessHandler; -import org.opensearch.security.rest.resources.access.verify.VerifyResourceAccessAction; -import org.opensearch.security.rest.resources.access.verify.VerifyResourceAccessRequest; -import org.opensearch.security.rest.resources.access.verify.VerifyResourceAccessResponse; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -public class TransportVerifyResourceAccessAction extends HandledTransportAction { - private static final Logger log = LogManager.getLogger(TransportVerifyResourceAccessAction.class); - private final ResourceAccessHandler resourceAccessHandler; - - @Inject - public TransportVerifyResourceAccessAction( - TransportService transportService, - ActionFilters actionFilters, - Client nodeClient, - ResourceAccessHandler resourceAccessHandler - ) { - super(VerifyResourceAccessAction.NAME, transportService, actionFilters, VerifyResourceAccessRequest::new); - this.resourceAccessHandler = resourceAccessHandler; - } - - @Override - protected void doExecute(Task task, VerifyResourceAccessRequest request, ActionListener listener) { - try { - resourceAccessHandler.hasPermission( - request.getResourceId(), - request.getResourceIndex(), - request.getScope(), - new ActionListener<>() { - @Override - public void onResponse(Boolean hasRequestedScopeAccess) { - StringBuilder sb = new StringBuilder(); - sb.append("User "); - sb.append(hasRequestedScopeAccess ? "has" : "does not have"); - sb.append(" requested scope "); - sb.append(request.getScope()); - sb.append(" access to "); - sb.append(request.getResourceId()); - - log.info(sb.toString()); - - listener.onResponse(new VerifyResourceAccessResponse(sb.toString())); - } - - @Override - public void onFailure(Exception e) { - log.info("Failed to check user permissions for resource {}", request.getResourceId(), e); - listener.onFailure(e); - } - } - ); - } catch (Exception e) { - log.info("Failed to check user permissions for resource {}", request.getResourceId(), e); - listener.onFailure(e); - } - } - -} From 6e0a87bf8873e30b910c91ed76170172276f5647 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 19 Jan 2025 02:01:42 -0500 Subject: [PATCH 116/201] Fixes checkStyle errors Signed-off-by: Darshit Chanpura --- .../opensearch/sample/SampleResourcePluginTests.java | 4 +++- .../actions/rest/create/CreateResourceRestAction.java | 1 + .../rest/resources/access/ResourceApiAction.java | 11 +++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index beec8a8c10..9922f5a9c0 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -17,7 +17,9 @@ import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; import static org.opensearch.security.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java index f1805e1820..1975298f3f 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java @@ -57,6 +57,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli } } + @SuppressWarnings("unchecked") private RestChannelConsumer updateResource(Map source, String resourceId, NodeClient client) throws IOException { String name = (String) source.get("name"); String description = source.containsKey("description") ? (String) source.get("description") : null; diff --git a/src/main/java/org/opensearch/security/rest/resources/access/ResourceApiAction.java b/src/main/java/org/opensearch/security/rest/resources/access/ResourceApiAction.java index 07a29de897..eeddda964b 100644 --- a/src/main/java/org/opensearch/security/rest/resources/access/ResourceApiAction.java +++ b/src/main/java/org/opensearch/security/rest/resources/access/ResourceApiAction.java @@ -31,12 +31,19 @@ import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; -import org.opensearch.security.resources.*; +import org.opensearch.security.resources.RecipientType; +import org.opensearch.security.resources.RecipientTypeRegistry; +import org.opensearch.security.resources.ResourceAccessHandler; +import org.opensearch.security.resources.ResourceSharing; +import org.opensearch.security.resources.ShareWith; import org.opensearch.security.spi.resources.Resource; import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; -import static org.opensearch.security.dlic.rest.api.Responses.*; +import static org.opensearch.security.dlic.rest.api.Responses.badRequest; +import static org.opensearch.security.dlic.rest.api.Responses.forbidden; +import static org.opensearch.security.dlic.rest.api.Responses.ok; +import static org.opensearch.security.dlic.rest.api.Responses.unauthorized; import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_RESOURCE_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; From a22313fd1e36906556075db9b393bb972e61100f Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 19 Jan 2025 02:12:05 -0500 Subject: [PATCH 117/201] Fixes artifact and integ-test workflow Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6300c70f89..6c1e05f6eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,18 +111,18 @@ jobs: - name: Checkout security uses: actions/checkout@v4 - - name: Run Integration Tests + - name: Publish SPI to Local Maven uses: gradle/gradle-build-action@v3 with: cache-disabled: true - arguments: | - integrationTest -Dbuild.snapshot=false + arguments: :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false - - name: Publish SPI to Local Maven + - name: Run Integration Tests uses: gradle/gradle-build-action@v3 with: cache-disabled: true - arguments: :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false + arguments: | + integrationTest -Dbuild.snapshot=false - name: Run SampleResourcePlugin Integration Tests uses: gradle/gradle-build-action@v3 @@ -158,6 +158,12 @@ jobs: - name: Checkout security uses: actions/checkout@v4 + - name: Publish SPI to Local Maven + uses: gradle/gradle-build-action@v3 + with: + cache-disabled: true + arguments: :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false + - name: Run Resource Tests uses: gradle/gradle-build-action@v3 with: @@ -284,6 +290,6 @@ jobs: test -s ./build/distributions/opensearch-security-$security_plugin_version.pom && \ test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version.pom - - name: List files in build directory on failure - if: failure() - run: ls -al ./build/distributions/ + - name: List files in build directory on failure + if: failure() + run: ls -al ./build/distributions/ From 194fec3ee1971ecfab642ff41c4befef1f6bf5a5 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 19 Jan 2025 02:24:26 -0500 Subject: [PATCH 118/201] Adds comment to resource access rest class and changes file name Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 4 +- ...ion.java => ResourceAccessRestAction.java} | 61 ++++++++++++++++--- 2 files changed, 56 insertions(+), 9 deletions(-) rename src/main/java/org/opensearch/security/rest/resources/access/{ResourceApiAction.java => ResourceAccessRestAction.java} (80%) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 6b1f435256..30691d47fd 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -190,7 +190,7 @@ import org.opensearch.security.rest.SecurityInfoAction; import org.opensearch.security.rest.SecurityWhoAmIAction; import org.opensearch.security.rest.TenantInfoAction; -import org.opensearch.security.rest.resources.access.ResourceApiAction; +import org.opensearch.security.rest.resources.access.ResourceAccessRestAction; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.setting.OpensearchDynamicSetting; @@ -689,7 +689,7 @@ public List getRestHandlers( ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT )) { - handlers.add(new ResourceApiAction(resourceAccessHandler)); + handlers.add(new ResourceAccessRestAction(resourceAccessHandler)); } log.debug("Added {} rest handler(s)", handlers.size()); } diff --git a/src/main/java/org/opensearch/security/rest/resources/access/ResourceApiAction.java b/src/main/java/org/opensearch/security/rest/resources/access/ResourceAccessRestAction.java similarity index 80% rename from src/main/java/org/opensearch/security/rest/resources/access/ResourceApiAction.java rename to src/main/java/org/opensearch/security/rest/resources/access/ResourceAccessRestAction.java index eeddda964b..c4b26ba949 100644 --- a/src/main/java/org/opensearch/security/rest/resources/access/ResourceApiAction.java +++ b/src/main/java/org/opensearch/security/rest/resources/access/ResourceAccessRestAction.java @@ -47,12 +47,15 @@ import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_RESOURCE_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; -public class ResourceApiAction extends BaseRestHandler { - private static final Logger LOGGER = LogManager.getLogger(ResourceApiAction.class); +/** + * This class handles the REST API for resource access management. + */ +public class ResourceAccessRestAction extends BaseRestHandler { + private static final Logger LOGGER = LogManager.getLogger(ResourceAccessRestAction.class); private final ResourceAccessHandler resourceAccessHandler; - public ResourceApiAction(ResourceAccessHandler resourceAccessHandler) { + public ResourceAccessRestAction(ResourceAccessHandler resourceAccessHandler) { this.resourceAccessHandler = resourceAccessHandler; } @@ -87,10 +90,19 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli }; } + /** + * Consume params early to avoid 400s. + * @param request from which the params must be consumed + */ private void consumeParams(RestRequest request) { request.param("resourceIndex", ""); } + /** + * Handle the list resources request. + * @param request the request to handle + * @param channel the channel to send the response to + */ private void handleListResources(RestRequest request, RestChannel channel) { String resourceIndex = request.param("resourceIndex", ""); resourceAccessHandler.getAccessibleResourcesForCurrentUser( @@ -99,6 +111,12 @@ private void handleListResources(RestRequest request, RestChannel channel) { ); } + /** + * Handle the share resource request. + * @param request the request to handle + * @param channel the channel to send the response to + * @throws IOException if an I/O error occurs + */ private void handleShareResource(RestRequest request, RestChannel channel) throws IOException { Map source; try (XContentParser parser = request.contentParser()) { @@ -116,6 +134,12 @@ private void handleShareResource(RestRequest request, RestChannel channel) throw ); } + /** + * Handle the revoke resource request. + * @param request the request to handle + * @param channel the channel to send the response to + * @throws IOException if an I/O error occurs + */ @SuppressWarnings("unchecked") private void handleRevokeResource(RestRequest request, RestChannel channel) throws IOException { Map source; @@ -140,6 +164,12 @@ private void handleRevokeResource(RestRequest request, RestChannel channel) thro ); } + /** + * Handle the verify request. + * @param request the request to handle + * @param channel the channel to send the response to + * @throws IOException if an I/O error occurs + */ private void handleVerifyRequest(RestRequest request, RestChannel channel) throws IOException { Map source; try (XContentParser parser = request.contentParser()) { @@ -158,9 +188,14 @@ private void handleVerifyRequest(RestRequest request, RestChannel channel) throw ); } + /** + * Parse the share with structure from the request body. + * @param source the request body + * @return the parsed ShareWith object + * @throws IOException if an I/O error occurs + */ @SuppressWarnings("unchecked") private ShareWith parseShareWith(Map source) throws IOException { - // Parse request body into ShareWith object Map shareWithMap = (Map) source.get("share_with"); if (shareWithMap == null || shareWithMap.isEmpty()) { throw new IllegalArgumentException("share_with is required and cannot be empty"); @@ -178,18 +213,30 @@ private ShareWith parseShareWith(Map source) throws IOException } } + /** + * Send the appropriate response to the channel. + * @param channel the channel to send the response to + * @param response the response to send + * @throws IOException if an I/O error occurs + */ @SuppressWarnings("unchecked") private void sendResponse(RestChannel channel, Object response) throws IOException { - if (response instanceof Set) { + if (response instanceof Set) { // list Set resources = (Set) response; ok(channel, (builder, params) -> builder.startObject().field("resources", resources).endObject()); - } else if (response instanceof ResourceSharing resourceSharing) { + } else if (response instanceof ResourceSharing resourceSharing) { // share & revoke ok(channel, (resourceSharing::toXContent)); - } else if (response instanceof Boolean) { + } else if (response instanceof Boolean) { // verify_access ok(channel, (builder, params) -> builder.startObject().field("has_permission", String.valueOf(response)).endObject()); } } + /** + * Handle errors that occur during request processing. + * @param channel the channel to send the error response to + * @param message the error message + * @param e the exception that caused the error + */ private void handleError(RestChannel channel, String message, Exception e) { LOGGER.error(message, e); if (message.contains("not authorized")) { From bf81c46b6652f294e2fe6c37a886875c82e92910 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 19 Jan 2025 02:36:55 -0500 Subject: [PATCH 119/201] Adds : to avoid sample-resource-plugin tests from being triggered unnecessarily Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c1e05f6eb..9cbb5b42fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,7 +122,7 @@ jobs: with: cache-disabled: true arguments: | - integrationTest -Dbuild.snapshot=false + :integrationTest -Dbuild.snapshot=false - name: Run SampleResourcePlugin Integration Tests uses: gradle/gradle-build-action@v3 @@ -169,7 +169,7 @@ jobs: with: cache-disabled: true arguments: | - integrationTest -Dbuild.snapshot=false --tests org.opensearch.security.ResourceFocusedTests + :integrationTest -Dbuild.snapshot=false --tests org.opensearch.security.ResourceFocusedTests backward-compatibility-build: runs-on: ubuntu-latest @@ -249,12 +249,6 @@ jobs: security_plugin_version_only_number=$(echo $security_plugin_version_no_snapshot | cut -d- -f1) test_qualifier=alpha2 - # Export variables to GitHub Environment - echo "SECURITY_PLUGIN_VERSION=$security_plugin_version" >> $GITHUB_ENV - echo "SECURITY_PLUGIN_VERSION_NO_SNAPSHOT=$security_plugin_version_no_snapshot" >> $GITHUB_ENV - echo "SECURITY_PLUGIN_VERSION_ONLY_NUMBER=$security_plugin_version_only_number" >> $GITHUB_ENV - echo "TEST_QUALIFIER=$test_qualifier" >> $GITHUB_ENV - # Debug print versions echo "Versions:" echo $security_plugin_version From 6d5c80fdf847985061a60ed984413f3b9fa791a9 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 19 Jan 2025 18:51:35 -0500 Subject: [PATCH 120/201] Fixes build-artifacts ci Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cbb5b42fb..c8e5202c29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -257,10 +257,10 @@ jobs: echo $test_qualifier # Publish SPI - ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal - ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false - ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier - ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier + ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version.jar + ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_no_snapshot.jar + ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier.jar + ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar # Build artifacts ./gradlew clean assemble && \ @@ -281,8 +281,7 @@ jobs: ./gradlew clean publishPluginZipPublicationToZipStagingRepository && \ test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ - test -s ./build/distributions/opensearch-security-$security_plugin_version.pom && \ - test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version.pom + test -s ./build/distributions/opensearch-security-$security_plugin_version.pom - name: List files in build directory on failure if: failure() From ff56d24e5570beb0fbf42b5727346601f4e82484 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 19 Jan 2025 19:52:44 -0500 Subject: [PATCH 121/201] Updates sample plugin readme Signed-off-by: Darshit Chanpura --- sample-resource-plugin/README.md | 102 +++++-------------------------- 1 file changed, 14 insertions(+), 88 deletions(-) diff --git a/sample-resource-plugin/README.md b/sample-resource-plugin/README.md index ccd73db983..f568544df2 100644 --- a/sample-resource-plugin/README.md +++ b/sample-resource-plugin/README.md @@ -1,14 +1,10 @@ # Resource Sharing and Access Control Plugin -This plugin demonstrates resource sharing and access control functionality, providing APIs to create, manage, and verify access to resources. The plugin enables fine-grained permissions for sharing and accessing resources, making it suitable for systems requiring robust security and collaboration. +This plugin demonstrates resource sharing and access control functionality, providing sample resource APIs and marking it as a resource sharing plugin via resource-sharing-spi. The access control is implemented on Security plugin and will be performed under the hood. ## Features -- Create and delete resources. -- Share resources with specific users, roles and/or backend_roles with specific scope(s). -- Revoke access to shared resources for a list of or all scopes. -- Verify access permissions for a given user within a given scope. -- List all resources accessible to current user. +- Create, update and delete resources. ## API Endpoints @@ -16,7 +12,7 @@ The plugin exposes the following six API endpoints: ### 1. Create Resource - **Endpoint:** `POST /_plugins/sample_resource_sharing/create` -- **Description:** Creates a new resource. Also creates a resource sharing entry if security plugin is enabled. +- **Description:** Creates a new resource. Behind the scenes a resource sharing entry will be created if security plugin is installed and feature is enabled. - **Request Body:** ```json { @@ -29,99 +25,29 @@ The plugin exposes the following six API endpoints: "message": "Resource created successfully." } ``` - -### 2. Delete Resource -- **Endpoint:** `DELETE /_plugins/sample_resource_sharing/{resource_id}` -- **Description:** Deletes a specified resource owned by the requesting user. -- **Response:** +### 2. Update Resource +- **Endpoint:** `POST /_plugins/sample_resource_sharing/update/{resourceId}` +- **Description:** Updates a resource. +- **Request Body:** ```json { - "message": "Resource deleted successfully." + "name": "" } ``` - -### 3. Share Resource -- **Endpoint:** `POST /_plugins/sample_resource_sharing/share` -- **Description:** Shares a resource with specified users or roles with defined scope. -- **Request Body:** - ```json - { - "resource_id" : "{{ADMIN_RESOURCE_ID}}", - "share_with" : { - "SAMPLE_FULL_ACCESS": { - "users": ["test"], - "roles": ["test_role"], - "backend_roles": ["test_backend_role"] - }, - "READ_ONLY": { - "users": ["test"], - "roles": ["test_role"], - "backend_roles": ["test_backend_role"] - }, - "READ_WRITE": { - "users": ["test"], - "roles": ["test_role"], - "backend_roles": ["test_backend_role"] - } - } - } - ``` -- **Response:** - ```json - { - "message": "Resource shared successfully." - } - ``` - -### 4. Revoke Access -- **Endpoint:** `POST /_plugins/sample_resource_sharing/revoke` -- **Description:** Revokes access to a resource for specified users or roles. -- **Request Body:** - ```json - { - "resource_id" : "", - "entities" : { - "users": ["test", "admin"], - "roles": ["test_role", "all_access"], - "backend_roles": ["test_backend_role", "admin"] - }, - "scopes": ["SAMPLE_FULL_ACCESS", "READ_ONLY", "READ_WRITE"] - } - ``` -- **Response:** - ```json - { - "message": "Resource access revoked successfully." - } - ``` - -### 5. Verify Access -- **Endpoint:** `GET /_plugins/sample_resource_sharing/verify_resource_access` -- **Description:** Verifies if a user or role has access to a specific resource with a specific scope. -- **Request Body:** - ```json - { - "resource_id": "", - "scope": "SAMPLE_FULL_ACCESS" - } - ``` - **Response:** ```json { - "message": "User has requested scope SAMPLE_FULL_ACCESS access to " + "message": "Resource updated successfully." } ``` -### 6. List Accessible Resources -- **Endpoint:** `GET /_plugins/sample_resource_sharing/list` -- **Description:** Lists all resources accessible to the requesting user or role. +### 3. Delete Resource +- **Endpoint:** `DELETE /_plugins/sample_resource_sharing/delete/{resource_id}` +- **Description:** Deletes a specified resource owned by the requesting user. - **Response:** ```json { - "resource-ids": [ - "", - "" - ] + "message": "Resource deleted successfully." } ``` @@ -140,7 +66,7 @@ The plugin exposes the following six API endpoints: 3. Build and deploy the plugin: ```bash $ ./gradlew clean build -x test -x integrationTest -x spotbugsIntegrationTest - $ ./bin/opensearch-plugin install file: /sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-3.0.0.0-SNAPSHOT.zip + $ ./bin/opensearch-plugin install file: /sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-.zip ``` ## License From c48a02b8dcf02dd8ed01c63acaac6c063bc0d33e Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 19 Jan 2025 22:19:16 -0500 Subject: [PATCH 122/201] Removes lingering putPersistent calls Signed-off-by: Darshit Chanpura --- .../java/org/opensearch/security/filter/SecurityFilter.java | 1 - .../security/resources/ResourceSharingIndexListener.java | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index df165daecb..6b23fb6b53 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -339,7 +339,6 @@ private void ap log.info("Transport auth in passive mode and no user found. Injecting default user"); user = User.DEFAULT_TRANSPORT_USER; threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); - threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER, user); } else { log.error( "No user found for " diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java index 9b6f7f1832..d32a49c80e 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -19,6 +19,7 @@ import org.opensearch.index.engine.Engine; import org.opensearch.index.shard.IndexingOperationListener; import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; @@ -87,8 +88,9 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re String resourceId = index.id(); - User user = (User) threadPool.getThreadContext().getPersistent(ConfigConstants.OPENDISTRO_SECURITY_USER); - + final UserSubjectImpl userSubject = (UserSubjectImpl) threadPool.getThreadContext() + .getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); + final User user = userSubject.getUser(); try { ResourceSharing sharing = this.resourceSharingIndexHandler.indexResourceSharing( resourceId, From 47340d3eb288fcc863d3eca7ede591c90a094cf1 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 27 Jan 2025 20:13:05 +0530 Subject: [PATCH 123/201] Adds dls tests Signed-off-by: Darshit Chanpura --- .../SecurityFlsDlsIndexSearcherWrapper.java | 2 +- .../resources/ResourceAccessHandler.java | 6 - .../ResourceSharingIndexListener.java | 2 + .../dlic/dlsfls/DlsResourceSharingTest.java | 129 ++++++++++++++++++ src/test/resources/dlsfls/internal_users.yml | 14 ++ src/test/resources/dlsfls/roles.yml | 13 ++ src/test/resources/dlsfls/roles_mapping.yml | 4 + 7 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 src/test/java/org/opensearch/security/dlic/dlsfls/DlsResourceSharingTest.java diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java index 9a05f4f50b..b54a9412e0 100644 --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java @@ -128,7 +128,7 @@ protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdm final String indexName = (shardId != null) ? shardId.getIndexName() : null; if (log.isTraceEnabled()) { - log.trace("dlsFlsWrap(); index: {}; isAdmin: {}", indexName, isAdmin); + log.trace("dlsFlsWrap(); index: {}; privilegeEvaluationContext: {}", index.getName(), privilegesEvaluationContext); } // 1. If user is admin, or we have no shard/index info, just wrap with default logic (no doc-level restriction). diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 01deb71d66..5dfd75ee9d 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -617,12 +617,6 @@ public Query createResourceDLSQuery(Set resourceIds, QueryShardContext q public DlsRestriction createResourceDLSRestriction(Set resourceIds, NamedXContentRegistry xContentRegistry) throws JsonProcessingException, PrivilegesConfigurationValidationException { - // resourceIds.isEmpty() is true when user doesn't have access to any resources - if (resourceIds.isEmpty()) { - LOGGER.info("No resources found for user. Enforcing full restriction."); - return DlsRestriction.FULL; - } - String jsonQuery = String.format( "{ \"bool\": { \"filter\": [ { \"terms\": { \"_id\": %s } } ] } }", DefaultObjectMapper.writeValueAsString(resourceIds, true) diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java index d32a49c80e..00531f49c1 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -9,6 +9,7 @@ package org.opensearch.security.resources; import java.io.IOException; +import java.util.Objects; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -92,6 +93,7 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re .getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); final User user = userSubject.getUser(); try { + Objects.requireNonNull(user); ResourceSharing sharing = this.resourceSharingIndexHandler.indexResourceSharing( resourceId, resourceIndex, diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsResourceSharingTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsResourceSharingTest.java new file mode 100644 index 0000000000..ecd077af8e --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsResourceSharingTest.java @@ -0,0 +1,129 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.dlic.dlsfls; + +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.junit.Test; + +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.support.WriteRequest.RefreshPolicy; +import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.security.OpenSearchSecurityPlugin; +import org.opensearch.security.resources.ResourceSharingConstants; +import org.opensearch.security.spi.resources.ResourceAccessScope; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + * These tests are flaky for some reason, but pass on retries all the time + */ +public class DlsResourceSharingTest extends AbstractDlsFlsTest { + + @Override + protected void populateData(Client tc) { + + tc.index( + new IndexRequest("resources").id("0").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"name\": \"A\"}", XContentType.JSON) + ).actionGet(); + tc.index( + new IndexRequest("resources").id("1").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"name\": \"B\"}", XContentType.JSON) + ).actionGet(); + + // create a resource-sharing entry + tc.index( + new IndexRequest(ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX).id("0") + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(jsonPayload("0", "share_user"), XContentType.JSON) + ).actionGet(); + tc.index( + new IndexRequest(ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX).id("1") + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .source(jsonPayload("1", "non_share_user"), XContentType.JSON) + ).actionGet(); + + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + } + tc.search(new SearchRequest().indices(".opendistro_security")).actionGet(); + tc.search(new SearchRequest().indices(ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX)).actionGet(); + tc.search(new SearchRequest().indices("resources")).actionGet(); + + OpenSearchSecurityPlugin.getResourceIndicesMutable().add("resources"); + } + + private String jsonPayload(String resourceId, String shareWithUser) { + ; + + return String.format( + "{" + + " \"source_idx\": \"resources\"," + + " \"resource_id\": \"%s\"," + + " \"created_by\": {" + + " \"user\": \"admin\"" + + " }," + + "\"share_with\":{" + + "\"" + + ResourceAccessScope.PUBLIC + + "\":{" + + "\"users\": [\"%s\"]" + + "}" + + "}" + + "}", + resourceId, + shareWithUser + ); + } + + @Test + public void testDLSForResourceSharingWithShareUser() throws Exception { + final Settings settings = Settings.builder().put(ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, true).build(); + setup(settings); + + HttpResponse res; + + // Verify that share_user can see exactly 1 document in the resources index + // and that it is the one with name "A" (doc _id=0) + res = rh.executeGetRequest("/resources/_search?pretty&size=10", encodeBasicHeader("share_user", "password")); + assertThat(res.getStatusCode(), is(HttpStatus.SC_OK)); + // Should see exactly 1 hit + Assert.assertTrue("share_user should see only 1 document", res.getBody().contains("\"value\" : 1")); + // That document should be "A" + Assert.assertTrue("share_user should see 'A'", res.getBody().contains("\"name\" : \"A\"")); + // Should NOT see "B" + Assert.assertFalse("share_user should NOT see 'B'", res.getBody().contains("\"name\" : \"B\"")); + } + + @Test + public void testNonDls() throws Exception { + setup(); + + HttpResponse res; + + // Verify that share_user can see both documents + res = rh.executeGetRequest("/resources/_search?pretty&size=10", encodeBasicHeader("share_user", "password")); + assertThat(res.getStatusCode(), is(HttpStatus.SC_OK)); + // Should see exactly 2 hit + Assert.assertTrue("share_user should see 2 documents", res.getBody().contains("\"value\" : 2")); + Assert.assertTrue("share_user should see 'A'", res.getBody().contains("\"name\" : \"A\"")); + Assert.assertTrue("share_user should see 'B'", res.getBody().contains("\"name\" : \"B\"")); + } + +} diff --git a/src/test/resources/dlsfls/internal_users.yml b/src/test/resources/dlsfls/internal_users.yml index c3347c103f..6bb82bd993 100644 --- a/src/test/resources/dlsfls/internal_users.yml +++ b/src/test/resources/dlsfls/internal_users.yml @@ -179,3 +179,17 @@ date_math: fls_exists: #password hash: $2a$12$YCBrpxYyFusK609FurY5Ee3BlmuzWw0qHwpwqEyNhM2.XnQY3Bxpe +share_user: + hash: "$2a$12$YCBrpxYyFusK609FurY5Ee3BlmuzWw0qHwpwqEyNhM2.XnQY3Bxpe" + reserved: false + hidden: false + backend_roles: [] + attributes: {} + description: "Migrated from v6" +non_share_user: + hash: "$2a$12$YCBrpxYyFusK609FurY5Ee3BlmuzWw0qHwpwqEyNhM2.XnQY3Bxpe" + reserved: false + hidden: false + backend_roles: [] + attributes: {} + description: "Migrated from v6" diff --git a/src/test/resources/dlsfls/roles.yml b/src/test/resources/dlsfls/roles.yml index 185116e2bb..5dcc0fd55a 100644 --- a/src/test/resources/dlsfls/roles.yml +++ b/src/test/resources/dlsfls/roles.yml @@ -2491,3 +2491,16 @@ terms_index_with_dls: masked_fields: null allowed_actions: - "OPENDISTRO_SECURITY_READ" + +opendistro_security_resources_access: + reserved: false + hidden: false + description: "Migrated from v6 (all types mapped)" + cluster_permissions: + - "*" + index_permissions: + - index_patterns: + - "resources*" + allowed_actions: + - "*" + tenant_permissions: [] diff --git a/src/test/resources/dlsfls/roles_mapping.yml b/src/test/resources/dlsfls/roles_mapping.yml index a37299908d..b9f26ad6fa 100644 --- a/src/test/resources/dlsfls/roles_mapping.yml +++ b/src/test/resources/dlsfls/roles_mapping.yml @@ -251,3 +251,7 @@ logs_index_with_dls: terms_index_with_dls: users: - dept_manager + +opendistro_security_resources_access: + users: + - share_user From 24a8727ed362cf2774135a213b34c946f53b3515 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 27 Jan 2025 20:34:37 +0530 Subject: [PATCH 124/201] Bump test-retry dep for sample plugin Signed-off-by: Darshit Chanpura --- sample-resource-plugin/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle index e3c057cf3f..af64315511 100644 --- a/sample-resource-plugin/build.gradle +++ b/sample-resource-plugin/build.gradle @@ -4,7 +4,7 @@ */ plugins { - id "org.gradle.test-retry" version "1.6.0" + id "org.gradle.test-retry" version "1.6.1" } apply plugin: 'opensearch.opensearchplugin' apply plugin: 'opensearch.testclusters' From 0caf9a2d766abda2879e9ab309524d3e0cceab91 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 18 Feb 2025 13:25:44 -0500 Subject: [PATCH 125/201] Removes test-retry version dep for sample plugin and refactors client import to conform to changes in core Signed-off-by: Darshit Chanpura --- sample-resource-plugin/build.gradle | 2 +- .../main/java/org/opensearch/sample/SampleResourcePlugin.java | 2 +- .../resource/actions/rest/create/CreateResourceRestAction.java | 2 +- .../resource/actions/rest/delete/DeleteResourceRestAction.java | 2 +- .../actions/transport/CreateResourceTransportAction.java | 2 +- .../actions/transport/DeleteResourceTransportAction.java | 2 +- .../actions/transport/UpdateResourceTransportAction.java | 2 +- .../security/resources/ResourceSharingIndexHandler.java | 2 +- .../security/resources/ResourceSharingIndexListener.java | 2 +- .../rest/resources/access/ResourceAccessRestAction.java | 2 +- .../opensearch/security/dlic/dlsfls/DlsResourceSharingTest.java | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle index af64315511..4818b3c3ce 100644 --- a/sample-resource-plugin/build.gradle +++ b/sample-resource-plugin/build.gradle @@ -4,7 +4,7 @@ */ plugins { - id "org.gradle.test-retry" version "1.6.1" + id "org.gradle.test-retry" } apply plugin: 'opensearch.opensearchplugin' apply plugin: 'opensearch.testclusters' diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index 6d386b85cb..70472f0b6d 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -17,7 +17,6 @@ import org.apache.logging.log4j.Logger; import org.opensearch.action.ActionRequest; -import org.opensearch.client.Client; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.node.DiscoveryNodes; import org.opensearch.cluster.service.ClusterService; @@ -49,6 +48,7 @@ import org.opensearch.security.spi.resources.ResourceParser; import org.opensearch.security.spi.resources.ResourceSharingExtension; import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.client.Client; import org.opensearch.watcher.ResourceWatcherService; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java index 1975298f3f..370d39e50f 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java @@ -12,12 +12,12 @@ import java.util.List; import java.util.Map; -import org.opensearch.client.node.NodeClient; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.RestRequest; import org.opensearch.rest.action.RestToXContentListener; import org.opensearch.sample.SampleResource; +import org.opensearch.transport.client.node.NodeClient; import static org.opensearch.rest.RestRequest.Method.POST; import static org.opensearch.rest.RestRequest.Method.PUT; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java index 699b5e0303..df53f54bd1 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java @@ -10,11 +10,11 @@ import java.util.List; -import org.opensearch.client.node.NodeClient; import org.opensearch.core.common.Strings; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.RestRequest; import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.transport.client.node.NodeClient; import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.DELETE; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java index 21c994f7fa..786588eff1 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java @@ -17,7 +17,6 @@ import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.action.support.WriteRequest; -import org.opensearch.client.Client; import org.opensearch.common.inject.Inject; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; @@ -29,6 +28,7 @@ import org.opensearch.security.spi.resources.Resource; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; +import org.opensearch.transport.client.Client; import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java index 4ce8954bfe..39265d49cd 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java @@ -18,7 +18,6 @@ import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.action.support.WriteRequest; -import org.opensearch.client.Client; import org.opensearch.common.inject.Inject; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; @@ -27,6 +26,7 @@ import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceResponse; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; +import org.opensearch.transport.client.Client; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java index 9dda0f4e4b..1275f12b91 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java @@ -17,7 +17,6 @@ import org.opensearch.action.support.HandledTransportAction; import org.opensearch.action.support.WriteRequest; import org.opensearch.action.update.UpdateRequest; -import org.opensearch.client.Client; import org.opensearch.common.inject.Inject; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; @@ -29,6 +28,7 @@ import org.opensearch.security.spi.resources.Resource; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; +import org.opensearch.transport.client.Client; import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 4ba251370a..7f6a753d38 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -37,7 +37,6 @@ import org.opensearch.action.search.SearchResponse; import org.opensearch.action.search.SearchScrollRequest; import org.opensearch.action.support.WriteRequest; -import org.opensearch.client.Client; import org.opensearch.common.unit.TimeValue; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.common.xcontent.LoggingDeprecationHandler; @@ -71,6 +70,7 @@ import org.opensearch.security.spi.resources.ResourceParser; import org.opensearch.security.spi.resources.ResourceSharingException; import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.client.Client; import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java index 00531f49c1..eb0447e7b4 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java @@ -14,7 +14,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.client.Client; import org.opensearch.core.action.ActionListener; import org.opensearch.core.index.shard.ShardId; import org.opensearch.index.engine.Engine; @@ -24,6 +23,7 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.client.Client; /** * This class implements an index operation listener for operations performed on resources stored in plugin's indices diff --git a/src/main/java/org/opensearch/security/rest/resources/access/ResourceAccessRestAction.java b/src/main/java/org/opensearch/security/rest/resources/access/ResourceAccessRestAction.java index c4b26ba949..ecc7d8fbc9 100644 --- a/src/main/java/org/opensearch/security/rest/resources/access/ResourceAccessRestAction.java +++ b/src/main/java/org/opensearch/security/rest/resources/access/ResourceAccessRestAction.java @@ -19,7 +19,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.client.node.NodeClient; import org.opensearch.common.xcontent.LoggingDeprecationHandler; import org.opensearch.common.xcontent.XContentFactory; import org.opensearch.common.xcontent.XContentType; @@ -37,6 +36,7 @@ import org.opensearch.security.resources.ResourceSharing; import org.opensearch.security.resources.ShareWith; import org.opensearch.security.spi.resources.Resource; +import org.opensearch.transport.client.node.NodeClient; import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsResourceSharingTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsResourceSharingTest.java index ecd077af8e..8e59ef2898 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsResourceSharingTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsResourceSharingTest.java @@ -18,7 +18,6 @@ import org.opensearch.action.index.IndexRequest; import org.opensearch.action.search.SearchRequest; import org.opensearch.action.support.WriteRequest.RefreshPolicy; -import org.opensearch.client.Client; import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; import org.opensearch.security.OpenSearchSecurityPlugin; @@ -26,6 +25,7 @@ import org.opensearch.security.spi.resources.ResourceAccessScope; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; +import org.opensearch.transport.client.Client; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; From a7abb9329457d33ccf588bea86e226be2c924488 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 18 Feb 2025 16:24:25 -0500 Subject: [PATCH 126/201] Changes version on sample plugin and spi Signed-off-by: Darshit Chanpura --- sample-resource-plugin/build.gradle | 2 +- spi/build.gradle | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle index 4818b3c3ce..221456a842 100644 --- a/sample-resource-plugin/build.gradle +++ b/sample-resource-plugin/build.gradle @@ -36,7 +36,7 @@ ext { projectSubstitutions = [:] licenseFile = rootProject.file('LICENSE.txt') noticeFile = rootProject.file('NOTICE.txt') - opensearch_version = System.getProperty("opensearch.version", "3.0.0-SNAPSHOT") + opensearch_version = System.getProperty("opensearch.version", "3.0.0-alpha1-SNAPSHOT") isSnapshot = "true" == System.getProperty("build.snapshot", "true") buildVersionQualifier = System.getProperty("build.version_qualifier", "") diff --git a/spi/build.gradle b/spi/build.gradle index f23861c448..ee79bc0785 100644 --- a/spi/build.gradle +++ b/spi/build.gradle @@ -9,7 +9,7 @@ plugins { } ext { - opensearch_version = System.getProperty("opensearch.version", "3.0.0-SNAPSHOT") + opensearch_version = System.getProperty("opensearch.version", "3.0.0-alpha1-SNAPSHOT") } repositories { @@ -23,8 +23,8 @@ dependencies { } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } task sourcesJar(type: Jar) { From 0e7f96ba73424788b8c84d44230ddf2a89eb7628 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 20 Feb 2025 11:16:03 -0500 Subject: [PATCH 127/201] Mark resource-sharing index as hidden Signed-off-by: Darshit Chanpura --- .../org/opensearch/security/SearchOperationTest.java | 2 -- .../security/resources/ResourceSharingIndexHandler.java | 9 ++++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java b/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java index 28ab6c9026..e8e15d1910 100644 --- a/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java +++ b/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java @@ -143,7 +143,6 @@ import static org.opensearch.security.Song.TITLE_POISON; import static org.opensearch.security.Song.TITLE_SONG_1_PLUS_1; import static org.opensearch.security.auditlog.impl.AuditCategory.INDEX_EVENT; -import static org.opensearch.security.support.ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; import static org.opensearch.test.framework.audit.AuditMessagePredicate.auditPredicate; @@ -384,7 +383,6 @@ public class SearchOperationTest { new AuditConfiguration(true).compliance(new AuditCompliance().enabled(true)) .filters(new AuditFilters().enabledRest(true).enabledTransport(true)) ) - .nodeSettings(Map.of(OPENSEARCH_RESOURCE_SHARING_ENABLED, false)) .build(); @Rule diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 7f6a753d38..0ac9c664f5 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -97,7 +97,14 @@ public ResourceSharingIndexHandler(final String indexName, final Client client, this.auditLog = auditLog; } - public final static Map INDEX_SETTINGS = Map.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); + public final static Map INDEX_SETTINGS = Map.of( + "index.number_of_shards", + 1, + "index.auto_expand_replicas", + "0-all", + "index.hidden", + "true" + ); /** * Creates the resource sharing index if it doesn't already exist. From d2a02b97614370c31c65cdc5efe918ef342c2d4e Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 20 Feb 2025 11:36:23 -0500 Subject: [PATCH 128/201] Removes DLS related changes that were introduced in this PR Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 6 +- .../configuration/DlsFlsValveImpl.java | 140 ++++++------------ .../SecurityFlsDlsIndexSearcherWrapper.java | 128 ++-------------- .../privileges/dlsfls/DlsRestriction.java | 2 +- .../privileges/dlsfls/DocumentPrivileges.java | 6 +- .../resources/ResourceAccessHandler.java | 49 ------ .../dlic/dlsfls/DlsResourceSharingTest.java | 129 ---------------- src/test/resources/dlsfls/internal_users.yml | 14 -- src/test/resources/dlsfls/roles.yml | 13 -- src/test/resources/dlsfls/roles_mapping.yml | 4 - 10 files changed, 68 insertions(+), 423 deletions(-) delete mode 100644 src/test/java/org/opensearch/security/dlic/dlsfls/DlsResourceSharingTest.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index bf3d19fc06..e253f0afd6 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -743,8 +743,7 @@ public void onIndexModule(IndexModule indexModule) { ciol, evaluator, dlsFlsValve::getCurrentConfig, - dlsFlsBaseContext, - resourceAccessHandler + dlsFlsBaseContext ) ); @@ -1194,8 +1193,7 @@ public Collection createComponents( resolver, xContentRegistry, threadPool, - dlsFlsBaseContext, - resourceAccessHandler + dlsFlsBaseContext ); cr.subscribeOnChange(configMap -> { ((DlsFlsValveImpl) dlsFlsValve).updateConfiguration(cr.getConfiguration(CType.ROLES)); }); } diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 7496c9fe73..74c5b4d3be 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -17,7 +17,6 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.StreamSupport; @@ -77,7 +76,6 @@ import org.opensearch.security.privileges.dlsfls.FieldMasking; import org.opensearch.security.privileges.dlsfls.IndexToRuleMap; import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.resources.ResourceAccessHandler; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; @@ -101,8 +99,6 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve { private final AtomicReference dlsFlsProcessedConfig = new AtomicReference<>(); private final FieldMasking.Config fieldMaskingConfig; private final Settings settings; - private final ResourceAccessHandler resourceAccessHandler; - private final boolean isResourceSharingEnabled; public DlsFlsValveImpl( Settings settings, @@ -111,8 +107,7 @@ public DlsFlsValveImpl( IndexNameExpressionResolver resolver, NamedXContentRegistry namedXContentRegistry, ThreadPool threadPool, - DlsFlsBaseContext dlsFlsBaseContext, - ResourceAccessHandler resourceAccessHandler + DlsFlsBaseContext dlsFlsBaseContext ) { super(); this.nodeClient = nodeClient; @@ -124,11 +119,6 @@ public DlsFlsValveImpl( this.fieldMaskingConfig = FieldMasking.Config.fromSettings(settings); this.dlsFlsBaseContext = dlsFlsBaseContext; this.settings = settings; - this.resourceAccessHandler = resourceAccessHandler; - this.isResourceSharingEnabled = settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - ); clusterService.addListener(event -> { DlsFlsProcessedConfig config = dlsFlsProcessedConfig.get(); @@ -363,7 +353,6 @@ public void handleSearchContext(SearchContext searchContext, ThreadPool threadPo try { String index = searchContext.indexShard().indexSettings().getIndex().getName(); - assert !Strings.isNullOrEmpty(index); if (log.isTraceEnabled()) { log.trace("handleSearchContext(); index: {}", index); } @@ -389,93 +378,57 @@ public void handleSearchContext(SearchContext searchContext, ThreadPool threadPo } PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); - if (privilegesEvaluationContext == null) { return; } DlsFlsProcessedConfig config = this.dlsFlsProcessedConfig.get(); - if (this.isResourceSharingEnabled && OpenSearchSecurityPlugin.getResourceIndices().contains(index)) { - CountDownLatch latch = new CountDownLatch(1); - - threadPool.generic() - .submit( - () -> this.resourceAccessHandler.getAccessibleResourceIdsForCurrentUser(index, ActionListener.wrap(resourceIds -> { - try { - log.info("Creating a DLS restriction for resource IDs: {}", resourceIds); - // Create a DLS restriction and apply it - DlsRestriction dlsRestriction = this.resourceAccessHandler.createResourceDLSRestriction( - resourceIds, - namedXContentRegistry - ); - applyDlsRestrictionToSearchContext(dlsRestriction, index, searchContext, mode); - } catch (Exception e) { - log.error("Error while creating or applying DLS restriction for index '{}': {}", index, e.getMessage()); - applyDlsRestrictionToSearchContext(DlsRestriction.FULL, index, searchContext, mode); - } finally { - latch.countDown(); // Release the latch - } - }, exception -> { - log.error("Failed to fetch resource IDs for index '{}': {}", index, exception.getMessage()); - // Apply a default restriction on failure - applyDlsRestrictionToSearchContext(DlsRestriction.FULL, index, searchContext, mode); - latch.countDown(); - })) - ); + DlsRestriction dlsRestriction = config.getDocumentPrivileges().getRestriction(privilegesEvaluationContext, index); - } else { - // Synchronous path for non-resource-sharing-enabled cases - DlsRestriction dlsRestriction = config.getDocumentPrivileges().getRestriction(privilegesEvaluationContext, index); - applyDlsRestrictionToSearchContext(dlsRestriction, index, searchContext, mode); + if (log.isTraceEnabled()) { + log.trace("handleSearchContext(); index: {}; dlsRestriction: {}", index, dlsRestriction); } - } catch (Exception e) { - log.error("Error in handleSearchContext()", e); - throw new RuntimeException("Error evaluating dls for a search query: " + e, e); - } - } - - private void applyDlsRestrictionToSearchContext(DlsRestriction dlsRestriction, String index, SearchContext searchContext, Mode mode) { - if (log.isTraceEnabled()) { - log.trace("handleSearchContext(); index: {}; dlsRestriction: {}", index, dlsRestriction); - } + DocumentAllowList documentAllowList = DocumentAllowList.get(threadContext); - DocumentAllowList documentAllowList = DocumentAllowList.get(threadContext); + if (documentAllowList.isEntryForIndexPresent(index)) { + // The documentAllowList is needed for two cases: + // - DLS rules which use "term lookup queries" and thus need to access indices for which no privileges are present + // - Dashboards multi tenancy which can redirect index accesses to indices for which no normal index privileges are present - if (documentAllowList.isEntryForIndexPresent(index)) { - // The documentAllowList is needed for two cases: - // - DLS rules which use "term lookup queries" and thus need to access indices for which no privileges are present - // - Dashboards multi tenancy which can redirect index accesses to indices for which no normal index privileges are present - - if (!dlsRestriction.isUnrestricted() && documentAllowList.isAllowed(index, "*")) { - dlsRestriction = DlsRestriction.NONE; - log.debug("Lifting DLS for {} due to present document allowlist", index); + if (!dlsRestriction.isUnrestricted() && documentAllowList.isAllowed(index, "*")) { + dlsRestriction = DlsRestriction.NONE; + log.debug("Lifting DLS for {} due to present document allowlist", index); + } } - } - if (!dlsRestriction.isUnrestricted()) { - if (mode == Mode.ADAPTIVE && dlsRestriction.containsTermLookupQuery()) { - // Special case for scroll operations: - // Normally, the check dlsFlsBaseContext.isDlsDoneOnFilterLevel() already aborts early if DLS filter level mode - // has been activated. However, this is not the case for scroll operations, as these lose the thread context value - // on which dlsFlsBaseContext.isDlsDoneOnFilterLevel() is based on. Thus, we need to check here again the deeper - // conditions. - log.trace("DlsRestriction: contains TLQ."); - return; - } + if (!dlsRestriction.isUnrestricted()) { + if (mode == Mode.ADAPTIVE && dlsRestriction.containsTermLookupQuery()) { + // Special case for scroll operations: + // Normally, the check dlsFlsBaseContext.isDlsDoneOnFilterLevel() already aborts early if DLS filter level mode + // has been activated. However, this is not the case for scroll operations, as these lose the thread context value + // on which dlsFlsBaseContext.isDlsDoneOnFilterLevel() is based on. Thus, we need to check here again the deeper + // conditions. + log.trace("DlsRestriction: contains TLQ."); + return; + } - assert searchContext.parsedQuery() != null; + assert searchContext.parsedQuery() != null; - BooleanQuery.Builder queryBuilder = dlsRestriction.toBooleanQueryBuilder( - searchContext.getQueryShardContext(), - (q) -> new ConstantScoreQuery(q) - ); + BooleanQuery.Builder queryBuilder = dlsRestriction.toBooleanQueryBuilder( + searchContext.getQueryShardContext(), + (q) -> new ConstantScoreQuery(q) + ); - queryBuilder.add(searchContext.parsedQuery().query(), Occur.MUST); + queryBuilder.add(searchContext.parsedQuery().query(), Occur.MUST); - searchContext.parsedQuery(new ParsedQuery(queryBuilder.build())); - searchContext.preProcess(true); + searchContext.parsedQuery(new ParsedQuery(queryBuilder.build())); + searchContext.preProcess(true); + } + } catch (Exception e) { + log.error("Error in handleSearchContext()", e); + throw new RuntimeException("Error evaluating dls for a search query: " + e, e); } } @@ -544,7 +497,10 @@ private static InternalAggregation aggregateBuckets(InternalAggregation aggregat return aggregation; } - private static List mergeBuckets(List buckets, Comparator comparator) { + private static List mergeBuckets( + List buckets, + Comparator comparator + ) { if (log.isDebugEnabled()) { log.debug("Merging buckets: {}", buckets.stream().map(b -> b.getKeyAsString()).collect(ImmutableList.toImmutableList())); } @@ -588,12 +544,12 @@ private Mode getDlsModeHeader() { private static class BucketMerger implements Consumer { private Comparator comparator; - private Bucket bucket = null; + private StringTerms.Bucket bucket = null; private int mergeCount; private long mergedDocCount; private long mergedDocCountError; private boolean showDocCountError = true; - private final ImmutableList.Builder builder; + private final ImmutableList.Builder builder; BucketMerger(Comparator comparator, int size) { this.comparator = Objects.requireNonNull(comparator); @@ -605,7 +561,7 @@ private void finalizeBucket() { builder.add(this.bucket); } else { builder.add( - new Bucket( + new StringTerms.Bucket( StringTermsGetter.getTerm(bucket), mergedDocCount, (InternalAggregations) bucket.getAggregations(), @@ -617,7 +573,7 @@ private void finalizeBucket() { } } - private void merge(Bucket bucket) { + private void merge(StringTerms.Bucket bucket) { if (this.bucket != null && (bucket == null || comparator.compare(this.bucket, bucket) != 0)) { finalizeBucket(); this.bucket = null; @@ -628,13 +584,13 @@ private void merge(Bucket bucket) { } } - public List getBuckets() { + public List getBuckets() { merge(null); return builder.build(); } @Override - public void accept(Bucket bucket) { + public void accept(StringTerms.Bucket bucket) { merge(bucket); mergeCount++; mergedDocCount += bucket.getDocCount(); @@ -651,7 +607,7 @@ public void accept(Bucket bucket) { private static class StringTermsGetter { private static final Field REDUCE_ORDER = getField(InternalTerms.class, "reduceOrder"); - private static final Field TERM_BYTES = getField(Bucket.class, "termBytes"); + private static final Field TERM_BYTES = getField(StringTerms.Bucket.class, "termBytes"); private static final Field FORMAT = getField(InternalTerms.Bucket.class, "format"); private StringTermsGetter() {} @@ -695,11 +651,11 @@ public static BucketOrder getReduceOrder(StringTerms stringTerms) { return getFieldValue(REDUCE_ORDER, stringTerms); } - public static BytesRef getTerm(Bucket bucket) { + public static BytesRef getTerm(StringTerms.Bucket bucket) { return getFieldValue(TERM_BYTES, bucket); } - public static DocValueFormat getDocValueFormat(Bucket bucket) { + public static DocValueFormat getDocValueFormat(StringTerms.Bucket bucket) { return getFieldValue(FORMAT, bucket); } } diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java index b54a9412e0..4f7a412097 100644 --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java @@ -16,8 +16,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.LongSupplier; import java.util.function.Supplier; @@ -31,14 +29,11 @@ import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; -import org.opensearch.core.action.ActionListener; -import org.opensearch.core.common.Strings; import org.opensearch.core.index.shard.ShardId; import org.opensearch.index.IndexService; import org.opensearch.index.mapper.SeqNoFieldMapper; import org.opensearch.index.query.QueryShardContext; import org.opensearch.index.shard.ShardUtils; -import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.compliance.ComplianceIndexingOperationListener; import org.opensearch.security.privileges.DocumentAllowList; @@ -50,8 +45,6 @@ import org.opensearch.security.privileges.dlsfls.DlsRestriction; import org.opensearch.security.privileges.dlsfls.FieldMasking; import org.opensearch.security.privileges.dlsfls.FieldPrivileges; -import org.opensearch.security.resources.ResourceAccessHandler; -import org.opensearch.security.spi.resources.ResourceSharingException; import org.opensearch.security.support.ConfigConstants; public class SecurityFlsDlsIndexSearcherWrapper extends SystemIndexSearcherWrapper { @@ -68,8 +61,6 @@ public class SecurityFlsDlsIndexSearcherWrapper extends SystemIndexSearcherWrapp private final LongSupplier nowInMillis; private final Supplier dlsFlsProcessedConfigSupplier; private final DlsFlsBaseContext dlsFlsBaseContext; - private final ResourceAccessHandler resourceAccessHandler; - private final boolean isResourceSharingEnabled; public SecurityFlsDlsIndexSearcherWrapper( final IndexService indexService, @@ -80,8 +71,7 @@ public SecurityFlsDlsIndexSearcherWrapper( final ComplianceIndexingOperationListener ciol, final PrivilegesEvaluator evaluator, final Supplier dlsFlsProcessedConfigSupplier, - final DlsFlsBaseContext dlsFlsBaseContext, - final ResourceAccessHandler resourceAccessHandler + final DlsFlsBaseContext dlsFlsBaseContext ) { super(indexService, settings, adminDNs, evaluator); Set metadataFieldsCopy; @@ -113,125 +103,36 @@ public SecurityFlsDlsIndexSearcherWrapper( log.debug("FLS/DLS {} enabled for index {}", this, indexService.index().getName()); this.dlsFlsProcessedConfigSupplier = dlsFlsProcessedConfigSupplier; this.dlsFlsBaseContext = dlsFlsBaseContext; - this.resourceAccessHandler = resourceAccessHandler; - this.isResourceSharingEnabled = settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - ); } @SuppressWarnings("unchecked") @Override protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdmin) throws IOException { + final ShardId shardId = ShardUtils.extractShardId(reader); PrivilegesEvaluationContext privilegesEvaluationContext = this.dlsFlsBaseContext.getPrivilegesEvaluationContext(); - final String indexName = (shardId != null) ? shardId.getIndexName() : null; if (log.isTraceEnabled()) { log.trace("dlsFlsWrap(); index: {}; privilegeEvaluationContext: {}", index.getName(), privilegesEvaluationContext); } - // 1. If user is admin, or we have no shard/index info, just wrap with default logic (no doc-level restriction). if (isAdmin || privilegesEvaluationContext == null) { - return wrapWithDefaultDlsFls(reader, shardId); - } - - assert !Strings.isNullOrEmpty(indexName); - // 2. If resource sharing is disabled or this is not a resource index, fallback to standard DLS/FLS logic. - if (!this.isResourceSharingEnabled || !OpenSearchSecurityPlugin.getResourceIndices().contains(indexName)) { - return wrapStandardDlsFls(privilegesEvaluationContext, reader, shardId, indexName, isAdmin); + return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader( + reader, + FieldPrivileges.FlsRule.ALLOW_ALL, + null, + indexService, + threadContext, + clusterService, + auditlog, + FieldMasking.FieldMaskingRule.ALLOW_ALL, + shardId, + metaFields + ); } - // TODO see if steps 3,4,5 can be changed to be completely asynchronous - // 3.Since we need DirectoryReader *now*, we'll block the thread using a CountDownLatch until the async call completes. - final AtomicReference> resourceIdsRef = new AtomicReference<>(Collections.emptySet()); - final AtomicReference exceptionRef = new AtomicReference<>(null); - final CountDownLatch latch = new CountDownLatch(1); - - // 4. Perform the async call to fetch resource IDs - this.resourceAccessHandler.getAccessibleResourceIdsForCurrentUser(indexName, ActionListener.wrap(resourceIds -> { - log.debug("Fetched resource IDs for index '{}': {}", indexName, resourceIds); - resourceIdsRef.set(resourceIds); - latch.countDown(); - }, ex -> { - log.error("Failed to fetch resource IDs for index '{}': {}", indexName, ex.getMessage(), ex); - exceptionRef.set(ex); - latch.countDown(); - })); - - // 5. Block until the async call completes try { - latch.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Interrupted while waiting for resource IDs", e); - } - - // 6. Throw any errors - if (exceptionRef.get() != null) { - throw new ResourceSharingException("Failed to get resource IDs for index: " + indexName, exceptionRef.get()); - } - - // 7. If the user has no accessible resources, produce a reader that yields zero documents - final Set resourceIds = resourceIdsRef.get(); - if (resourceIds.isEmpty()) { - log.debug("User has no accessible resources in index '{}'; returning EmptyDirectoryReader.", indexName); - return new EmptyFilterLeafReader.EmptyDirectoryReader(reader); - } - - // 8. Build the resource-based query to restrict docs - final QueryShardContext queryShardContext = this.indexService.newQueryShardContext(shardId.getId(), null, nowInMillis, null); - final Query resourceQuery = this.resourceAccessHandler.createResourceDLSQuery(resourceIds, queryShardContext); - - log.debug("Applying resource-based DLS query for index '{}'", indexName); - // 9. Wrap with a DLS/FLS DirectoryReader that includes doc-level restriction (resourceQuery), - // with FLS (ALLOW_ALL) since we don't need field-level restrictions here. - return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader( - reader, - FieldPrivileges.FlsRule.ALLOW_ALL, - resourceQuery, - indexService, - threadContext, - clusterService, - auditlog, - FieldMasking.FieldMaskingRule.ALLOW_ALL, - shardId, - metaFields - ); - } - - /** - * Wrap the reader with an "ALLOW_ALL" doc-level filter and field privileges, - * i.e., no doc-level or field-level restrictions. - */ - private DirectoryReader wrapWithDefaultDlsFls(DirectoryReader reader, ShardId shardId) throws IOException { - return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader( - reader, - FieldPrivileges.FlsRule.ALLOW_ALL, - null, // no doc-level restriction - indexService, - threadContext, - clusterService, - auditlog, - FieldMasking.FieldMaskingRule.ALLOW_ALL, - shardId, - metaFields - ); - } - - /** - * Fallback to your existing logic to handle DLS/FLS if the index is not a resource index, - * or if other conditions apply (like dlsFlsBaseContext usage, etc.). - */ - private DirectoryReader wrapStandardDlsFls( - PrivilegesEvaluationContext privilegesEvaluationContext, - DirectoryReader reader, - ShardId shardId, - String indexName, - boolean isAdmin - ) throws IOException { - try { DlsFlsProcessedConfig config = this.dlsFlsProcessedConfigSupplier.get(); DlsRestriction dlsRestriction; @@ -302,5 +203,4 @@ private DirectoryReader wrapStandardDlsFls( throw new OpenSearchException("Error while evaluating DLS/FLS", e); } } - } diff --git a/src/main/java/org/opensearch/security/privileges/dlsfls/DlsRestriction.java b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsRestriction.java index 01fccb78e6..242e0000a4 100644 --- a/src/main/java/org/opensearch/security/privileges/dlsfls/DlsRestriction.java +++ b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsRestriction.java @@ -53,7 +53,7 @@ public class DlsRestriction extends AbstractRuleBasedPrivileges.Rule { private final ImmutableList queries; - public DlsRestriction(List queries) { + DlsRestriction(List queries) { this.queries = ImmutableList.copyOf(queries); } diff --git a/src/main/java/org/opensearch/security/privileges/dlsfls/DocumentPrivileges.java b/src/main/java/org/opensearch/security/privileges/dlsfls/DocumentPrivileges.java index 40ebfd7282..2afcdd4b82 100644 --- a/src/main/java/org/opensearch/security/privileges/dlsfls/DocumentPrivileges.java +++ b/src/main/java/org/opensearch/security/privileges/dlsfls/DocumentPrivileges.java @@ -92,7 +92,7 @@ protected DlsRestriction compile(PrivilegesEvaluationContext context, Collection /** * The basic rules of DLS are queries. This class encapsulates single queries. */ - public static abstract class DlsQuery { + static abstract class DlsQuery { final String queryString; DlsQuery(String queryString) { @@ -118,7 +118,7 @@ public boolean equals(Object obj) { return Objects.equals(this.queryString, other.queryString); } - public static QueryBuilder parseQuery(String queryString, NamedXContentRegistry xContentRegistry) + protected QueryBuilder parseQuery(String queryString, NamedXContentRegistry xContentRegistry) throws PrivilegesConfigurationValidationException { try { XContentParser parser = JsonXContent.jsonXContent.createParser( @@ -193,7 +193,7 @@ public static class RenderedDlsQuery { private final QueryBuilder queryBuilder; private final String renderedSource; - public RenderedDlsQuery(QueryBuilder queryBuilder, String renderedSource) { + RenderedDlsQuery(QueryBuilder queryBuilder, String renderedSource) { this.queryBuilder = queryBuilder; this.renderedSource = renderedSource; } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 5dfd75ee9d..b0ac4bb2bc 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -11,35 +11,21 @@ package org.opensearch.security.resources; -import java.io.IOException; import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Future; -import com.fasterxml.jackson.core.JsonProcessingException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.lucene.search.Query; import org.opensearch.action.StepListener; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.index.query.BoolQueryBuilder; -import org.opensearch.index.query.ConstantScoreQueryBuilder; -import org.opensearch.index.query.QueryBuilder; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.index.query.QueryShardContext; -import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.privileges.PrivilegesConfigurationValidationException; -import org.opensearch.security.privileges.dlsfls.DlsRestriction; -import org.opensearch.security.privileges.dlsfls.DocumentPrivileges; import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.ResourceParser; import org.opensearch.security.spi.resources.ResourceSharingException; @@ -591,39 +577,4 @@ private void validateArguments(Object... args) { } } } - - /** - * Creates a DLS query for the given resource IDs. - * @param resourceIds The resource IDs to create the query for. - * @param queryShardContext The query shard context. - * @return The DLS query. - * @throws IOException If an I/O error occurs. - */ - public Query createResourceDLSQuery(Set resourceIds, QueryShardContext queryShardContext) throws IOException { - BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); - boolQueryBuilder.filter(QueryBuilders.termsQuery("_id", resourceIds)); - ConstantScoreQueryBuilder builder = new ConstantScoreQueryBuilder(boolQueryBuilder); - return builder.toQuery(queryShardContext); - } - - /** - * Creates a DLS restriction for the given resource IDs. - * @param resourceIds The resource IDs to create the restriction for. - * @param xContentRegistry The named XContent registry. - * @return The DLS restriction. - * @throws JsonProcessingException If an error occurs while processing JSON. - * @throws PrivilegesConfigurationValidationException If the privileges configuration is invalid. - */ - public DlsRestriction createResourceDLSRestriction(Set resourceIds, NamedXContentRegistry xContentRegistry) - throws JsonProcessingException, PrivilegesConfigurationValidationException { - - String jsonQuery = String.format( - "{ \"bool\": { \"filter\": [ { \"terms\": { \"_id\": %s } } ] } }", - DefaultObjectMapper.writeValueAsString(resourceIds, true) - ); - QueryBuilder queryBuilder = DocumentPrivileges.DlsQuery.parseQuery(jsonQuery, xContentRegistry); - DocumentPrivileges.RenderedDlsQuery renderedDlsQuery = new DocumentPrivileges.RenderedDlsQuery(queryBuilder, jsonQuery); - List documentPrivileges = List.of(renderedDlsQuery); - return new DlsRestriction(documentPrivileges); - } } diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsResourceSharingTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsResourceSharingTest.java deleted file mode 100644 index 8e59ef2898..0000000000 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsResourceSharingTest.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.dlsfls; - -import org.apache.http.HttpStatus; -import org.junit.Assert; -import org.junit.Test; - -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.support.WriteRequest.RefreshPolicy; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.security.OpenSearchSecurityPlugin; -import org.opensearch.security.resources.ResourceSharingConstants; -import org.opensearch.security.spi.resources.ResourceAccessScope; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; -import org.opensearch.transport.client.Client; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -/** - * These tests are flaky for some reason, but pass on retries all the time - */ -public class DlsResourceSharingTest extends AbstractDlsFlsTest { - - @Override - protected void populateData(Client tc) { - - tc.index( - new IndexRequest("resources").id("0").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"name\": \"A\"}", XContentType.JSON) - ).actionGet(); - tc.index( - new IndexRequest("resources").id("1").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"name\": \"B\"}", XContentType.JSON) - ).actionGet(); - - // create a resource-sharing entry - tc.index( - new IndexRequest(ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX).id("0") - .setRefreshPolicy(RefreshPolicy.IMMEDIATE) - .source(jsonPayload("0", "share_user"), XContentType.JSON) - ).actionGet(); - tc.index( - new IndexRequest(ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX).id("1") - .setRefreshPolicy(RefreshPolicy.IMMEDIATE) - .source(jsonPayload("1", "non_share_user"), XContentType.JSON) - ).actionGet(); - - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - } - tc.search(new SearchRequest().indices(".opendistro_security")).actionGet(); - tc.search(new SearchRequest().indices(ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX)).actionGet(); - tc.search(new SearchRequest().indices("resources")).actionGet(); - - OpenSearchSecurityPlugin.getResourceIndicesMutable().add("resources"); - } - - private String jsonPayload(String resourceId, String shareWithUser) { - ; - - return String.format( - "{" - + " \"source_idx\": \"resources\"," - + " \"resource_id\": \"%s\"," - + " \"created_by\": {" - + " \"user\": \"admin\"" - + " }," - + "\"share_with\":{" - + "\"" - + ResourceAccessScope.PUBLIC - + "\":{" - + "\"users\": [\"%s\"]" - + "}" - + "}" - + "}", - resourceId, - shareWithUser - ); - } - - @Test - public void testDLSForResourceSharingWithShareUser() throws Exception { - final Settings settings = Settings.builder().put(ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, true).build(); - setup(settings); - - HttpResponse res; - - // Verify that share_user can see exactly 1 document in the resources index - // and that it is the one with name "A" (doc _id=0) - res = rh.executeGetRequest("/resources/_search?pretty&size=10", encodeBasicHeader("share_user", "password")); - assertThat(res.getStatusCode(), is(HttpStatus.SC_OK)); - // Should see exactly 1 hit - Assert.assertTrue("share_user should see only 1 document", res.getBody().contains("\"value\" : 1")); - // That document should be "A" - Assert.assertTrue("share_user should see 'A'", res.getBody().contains("\"name\" : \"A\"")); - // Should NOT see "B" - Assert.assertFalse("share_user should NOT see 'B'", res.getBody().contains("\"name\" : \"B\"")); - } - - @Test - public void testNonDls() throws Exception { - setup(); - - HttpResponse res; - - // Verify that share_user can see both documents - res = rh.executeGetRequest("/resources/_search?pretty&size=10", encodeBasicHeader("share_user", "password")); - assertThat(res.getStatusCode(), is(HttpStatus.SC_OK)); - // Should see exactly 2 hit - Assert.assertTrue("share_user should see 2 documents", res.getBody().contains("\"value\" : 2")); - Assert.assertTrue("share_user should see 'A'", res.getBody().contains("\"name\" : \"A\"")); - Assert.assertTrue("share_user should see 'B'", res.getBody().contains("\"name\" : \"B\"")); - } - -} diff --git a/src/test/resources/dlsfls/internal_users.yml b/src/test/resources/dlsfls/internal_users.yml index 6bb82bd993..c3347c103f 100644 --- a/src/test/resources/dlsfls/internal_users.yml +++ b/src/test/resources/dlsfls/internal_users.yml @@ -179,17 +179,3 @@ date_math: fls_exists: #password hash: $2a$12$YCBrpxYyFusK609FurY5Ee3BlmuzWw0qHwpwqEyNhM2.XnQY3Bxpe -share_user: - hash: "$2a$12$YCBrpxYyFusK609FurY5Ee3BlmuzWw0qHwpwqEyNhM2.XnQY3Bxpe" - reserved: false - hidden: false - backend_roles: [] - attributes: {} - description: "Migrated from v6" -non_share_user: - hash: "$2a$12$YCBrpxYyFusK609FurY5Ee3BlmuzWw0qHwpwqEyNhM2.XnQY3Bxpe" - reserved: false - hidden: false - backend_roles: [] - attributes: {} - description: "Migrated from v6" diff --git a/src/test/resources/dlsfls/roles.yml b/src/test/resources/dlsfls/roles.yml index 5dcc0fd55a..185116e2bb 100644 --- a/src/test/resources/dlsfls/roles.yml +++ b/src/test/resources/dlsfls/roles.yml @@ -2491,16 +2491,3 @@ terms_index_with_dls: masked_fields: null allowed_actions: - "OPENDISTRO_SECURITY_READ" - -opendistro_security_resources_access: - reserved: false - hidden: false - description: "Migrated from v6 (all types mapped)" - cluster_permissions: - - "*" - index_permissions: - - index_patterns: - - "resources*" - allowed_actions: - - "*" - tenant_permissions: [] diff --git a/src/test/resources/dlsfls/roles_mapping.yml b/src/test/resources/dlsfls/roles_mapping.yml index b9f26ad6fa..a37299908d 100644 --- a/src/test/resources/dlsfls/roles_mapping.yml +++ b/src/test/resources/dlsfls/roles_mapping.yml @@ -251,7 +251,3 @@ logs_index_with_dls: terms_index_with_dls: users: - dept_manager - -opendistro_security_resources_access: - users: - - share_user From 9acc5e4904427ca795d7bbfdf19d87737207c8b0 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 20 Feb 2025 11:58:40 -0500 Subject: [PATCH 129/201] Updates Sample plugin Signed-off-by: Darshit Chanpura --- sample-resource-plugin/build.gradle | 3 +- .../sample/SampleResourcePluginTests.java | 172 +++++++++--------- 2 files changed, 87 insertions(+), 88 deletions(-) diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle index 221456a842..5b4447239e 100644 --- a/sample-resource-plugin/build.gradle +++ b/sample-resource-plugin/build.gradle @@ -38,7 +38,7 @@ ext { noticeFile = rootProject.file('NOTICE.txt') opensearch_version = System.getProperty("opensearch.version", "3.0.0-alpha1-SNAPSHOT") isSnapshot = "true" == System.getProperty("build.snapshot", "true") - buildVersionQualifier = System.getProperty("build.version_qualifier", "") + buildVersionQualifier = System.getProperty("build.version_qualifier", "alpha1") version_tokens = opensearch_version.tokenize('-') opensearch_build = version_tokens[0] + '.0' @@ -51,7 +51,6 @@ ext { } } - repositories { mavenLocal() mavenCentral() diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index 9922f5a9c0..532043019b 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -232,91 +232,91 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { } } - @Test - public void testDLSRestrictionForResourceByDirectlyUpdatingTheResourceIndex() throws Exception { - String resourceId; - // create sample resource - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - String sampleResource = "{\"name\":\"sample\"}"; - HttpResponse response = client.postJson(RESOURCE_INDEX_NAME + "/_doc", sampleResource); - response.assertStatusCode(HttpStatus.SC_CREATED); - - resourceId = response.bodyAsJsonNode().get("_id").asText(); - } - - // Create an entry in resource-sharing index - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually - String json = String.format( - "{" - + " \"source_idx\": \".sample_resource_sharing_plugin\"," - + " \"resource_id\": \"%s\"," - + " \"created_by\": {" - + " \"user\": \"admin\"" - + " }" - + "}", - resourceId - ); - HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); - assertThat(response.getStatusReason(), containsString("Created")); - // Also update the in-memory map and list - OpenSearchSecurityPlugin.getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); - ResourceProvider provider = new ResourceProvider( - SampleResource.class.getCanonicalName(), - RESOURCE_INDEX_NAME, - new SampleResourceParser() - ); - OpenSearchSecurityPlugin.getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); - - Thread.sleep(1000); - } - - // resource is still visible to super-admin - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - - HttpResponse response = client.postJson(RESOURCE_INDEX_NAME + "/_search", "{\"query\" : {\"match_all\" : {}}}"); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(1)); - assertThat(response.getBody(), containsString("sample")); - } - - String updatePayload = "{" + "\"doc\": {" + "\"name\": \"sampleUpdated\"" + "}" + "}"; - - // Update sample resource with shared_with user. This will fail since the resource has not been shared - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse updateResponse = client.postJson(RESOURCE_INDEX_NAME + "/_update/" + resourceId, updatePayload); - // it will show not found since the resource is not visible to shared_with_user - updateResponse.assertStatusCode(HttpStatus.SC_NOT_FOUND); - } - - // Admin is still allowed to update its own resource - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse updateResponse = client.postJson(RESOURCE_INDEX_NAME + "/_update/" + resourceId, updatePayload); - // it will show not found since the resource is not visible to shared_with_user - updateResponse.assertStatusCode(HttpStatus.SC_OK); - assertThat(updateResponse.bodyAsJsonNode().get("_shards").get("successful").asInt(), equalTo(1)); - } - - // Verify that share_with user does not have access to the resource - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse getResponse = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); - // it will show not found since the resource is not visible to shared_with_user - getResponse.assertStatusCode(HttpStatus.SC_NOT_FOUND); - } - - // share the resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload(resourceId)); - response.assertStatusCode(HttpStatus.SC_OK); - } - - // Verify that share_with user now has access to the resource - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse getResponse = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); - // it will show not found since the resource is not visible to shared_with_user - getResponse.assertStatusCode(HttpStatus.SC_OK); - assertThat(getResponse.getBody(), containsString("sampleUpdated")); - } - } +// @Test +// public void testDLSRestrictionForResourceByDirectlyUpdatingTheResourceIndex() throws Exception { +// String resourceId; +// // create sample resource +// try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { +// String sampleResource = "{\"name\":\"sample\"}"; +// HttpResponse response = client.postJson(RESOURCE_INDEX_NAME + "/_doc", sampleResource); +// response.assertStatusCode(HttpStatus.SC_CREATED); +// +// resourceId = response.bodyAsJsonNode().get("_id").asText(); +// } +// +// // Create an entry in resource-sharing index +// try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { +// // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually +// String json = String.format( +// "{" +// + " \"source_idx\": \".sample_resource_sharing_plugin\"," +// + " \"resource_id\": \"%s\"," +// + " \"created_by\": {" +// + " \"user\": \"admin\"" +// + " }" +// + "}", +// resourceId +// ); +// HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); +// assertThat(response.getStatusReason(), containsString("Created")); +// // Also update the in-memory map and list +// OpenSearchSecurityPlugin.getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); +// ResourceProvider provider = new ResourceProvider( +// SampleResource.class.getCanonicalName(), +// RESOURCE_INDEX_NAME, +// new SampleResourceParser() +// ); +// OpenSearchSecurityPlugin.getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); +// +// Thread.sleep(1000); +// } +// +// // resource is still visible to super-admin +// try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { +// +// HttpResponse response = client.postJson(RESOURCE_INDEX_NAME + "/_search", "{\"query\" : {\"match_all\" : {}}}"); +// response.assertStatusCode(HttpStatus.SC_OK); +// assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(1)); +// assertThat(response.getBody(), containsString("sample")); +// } +// +// String updatePayload = "{" + "\"doc\": {" + "\"name\": \"sampleUpdated\"" + "}" + "}"; +// +// // Update sample resource with shared_with user. This will fail since the resource has not been shared +// try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { +// HttpResponse updateResponse = client.postJson(RESOURCE_INDEX_NAME + "/_update/" + resourceId, updatePayload); +// // it will show not found since the resource is not visible to shared_with_user +// updateResponse.assertStatusCode(HttpStatus.SC_NOT_FOUND); +// } +// +// // Admin is still allowed to update its own resource +// try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { +// HttpResponse updateResponse = client.postJson(RESOURCE_INDEX_NAME + "/_update/" + resourceId, updatePayload); +// // it will show not found since the resource is not visible to shared_with_user +// updateResponse.assertStatusCode(HttpStatus.SC_OK); +// assertThat(updateResponse.bodyAsJsonNode().get("_shards").get("successful").asInt(), equalTo(1)); +// } +// +// // Verify that share_with user does not have access to the resource +// try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { +// HttpResponse getResponse = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); +// // it will show not found since the resource is not visible to shared_with_user +// getResponse.assertStatusCode(HttpStatus.SC_NOT_FOUND); +// } +// +// // share the resource with shared_with_user +// try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { +// HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload(resourceId)); +// response.assertStatusCode(HttpStatus.SC_OK); +// } +// +// // Verify that share_with user now has access to the resource +// try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { +// HttpResponse getResponse = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); +// // it will show not found since the resource is not visible to shared_with_user +// getResponse.assertStatusCode(HttpStatus.SC_OK); +// assertThat(getResponse.getBody(), containsString("sampleUpdated")); +// } +// } } From 66c47bff0054f3e6bc56e05d553c43331a50e92d Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 20 Feb 2025 13:50:50 -0500 Subject: [PATCH 130/201] Fixes broken build due to version qualifier change Signed-off-by: Darshit Chanpura --- .github/workflows/maven-publish.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index 2d4e7e1df0..d3a245200b 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -7,6 +7,9 @@ on: - 'main' - '1.*' - '2.*' + pull_request: + branches: + - 'resource-sharing-spi' # temporary addition - should be removed before merge jobs: build-and-publish-snapshots: From 27e7478cded5ab0905f6c1b761b1bae53756e591 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 20 Feb 2025 14:00:35 -0500 Subject: [PATCH 131/201] Reverts temp change to maven publish workflow Signed-off-by: Darshit Chanpura --- .github/workflows/maven-publish.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index d3a245200b..2d4e7e1df0 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -7,9 +7,6 @@ on: - 'main' - '1.*' - '2.*' - pull_request: - branches: - - 'resource-sharing-spi' # temporary addition - should be removed before merge jobs: build-and-publish-snapshots: From c6d736714a3f0946f9b934c794e9fc378857bc9b Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 20 Feb 2025 14:02:26 -0500 Subject: [PATCH 132/201] Removes DLS test from sample plugin Signed-off-by: Darshit Chanpura --- .../sample/SampleResourcePluginTests.java | 87 ------------------- 1 file changed, 87 deletions(-) diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index 532043019b..9478dbab37 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -232,91 +232,4 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { } } -// @Test -// public void testDLSRestrictionForResourceByDirectlyUpdatingTheResourceIndex() throws Exception { -// String resourceId; -// // create sample resource -// try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { -// String sampleResource = "{\"name\":\"sample\"}"; -// HttpResponse response = client.postJson(RESOURCE_INDEX_NAME + "/_doc", sampleResource); -// response.assertStatusCode(HttpStatus.SC_CREATED); -// -// resourceId = response.bodyAsJsonNode().get("_id").asText(); -// } -// -// // Create an entry in resource-sharing index -// try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { -// // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually -// String json = String.format( -// "{" -// + " \"source_idx\": \".sample_resource_sharing_plugin\"," -// + " \"resource_id\": \"%s\"," -// + " \"created_by\": {" -// + " \"user\": \"admin\"" -// + " }" -// + "}", -// resourceId -// ); -// HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); -// assertThat(response.getStatusReason(), containsString("Created")); -// // Also update the in-memory map and list -// OpenSearchSecurityPlugin.getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); -// ResourceProvider provider = new ResourceProvider( -// SampleResource.class.getCanonicalName(), -// RESOURCE_INDEX_NAME, -// new SampleResourceParser() -// ); -// OpenSearchSecurityPlugin.getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); -// -// Thread.sleep(1000); -// } -// -// // resource is still visible to super-admin -// try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { -// -// HttpResponse response = client.postJson(RESOURCE_INDEX_NAME + "/_search", "{\"query\" : {\"match_all\" : {}}}"); -// response.assertStatusCode(HttpStatus.SC_OK); -// assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(1)); -// assertThat(response.getBody(), containsString("sample")); -// } -// -// String updatePayload = "{" + "\"doc\": {" + "\"name\": \"sampleUpdated\"" + "}" + "}"; -// -// // Update sample resource with shared_with user. This will fail since the resource has not been shared -// try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { -// HttpResponse updateResponse = client.postJson(RESOURCE_INDEX_NAME + "/_update/" + resourceId, updatePayload); -// // it will show not found since the resource is not visible to shared_with_user -// updateResponse.assertStatusCode(HttpStatus.SC_NOT_FOUND); -// } -// -// // Admin is still allowed to update its own resource -// try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { -// HttpResponse updateResponse = client.postJson(RESOURCE_INDEX_NAME + "/_update/" + resourceId, updatePayload); -// // it will show not found since the resource is not visible to shared_with_user -// updateResponse.assertStatusCode(HttpStatus.SC_OK); -// assertThat(updateResponse.bodyAsJsonNode().get("_shards").get("successful").asInt(), equalTo(1)); -// } -// -// // Verify that share_with user does not have access to the resource -// try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { -// HttpResponse getResponse = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); -// // it will show not found since the resource is not visible to shared_with_user -// getResponse.assertStatusCode(HttpStatus.SC_NOT_FOUND); -// } -// -// // share the resource with shared_with_user -// try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { -// HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload(resourceId)); -// response.assertStatusCode(HttpStatus.SC_OK); -// } -// -// // Verify that share_with user now has access to the resource -// try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { -// HttpResponse getResponse = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); -// // it will show not found since the resource is not visible to shared_with_user -// getResponse.assertStatusCode(HttpStatus.SC_OK); -// assertThat(getResponse.getBody(), containsString("sampleUpdated")); -// } -// } - } From f83fb38ca68a1e2d9cfd78c5461250d8bd96813c Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 26 Feb 2025 13:05:36 -0500 Subject: [PATCH 133/201] Removes final occurence of DLS Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index e253f0afd6..fc8e75a6fb 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -49,8 +49,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Function; @@ -796,10 +794,7 @@ public Weight doCache(Weight weight, QueryCachingPolicy policy) { @Override public void onPreQueryPhase(SearchContext context) { - CompletableFuture.runAsync( - () -> { dlsFlsValve.handleSearchContext(context, threadPool, namedXContentRegistry.get()); }, - threadPool.generic() - ).orTimeout(5, TimeUnit.SECONDS).join(); + dlsFlsValve.handleSearchContext(context, threadPool, namedXContentRegistry.get()); } @Override @@ -1450,7 +1445,7 @@ public List> getSettings() { settings.add(Setting.simpleString(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, Property.NodeScope, Property.Filtered)); settings.add(Setting.groupSetting(ConfigConstants.SECURITY_AUTHCZ_IMPERSONATION_DN + ".", Property.NodeScope)); // not filtered - // here + // here settings.add(Setting.simpleString(ConfigConstants.SECURITY_CERT_OID, Property.NodeScope, Property.Filtered)); @@ -1466,8 +1461,8 @@ public List> getSettings() { );// not filtered here settings.add(Setting.boolSetting(ConfigConstants.SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED, false, Property.NodeScope));// not - // filtered - // here + // filtered + // here settings.add( Setting.boolSetting( @@ -1511,8 +1506,8 @@ public List> getSettings() { Setting.boolSetting(ConfigConstants.SECURITY_DFM_EMPTY_OVERRIDES_ALL, false, Property.NodeScope, Property.Filtered) ); settings.add(Setting.groupSetting(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + ".", Property.NodeScope)); // not - // filtered - // here + // filtered + // here settings.add(Setting.simpleString(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, Property.NodeScope, Property.Filtered)); settings.add( From bd5e0d01a8dc4d347f378b54d5cf30a2b2c6bc78 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 26 Feb 2025 13:19:31 -0500 Subject: [PATCH 134/201] Adds missing license headers and reverts changes to DefaultObjectMapper Signed-off-by: Darshit Chanpura --- .../AbstractSampleResourcePluginTests.java | 8 ++++++ ...rcePluginResourceSharingDisabledTests.java | 8 ++++++ .../sample/SampleResourcePluginTests.java | 8 ++++++ .../security/DefaultObjectMapper.java | 26 +++++-------------- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java index 6435c371ab..4127e6abc8 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java @@ -1,3 +1,11 @@ +/* + * 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. + */ + package org.opensearch.sample; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java index 62ba2e343c..e1a86684ed 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java @@ -1,3 +1,11 @@ +/* + * 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. + */ + package org.opensearch.sample; import java.util.Map; diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index 9478dbab37..27845bef52 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -1,3 +1,11 @@ +/* + * 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. + */ + package org.opensearch.sample; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; diff --git a/src/main/java/org/opensearch/security/DefaultObjectMapper.java b/src/main/java/org/opensearch/security/DefaultObjectMapper.java index 05ceabb86c..68a537c669 100644 --- a/src/main/java/org/opensearch/security/DefaultObjectMapper.java +++ b/src/main/java/org/opensearch/security/DefaultObjectMapper.java @@ -287,26 +287,12 @@ public static TypeFactory getTypeFactory() { return objectMapper.getTypeFactory(); } - @SuppressWarnings("removal") public static Set getFields(Class cls) { - final SecurityManager sm = System.getSecurityManager(); - - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - - try { - return AccessController.doPrivileged( - (PrivilegedExceptionAction>) () -> objectMapper.getSerializationConfig() - .introspect(getTypeFactory().constructType(cls)) - .findProperties() - .stream() - .map(BeanPropertyDefinition::getName) - .collect(ImmutableSet.toImmutableSet()) - ); - } catch (final PrivilegedActionException e) { - throw (RuntimeException) e.getCause(); - } - + return objectMapper.getSerializationConfig() + .introspect(getTypeFactory().constructType(cls)) + .findProperties() + .stream() + .map(BeanPropertyDefinition::getName) + .collect(ImmutableSet.toImmutableSet()); } } From 832b3fbe28296661fbcd288af080d430aeb2ae4e Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 26 Feb 2025 13:40:43 -0500 Subject: [PATCH 135/201] Makes Resource an interface Signed-off-by: Darshit Chanpura --- .../java/org/opensearch/sample/SampleResource.java | 12 ++---------- .../security/spi/resources/Resource.java | 12 ++---------- .../security/resources/ResourceAccessHandler.java | 14 ++++++-------- 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java index 23aae25d42..4a84191200 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java @@ -15,7 +15,6 @@ import java.util.Map; import org.opensearch.core.ParseField; -import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.ConstructingObjectParser; import org.opensearch.core.xcontent.XContentBuilder; @@ -25,21 +24,14 @@ import static org.opensearch.core.xcontent.ConstructingObjectParser.constructorArg; import static org.opensearch.core.xcontent.ConstructingObjectParser.optionalConstructorArg; -public class SampleResource extends Resource { +public class SampleResource implements Resource { private String name; private String description; private Map attributes; public SampleResource() throws IOException { - super(null); - } - - public SampleResource(StreamInput in) throws IOException { - super(in); - this.name = in.readString(); - this.description = in.readString(); - this.attributes = in.readMap(StreamInput::readString, StreamInput::readString); + super(); } @SuppressWarnings("unchecked") diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java b/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java index 18de796c8e..5af2ab7b26 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java @@ -8,26 +8,18 @@ package org.opensearch.security.spi.resources; -import java.io.IOException; - import org.opensearch.core.common.io.stream.NamedWriteable; -import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.xcontent.ToXContentFragment; /** * Marker interface for all resources */ -public abstract class Resource implements NamedWriteable, ToXContentFragment { +public interface Resource extends NamedWriteable, ToXContentFragment { /** * Abstract method to get the resource name. * Must be implemented by subclasses. * * @return resource name */ - public abstract String getResourceName(); - - /** - * Enforces that all subclasses have a constructor accepting StreamInput. - */ - protected Resource(StreamInput in) throws IOException {} + String getResourceName(); } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index b0ac4bb2bc..4fb89b4185 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -15,7 +15,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.concurrent.Future; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -69,12 +68,12 @@ public void initializeRecipientTypes() { } /** - * Returns a set of accessible resource IDs for the current user within the specified resource index. - * @param resourceIndex The resource index to check for accessible resources. - * @param listener The listener to be notified with the set of accessible resource IDs. + * Returns a set of accessible resource IDs for the current user within the specified resource index. * + * @param resourceIndex The resource index to check for accessible resources. + * @param listener The listener to be notified with the set of accessible resource IDs. */ - public Future getAccessibleResourceIdsForCurrentUser(String resourceIndex, ActionListener> listener) { + public void getAccessibleResourceIdsForCurrentUser(String resourceIndex, ActionListener> listener) { final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER ); @@ -84,7 +83,7 @@ public Future getAccessibleResourceIdsForCurrentUser(String resourceIndex, if (user == null) { LOGGER.info("Unable to fetch user details."); listener.onResponse(Collections.emptySet()); - return null; + return; } LOGGER.info("Listing accessible resources within the resource index {} for user: {}", resourceIndex, user.getName()); @@ -92,7 +91,7 @@ public Future getAccessibleResourceIdsForCurrentUser(String resourceIndex, // 2. If the user is admin, simply fetch all resources if (adminDNs.isAdmin(user)) { loadAllResources(resourceIndex, ActionListener.wrap(listener::onResponse, listener::onFailure)); - return null; + return; } // StepListener for the user’s "own" resources @@ -156,7 +155,6 @@ public Future getAccessibleResourceIdsForCurrentUser(String resourceIndex, LOGGER.debug("Found {} accessible resources for user {}", allResources.size(), user.getName()); listener.onResponse(allResources); }, listener::onFailure); - return null; } /** From d4581019882b70ffaee4095b1eff9b3e32d97170 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 26 Feb 2025 15:40:22 -0500 Subject: [PATCH 136/201] Updates sample plugin readme Signed-off-by: Darshit Chanpura --- sample-resource-plugin/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sample-resource-plugin/README.md b/sample-resource-plugin/README.md index f568544df2..d27c2cd0f2 100644 --- a/sample-resource-plugin/README.md +++ b/sample-resource-plugin/README.md @@ -2,6 +2,13 @@ This plugin demonstrates resource sharing and access control functionality, providing sample resource APIs and marking it as a resource sharing plugin via resource-sharing-spi. The access control is implemented on Security plugin and will be performed under the hood. +## PreRequisites + +Publish SPI to local maven before proceeding: +```shell +./gradlew clean :opensearch-resource-sharing-spi:publishToMavenLocal +``` + ## Features - Create, update and delete resources. From c4324f7ce8931fca5d56c1ea28135e01ae497ecc Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 14:47:35 -0500 Subject: [PATCH 137/201] Remove duplicate SPI publish Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 6 - .../resources/ResourceSharingException.java | 28 - .../security/auth/UserSubjectImpl.java | 55 - .../security/resources/CreatedBy.java | 89 -- .../security/resources/Creator.java | 32 - .../security/resources/Recipient.java | 25 - .../security/resources/RecipientType.java | 24 - .../resources/RecipientTypeRegistry.java | 33 - .../resources/ResourceAccessHandler.java | 578 ------- .../security/resources/ResourceSharing.java | 207 --- .../resources/ResourceSharingConstants.java | 16 - .../ResourceSharingIndexHandler.java | 1411 ----------------- .../ResourceSharingIndexListener.java | 132 -- ...ourceSharingIndexManagementRepository.java | 53 - .../security/resources/ShareWith.java | 104 -- .../security/resources/SharedWithScope.java | 169 -- .../security/resources/package-info.java | 12 - .../access/ResourceAccessRestAction.java | 249 --- 18 files changed, 3223 deletions(-) delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingException.java delete mode 100644 src/main/java/org/opensearch/security/auth/UserSubjectImpl.java delete mode 100644 src/main/java/org/opensearch/security/resources/CreatedBy.java delete mode 100644 src/main/java/org/opensearch/security/resources/Creator.java delete mode 100644 src/main/java/org/opensearch/security/resources/Recipient.java delete mode 100644 src/main/java/org/opensearch/security/resources/RecipientType.java delete mode 100644 src/main/java/org/opensearch/security/resources/RecipientTypeRegistry.java delete mode 100644 src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java delete mode 100644 src/main/java/org/opensearch/security/resources/ResourceSharing.java delete mode 100644 src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java delete mode 100644 src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java delete mode 100644 src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java delete mode 100644 src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java delete mode 100644 src/main/java/org/opensearch/security/resources/ShareWith.java delete mode 100644 src/main/java/org/opensearch/security/resources/SharedWithScope.java delete mode 100644 src/main/java/org/opensearch/security/resources/package-info.java delete mode 100644 src/main/java/org/opensearch/security/rest/resources/access/ResourceAccessRestAction.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8e5202c29..ca9088387f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -158,12 +158,6 @@ jobs: - name: Checkout security uses: actions/checkout@v4 - - name: Publish SPI to Local Maven - uses: gradle/gradle-build-action@v3 - with: - cache-disabled: true - arguments: :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false - - name: Run Resource Tests uses: gradle/gradle-build-action@v3 with: diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingException.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingException.java deleted file mode 100644 index e669341726..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingException.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.opensearch.security.spi.resources; - -import java.io.IOException; - -import org.opensearch.OpenSearchException; -import org.opensearch.core.common.io.stream.StreamInput; - -/** - * This class represents an exception that occurs during resource sharing operations. - * It extends the OpenSearchException class. - */ -public class ResourceSharingException extends OpenSearchException { - public ResourceSharingException(Throwable cause) { - super(cause); - } - - public ResourceSharingException(String msg, Object... args) { - super(msg, args); - } - - public ResourceSharingException(String msg, Throwable cause, Object... args) { - super(msg, cause, args); - } - - public ResourceSharingException(StreamInput in) throws IOException { - super(in); - } -} diff --git a/src/main/java/org/opensearch/security/auth/UserSubjectImpl.java b/src/main/java/org/opensearch/security/auth/UserSubjectImpl.java deleted file mode 100644 index a28ed8dd63..0000000000 --- a/src/main/java/org/opensearch/security/auth/UserSubjectImpl.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * 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. - * - */ -package org.opensearch.security.auth; - -import java.security.Principal; -import java.util.concurrent.Callable; - -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.identity.NamedPrincipal; -import org.opensearch.identity.UserSubject; -import org.opensearch.identity.tokens.AuthToken; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; - -public class UserSubjectImpl implements UserSubject { - private final NamedPrincipal userPrincipal; - private final ThreadPool threadPool; - private final User user; - - UserSubjectImpl(ThreadPool threadPool, User user) { - this.threadPool = threadPool; - this.user = user; - this.userPrincipal = new NamedPrincipal(user.getName()); - } - - @Override - public void authenticate(AuthToken authToken) { - // not implemented - } - - @Override - public Principal getPrincipal() { - return userPrincipal; - } - - @Override - public T runAs(Callable callable) throws Exception { - try (ThreadContext.StoredContext ctx = threadPool.getThreadContext().stashContext()) { - threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); - return callable.call(); - } - } - - public User getUser() { - return user; - } -} diff --git a/src/main/java/org/opensearch/security/resources/CreatedBy.java b/src/main/java/org/opensearch/security/resources/CreatedBy.java deleted file mode 100644 index af27001663..0000000000 --- a/src/main/java/org/opensearch/security/resources/CreatedBy.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.resources; - -import java.io.IOException; - -import org.opensearch.core.common.io.stream.NamedWriteable; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentFragment; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.core.xcontent.XContentParser; - -/** - * This class is used to store information about the creator of a resource. - * Concrete implementation will be provided by security plugin - * - * @opensearch.experimental - */ -public class CreatedBy implements ToXContentFragment, NamedWriteable { - - private final Enum creatorType; - private final String creator; - - public CreatedBy(Enum creatorType, String creator) { - this.creatorType = creatorType; - this.creator = creator; - } - - public CreatedBy(StreamInput in) throws IOException { - this.creatorType = in.readEnum(Creator.class); - this.creator = in.readString(); - } - - public String getCreator() { - return creator; - } - - public Enum getCreatorType() { - return creatorType; - } - - @Override - public String toString() { - return "CreatedBy {" + this.creatorType + "='" + this.creator + '\'' + '}'; - } - - @Override - public String getWriteableName() { - return "created_by"; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeEnum(Creator.valueOf(creatorType.name())); - out.writeString(creator); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return builder.startObject().field(String.valueOf(creatorType), creator).endObject(); - } - - public static CreatedBy fromXContent(XContentParser parser) throws IOException { - String creator = null; - Enum creatorType = null; - XContentParser.Token token; - - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - creatorType = Creator.fromName(parser.currentName()); - } else if (token == XContentParser.Token.VALUE_STRING) { - creator = parser.text(); - } - } - - if (creator == null) { - throw new IllegalArgumentException(creatorType + " is required"); - } - - return new CreatedBy(creatorType, creator); - } -} diff --git a/src/main/java/org/opensearch/security/resources/Creator.java b/src/main/java/org/opensearch/security/resources/Creator.java deleted file mode 100644 index ee2e9de7ab..0000000000 --- a/src/main/java/org/opensearch/security/resources/Creator.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.resources; - -public enum Creator { - USER("user"); - - private final String name; - - Creator(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public static Creator fromName(String name) { - for (Creator creator : values()) { - if (creator.name.equalsIgnoreCase(name)) { // Case-insensitive comparison - return creator; - } - } - throw new IllegalArgumentException("No enum constant for name: " + name); - } -} diff --git a/src/main/java/org/opensearch/security/resources/Recipient.java b/src/main/java/org/opensearch/security/resources/Recipient.java deleted file mode 100644 index 354f75fc0f..0000000000 --- a/src/main/java/org/opensearch/security/resources/Recipient.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.resources; - -public enum Recipient { - USERS("users"), - ROLES("roles"), - BACKEND_ROLES("backend_roles"); - - private final String name; - - Recipient(String name) { - this.name = name; - } - - public String getName() { - return name; - } -} diff --git a/src/main/java/org/opensearch/security/resources/RecipientType.java b/src/main/java/org/opensearch/security/resources/RecipientType.java deleted file mode 100644 index bfe2bfec12..0000000000 --- a/src/main/java/org/opensearch/security/resources/RecipientType.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.resources; - -/** - * This class determines a type of recipient a resource can be shared with. - * An example type would be a user or a role. - * This class is used to determine the type of recipient a resource can be shared with. - * - * @opensearch.experimental - */ -public record RecipientType(String type) { - - @Override - public String toString() { - return type; - } -} diff --git a/src/main/java/org/opensearch/security/resources/RecipientTypeRegistry.java b/src/main/java/org/opensearch/security/resources/RecipientTypeRegistry.java deleted file mode 100644 index 95da5debef..0000000000 --- a/src/main/java/org/opensearch/security/resources/RecipientTypeRegistry.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.resources; - -import java.util.HashMap; -import java.util.Map; - -/** - * This class determines a collection of recipient types a resource can be shared with. - * - * @opensearch.experimental - */ -public class RecipientTypeRegistry { - private static final Map REGISTRY = new HashMap<>(); - - public static void registerRecipientType(String key, RecipientType recipientType) { - REGISTRY.put(key, recipientType); - } - - public static RecipientType fromValue(String value) { - RecipientType type = REGISTRY.get(value); - if (type == null) { - throw new IllegalArgumentException("Unknown RecipientType: " + value + ". Must be 1 of these: " + REGISTRY.values()); - } - return type; - } -} diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java deleted file mode 100644 index 4fb89b4185..0000000000 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ /dev/null @@ -1,578 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.resources; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.action.StepListener; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.core.action.ActionListener; -import org.opensearch.security.OpenSearchSecurityPlugin; -import org.opensearch.security.auth.UserSubjectImpl; -import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.spi.resources.Resource; -import org.opensearch.security.spi.resources.ResourceParser; -import org.opensearch.security.spi.resources.ResourceSharingException; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; - -/** - * This class handles resource access permissions for users and roles. - * It provides methods to check if a user has permission to access a resource - * based on the resource sharing configuration. - */ -public class ResourceAccessHandler { - private static final Logger LOGGER = LogManager.getLogger(ResourceAccessHandler.class); - - private final ThreadContext threadContext; - private final ResourceSharingIndexHandler resourceSharingIndexHandler; - private final AdminDNs adminDNs; - - public ResourceAccessHandler( - final ThreadPool threadPool, - final ResourceSharingIndexHandler resourceSharingIndexHandler, - AdminDNs adminDns - ) { - this.threadContext = threadPool.getThreadContext(); - this.resourceSharingIndexHandler = resourceSharingIndexHandler; - this.adminDNs = adminDns; - } - - /** - * Initializes the recipient types for users, roles, and backend roles. - * These recipient types are used to identify the types of recipients for resource sharing. - */ - public void initializeRecipientTypes() { - RecipientTypeRegistry.registerRecipientType(Recipient.USERS.getName(), new RecipientType(Recipient.USERS.getName())); - RecipientTypeRegistry.registerRecipientType(Recipient.ROLES.getName(), new RecipientType(Recipient.ROLES.getName())); - RecipientTypeRegistry.registerRecipientType( - Recipient.BACKEND_ROLES.getName(), - new RecipientType(Recipient.BACKEND_ROLES.getName()) - ); - } - - /** - * Returns a set of accessible resource IDs for the current user within the specified resource index. - * - * @param resourceIndex The resource index to check for accessible resources. - * @param listener The listener to be notified with the set of accessible resource IDs. - */ - public void getAccessibleResourceIdsForCurrentUser(String resourceIndex, ActionListener> listener) { - final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( - ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER - ); - final User user = (userSubject == null) ? null : userSubject.getUser(); - - // If no user is authenticated, return an empty set - if (user == null) { - LOGGER.info("Unable to fetch user details."); - listener.onResponse(Collections.emptySet()); - return; - } - - LOGGER.info("Listing accessible resources within the resource index {} for user: {}", resourceIndex, user.getName()); - - // 2. If the user is admin, simply fetch all resources - if (adminDNs.isAdmin(user)) { - loadAllResources(resourceIndex, ActionListener.wrap(listener::onResponse, listener::onFailure)); - return; - } - - // StepListener for the user’s "own" resources - StepListener> ownResourcesListener = new StepListener<>(); - - // StepListener for resources shared with the user’s name - StepListener> userNameResourcesListener = new StepListener<>(); - - // StepListener for resources shared with the user’s roles - StepListener> rolesResourcesListener = new StepListener<>(); - - // StepListener for resources shared with the user’s backend roles - StepListener> backendRolesResourcesListener = new StepListener<>(); - - // Load own resources for the user. - loadOwnResources(resourceIndex, user.getName(), ownResourcesListener); - - // Load resources shared with the user by its name. - ownResourcesListener.whenComplete( - ownResources -> loadSharedWithResources( - resourceIndex, - Set.of(user.getName()), - Recipient.USERS.getName(), - userNameResourcesListener - ), - listener::onFailure - ); - - // Load resources shared with the user’s roles. - userNameResourcesListener.whenComplete( - userNameResources -> loadSharedWithResources( - resourceIndex, - user.getSecurityRoles(), - Recipient.ROLES.getName(), - rolesResourcesListener - ), - listener::onFailure - ); - - // Load resources shared with the user’s backend roles. - rolesResourcesListener.whenComplete( - rolesResources -> loadSharedWithResources( - resourceIndex, - user.getRoles(), - Recipient.BACKEND_ROLES.getName(), - backendRolesResourcesListener - ), - listener::onFailure - ); - - // Combine all results and pass them back to the original listener. - backendRolesResourcesListener.whenComplete(backendRolesResources -> { - Set allResources = new HashSet<>(); - - // Retrieve results from each StepListener - allResources.addAll(ownResourcesListener.result()); - allResources.addAll(userNameResourcesListener.result()); - allResources.addAll(rolesResourcesListener.result()); - allResources.addAll(backendRolesResourcesListener.result()); - - LOGGER.debug("Found {} accessible resources for user {}", allResources.size(), user.getName()); - listener.onResponse(allResources); - }, listener::onFailure); - } - - /** - * Returns a set of accessible resources for the current user within the specified resource index. - * - * @param resourceIndex The resource index to check for accessible resources. - * @param listener The listener to be notified with the set of accessible resources. - */ - @SuppressWarnings("unchecked") - public void getAccessibleResourcesForCurrentUser(String resourceIndex, ActionListener> listener) { - try { - validateArguments(resourceIndex); - - ResourceParser parser = OpenSearchSecurityPlugin.getResourceProviders().get(resourceIndex).getResourceParser(); - - StepListener> resourceIdsListener = new StepListener<>(); - StepListener> resourcesListener = new StepListener<>(); - - // Fetch resource IDs - getAccessibleResourceIdsForCurrentUser(resourceIndex, resourceIdsListener); - - // Fetch docs - resourceIdsListener.whenComplete(resourceIds -> { - if (resourceIds.isEmpty()) { - // No accessible resources => immediately respond with empty set - listener.onResponse(Collections.emptySet()); - } else { - // Fetch the resource documents asynchronously - this.resourceSharingIndexHandler.getResourceDocumentsFromIds(resourceIds, resourceIndex, parser, resourcesListener); - } - }, listener::onFailure); - - // Send final response - resourcesListener.whenComplete( - listener::onResponse, - ex -> listener.onFailure(new ResourceSharingException("Failed to get accessible resources: " + ex.getMessage(), ex)) - ); - } catch (Exception e) { - listener.onFailure(new ResourceSharingException("Failed to process accessible resources request: " + e.getMessage(), e)); - } - } - - /** - * Checks whether current user has given permission (scope) to access given resource. - * - * @param resourceId The resource ID to check access for. - * @param resourceIndex The resource index containing the resource. - * @param scope The permission scope to check. - * @param listener The listener to be notified with the permission check result. - */ - public void hasPermission(String resourceId, String resourceIndex, String scope, ActionListener listener) { - validateArguments(resourceId, resourceIndex, scope); - - final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( - ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER - ); - final User user = (userSubject == null) ? null : userSubject.getUser(); - - if (user == null) { - LOGGER.warn("No authenticated user found in ThreadContext"); - listener.onResponse(false); - return; - } - - LOGGER.info("Checking if user '{}' has '{}' permission to resource '{}'", user.getName(), scope, resourceId); - - if (adminDNs.isAdmin(user)) { - LOGGER.info("User '{}' is admin, automatically granted '{}' permission on '{}'", user.getName(), scope, resourceId); - listener.onResponse(true); - return; - } - - Set userRoles = user.getSecurityRoles(); - Set userBackendRoles = user.getRoles(); - - this.resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId, ActionListener.wrap(document -> { - if (document == null) { - LOGGER.warn("Resource '{}' not found in index '{}'", resourceId, resourceIndex); - listener.onResponse(false); - return; - } - - if (isSharedWithEveryone(document) - || isOwnerOfResource(document, user.getName()) - || isSharedWithEntity(document, Recipient.USERS, Set.of(user.getName()), scope) - || isSharedWithEntity(document, Recipient.ROLES, userRoles, scope) - || isSharedWithEntity(document, Recipient.BACKEND_ROLES, userBackendRoles, scope)) { - - LOGGER.info("User '{}' has '{}' permission to resource '{}'", user.getName(), scope, resourceId); - listener.onResponse(true); - } else { - LOGGER.info("User '{}' does not have '{}' permission to resource '{}'", user.getName(), scope, resourceId); - listener.onResponse(false); - } - }, exception -> { - LOGGER.error( - "Failed to fetch resource sharing document for resource '{}' in index '{}': {}", - resourceId, - resourceIndex, - exception.getMessage() - ); - listener.onFailure(exception); - })); - } - - /** - * Shares a resource with the specified users, roles, and backend roles. - * @param resourceId The resource ID to share. - * @param resourceIndex The index where resource is store - * @param shareWith The users, roles, and backend roles as well as scope to share the resource with. - * @param listener The listener to be notified with the updated ResourceSharing document. - */ - public void shareWith(String resourceId, String resourceIndex, ShareWith shareWith, ActionListener listener) { - validateArguments(resourceId, resourceIndex, shareWith); - - final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( - ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER - ); - final User user = (userSubject == null) ? null : userSubject.getUser(); - - if (user == null) { - LOGGER.warn("No authenticated user found in the ThreadContext."); - listener.onFailure(new ResourceSharingException("No authenticated user found.")); - return; - } - - LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith.toString()); - - boolean isAdmin = adminDNs.isAdmin(user); - - this.resourceSharingIndexHandler.updateResourceSharingInfo( - resourceId, - resourceIndex, - user.getName(), - shareWith, - isAdmin, - ActionListener.wrap( - // On success, return the updated ResourceSharing - updatedResourceSharing -> { - LOGGER.info("Successfully shared resource {} with {}", resourceId, shareWith.toString()); - listener.onResponse(updatedResourceSharing); - }, - // On failure, log and pass the exception along - e -> { - LOGGER.error("Failed to share resource {} with {}: {}", resourceId, shareWith.toString(), e.getMessage()); - listener.onFailure(e); - } - ) - ); - } - - /** - * Revokes access to a resource for the specified users, roles, and backend roles. - * @param resourceId The resource ID to revoke access from. - * @param resourceIndex The index where resource is store - * @param revokeAccess The users, roles, and backend roles to revoke access for. - * @param scopes The permission scopes to revoke access for. - * @param listener The listener to be notified with the updated ResourceSharing document. - */ - public void revokeAccess( - String resourceId, - String resourceIndex, - Map> revokeAccess, - Set scopes, - ActionListener listener - ) { - // Validate input - validateArguments(resourceId, resourceIndex, revokeAccess, scopes); - - // Retrieve user - final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( - ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER - ); - final User user = (userSubject == null) ? null : userSubject.getUser(); - - if (user != null) { - LOGGER.info("User {} revoking access to resource {} for {} for scopes {} ", user.getName(), resourceId, revokeAccess, scopes); - } else { - listener.onFailure( - new ResourceSharingException( - "Failed to revoke access to resource {} for {} for scopes {} with no authenticated user", - resourceId, - revokeAccess, - scopes - ) - ); - } - - boolean isAdmin = (user != null) && adminDNs.isAdmin(user); - - this.resourceSharingIndexHandler.revokeAccess( - resourceId, - resourceIndex, - revokeAccess, - scopes, - (user != null ? user.getName() : null), - isAdmin, - ActionListener.wrap(listener::onResponse, exception -> { - LOGGER.error("Failed to revoke access to resource {} in index {}: {}", resourceId, resourceIndex, exception.getMessage()); - listener.onFailure(exception); - }) - ); - } - - /** - * Deletes a resource sharing record by its ID and the resource index it belongs to. - * @param resourceId The resource ID to delete. - * @param resourceIndex The resource index containing the resource. - * @param listener The listener to be notified with the deletion result. - */ - public void deleteResourceSharingRecord(String resourceId, String resourceIndex, ActionListener listener) { - try { - validateArguments(resourceId, resourceIndex); - - final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( - ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER - ); - final User user = (userSubject == null) ? null : userSubject.getUser(); - - if (user != null) { - LOGGER.info( - "Deleting resource sharing record for resource {} in {} created by {}", - resourceId, - resourceIndex, - user.getName() - ); - } else { - listener.onFailure(new ResourceSharingException("No authenticated user available.")); - } - - StepListener fetchDocListener = new StepListener<>(); - StepListener deleteDocListener = new StepListener<>(); - - resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId, fetchDocListener); - - fetchDocListener.whenComplete(document -> { - if (document == null) { - LOGGER.info("Document {} does not exist in index {}", resourceId, resourceIndex); - listener.onResponse(false); - return; - } - - boolean isAdmin = (user != null && adminDNs.isAdmin(user)); - boolean isOwner = (user != null && isOwnerOfResource(document, user.getName())); - - if (!isAdmin && !isOwner) { - LOGGER.info( - "User {} does not have access to delete the record {}", - (user == null ? "UNKNOWN" : user.getName()), - resourceId - ); - // Not allowed => no deletion - listener.onResponse(false); - } else { - resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex, deleteDocListener); - } - }, listener::onFailure); - - deleteDocListener.whenComplete(listener::onResponse, listener::onFailure); - } catch (Exception e) { - LOGGER.error("Failed to delete resource sharing record for resource {}", resourceId, e); - listener.onFailure(e); - } - } - - /** - * Deletes all resource sharing records for the current user. - * @param listener The listener to be notified with the deletion result. - */ - public void deleteAllResourceSharingRecordsForCurrentUser(ActionListener listener) { - final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( - ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER - ); - final User user = (userSubject == null) ? null : userSubject.getUser(); - - if (user == null) { - listener.onFailure(new ResourceSharingException("No authenticated user available.")); - return; - } - - LOGGER.info("Deleting all resource sharing records for user {}", user.getName()); - - resourceSharingIndexHandler.deleteAllRecordsForUser(user.getName(), ActionListener.wrap(listener::onResponse, exception -> { - LOGGER.error( - "Failed to delete all resource sharing records for user {}: {}", - user.getName(), - exception.getMessage(), - exception - ); - listener.onFailure(exception); - })); - } - - /** - * Loads all resources within the specified resource index. - * - * @param resourceIndex The resource index to load resources from. - * @param listener The listener to be notified with the set of resource IDs. - */ - private void loadAllResources(String resourceIndex, ActionListener> listener) { - this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex, listener); - } - - /** - * Loads resources owned by the specified user within the given resource index. - * - * @param resourceIndex The resource index to load resources from. - * @param userName The username of the owner. - * @param listener The listener to be notified with the set of resource IDs. - */ - private void loadOwnResources(String resourceIndex, String userName, ActionListener> listener) { - this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName, listener); - } - - /** - * Loads resources shared with the specified entities within the given resource index, including public resources. - * - * @param resourceIndex The resource index to load resources from. - * @param entities The set of entities to check for shared resources. - * @param recipientType The type of entity (e.g., users, roles, backend_roles). - * @param listener The listener to be notified with the set of resource IDs. - */ - private void loadSharedWithResources( - String resourceIndex, - Set entities, - String recipientType, - ActionListener> listener - ) { - Set entitiesCopy = new HashSet<>(entities); - // To allow "public" resources to be matched for any user, role, backend_role - entitiesCopy.add("*"); - this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entitiesCopy, recipientType, listener); - } - - /** - * Checks if the given resource is owned by the specified user. - * - * @param document The ResourceSharing document to check. - * @param userName The username to check ownership against. - * @return True if the resource is owned by the user, false otherwise. - */ - private boolean isOwnerOfResource(ResourceSharing document, String userName) { - return document.getCreatedBy() != null && document.getCreatedBy().getCreator().equals(userName); - } - - /** - * Checks if the given resource is shared with the specified entities and scope. - * - * @param document The ResourceSharing document to check. - * @param recipient The recipient entity - * @param entities The set of entities to check for sharing. - * @param scope The permission scope to check. - * @return True if the resource is shared with the entities and scope, false otherwise. - */ - private boolean isSharedWithEntity(ResourceSharing document, Recipient recipient, Set entities, String scope) { - for (String entity : entities) { - if (checkSharing(document, recipient, entity, scope)) { - return true; - } - } - return false; - } - - /** - * Checks if the given resource is shared with everyone. - * - * @param document The ResourceSharing document to check. - * @return True if the resource is shared with everyone, false otherwise. - */ - private boolean isSharedWithEveryone(ResourceSharing document) { - return document.getShareWith() != null - && document.getShareWith().getSharedWithScopes().stream().anyMatch(sharedWithScope -> sharedWithScope.getScope().equals("*")); - } - - /** - * Checks if the given resource is shared with the specified entity and scope. - * - * @param document The ResourceSharing document to check. - * @param recipient The recipient entity - * @param identifier The identifier of the entity to check for sharing. - * @param scope The permission scope to check. - * @return True if the resource is shared with the entity and scope, false otherwise. - */ - private boolean checkSharing(ResourceSharing document, Recipient recipient, String identifier, String scope) { - if (document.getShareWith() == null) { - return false; - } - - return document.getShareWith() - .getSharedWithScopes() - .stream() - .filter(sharedWithScope -> sharedWithScope.getScope().equals(scope)) - .findFirst() - .map(sharedWithScope -> { - SharedWithScope.ScopeRecipients scopePermissions = sharedWithScope.getSharedWithPerScope(); - Map> recipients = scopePermissions.getRecipients(); - - return switch (recipient) { - case Recipient.USERS, Recipient.ROLES, Recipient.BACKEND_ROLES -> recipients.get( - RecipientTypeRegistry.fromValue(recipient.getName()) - ).contains(identifier); - }; - }) - .orElse(false); // Return false if no matching scope is found - } - - private void validateArguments(Object... args) { - if (args == null) { - throw new IllegalArgumentException("Arguments cannot be null"); - } - for (Object arg : args) { - if (arg == null) { - throw new IllegalArgumentException("Argument cannot be null"); - } - // Additional check for String type arguments - if (arg instanceof String && ((String) arg).trim().isEmpty()) { - throw new IllegalArgumentException("Arguments cannot be empty"); - } - } - } -} diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharing.java b/src/main/java/org/opensearch/security/resources/ResourceSharing.java deleted file mode 100644 index 6dd6734a87..0000000000 --- a/src/main/java/org/opensearch/security/resources/ResourceSharing.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.resources; - -import java.io.IOException; -import java.util.Objects; - -import org.opensearch.core.common.io.stream.NamedWriteable; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentFragment; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.core.xcontent.XContentParser; - -/** - * Represents a resource sharing configuration that manages access control for OpenSearch resources. - * This class holds information about shared resources including their source, creator, and sharing permissions. - * - *

          This class implements {@link ToXContentFragment} for JSON serialization and {@link NamedWriteable} - * for stream-based serialization.

          - * - * The class maintains information about: - *
            - *
          • The source index where the resource is defined
          • - *
          • The unique identifier of the resource
          • - *
          • The creator's information
          • - *
          • The sharing permissions and recipients
          • - *
          - * - * - * @see org.opensearch.security.resources.CreatedBy - * @see org.opensearch.security.resources.ShareWith - * @opensearch.experimental - */ -public class ResourceSharing implements ToXContentFragment, NamedWriteable { - - /** - * The index where the resource is defined - */ - private String sourceIdx; - - /** - * The unique identifier of the resource - */ - private String resourceId; - - /** - * Information about who created the resource - */ - private CreatedBy createdBy; - - /** - * Information about with whom the resource is shared with - */ - private ShareWith shareWith; - - public ResourceSharing(String sourceIdx, String resourceId, CreatedBy createdBy, ShareWith shareWith) { - this.sourceIdx = sourceIdx; - this.resourceId = resourceId; - this.createdBy = createdBy; - this.shareWith = shareWith; - } - - public String getSourceIdx() { - return sourceIdx; - } - - public void setSourceIdx(String sourceIdx) { - this.sourceIdx = sourceIdx; - } - - public String getResourceId() { - return resourceId; - } - - public void setResourceId(String resourceId) { - this.resourceId = resourceId; - } - - public CreatedBy getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(CreatedBy createdBy) { - this.createdBy = createdBy; - } - - public ShareWith getShareWith() { - return shareWith; - } - - public void setShareWith(ShareWith shareWith) { - this.shareWith = shareWith; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ResourceSharing resourceSharing = (ResourceSharing) o; - return Objects.equals(getSourceIdx(), resourceSharing.getSourceIdx()) - && Objects.equals(getResourceId(), resourceSharing.getResourceId()) - && Objects.equals(getCreatedBy(), resourceSharing.getCreatedBy()) - && Objects.equals(getShareWith(), resourceSharing.getShareWith()); - } - - @Override - public int hashCode() { - return Objects.hash(getSourceIdx(), getResourceId(), getCreatedBy(), getShareWith()); - } - - @Override - public String toString() { - return "Resource {" - + "sourceIdx='" - + sourceIdx - + '\'' - + ", resourceId='" - + resourceId - + '\'' - + ", createdBy=" - + createdBy - + ", sharedWith=" - + shareWith - + '}'; - } - - @Override - public String getWriteableName() { - return "resource_sharing"; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(sourceIdx); - out.writeString(resourceId); - createdBy.writeTo(out); - if (shareWith != null) { - out.writeBoolean(true); - shareWith.writeTo(out); - } else { - out.writeBoolean(false); - } - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject().field("source_idx", sourceIdx).field("resource_id", resourceId).field("created_by"); - createdBy.toXContent(builder, params); - if (shareWith != null && !shareWith.getSharedWithScopes().isEmpty()) { - builder.field("share_with"); - shareWith.toXContent(builder, params); - } - return builder.endObject(); - } - - public static ResourceSharing fromXContent(XContentParser parser) throws IOException { - String sourceIdx = null; - String resourceId = null; - CreatedBy createdBy = null; - ShareWith shareWith = null; - - String currentFieldName = null; - XContentParser.Token token; - - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } else { - switch (Objects.requireNonNull(currentFieldName)) { - case "source_idx": - sourceIdx = parser.text(); - break; - case "resource_id": - resourceId = parser.text(); - break; - case "created_by": - createdBy = CreatedBy.fromXContent(parser); - break; - case "share_with": - shareWith = ShareWith.fromXContent(parser); - break; - default: - parser.skipChildren(); - break; - } - } - } - - validateRequiredField("source_idx", sourceIdx); - validateRequiredField("resource_id", resourceId); - validateRequiredField("created_by", createdBy); - - return new ResourceSharing(sourceIdx, resourceId, createdBy, shareWith); - } - - private static void validateRequiredField(String field, T value) { - if (value == null) { - throw new IllegalArgumentException(field + " is required"); - } - } -} diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java b/src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java deleted file mode 100644 index a6ed3f2b03..0000000000 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ -package org.opensearch.security.resources; - -public class ResourceSharingConstants { - // Resource sharing index - public static final String OPENSEARCH_RESOURCE_SHARING_INDEX = ".opensearch_resource_sharing"; -} diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java deleted file mode 100644 index 0ac9c664f5..0000000000 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ /dev/null @@ -1,1411 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * 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. - * - */ -package org.opensearch.security.resources; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Callable; - -import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.action.DocWriteRequest; -import org.opensearch.action.StepListener; -import org.opensearch.action.admin.indices.create.CreateIndexRequest; -import org.opensearch.action.admin.indices.create.CreateIndexResponse; -import org.opensearch.action.get.MultiGetItemResponse; -import org.opensearch.action.get.MultiGetRequest; -import org.opensearch.action.index.IndexRequest; -import org.opensearch.action.index.IndexResponse; -import org.opensearch.action.search.ClearScrollRequest; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.action.search.SearchScrollRequest; -import org.opensearch.action.support.WriteRequest; -import org.opensearch.common.unit.TimeValue; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.common.xcontent.LoggingDeprecationHandler; -import org.opensearch.common.xcontent.XContentFactory; -import org.opensearch.common.xcontent.XContentHelper; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.action.ActionListener; -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.index.IndexNotFoundException; -import org.opensearch.index.query.BoolQueryBuilder; -import org.opensearch.index.query.MultiMatchQueryBuilder; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.index.reindex.BulkByScrollResponse; -import org.opensearch.index.reindex.DeleteByQueryAction; -import org.opensearch.index.reindex.DeleteByQueryRequest; -import org.opensearch.index.reindex.UpdateByQueryAction; -import org.opensearch.index.reindex.UpdateByQueryRequest; -import org.opensearch.script.Script; -import org.opensearch.script.ScriptType; -import org.opensearch.search.Scroll; -import org.opensearch.search.SearchHit; -import org.opensearch.search.builder.SearchSourceBuilder; -import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.spi.resources.Resource; -import org.opensearch.security.spi.resources.ResourceAccessScope; -import org.opensearch.security.spi.resources.ResourceParser; -import org.opensearch.security.spi.resources.ResourceSharingException; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.client.Client; - -import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; - -/** - * This class handles the creation and management of the resource sharing index. - * It provides methods to create the index, index resource sharing entries along with updates and deletion, retrieve shared resources. - */ -public class ResourceSharingIndexHandler { - - private static final Logger LOGGER = LogManager.getLogger(ResourceSharingIndexHandler.class); - - private final Client client; - - private final String resourceSharingIndex; - - private final ThreadPool threadPool; - - private final AuditLog auditLog; - - public ResourceSharingIndexHandler(final String indexName, final Client client, final ThreadPool threadPool, final AuditLog auditLog) { - this.resourceSharingIndex = indexName; - this.client = client; - this.threadPool = threadPool; - this.auditLog = auditLog; - } - - public final static Map INDEX_SETTINGS = Map.of( - "index.number_of_shards", - 1, - "index.auto_expand_replicas", - "0-all", - "index.hidden", - "true" - ); - - /** - * Creates the resource sharing index if it doesn't already exist. - * This method initializes the index with predefined mappings and settings - * for storing resource sharing information. - * The index will be created with the following structure: - * - source_idx (keyword): The source index containing the original document - * - resource_id (keyword): The ID of the shared resource - * - created_by (object): Information about the user who created the sharing - * - user (keyword): Username of the creator - * - share_with (object): Access control configuration for shared resources - * - [group_name] (object): Name of the access group - * - users (array): List of users with access - * - roles (array): List of roles with access - * - backend_roles (array): List of backend roles with access - * - * @throws RuntimeException if there are issues reading/writing index settings - * or communicating with the cluster - */ - - public void createResourceSharingIndexIfAbsent(Callable callable) { - // TODO: Once stashContext is replaced with switchContext this call will have to be modified - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - - CreateIndexRequest cir = new CreateIndexRequest(resourceSharingIndex).settings(INDEX_SETTINGS).waitForActiveShards(1); - ActionListener cirListener = ActionListener.wrap(response -> { - LOGGER.info("Resource sharing index {} created.", resourceSharingIndex); - if (callable != null) { - callable.call(); - } - }, (failResponse) -> { - /* Index already exists, ignore and continue */ - LOGGER.info("Index {} already exists.", resourceSharingIndex); - try { - if (callable != null) { - callable.call(); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - this.client.admin().indices().create(cir, cirListener); - } - } - - /** - * Creates or updates a resource sharing record in the dedicated resource sharing index. - * This method handles the persistence of sharing metadata for resources, including - * the creator information and sharing permissions. - * - * @param resourceId The unique identifier of the resource being shared - * @param resourceIndex The source index where the original resource is stored - * @param createdBy Object containing information about the user creating/updating the sharing - * @param shareWith Object containing the sharing permissions' configuration. Can be null for initial creation. - * When provided, it should contain the access control settings for different groups: - * { - * "group_name": { - * "users": ["user1", "user2"], - * "roles": ["role1", "role2"], - * "backend_roles": ["backend_role1"] - * } - * } - * - * @return ResourceSharing Returns resourceSharing object if the operation was successful, null otherwise - * @throws IOException if there are issues with index operations or JSON processing - */ - public ResourceSharing indexResourceSharing(String resourceId, String resourceIndex, CreatedBy createdBy, ShareWith shareWith) - throws IOException { - // TODO: Once stashContext is replaced with switchContext this call will have to be modified - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - ResourceSharing entry = new ResourceSharing(resourceIndex, resourceId, createdBy, shareWith); - - IndexRequest ir = client.prepareIndex(resourceSharingIndex) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .setSource(entry.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) - .setOpType(DocWriteRequest.OpType.CREATE) // only create if an entry doesn't exist - .request(); - - ActionListener irListener = ActionListener.wrap( - idxResponse -> LOGGER.info("Successfully created {} entry.", resourceSharingIndex), - (failResponse) -> { - LOGGER.error(failResponse.getMessage()); - LOGGER.info("Failed to create {} entry.", resourceSharingIndex); - } - ); - client.index(ir, irListener); - return entry; - } catch (Exception e) { - LOGGER.info("Failed to create {} entry.", resourceSharingIndex, e); - throw new ResourceSharingException("Failed to create " + resourceSharingIndex + " entry.", e); - } - } - - /** - * Fetches all resource sharing records that match the specified system index. This method retrieves - * a list of resource IDs associated with the given system index from the resource sharing index. - * - *

          The method executes the following steps: - *

            - *
          1. Creates a search request with term query matching the system index
          2. - *
          3. Applies source filtering to only fetch resource_id field
          4. - *
          5. Executes the search with a limit of 10000 documents
          6. - *
          7. Processes the results to extract resource IDs
          8. - *
          - * - *

          Example query structure: - *

          -    * {
          -    *   "query": {
          -    *     "term": {
          -    *       "source_idx": "resource_index_name"
          -    *     }
          -    *   },
          -    *   "_source": ["resource_id"],
          -    *   "size": 10000
          -    * }
          -    * 
          - * - * @param pluginIndex The source index to match against the source_idx field - * @param listener The listener to be notified when the operation completes. - * The listener receives a set of resource IDs as a result. - * @apiNote This method: - *
            - *
          • Uses source filtering for optimal performance
          • - *
          • Performs exact matching on the source_idx field
          • - *
          • Returns an empty list instead of throwing exceptions
          • - *
          - */ - public void fetchAllDocuments(String pluginIndex, ActionListener> listener) { - LOGGER.debug("Fetching all documents asynchronously from {} where source_idx = {}", resourceSharingIndex, pluginIndex); - - try (final ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query( - QueryBuilders.termQuery("source_idx.keyword", pluginIndex) - ).size(10000).fetchSource(new String[] { "resource_id" }, null); - - searchRequest.source(searchSourceBuilder); - - client.search(searchRequest, new ActionListener<>() { - @Override - public void onResponse(SearchResponse searchResponse) { - try { - Set resourceIds = new HashSet<>(); - - SearchHit[] hits = searchResponse.getHits().getHits(); - for (SearchHit hit : hits) { - Map sourceAsMap = hit.getSourceAsMap(); - if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { - resourceIds.add(sourceAsMap.get("resource_id").toString()); - } - } - - LOGGER.debug("Found {} documents in {} for source_idx: {}", resourceIds.size(), resourceSharingIndex, pluginIndex); - - listener.onResponse(resourceIds); - } catch (Exception e) { - LOGGER.error( - "Error while processing search response from {} for source_idx: {}", - resourceSharingIndex, - pluginIndex, - e - ); - listener.onFailure(e); - } - } - - @Override - public void onFailure(Exception e) { - LOGGER.error("Failed to fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); - listener.onFailure(e); - } - }); - } catch (Exception e) { - LOGGER.error("Failed to initiate fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); - listener.onFailure(e); - } - } - - /** - * Fetches documents that match the specified system index and have specific access type values. - * This method uses scroll API to handle large result sets efficiently. - * - *

          The method executes the following steps: - *

            - *
          1. Validates the RecipientType parameter
          2. - *
          3. Creates a scrolling search request with a compound query
          4. - *
          5. Processes results in batches using scroll API
          6. - *
          7. Collects all matching resource IDs
          8. - *
          9. Cleans up scroll context
          10. - *
          - * - *

          Example query structure: - *

          -    * {
          -    *   "query": {
          -    *     "bool": {
          -    *       "must": [
          -    *         { "term": { "source_idx": "resource_index_name" } },
          -    *         {
          -    *           "bool": {
          -    *             "should": [
          -    *               {
          -    *                 "nested": {
          -    *                   "path": "share_with.*.RecipientType",
          -    *                   "query": {
          -    *                     "term": { "share_with.*.RecipientType": "entity_value" }
          -    *                   }
          -    *                 }
          -    *               }
          -    *             ],
          -    *             "minimum_should_match": 1
          -    *           }
          -    *         }
          -    *       ]
          -    *     }
          -    *   },
          -    *   "_source": ["resource_id"],
          -    *   "size": 1000
          -    * }
          -    * 
          - * - * @param pluginIndex The source index to match against the source_idx field - * @param entities Set of values to match in the specified RecipientType field - * @param recipientType The type of association with the resource. Must be one of: - *
            - *
          • "users" - for user-based access
          • - *
          • "roles" - for role-based access
          • - *
          • "backend_roles" - for backend role-based access
          • - *
          - * @param listener The listener to be notified when the operation completes. - * The listener receives a set of resource IDs as a result. - * @throws RuntimeException if the search operation fails - * - * @apiNote This method: - *
            - *
          • Uses scroll API with 1-minute timeout
          • - *
          • Processes results in batches of 1000 documents
          • - *
          • Performs source filtering for optimization
          • - *
          • Uses nested queries for accessing array elements
          • - *
          • Properly cleans up scroll context after use
          • - *
          - */ - - public void fetchDocumentsForAllScopes( - String pluginIndex, - Set entities, - String recipientType, - ActionListener> listener - ) { - // "*" must match all scopes - fetchDocumentsForAGivenScope(pluginIndex, entities, recipientType, "*", listener); - } - - /** - * Fetches documents that match the specified system index and have specific access type values for a given scope. - * This method uses scroll API to handle large result sets efficiently. - * - *

          The method executes the following steps: - *

            - *
          1. Validates the RecipientType parameter
          2. - *
          3. Creates a scrolling search request with a compound query
          4. - *
          5. Processes results in batches using scroll API
          6. - *
          7. Collects all matching resource IDs
          8. - *
          9. Cleans up scroll context
          10. - *
          - * - *

          Example query structure: - *

          -     * {
          -     *   "query": {
          -     *     "bool": {
          -     *       "must": [
          -     *         { "term": { "source_idx": "resource_index_name" } },
          -     *         {
          -     *           "bool": {
          -     *             "should": [
          -     *               {
          -     *                 "nested": {
          -     *                   "path": "share_with.scope.RecipientType",
          -     *                   "query": {
          -     *                     "term": { "share_with.scope.RecipientType": "entity_value" }
          -     *                   }
          -     *                 }
          -     *               }
          -     *             ],
          -     *             "minimum_should_match": 1
          -     *           }
          -     *         }
          -     *       ]
          -     *     }
          -     *   },
          -     *   "_source": ["resource_id"],
          -     *   "size": 1000
          -     * }
          -     * 
          - * - * @param pluginIndex The source index to match against the source_idx field - * @param entities Set of values to match in the specified RecipientType field - * @param recipientType The type of association with the resource. Must be one of: - *
            - *
          • "users" - for user-based access
          • - *
          • "roles" - for role-based access
          • - *
          • "backend_roles" - for backend role-based access
          • - *
          - * @param scope The scope of the access. Should be implementation of {@link ResourceAccessScope} - * @param listener The listener to be notified when the operation completes. - * The listener receives a set of resource IDs as a result. - * @throws RuntimeException if the search operation fails - * - * @apiNote This method: - *
            - *
          • Uses scroll API with 1-minute timeout
          • - *
          • Processes results in batches of 1000 documents
          • - *
          • Performs source filtering for optimization
          • - *
          • Uses nested queries for accessing array elements
          • - *
          • Properly cleans up scroll context after use
          • - *
          - */ - public void fetchDocumentsForAGivenScope( - String pluginIndex, - Set entities, - String recipientType, - String scope, - ActionListener> listener - ) { - LOGGER.debug( - "Fetching documents asynchronously from index: {}, where share_with.{}.{} contains any of {}", - pluginIndex, - scope, - recipientType, - entities - ); - - final Set resourceIds = new HashSet<>(); - final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); - - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); - searchRequest.scroll(scroll); - - BoolQueryBuilder boolQuery = QueryBuilders.boolQuery().must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)); - - BoolQueryBuilder shouldQuery = QueryBuilders.boolQuery(); - if ("*".equals(scope)) { - for (String entity : entities) { - shouldQuery.should( - QueryBuilders.multiMatchQuery(entity, "share_with.*." + recipientType + ".keyword") - .type(MultiMatchQueryBuilder.Type.BEST_FIELDS) - ); - } - } else { - for (String entity : entities) { - shouldQuery.should(QueryBuilders.termQuery("share_with." + scope + "." + recipientType + ".keyword", entity)); - } - } - shouldQuery.minimumShouldMatch(1); - - boolQuery.must(QueryBuilders.existsQuery("share_with")).must(shouldQuery); - - executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery, ActionListener.wrap(success -> { - LOGGER.debug("Found {} documents matching the criteria in {}", resourceIds.size(), resourceSharingIndex); - listener.onResponse(resourceIds); - - }, exception -> { - LOGGER.error( - "Search failed for pluginIndex={}, scope={}, recipientType={}, entities={}", - pluginIndex, - scope, - recipientType, - entities, - exception - ); - listener.onFailure(exception); - - })); - } catch (Exception e) { - LOGGER.error( - "Failed to initiate fetch from {} for criteria - pluginIndex: {}, scope: {}, RecipientType: {}, entities: {}", - resourceSharingIndex, - pluginIndex, - scope, - recipientType, - entities, - e - ); - listener.onFailure(new RuntimeException("Failed to fetch documents: " + e.getMessage(), e)); - } - } - - /** - * Fetches documents from the resource sharing index that match a specific field value. - * This method uses scroll API to efficiently handle large result sets and performs exact - * matching on both system index and the specified field. - * - *

          The method executes the following steps: - *

            - *
          1. Validates input parameters for null/empty values
          2. - *
          3. Creates a scrolling search request with a bool query
          4. - *
          5. Processes results in batches using scroll API
          6. - *
          7. Extracts resource IDs from matching documents
          8. - *
          9. Cleans up scroll context after completion
          10. - *
          - * - *

          Example query structure: - *

          -     * {
          -     *   "query": {
          -     *     "bool": {
          -     *       "must": [
          -     *         { "term": { "source_idx": "system_index_value" } },
          -     *         { "term": { "field_name": "field_value" } }
          -     *       ]
          -     *     }
          -     *   },
          -     *   "_source": ["resource_id"],
          -     *   "size": 1000
          -     * }
          -     * 
          - * - * @param pluginIndex The source index to match against the source_idx field - * @param field The field name to search in. Must be a valid field in the index mapping - * @param value The value to match for the specified field. Performs exact term matching - * @param listener The listener to be notified when the operation completes. - * The listener receives a set of resource IDs as a result. - * - * @throws IllegalArgumentException if any parameter is null or empty - * @throws RuntimeException if the search operation fails, wrapping the underlying exception - * - * @apiNote This method: - *
            - *
          • Uses scroll API with 1-minute timeout for handling large result sets
          • - *
          • Performs exact term matching (not analyzed) on field values
          • - *
          • Processes results in batches of 1000 documents
          • - *
          • Uses source filtering to only fetch resource_id field
          • - *
          • Automatically cleans up scroll context after use
          • - *
          - * - * Example usage: - *
          -     * Set resources = fetchDocumentsByField("myIndex", "status", "active");
          -     * 
          - */ - public void fetchDocumentsByField(String pluginIndex, String field, String value, ActionListener> listener) { - if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(field) || StringUtils.isBlank(value)) { - listener.onFailure(new IllegalArgumentException("pluginIndex, field, and value must not be null or empty")); - return; - } - - LOGGER.debug("Fetching documents from index: {}, where {} = {}", pluginIndex, field, value); - - Set resourceIds = new HashSet<>(); - final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); - - // TODO: Once stashContext is replaced with switchContext this call will have to be modified - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); - searchRequest.scroll(scroll); - - BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)) - .must(QueryBuilders.termQuery(field + ".keyword", value)); - - executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery, ActionListener.wrap(success -> { - LOGGER.info("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); - listener.onResponse(resourceIds); - }, exception -> { - LOGGER.error("Failed to fetch documents from {} where {} = {}", resourceSharingIndex, field, value, exception); - listener.onFailure(new RuntimeException("Failed to fetch documents: " + exception.getMessage(), exception)); - })); - } catch (Exception e) { - LOGGER.error("Failed to initiate fetch from {} where {} = {}", resourceSharingIndex, field, value, e); - listener.onFailure(new RuntimeException("Failed to initiate fetch: " + e.getMessage(), e)); - } - - } - - /** - * Fetches a specific resource sharing document by its resource ID and system index. - * This method performs an exact match search and parses the result into a ResourceSharing object. - * - *

          The method executes the following steps: - *

            - *
          1. Validates input parameters for null/empty values
          2. - *
          3. Creates a search request with a bool query for exact matching
          4. - *
          5. Executes the search with a limit of 1 document
          6. - *
          7. Parses the result using XContent parser if found
          8. - *
          9. Returns null if no matching document exists
          10. - *
          - * - *

          Example query structure: - *

          -    * {
          -    *   "query": {
          -    *     "bool": {
          -    *       "must": [
          -    *         { "term": { "source_idx": "resource_index_name" } },
          -    *         { "term": { "resource_id": "resource_id_value" } }
          -    *       ]
          -    *     }
          -    *   },
          -    *   "size": 1
          -    * }
          -    * 
          - * - * @param pluginIndex The source index to match against the source_idx field - * @param resourceId The resource ID to fetch. Must exactly match the resource_id field - * @param listener The listener to be notified when the operation completes. - * The listener receives the parsed ResourceSharing object or null if not found - * - * @throws IllegalArgumentException if pluginIndexName or resourceId is null or empty - * @throws RuntimeException if the search operation fails or parsing errors occur, - * wrapping the underlying exception - * - * @apiNote This method: - *
            - *
          • Uses term queries for exact matching
          • - *
          • Expects only one matching document per resource ID
          • - *
          • Uses XContent parsing for consistent object creation
          • - *
          • Returns null instead of throwing exceptions for non-existent documents
          • - *
          • Provides detailed logging for troubleshooting
          • - *
          - * - * Example usage: - *
          -    * ResourceSharing sharing = fetchDocumentById("myIndex", "resource123");
          -    * if (sharing != null) {
          -    *     // Process the resource sharing object
          -    * }
          -    * 
          - */ - public void fetchDocumentById(String pluginIndex, String resourceId, ActionListener listener) { - if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(resourceId)) { - listener.onFailure(new IllegalArgumentException("pluginIndex and resourceId must not be null or empty")); - return; - } - LOGGER.debug("Fetching document from index: {}, resourceId: {}", pluginIndex, resourceId); - - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)) - .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); - - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery).size(1); // There is only one document for - // a single resource - - SearchRequest searchRequest = new SearchRequest(resourceSharingIndex).source(searchSourceBuilder); - - client.search(searchRequest, new ActionListener<>() { - @Override - public void onResponse(SearchResponse searchResponse) { - try { - SearchHit[] hits = searchResponse.getHits().getHits(); - if (hits.length == 0) { - LOGGER.debug("No document found for resourceId: {} in index: {}", resourceId, pluginIndex); - listener.onResponse(null); - return; - } - - SearchHit hit = hits[0]; - try ( - XContentParser parser = XContentType.JSON.xContent() - .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, hit.getSourceAsString()) - ) { - parser.nextToken(); - ResourceSharing resourceSharing = ResourceSharing.fromXContent(parser); - - LOGGER.debug("Successfully fetched document for resourceId: {} from index: {}", resourceId, pluginIndex); - - listener.onResponse(resourceSharing); - } - } catch (Exception e) { - LOGGER.error("Failed to parse document for resourceId: {} from index: {}", resourceId, pluginIndex, e); - listener.onFailure( - new ResourceSharingException( - "Failed to parse document for resourceId: " + resourceId + " from index: " + pluginIndex, - e - ) - ); - } - } - - @Override - public void onFailure(Exception e) { - - LOGGER.error("Failed to fetch document for resourceId: {} from index: {}", resourceId, pluginIndex, e); - listener.onFailure( - new ResourceSharingException( - "Failed to fetch document for resourceId: " + resourceId + " from index: " + pluginIndex, - e - ) - ); - - } - }); - } catch (Exception e) { - LOGGER.error("Failed to fetch document for resourceId: {} from index: {}", resourceId, pluginIndex, e); - listener.onFailure( - new ResourceSharingException("Failed to fetch document for resourceId: " + resourceId + " from index: " + pluginIndex, e) - ); - } - } - - /** - * Helper method to execute a search request and collect resource IDs from the results. - * @param resourceIds List to collect resource IDs - * @param scroll Search Scroll - * @param searchRequest Request to execute - * @param boolQuery Query to execute with the request - * @param listener Listener to be notified when the operation completes - */ - private void executeSearchRequest( - Set resourceIds, - Scroll scroll, - SearchRequest searchRequest, - BoolQueryBuilder boolQuery, - ActionListener listener - ) { - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery) - .size(1000) - .fetchSource(new String[] { "resource_id" }, null); - - searchRequest.source(searchSourceBuilder); - - StepListener searchStep = new StepListener<>(); - - client.search(searchRequest, searchStep); - - searchStep.whenComplete(initialResponse -> { - String scrollId = initialResponse.getScrollId(); - processScrollResults(resourceIds, scroll, scrollId, initialResponse.getHits().getHits(), listener); - }, listener::onFailure); - } - - /** - * Helper method to process scroll results recursively. - * @param resourceIds List to collect resource IDs - * @param scroll Search Scroll - * @param scrollId Scroll ID - * @param hits Search hits - * @param listener Listener to be notified when the operation completes - */ - private void processScrollResults( - Set resourceIds, - Scroll scroll, - String scrollId, - SearchHit[] hits, - ActionListener listener - ) { - // If no hits, clean up and complete - if (hits == null || hits.length == 0) { - clearScroll(scrollId, listener); - return; - } - - // Process current batch of hits - for (SearchHit hit : hits) { - Map sourceAsMap = hit.getSourceAsMap(); - if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { - resourceIds.add(sourceAsMap.get("resource_id").toString()); - } - } - - // Prepare next scroll request - SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); - scrollRequest.scroll(scroll); - - // Execute next scroll - client.searchScroll(scrollRequest, ActionListener.wrap(scrollResponse -> { - // Process next batch recursively - processScrollResults(resourceIds, scroll, scrollResponse.getScrollId(), scrollResponse.getHits().getHits(), listener); - }, e -> { - // Clean up scroll context on failure - clearScroll(scrollId, ActionListener.wrap(r -> listener.onFailure(e), ex -> { - e.addSuppressed(ex); - listener.onFailure(e); - })); - })); - } - - /** - * Helper method to clear scroll context. - * @param scrollId Scroll ID - * @param listener Listener to be notified when the operation completes - */ - private void clearScroll(String scrollId, ActionListener listener) { - if (scrollId == null) { - listener.onResponse(null); - return; - } - - ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); - clearScrollRequest.addScrollId(scrollId); - - client.clearScroll(clearScrollRequest, ActionListener.wrap(r -> listener.onResponse(null), e -> { - LOGGER.warn("Failed to clear scroll context", e); - listener.onResponse(null); - })); - } - - /** - * Updates the sharing configuration for an existing resource in the resource sharing index. - * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map, Set, String, boolean, ActionListener)} - * This method modifies the sharing permissions for a specific resource identified by its - * resource ID and source index. - * - * @param resourceId The unique identifier of the resource whose sharing configuration needs to be updated - * @param sourceIdx The source index where the original resource is stored - * @param requestUserName The user requesting to share the resource - * @param shareWith Updated sharing configuration object containing access control settings: - * { - * "scope": { - * "users": ["user1", "user2"], - * "roles": ["role1", "role2"], - * "backend_roles": ["backend_role1"] - * } - * } - * @param isAdmin Boolean indicating whether the user requesting to share is an admin or not - * @param listener Listener to be notified when the operation completes - * - * @throws RuntimeException if there's an error during the update operation - */ - public void updateResourceSharingInfo( - String resourceId, - String sourceIdx, - String requestUserName, - ShareWith shareWith, - boolean isAdmin, - ActionListener listener - ) { - XContentBuilder builder; - Map shareWithMap; - try { - builder = XContentFactory.jsonBuilder(); - shareWith.toXContent(builder, ToXContent.EMPTY_PARAMS); - String json = builder.toString(); - shareWithMap = DefaultObjectMapper.readValue(json, new TypeReference<>() { - }); - } catch (IOException e) { - LOGGER.error("Failed to build json content", e); - listener.onFailure(new ResourceSharingException("Failed to build json content", e)); - return; - } - - StepListener fetchDocListener = new StepListener<>(); - StepListener updateScriptListener = new StepListener<>(); - StepListener updatedSharingListener = new StepListener<>(); - - // Fetch resource sharing doc - fetchDocumentById(sourceIdx, resourceId, fetchDocListener); - - // build update script - fetchDocListener.whenComplete(currentSharingInfo -> { - // Check if user can share. At present only the resource creator and admin is allowed to share the resource - if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { - - LOGGER.error("User {} is not authorized to share resource {}", requestUserName, resourceId); - listener.onFailure( - new ResourceSharingException("User " + requestUserName + " is not authorized to share resource " + resourceId) - ); - } - - Script updateScript = new Script(ScriptType.INLINE, "painless", """ - if (ctx._source.share_with == null) { - ctx._source.share_with = [:]; - } - - for (def entry : params.shareWith.entrySet()) { - def scopeName = entry.getKey(); - def newScope = entry.getValue(); - - if (!ctx._source.share_with.containsKey(scopeName)) { - def newScopeEntry = [:]; - for (def field : newScope.entrySet()) { - if (field.getValue() != null && !field.getValue().isEmpty()) { - newScopeEntry[field.getKey()] = new HashSet(field.getValue()); - } - } - ctx._source.share_with[scopeName] = newScopeEntry; - } else { - def existingScope = ctx._source.share_with[scopeName]; - - for (def field : newScope.entrySet()) { - def fieldName = field.getKey(); - def newValues = field.getValue(); - - if (newValues != null && !newValues.isEmpty()) { - if (!existingScope.containsKey(fieldName)) { - existingScope[fieldName] = new HashSet(); - } - - for (def value : newValues) { - if (!existingScope[fieldName].contains(value)) { - existingScope[fieldName].add(value); - } - } - } - } - } - } - """, Collections.singletonMap("shareWith", shareWithMap)); - - updateByQueryResourceSharing(sourceIdx, resourceId, updateScript, updateScriptListener); - - }, listener::onFailure); - - // Build & return the updated ResourceSharing - updateScriptListener.whenComplete(success -> { - if (!success) { - LOGGER.error("Failed to update resource sharing info for resource {}", resourceId); - listener.onResponse(null); - return; - } - // TODO check if this should be replaced by Java in-memory computation (current intuition is that it will be more memory - // intensive to do it in java) - fetchDocumentById(sourceIdx, resourceId, updatedSharingListener); - }, listener::onFailure); - - updatedSharingListener.whenComplete(listener::onResponse, listener::onFailure); - } - - /** - * Updates resource sharing entries that match the specified source index and resource ID - * using the provided update script. This method performs an update-by-query operation - * in the resource sharing index. - * - *

          The method executes the following steps: - *

            - *
          1. Creates a bool query to match exact source index and resource ID
          2. - *
          3. Constructs an update-by-query request with the query and update script
          4. - *
          5. Executes the update operation
          6. - *
          7. Returns success/failure status based on update results
          8. - *
          - * - *

          Example document matching structure: - *

          -     * {
          -     *   "source_idx": "source_index_name",
          -     *   "resource_id": "resource_id_value",
          -     *   "share_with": {
          -     *     // sharing configuration to be updated
          -     *   }
          -     * }
          -     * 
          - * - * @param sourceIdx The source index to match in the query (exact match) - * @param resourceId The resource ID to match in the query (exact match) - * @param updateScript The script containing the update operations to be performed. - * This script defines how the matching documents should be modified - * @param listener Listener to be notified when the operation completes - * - * @apiNote This method: - *
            - *
          • Uses term queries for exact matching of source_idx and resource_id
          • - *
          • Returns false for both "no matching documents" and "operation failure" cases
          • - *
          • Logs the complete update request for debugging purposes
          • - *
          • Provides detailed logging for success and failure scenarios
          • - *
          - * - * @implNote The update operation uses a bool query with two must clauses: - *
          -     * {
          -     *   "query": {
          -     *     "bool": {
          -     *       "must": [
          -     *         { "term": { "source_idx.keyword": sourceIdx } },
          -     *         { "term": { "resource_id.keyword": resourceId } }
          -     *       ]
          -     *     }
          -     *   }
          -     * }
          -     * 
          - */ - private void updateByQueryResourceSharing(String sourceIdx, String resourceId, Script updateScript, ActionListener listener) { - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - BoolQueryBuilder query = QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) - .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); - - UpdateByQueryRequest ubq = new UpdateByQueryRequest(resourceSharingIndex).setQuery(query) - .setScript(updateScript) - .setRefresh(true); - - client.execute(UpdateByQueryAction.INSTANCE, ubq, new ActionListener<>() { - @Override - public void onResponse(BulkByScrollResponse response) { - long updated = response.getUpdated(); - if (updated > 0) { - LOGGER.info("Successfully updated {} documents in {}.", updated, resourceSharingIndex); - listener.onResponse(true); - } else { - LOGGER.info( - "No documents found to update in {} for source_idx: {} and resource_id: {}", - resourceSharingIndex, - sourceIdx, - resourceId - ); - listener.onResponse(false); - } - - } - - @Override - public void onFailure(Exception e) { - - LOGGER.error("Failed to update documents in {}.", resourceSharingIndex, e); - listener.onFailure(e); - - } - }); - } catch (Exception e) { - LOGGER.error("Failed to update documents in {} before request submission.", resourceSharingIndex, e); - listener.onFailure(e); - } - } - - /** - * Revokes access for specified entities from a resource sharing document. This method removes the specified - * entities (users, roles, or backend roles) from the existing sharing configuration while preserving other - * sharing settings. - * - *

          The method performs the following steps: - *

            - *
          1. Fetches the existing document
          2. - *
          3. Removes specified entities from their respective lists in all sharing groups
          4. - *
          5. Updates the document if modifications were made
          6. - *
          7. Returns the updated resource sharing configuration
          8. - *
          - * - *

          Example document structure: - *

          -     * {
          -     *   "source_idx": "resource_index_name",
          -     *   "resource_id": "resource_id",
          -     *   "share_with": {
          -     *     "scope": {
          -     *       "users": ["user1", "user2"],
          -     *       "roles": ["role1", "role2"],
          -     *       "backend_roles": ["backend_role1"]
          -     *     }
          -     *   }
          -     * }
          -     * 
          - * - * @param resourceId The ID of the resource from which to revoke access - * @param sourceIdx The name of the system index where the resource exists - * @param revokeAccess A map containing entity types (USER, ROLE, BACKEND_ROLE) and their corresponding - * values to be removed from the sharing configuration - * @param scopes A list of scopes to revoke access from. If null or empty, access is revoked from all scopes - * @param requestUserName The user trying to revoke the accesses - * @param isAdmin Boolean indicating whether the user is an admin or not - * @param listener Listener to be notified when the operation completes - * @throws IllegalArgumentException if resourceId, sourceIdx is null/empty, or if revokeAccess is null/empty - * @throws RuntimeException if the update operation fails or encounters an error - * - * @see RecipientType - * @see ResourceSharing - * - * @apiNote This method modifies the existing document. If no modifications are needed (i.e., specified - * entities don't exist in the current configuration), the original document is returned unchanged. - * @example - *
          -     * Map> revokeAccess = new HashMap<>();
          -     * revokeAccess.put(RecipientType.USER, Set.of("user1", "user2"));
          -     * revokeAccess.put(RecipientType.ROLE, Set.of("role1"));
          -     * ResourceSharing updated = revokeAccess("resourceId", "pluginIndex", revokeAccess);
          -     * 
          - */ - public void revokeAccess( - String resourceId, - String sourceIdx, - Map> revokeAccess, - Set scopes, - String requestUserName, - boolean isAdmin, - ActionListener listener - ) { - if (StringUtils.isBlank(resourceId) || StringUtils.isBlank(sourceIdx) || revokeAccess == null || revokeAccess.isEmpty()) { - listener.onFailure(new IllegalArgumentException("resourceId, sourceIdx, and revokeAccess must not be null or empty")); - return; - } - - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - - LOGGER.debug( - "Revoking access for resource {} in {} for entities: {} and scopes: {}", - resourceId, - sourceIdx, - revokeAccess, - scopes - ); - - StepListener currentSharingListener = new StepListener<>(); - StepListener revokeUpdateListener = new StepListener<>(); - StepListener updatedSharingListener = new StepListener<>(); - - // Fetch the current ResourceSharing document - fetchDocumentById(sourceIdx, resourceId, currentSharingListener); - - // Check permissions & build revoke script - currentSharingListener.whenComplete(currentSharingInfo -> { - // Only admin or the creator of the resource is currently allowed to revoke access - if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { - listener.onFailure( - new ResourceSharingException( - "User " + requestUserName + " is not authorized to revoke access to resource " + resourceId - ) - ); - } - - Map revoke = new HashMap<>(); - for (Map.Entry> entry : revokeAccess.entrySet()) { - revoke.put(entry.getKey().type().toLowerCase(), new ArrayList<>(entry.getValue())); - } - List scopesToUse = (scopes != null) ? new ArrayList<>(scopes) : new ArrayList<>(); - - Script revokeScript = new Script(ScriptType.INLINE, "painless", """ - if (ctx._source.share_with != null) { - Set scopesToProcess = new HashSet(params.scopes.isEmpty() ? ctx._source.share_with.keySet() : params.scopes); - - for (def scopeName : scopesToProcess) { - if (ctx._source.share_with.containsKey(scopeName)) { - def existingScope = ctx._source.share_with.get(scopeName); - - for (def entry : params.revokeAccess.entrySet()) { - def RecipientType = entry.getKey(); - def entitiesToRemove = entry.getValue(); - - if (existingScope.containsKey(RecipientType) && existingScope[RecipientType] != null) { - if (!(existingScope[RecipientType] instanceof HashSet)) { - existingScope[RecipientType] = new HashSet(existingScope[RecipientType]); - } - - existingScope[RecipientType].removeAll(entitiesToRemove); - - if (existingScope[RecipientType].isEmpty()) { - existingScope.remove(RecipientType); - } - } - } - - if (existingScope.isEmpty()) { - ctx._source.share_with.remove(scopeName); - } - } - } - } - """, Map.of("revokeAccess", revoke, "scopes", scopesToUse)); - updateByQueryResourceSharing(sourceIdx, resourceId, revokeScript, revokeUpdateListener); - - }, listener::onFailure); - - // Return doc or null based on successful result, fail otherwise - revokeUpdateListener.whenComplete(success -> { - if (!success) { - LOGGER.error("Failed to revoke access for resource {} in index {} (no docs updated).", resourceId, sourceIdx); - listener.onResponse(null); - return; - } - // TODO check if this should be replaced by Java in-memory computation (current intuition is that it will be more memory - // intensive to do it in java) - fetchDocumentById(sourceIdx, resourceId, updatedSharingListener); - }, listener::onFailure); - - updatedSharingListener.whenComplete(listener::onResponse, listener::onFailure); - } - } - - /** - * Deletes resource sharing records that match the specified source index and resource ID. - * This method performs a delete-by-query operation in the resource sharing index. - * - *

          The method executes the following steps: - *

            - *
          1. Creates a delete-by-query request with a bool query
          2. - *
          3. Matches documents based on exact source index and resource ID
          4. - *
          5. Executes the delete operation with immediate refresh
          6. - *
          7. Returns the success/failure status based on deletion results
          8. - *
          - * - *

          Example document structure that will be deleted: - *

          -     * {
          -     *   "source_idx": "source_index_name",
          -     *   "resource_id": "resource_id_value",
          -     *   "share_with": {
          -     *     // sharing configuration
          -     *   }
          -     * }
          -     * 
          - * - * @param sourceIdx The source index to match in the query (exact match) - * @param resourceId The resource ID to match in the query (exact match) - * @param listener The listener to be notified when the operation completes - * @throws IllegalArgumentException if sourceIdx or resourceId is null/empty - * @throws RuntimeException if the delete operation fails or encounters an error - * - * @implNote The delete operation uses a bool query with two must clauses to ensure exact matching: - *
          -     * {
          -     *   "query": {
          -     *     "bool": {
          -     *       "must": [
          -     *         { "term": { "source_idx": sourceIdx } },
          -     *         { "term": { "resource_id": resourceId } }
          -     *       ]
          -     *     }
          -     *   }
          -     * }
          -     * 
          - */ - public void deleteResourceSharingRecord(String resourceId, String sourceIdx, ActionListener listener) { - LOGGER.debug( - "Deleting documents asynchronously from {} where source_idx = {} and resource_id = {}", - resourceSharingIndex, - sourceIdx, - resourceId - ); - - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - DeleteByQueryRequest dbq = new DeleteByQueryRequest(resourceSharingIndex).setQuery( - QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) - .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)) - ).setRefresh(true); - - client.execute(DeleteByQueryAction.INSTANCE, dbq, new ActionListener<>() { - @Override - public void onResponse(BulkByScrollResponse response) { - - long deleted = response.getDeleted(); - if (deleted > 0) { - LOGGER.info("Successfully deleted {} documents from {}", deleted, resourceSharingIndex); - listener.onResponse(true); - } else { - LOGGER.info( - "No documents found to delete in {} for source_idx: {} and resource_id: {}", - resourceSharingIndex, - sourceIdx, - resourceId - ); - // No documents were deleted - listener.onResponse(false); - } - } - - @Override - public void onFailure(Exception e) { - LOGGER.error("Failed to delete documents from {}", resourceSharingIndex, e); - listener.onFailure(e); - - } - }); - } catch (Exception e) { - LOGGER.error("Failed to delete documents from {} before request submission", resourceSharingIndex, e); - listener.onFailure(e); - } - } - - /** - * Deletes all resource sharing records that were created by a specific user. - * This method performs a delete-by-query operation to remove all documents where - * the created_by.user field matches the specified username. - * - *

          The method executes the following steps: - *

            - *
          1. Validates the input username parameter
          2. - *
          3. Creates a delete-by-query request with term query matching
          4. - *
          5. Executes the delete operation with immediate refresh
          6. - *
          7. Returns the operation status based on number of deleted documents
          8. - *
          - * - *

          Example query structure: - *

          -    * {
          -    *   "query": {
          -    *     "term": {
          -    *       "created_by.user": "username"
          -    *     }
          -    *   }
          -    * }
          -    * 
          - * - * @param name The username to match against the created_by.user field - * @param listener The listener to be notified when the operation completes - * @throws IllegalArgumentException if name is null or empty - * - * - * @implNote Implementation details: - *
            - *
          • Uses DeleteByQueryRequest for efficient bulk deletion
          • - *
          • Sets refresh=true for immediate consistency
          • - *
          • Uses term query for exact username matching
          • - *
          • Implements comprehensive error handling and logging
          • - *
          - * - * Example usage: - *
          -    * boolean success = deleteAllRecordsForUser("john.doe");
          -    * if (success) {
          -    *     // Records were successfully deleted
          -    * } else {
          -    *     // No matching records found or operation failed
          -    * }
          -    * 
          - */ - public void deleteAllRecordsForUser(String name, ActionListener listener) { - if (StringUtils.isBlank(name)) { - listener.onFailure(new IllegalArgumentException("Username must not be null or empty")); - return; - } - - LOGGER.debug("Deleting all records for user {} asynchronously", name); - - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - DeleteByQueryRequest deleteRequest = new DeleteByQueryRequest(resourceSharingIndex).setQuery( - QueryBuilders.termQuery("created_by.user", name) - ).setRefresh(true); - - client.execute(DeleteByQueryAction.INSTANCE, deleteRequest, new ActionListener<>() { - @Override - public void onResponse(BulkByScrollResponse response) { - long deletedDocs = response.getDeleted(); - if (deletedDocs > 0) { - LOGGER.info("Successfully deleted {} documents created by user {}", deletedDocs, name); - listener.onResponse(true); - } else { - LOGGER.info("No documents found for user {}", name); - // No documents matched => success = false - listener.onResponse(false); - } - } - - @Override - public void onFailure(Exception e) { - LOGGER.error("Failed to delete documents for user {}", name, e); - listener.onFailure(e); - } - }); - } catch (Exception e) { - LOGGER.error("Failed to delete documents for user {} before request submission", name, e); - listener.onFailure(e); - } - } - - /** - * Fetches all documents from the specified resource index and deserializes them into the specified class. - * - * @param resourceIndex The resource index to fetch documents from. - * @param parser The class to deserialize the documents into a specified type defined by the parser. - * @param listener The listener to be notified with the set of deserialized documents. - * @param The type of the deserialized documents. - */ - public void getResourceDocumentsFromIds( - Set resourceIds, - String resourceIndex, - ResourceParser parser, - ActionListener> listener - ) { - if (resourceIds.isEmpty()) { - listener.onResponse(new HashSet<>()); - return; - } - - // stashing Context to avoid permission issues in-case resourceIndex is a system index - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - MultiGetRequest request = new MultiGetRequest(); - for (String id : resourceIds) { - request.add(new MultiGetRequest.Item(resourceIndex, id)); - } - - client.multiGet(request, ActionListener.wrap(response -> { - Set result = new HashSet<>(); - try { - for (MultiGetItemResponse itemResponse : response.getResponses()) { - if (!itemResponse.isFailed() && itemResponse.getResponse().isExists()) { - BytesReference sourceAsString = itemResponse.getResponse().getSourceAsBytesRef(); - XContentParser xContentParser = XContentHelper.createParser( - NamedXContentRegistry.EMPTY, - LoggingDeprecationHandler.INSTANCE, - sourceAsString, - XContentType.JSON - ); - T resource = parser.parseXContent(xContentParser); - result.add(resource); - } - } - listener.onResponse(result); - } catch (Exception e) { - listener.onFailure(new ResourceSharingException("Failed to parse resources: " + e.getMessage(), e)); - } - }, e -> { - if (e instanceof IndexNotFoundException) { - LOGGER.error("Index {} does not exist", resourceIndex, e); - listener.onFailure(e); - } else { - LOGGER.error("Failed to fetch resources with ids {} from index {}", resourceIds, resourceIndex, e); - listener.onFailure(new ResourceSharingException("Failed to fetch resources: " + e.getMessage(), e)); - } - })); - } - } - -} diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java deleted file mode 100644 index eb0447e7b4..0000000000 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexListener.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.resources; - -import java.io.IOException; -import java.util.Objects; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.core.action.ActionListener; -import org.opensearch.core.index.shard.ShardId; -import org.opensearch.index.engine.Engine; -import org.opensearch.index.shard.IndexingOperationListener; -import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.auth.UserSubjectImpl; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.client.Client; - -/** - * This class implements an index operation listener for operations performed on resources stored in plugin's indices - * These indices are defined on bootstrap and configured to listen in OpenSearchSecurityPlugin.java - */ -public class ResourceSharingIndexListener implements IndexingOperationListener { - - private final static Logger log = LogManager.getLogger(ResourceSharingIndexListener.class); - - private static final ResourceSharingIndexListener INSTANCE = new ResourceSharingIndexListener(); - private ResourceSharingIndexHandler resourceSharingIndexHandler; - - private boolean initialized; - - private ThreadPool threadPool; - - private ResourceSharingIndexListener() {} - - public static ResourceSharingIndexListener getInstance() { - return ResourceSharingIndexListener.INSTANCE; - } - - /** - * Initializes the ResourceSharingIndexListener with the provided ThreadPool and Client. - * This method is called during the plugin's initialization process. - * - * @param threadPool The ThreadPool instance to be used for executing operations. - * @param client The Client instance to be used for interacting with OpenSearch. - */ - public void initialize(ThreadPool threadPool, Client client, AuditLog auditLog) { - - if (initialized) { - return; - } - - initialized = true; - this.threadPool = threadPool; - this.resourceSharingIndexHandler = new ResourceSharingIndexHandler( - ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX, - client, - threadPool, - auditLog - ); - - } - - public boolean isInitialized() { - return initialized; - } - - /** - * This method is called after an index operation is performed. - * It creates a resource sharing entry in the dedicated resource sharing index. - * @param shardId The shard ID of the index where the operation was performed. - * @param index The index where the operation was performed. - * @param result The result of the index operation. - */ - @Override - public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { - - String resourceIndex = shardId.getIndexName(); - log.info("postIndex called on {}", resourceIndex); - - String resourceId = index.id(); - - final UserSubjectImpl userSubject = (UserSubjectImpl) threadPool.getThreadContext() - .getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); - final User user = userSubject.getUser(); - try { - Objects.requireNonNull(user); - ResourceSharing sharing = this.resourceSharingIndexHandler.indexResourceSharing( - resourceId, - resourceIndex, - new CreatedBy(Creator.USER, user.getName()), - null - ); - log.info("Successfully created a resource sharing entry {}", sharing); - } catch (IOException e) { - log.info("Failed to create a resource sharing entry for resource: {}", resourceId); - } - } - - /** - * This method is called after a delete operation is performed. - * It deletes the corresponding resource sharing entry from the dedicated resource sharing index. - * @param shardId The shard ID of the index where the delete operation was performed. - * @param delete The delete operation that was performed. - * @param result The result of the delete operation. - */ - @Override - public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { - - String resourceIndex = shardId.getIndexName(); - log.info("postDelete called on {}", resourceIndex); - - String resourceId = delete.id(); - - this.resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex, ActionListener.wrap(deleted -> { - if (deleted) { - log.info("Successfully deleted resource sharing entry for resource {}", resourceId); - } else { - log.info("No resource sharing entry found for resource {}", resourceId); - } - }, exception -> log.error("Failed to delete resource sharing entry for resource {}", resourceId, exception))); - } -} diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java deleted file mode 100644 index 9ad7e18975..0000000000 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.resources; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -public class ResourceSharingIndexManagementRepository { - - private static final Logger log = LogManager.getLogger(ResourceSharingIndexManagementRepository.class); - - private final ResourceSharingIndexHandler resourceSharingIndexHandler; - private final boolean resourceSharingEnabled; - - protected ResourceSharingIndexManagementRepository( - final ResourceSharingIndexHandler resourceSharingIndexHandler, - boolean isResourceSharingEnabled - ) { - this.resourceSharingIndexHandler = resourceSharingIndexHandler; - this.resourceSharingEnabled = isResourceSharingEnabled; - } - - public static ResourceSharingIndexManagementRepository create( - ResourceSharingIndexHandler resourceSharingIndexHandler, - boolean isResourceSharingEnabled - ) { - return new ResourceSharingIndexManagementRepository(resourceSharingIndexHandler, isResourceSharingEnabled); - } - - /** - * Creates the resource sharing index if it doesn't already exist. - * This method is called during the initialization phase of the repository. - * It ensures that the index is set up with the necessary mappings and settings - * before any operations are performed on the index. - */ - public void createResourceSharingIndexIfAbsent() { - // TODO check if this should be wrapped in an atomic completable future - if (resourceSharingEnabled) { - log.info("Attempting to create Resource Sharing index"); - this.resourceSharingIndexHandler.createResourceSharingIndexIfAbsent(() -> null); - } - - } -} diff --git a/src/main/java/org/opensearch/security/resources/ShareWith.java b/src/main/java/org/opensearch/security/resources/ShareWith.java deleted file mode 100644 index 2a8e047761..0000000000 --- a/src/main/java/org/opensearch/security/resources/ShareWith.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.resources; - -import java.io.IOException; -import java.util.HashSet; -import java.util.Set; - -import org.opensearch.core.common.io.stream.NamedWriteable; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentFragment; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.core.xcontent.XContentParser; - -/** - * - * This class contains information about whom a resource is shared with and at what scope. - * Example: - * "share_with": { - * "read_only": { - * "users": [], - * "roles": [], - * "backend_roles": [] - * }, - * "read_write": { - * "users": [], - * "roles": [], - * "backend_roles": [] - * } - * } - * - * @opensearch.experimental - */ -public class ShareWith implements ToXContentFragment, NamedWriteable { - - /** - * A set of objects representing the scopes and their associated users, roles, and backend roles. - */ - private final Set sharedWithScopes; - - public ShareWith(Set sharedWithScopes) { - this.sharedWithScopes = sharedWithScopes; - } - - public ShareWith(StreamInput in) throws IOException { - this.sharedWithScopes = in.readSet(SharedWithScope::new); - } - - public Set getSharedWithScopes() { - return sharedWithScopes; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - - for (SharedWithScope scope : sharedWithScopes) { - scope.toXContent(builder, params); - } - - return builder.endObject(); - } - - public static ShareWith fromXContent(XContentParser parser) throws IOException { - Set sharedWithScopes = new HashSet<>(); - - if (parser.currentToken() != XContentParser.Token.START_OBJECT) { - parser.nextToken(); - } - - XContentParser.Token token; - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - // Each field in the object represents a SharedWithScope - if (token == XContentParser.Token.FIELD_NAME) { - SharedWithScope scope = SharedWithScope.fromXContent(parser); - sharedWithScopes.add(scope); - } - } - - return new ShareWith(sharedWithScopes); - } - - @Override - public String getWriteableName() { - return "share_with"; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeCollection(sharedWithScopes); - } - - @Override - public String toString() { - return "ShareWith " + sharedWithScopes; - } -} diff --git a/src/main/java/org/opensearch/security/resources/SharedWithScope.java b/src/main/java/org/opensearch/security/resources/SharedWithScope.java deleted file mode 100644 index 5a0bbc01b4..0000000000 --- a/src/main/java/org/opensearch/security/resources/SharedWithScope.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.resources; - -import java.io.IOException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.opensearch.core.common.io.stream.NamedWriteable; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentFragment; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.core.xcontent.XContentParser; - -/** - * This class represents the scope at which a resource is shared with. - * Example: - * "read_only": { - * "users": [], - * "roles": [], - * "backend_roles": [] - * } - * where "users", "roles" and "backend_roles" are the recipient entities - * - * @opensearch.experimental - */ -public class SharedWithScope implements ToXContentFragment, NamedWriteable { - - private final String scope; - - private final ScopeRecipients scopeRecipients; - - public SharedWithScope(String scope, ScopeRecipients scopeRecipients) { - this.scope = scope; - this.scopeRecipients = scopeRecipients; - } - - public SharedWithScope(StreamInput in) throws IOException { - this.scope = in.readString(); - this.scopeRecipients = new ScopeRecipients(in); - } - - public String getScope() { - return scope; - } - - public ScopeRecipients getSharedWithPerScope() { - return scopeRecipients; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.field(scope); - builder.startObject(); - - scopeRecipients.toXContent(builder, params); - - return builder.endObject(); - } - - public static SharedWithScope fromXContent(XContentParser parser) throws IOException { - String scope = parser.currentName(); - - parser.nextToken(); - - ScopeRecipients scopeRecipients = ScopeRecipients.fromXContent(parser); - - return new SharedWithScope(scope, scopeRecipients); - } - - @Override - public String toString() { - return "{" + scope + ": " + scopeRecipients + '}'; - } - - @Override - public String getWriteableName() { - return "shared_with_scope"; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(scope); - out.writeNamedWriteable(scopeRecipients); - } - - /** - * This class represents the entities with whom a resource is shared with for a given scope. - * - * @opensearch.experimental - */ - public static class ScopeRecipients implements ToXContentFragment, NamedWriteable { - - private final Map> recipients; - - public ScopeRecipients(Map> recipients) { - if (recipients == null) { - throw new IllegalArgumentException("Recipients map cannot be null"); - } - this.recipients = recipients; - } - - public ScopeRecipients(StreamInput in) throws IOException { - this.recipients = in.readMap( - key -> RecipientTypeRegistry.fromValue(key.readString()), - input -> input.readSet(StreamInput::readString) - ); - } - - public Map> getRecipients() { - return recipients; - } - - @Override - public String getWriteableName() { - return "scope_recipients"; - } - - public static ScopeRecipients fromXContent(XContentParser parser) throws IOException { - Map> recipients = new HashMap<>(); - - XContentParser.Token token; - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - String fieldName = parser.currentName(); - RecipientType recipientType = RecipientTypeRegistry.fromValue(fieldName); - - parser.nextToken(); - Set values = new HashSet<>(); - while (parser.nextToken() != XContentParser.Token.END_ARRAY) { - values.add(parser.text()); - } - recipients.put(recipientType, values); - } - } - - return new ScopeRecipients(recipients); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeMap( - recipients, - (streamOutput, recipientType) -> streamOutput.writeString(recipientType.type()), - (streamOutput, strings) -> streamOutput.writeCollection(strings, StreamOutput::writeString) - ); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - if (recipients.isEmpty()) { - return builder; - } - for (Map.Entry> entry : recipients.entrySet()) { - builder.array(entry.getKey().type(), entry.getValue().toArray()); - } - return builder; - } - } -} diff --git a/src/main/java/org/opensearch/security/resources/package-info.java b/src/main/java/org/opensearch/security/resources/package-info.java deleted file mode 100644 index 855bdf81af..0000000000 --- a/src/main/java/org/opensearch/security/resources/package-info.java +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.resources; diff --git a/src/main/java/org/opensearch/security/rest/resources/access/ResourceAccessRestAction.java b/src/main/java/org/opensearch/security/rest/resources/access/ResourceAccessRestAction.java deleted file mode 100644 index ecc7d8fbc9..0000000000 --- a/src/main/java/org/opensearch/security/rest/resources/access/ResourceAccessRestAction.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.rest.resources.access; - -import java.io.IOException; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import com.google.common.collect.ImmutableList; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.common.xcontent.LoggingDeprecationHandler; -import org.opensearch.common.xcontent.XContentFactory; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.core.action.ActionListener; -import org.opensearch.core.rest.RestStatus; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestRequest; -import org.opensearch.security.resources.RecipientType; -import org.opensearch.security.resources.RecipientTypeRegistry; -import org.opensearch.security.resources.ResourceAccessHandler; -import org.opensearch.security.resources.ResourceSharing; -import org.opensearch.security.resources.ShareWith; -import org.opensearch.security.spi.resources.Resource; -import org.opensearch.transport.client.node.NodeClient; - -import static org.opensearch.rest.RestRequest.Method.GET; -import static org.opensearch.rest.RestRequest.Method.POST; -import static org.opensearch.security.dlic.rest.api.Responses.badRequest; -import static org.opensearch.security.dlic.rest.api.Responses.forbidden; -import static org.opensearch.security.dlic.rest.api.Responses.ok; -import static org.opensearch.security.dlic.rest.api.Responses.unauthorized; -import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_RESOURCE_ROUTE_PREFIX; -import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; - -/** - * This class handles the REST API for resource access management. - */ -public class ResourceAccessRestAction extends BaseRestHandler { - private static final Logger LOGGER = LogManager.getLogger(ResourceAccessRestAction.class); - - private final ResourceAccessHandler resourceAccessHandler; - - public ResourceAccessRestAction(ResourceAccessHandler resourceAccessHandler) { - this.resourceAccessHandler = resourceAccessHandler; - } - - @Override - public List routes() { - return addRoutesPrefix( - ImmutableList.of( - new Route(GET, "/list/{resourceIndex}"), - new Route(POST, "/revoke"), - new Route(POST, "/share"), - new Route(POST, "/verify_access") - ), - PLUGIN_RESOURCE_ROUTE_PREFIX - ); - } - - @Override - public String getName() { - return "resource_api_action"; - } - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - consumeParams(request); // early consume params to avoid 400s - String path = request.path().split(PLUGIN_RESOURCE_ROUTE_PREFIX)[1].split("/")[1]; - return switch (path) { - case "list" -> channel -> handleListResources(request, channel); - case "revoke" -> channel -> handleRevokeResource(request, channel); - case "share" -> channel -> handleShareResource(request, channel); - case "verify_access" -> channel -> handleVerifyRequest(request, channel); - default -> channel -> badRequest(channel, "Unknown route: " + path); - }; - } - - /** - * Consume params early to avoid 400s. - * @param request from which the params must be consumed - */ - private void consumeParams(RestRequest request) { - request.param("resourceIndex", ""); - } - - /** - * Handle the list resources request. - * @param request the request to handle - * @param channel the channel to send the response to - */ - private void handleListResources(RestRequest request, RestChannel channel) { - String resourceIndex = request.param("resourceIndex", ""); - resourceAccessHandler.getAccessibleResourcesForCurrentUser( - resourceIndex, - ActionListener.wrap(resources -> sendResponse(channel, resources), e -> handleError(channel, e.getMessage(), e)) - ); - } - - /** - * Handle the share resource request. - * @param request the request to handle - * @param channel the channel to send the response to - * @throws IOException if an I/O error occurs - */ - private void handleShareResource(RestRequest request, RestChannel channel) throws IOException { - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - String resourceId = (String) source.get("resource_id"); - String resourceIndex = (String) source.get("resource_index"); - - ShareWith shareWith = parseShareWith(source); - resourceAccessHandler.shareWith( - resourceId, - resourceIndex, - shareWith, - ActionListener.wrap(response -> sendResponse(channel, response), e -> handleError(channel, e.getMessage(), e)) - ); - } - - /** - * Handle the revoke resource request. - * @param request the request to handle - * @param channel the channel to send the response to - * @throws IOException if an I/O error occurs - */ - @SuppressWarnings("unchecked") - private void handleRevokeResource(RestRequest request, RestChannel channel) throws IOException { - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - - String resourceId = (String) source.get("resource_id"); - String resourceIndex = (String) source.get("resource_index"); - - Map> revokeSource = (Map>) source.get("entities"); - Map> revoke = revokeSource.entrySet() - .stream() - .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); - Set scopes = new HashSet<>(source.containsKey("scopes") ? (List) source.get("scopes") : List.of()); - resourceAccessHandler.revokeAccess( - resourceId, - resourceIndex, - revoke, - scopes, - ActionListener.wrap(response -> sendResponse(channel, response), e -> handleError(channel, e.getMessage(), e)) - ); - } - - /** - * Handle the verify request. - * @param request the request to handle - * @param channel the channel to send the response to - * @throws IOException if an I/O error occurs - */ - private void handleVerifyRequest(RestRequest request, RestChannel channel) throws IOException { - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - - String resourceId = (String) source.get("resource_id"); - String resourceIndex = (String) source.get("resource_index"); - String scope = (String) source.get("scope"); - - resourceAccessHandler.hasPermission( - resourceId, - resourceIndex, - scope, - ActionListener.wrap(response -> sendResponse(channel, response), e -> handleError(channel, e.getMessage(), e)) - ); - } - - /** - * Parse the share with structure from the request body. - * @param source the request body - * @return the parsed ShareWith object - * @throws IOException if an I/O error occurs - */ - @SuppressWarnings("unchecked") - private ShareWith parseShareWith(Map source) throws IOException { - Map shareWithMap = (Map) source.get("share_with"); - if (shareWithMap == null || shareWithMap.isEmpty()) { - throw new IllegalArgumentException("share_with is required and cannot be empty"); - } - - String jsonString = XContentFactory.jsonBuilder().map(shareWithMap).toString(); - - try ( - XContentParser parser = XContentType.JSON.xContent() - .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, jsonString) - ) { - return ShareWith.fromXContent(parser); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); - } - } - - /** - * Send the appropriate response to the channel. - * @param channel the channel to send the response to - * @param response the response to send - * @throws IOException if an I/O error occurs - */ - @SuppressWarnings("unchecked") - private void sendResponse(RestChannel channel, Object response) throws IOException { - if (response instanceof Set) { // list - Set resources = (Set) response; - ok(channel, (builder, params) -> builder.startObject().field("resources", resources).endObject()); - } else if (response instanceof ResourceSharing resourceSharing) { // share & revoke - ok(channel, (resourceSharing::toXContent)); - } else if (response instanceof Boolean) { // verify_access - ok(channel, (builder, params) -> builder.startObject().field("has_permission", String.valueOf(response)).endObject()); - } - } - - /** - * Handle errors that occur during request processing. - * @param channel the channel to send the error response to - * @param message the error message - * @param e the exception that caused the error - */ - private void handleError(RestChannel channel, String message, Exception e) { - LOGGER.error(message, e); - if (message.contains("not authorized")) { - forbidden(channel, message); - } else if (message.contains("no authenticated")) { - unauthorized(channel); - } - channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, message)); - } -} From cf6da27e9155052e873ab04995a3e04326acec97 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 14:49:10 -0500 Subject: [PATCH 138/201] Introduces a common library which contains all information related to resource access rest and transport handlers Signed-off-by: Darshit Chanpura --- common/build.gradle | 111 ++ .../security/common/DefaultObjectMapper.java | 298 ++++ .../common/auditlog/impl/AuditCategory.java | 40 + .../security/common/auth/UserSubjectImpl.java | 55 + .../common/configuration/AdminDNs.java | 162 ++ .../common/dlic/rest/api/Responses.java | 106 ++ .../security/common/resources/CreatedBy.java | 89 ++ .../security/common/resources/Creator.java | 32 + .../security/common/resources/Recipient.java | 25 + .../common/resources/RecipientType.java | 24 + .../resources/RecipientTypeRegistry.java | 33 + .../resources/ResourceAccessHandler.java | 581 +++++++ .../common/resources/ResourcePluginInfo.java | 54 + .../common/resources/ResourceSharing.java | 206 +++ .../resources/ResourceSharingConstants.java | 16 + .../resources/ResourceSharingException.java | 39 + .../ResourceSharingIndexHandler.java | 1393 +++++++++++++++++ .../ResourceSharingIndexListener.java | 168 ++ ...ourceSharingIndexManagementRepository.java | 53 + .../security/common/resources/ShareWith.java | 103 ++ .../common/resources/SharedWithScope.java | 169 ++ .../common/resources/package-info.java | 14 + .../resources/rest/ResourceAccessAction.java | 22 + .../resources/rest/ResourceAccessRequest.java | 154 ++ .../rest/ResourceAccessRequestParams.java | 26 + .../rest/ResourceAccessResponse.java | 96 ++ .../rest/ResourceAccessRestAction.java | 146 ++ .../rest/ResourceAccessTransportAction.java | 101 ++ .../common/support/ConfigConstants.java | 399 +++++ .../security/common/support/Utils.java | 285 ++++ .../common/support/WildcardMatcher.java | 556 +++++++ .../security/common/user/AuthCredentials.java | 254 +++ .../common/user/CustomAttributesAware.java | 34 + .../opensearch/security/common/user/User.java | 312 ++++ .../security/common/auth/UserSubjectImpl.java | 55 + 35 files changed, 6211 insertions(+) create mode 100644 common/build.gradle create mode 100644 common/src/main/java/org/opensearch/security/common/DefaultObjectMapper.java create mode 100644 common/src/main/java/org/opensearch/security/common/auditlog/impl/AuditCategory.java create mode 100644 common/src/main/java/org/opensearch/security/common/auth/UserSubjectImpl.java create mode 100644 common/src/main/java/org/opensearch/security/common/configuration/AdminDNs.java create mode 100644 common/src/main/java/org/opensearch/security/common/dlic/rest/api/Responses.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/CreatedBy.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/Creator.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/Recipient.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/RecipientType.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/RecipientTypeRegistry.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceSharing.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceSharingException.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexListener.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ShareWith.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/SharedWithScope.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/package-info.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java create mode 100644 common/src/main/java/org/opensearch/security/common/support/ConfigConstants.java create mode 100644 common/src/main/java/org/opensearch/security/common/support/Utils.java create mode 100644 common/src/main/java/org/opensearch/security/common/support/WildcardMatcher.java create mode 100644 common/src/main/java/org/opensearch/security/common/user/AuthCredentials.java create mode 100644 common/src/main/java/org/opensearch/security/common/user/CustomAttributesAware.java create mode 100644 common/src/main/java/org/opensearch/security/common/user/User.java create mode 100644 common/test/java/org/opensearch/security/common/auth/UserSubjectImpl.java diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000000..ecbbffd75a --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,111 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + id 'java' + id 'maven-publish' +} + +ext { + opensearch_version = System.getProperty("opensearch.version", "3.0.0-alpha1-SNAPSHOT") + isSnapshot = "true" == System.getProperty("build.snapshot", "true") + buildVersionQualifier = System.getProperty("build.version_qualifier", "alpha1") + + // 2.0.0-rc1-SNAPSHOT -> 2.0.0.0-rc1-SNAPSHOT + version_tokens = opensearch_version.tokenize('-') + opensearch_build = version_tokens[0] + '.0' + + common_utils_version = System.getProperty("common_utils.version", '3.0.0.0-alpha1-SNAPSHOT') + + kafka_version = '3.7.1' + open_saml_version = '5.1.3' + open_saml_shib_version = "9.1.3" + one_login_java_saml = '2.9.0' + jjwt_version = '0.12.6' + guava_version = '33.4.0-jre' + jaxb_version = '2.3.9' + spring_version = '5.3.39' + + if (buildVersionQualifier) { + opensearch_build += "-${buildVersionQualifier}" + } + if (isSnapshot) { + opensearch_build += "-SNAPSHOT" + } +} + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } +} + +dependencies { + // Main implementation dependencies + compileOnly "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" + compileOnly "org.opensearch.plugin:lang-painless:${opensearch_version}" +// compileOnly "org.opensearch:opensearch:${opensearch_version}" + compileOnly "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" + compileOnly "com.google.guava:guava:${guava_version}" + compileOnly "org.apache.commons:commons-lang3:${versions.commonslang}" + compileOnly 'com.password4j:password4j:1.8.2' +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +task sourcesJar(type: Jar) { + archiveClassifier.set 'sources' + from sourceSets.main.allJava +} + +task javadocJar(type: Jar) { + archiveClassifier.set 'javadoc' + from tasks.javadoc +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar + pom { + name.set("OpenSearch Security Common") + description.set("OpenSearch Security Common") + url.set("https://github.com/opensearch-project/security") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + scm { + connection.set("scm:git@github.com:opensearch-project/security.git") + developerConnection.set("scm:git@github.com:opensearch-project/security.git") + url.set("https://github.com/opensearch-project/security.git") + } + developers { + developer { + name.set("OpenSearch Contributors") + url.set("https://github.com/opensearch-project") + } + } + } + } + } + repositories { + maven { + name = "Snapshots" + url = "https://aws.oss.sonatype.org/content/repositories/snapshots" + credentials { + username "$System.env.SONATYPE_USERNAME" + password "$System.env.SONATYPE_PASSWORD" + } + } + } +} diff --git a/common/src/main/java/org/opensearch/security/common/DefaultObjectMapper.java b/common/src/main/java/org/opensearch/security/common/DefaultObjectMapper.java new file mode 100644 index 0000000000..7a2dc137a6 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/DefaultObjectMapper.java @@ -0,0 +1,298 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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.common; + +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.InjectableValues; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import org.opensearch.SpecialPermission; + +class ConfigMapSerializer extends StdSerializer> { + private static final Set SENSITIVE_CONFIG_KEYS = Set.of("password"); + + @SuppressWarnings("unchecked") + public ConfigMapSerializer() { + // Pass Map.class to the superclass + super((Class>) (Class) Map.class); + } + + @Override + public void serialize(Map value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + for (Map.Entry entry : value.entrySet()) { + if (SENSITIVE_CONFIG_KEYS.contains(entry.getKey())) { + gen.writeStringField(entry.getKey(), "******"); // Redact + } else { + gen.writeObjectField(entry.getKey(), entry.getValue()); + } + } + gen.writeEndObject(); + } +} + +public class DefaultObjectMapper { + public static final ObjectMapper objectMapper = new ObjectMapper(); + public final static ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); + private static final ObjectMapper defaulOmittingObjectMapper = new ObjectMapper(); + + static { + objectMapper.setSerializationInclusion(Include.NON_NULL); + // exclude sensitive information from the request body, + // if jackson cant parse the entity, e.g. passwords, hashes and so on, + // but provides which property is unknown + objectMapper.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); + defaulOmittingObjectMapper.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); + YAML_MAPPER.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); + // objectMapper.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); + objectMapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); + defaulOmittingObjectMapper.setSerializationInclusion(Include.NON_DEFAULT); + defaulOmittingObjectMapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); + YAML_MAPPER.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); + } + + private DefaultObjectMapper() {} + + public static void inject(final InjectableValues.Std injectableValues) { + objectMapper.setInjectableValues(injectableValues); + YAML_MAPPER.setInjectableValues(injectableValues); + defaulOmittingObjectMapper.setInjectableValues(injectableValues); + } + + public static boolean getOrDefault(Map properties, String key, boolean defaultValue) throws JsonProcessingException { + Object value = properties.get(key); + if (value == null) { + return defaultValue; + } else if (value instanceof Boolean) { + return (boolean) value; + } else if (value instanceof String) { + String text = ((String) value).trim(); + if ("true".equals(text) || "True".equals(text)) { + return true; + } + if ("false".equals(text) || "False".equals(text)) { + return false; + } + throw InvalidFormatException.from( + null, + "Cannot deserialize value of type 'boolean' from String \"" + text + "\": only \"true\" or \"false\" recognized)", + null, + Boolean.class + ); + } + throw MismatchedInputException.from( + null, + Boolean.class, + "Cannot deserialize instance of 'boolean' out of '" + value + "' (Property: " + key + ")" + ); + } + + @SuppressWarnings("unchecked") + public static T getOrDefault(Map properties, String key, T defaultValue) { + T value = (T) properties.get(key); + return value != null ? value : defaultValue; + } + + @SuppressWarnings("removal") + public static T readTree(JsonNode node, Class clazz) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.treeToValue(node, clazz)); + } catch (final PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + } + + @SuppressWarnings("removal") + public static T readValue(String string, Class clazz) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.readValue(string, clazz)); + } catch (final PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + } + + @SuppressWarnings("removal") + public static JsonNode readTree(String string) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.readTree(string)); + } catch (final PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + } + + @SuppressWarnings("removal") + public static String writeValueAsString(Object value, boolean omitDefaults) throws JsonProcessingException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged( + (PrivilegedExceptionAction) () -> (omitDefaults ? defaulOmittingObjectMapper : objectMapper).writeValueAsString( + value + ) + ); + } catch (final PrivilegedActionException e) { + throw (JsonProcessingException) e.getCause(); + } + + } + + @SuppressWarnings("removal") + public static String writeValueAsStringAndRedactSensitive(Object value) throws JsonProcessingException { + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + SimpleModule module = new SimpleModule(); + module.addSerializer(new ConfigMapSerializer()); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(module); + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> mapper.writeValueAsString(value)); + } catch (final PrivilegedActionException e) { + throw (JsonProcessingException) e.getCause(); + } + + } + + @SuppressWarnings("removal") + public static T readValue(String string, TypeReference tr) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged(new PrivilegedExceptionAction() { + @Override + public T run() throws Exception { + return objectMapper.readValue(string, tr); + } + }); + } catch (final PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + + } + + @SuppressWarnings("removal") + public static T readValue(String string, JavaType jt) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.readValue(string, jt)); + } catch (final PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + } + + @SuppressWarnings("removal") + public static T convertValue(JsonNode jsonNode, JavaType jt) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.convertValue(jsonNode, jt)); + } catch (final PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + } + + public static TypeFactory getTypeFactory() { + return objectMapper.getTypeFactory(); + } + + public static Set getFields(Class cls) { + return objectMapper.getSerializationConfig() + .introspect(getTypeFactory().constructType(cls)) + .findProperties() + .stream() + .map(BeanPropertyDefinition::getName) + .collect(ImmutableSet.toImmutableSet()); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/auditlog/impl/AuditCategory.java b/common/src/main/java/org/opensearch/security/common/auditlog/impl/AuditCategory.java new file mode 100644 index 0000000000..3526404bbd --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/auditlog/impl/AuditCategory.java @@ -0,0 +1,40 @@ +/* + * 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.common.auditlog.impl; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; + +public enum AuditCategory { + BAD_HEADERS, + FAILED_LOGIN, + MISSING_PRIVILEGES, + GRANTED_PRIVILEGES, + OPENDISTRO_SECURITY_INDEX_ATTEMPT, + SSL_EXCEPTION, + AUTHENTICATED, + INDEX_EVENT, + COMPLIANCE_DOC_READ, + COMPLIANCE_DOC_WRITE, + COMPLIANCE_EXTERNAL_CONFIG, + COMPLIANCE_INTERNAL_CONFIG_READ, + COMPLIANCE_INTERNAL_CONFIG_WRITE; + + public static Set parse(final Collection categories) { + if (categories.isEmpty()) return Collections.emptySet(); + + return categories.stream().map(String::toUpperCase).map(AuditCategory::valueOf).collect(ImmutableSet.toImmutableSet()); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/auth/UserSubjectImpl.java b/common/src/main/java/org/opensearch/security/common/auth/UserSubjectImpl.java new file mode 100644 index 0000000000..620250be53 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/auth/UserSubjectImpl.java @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security.common.auth; + +import java.security.Principal; +import java.util.concurrent.Callable; + +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.identity.NamedPrincipal; +import org.opensearch.identity.UserSubject; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.security.common.support.ConfigConstants; +import org.opensearch.security.common.user.User; +import org.opensearch.threadpool.ThreadPool; + +public class UserSubjectImpl implements UserSubject { + private final NamedPrincipal userPrincipal; + private final ThreadPool threadPool; + private final User user; + + public UserSubjectImpl(ThreadPool threadPool, User user) { + this.threadPool = threadPool; + this.user = user; + this.userPrincipal = new NamedPrincipal(user.getName()); + } + + @Override + public void authenticate(AuthToken authToken) { + // not implemented + } + + @Override + public Principal getPrincipal() { + return userPrincipal; + } + + @Override + public T runAs(Callable callable) throws Exception { + try (ThreadContext.StoredContext ctx = threadPool.getThreadContext().stashContext()) { + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); + return callable.call(); + } + } + + public User getUser() { + return user; + } +} diff --git a/common/src/main/java/org/opensearch/security/common/configuration/AdminDNs.java b/common/src/main/java/org/opensearch/security/common/configuration/AdminDNs.java new file mode 100644 index 0000000000..22647e6685 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/configuration/AdminDNs.java @@ -0,0 +1,162 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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.common.configuration; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +import com.google.common.collect.ImmutableMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.common.support.ConfigConstants; +import org.opensearch.security.common.support.WildcardMatcher; +import org.opensearch.security.common.user.User; + +public class AdminDNs { + + protected final Logger log = LogManager.getLogger(AdminDNs.class); + private final Set adminDn = new HashSet(); + private final Set adminUsernames = new HashSet(); + private final Map allowedDnsImpersonations; + private final Map allowedRestImpersonations; + private boolean injectUserEnabled; + private boolean injectAdminUserEnabled; + + public AdminDNs(final Settings settings) { + + this.injectUserEnabled = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false); + this.injectAdminUserEnabled = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED, false); + + final List adminDnsA = settings.getAsList(ConfigConstants.SECURITY_AUTHCZ_ADMIN_DN, Collections.emptyList()); + + for (String dn : adminDnsA) { + try { + log.debug("{} is registered as an admin dn", dn); + adminDn.add(new LdapName(dn)); + } catch (final InvalidNameException e) { + // make sure to log correctly depending on user injection settings + if (injectUserEnabled && injectAdminUserEnabled) { + if (log.isDebugEnabled()) { + log.debug("Admin DN not an LDAP name, but admin user injection enabled. Will add {} to admin usernames", dn); + } + adminUsernames.add(dn); + } else { + log.error("Unable to parse admin dn {}", dn, e); + } + } + } + + log.debug("Loaded {} admin DN's {}", adminDn.size(), adminDn); + + final Settings impersonationDns = settings.getByPrefix(ConfigConstants.SECURITY_AUTHCZ_IMPERSONATION_DN + "."); + + allowedDnsImpersonations = impersonationDns.keySet() + .stream() + .map(this::toLdapName) + .filter(Objects::nonNull) + .collect( + ImmutableMap.toImmutableMap( + Function.identity(), + ldapName -> WildcardMatcher.from(settings.getAsList(ConfigConstants.SECURITY_AUTHCZ_IMPERSONATION_DN + "." + ldapName)) + ) + ); + + log.debug("Loaded {} impersonation DN's {}", allowedDnsImpersonations.size(), allowedDnsImpersonations); + + final Settings impersonationUsersRest = settings.getByPrefix(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + "."); + + allowedRestImpersonations = impersonationUsersRest.keySet() + .stream() + .collect( + ImmutableMap.toImmutableMap( + Function.identity(), + user -> WildcardMatcher.from(settings.getAsList(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + "." + user)) + ) + ); + + log.debug("Loaded {} impersonation users for REST {}", allowedRestImpersonations.size(), allowedRestImpersonations); + } + + private LdapName toLdapName(String dn) { + try { + return new LdapName(dn); + } catch (final InvalidNameException e) { + log.error("Unable to parse allowedImpersonations dn {}", dn, e); + } + return null; + } + + public boolean isAdmin(User user) { + if (isAdminDN(user.getName())) { + return true; + } + + // ThreadContext injected user, may be admin user, only if both flags are enabled and user is injected + if (injectUserEnabled && injectAdminUserEnabled && user.isInjected() && adminUsernames.contains(user.getName())) { + return true; + } + return false; + } + + public boolean isAdminDN(String dn) { + + if (dn == null) return false; + + try { + return isAdminDN(new LdapName(dn)); + } catch (InvalidNameException e) { + return false; + } + } + + private boolean isAdminDN(LdapName dn) { + if (dn == null) return false; + + boolean isAdmin = adminDn.contains(dn); + + if (log.isTraceEnabled()) { + log.trace("Is principal {} an admin cert? {}", dn.toString(), isAdmin); + } + + return isAdmin; + } + + public boolean isRestImpersonationAllowed(final String originalUser, final String impersonated) { + return (originalUser != null) + ? allowedRestImpersonations.getOrDefault(originalUser, WildcardMatcher.NONE).test(impersonated) + : false; + } +} diff --git a/common/src/main/java/org/opensearch/security/common/dlic/rest/api/Responses.java b/common/src/main/java/org/opensearch/security/common/dlic/rest/api/Responses.java new file mode 100644 index 0000000000..e2258e9e6e --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/dlic/rest/api/Responses.java @@ -0,0 +1,106 @@ +/* + * 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.common.dlic.rest.api; + +import java.io.IOException; + +import org.opensearch.ExceptionsHelper; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; + +public class Responses { + + public static void ok(final RestChannel channel, final String message) { + response(channel, RestStatus.OK, message); + } + + public static void ok(final RestChannel channel, final ToXContent toXContent) { + response(channel, RestStatus.OK, toXContent); + } + + public static void created(final RestChannel channel, final String message) { + response(channel, RestStatus.CREATED, message); + } + + public static void methodNotImplemented(final RestChannel channel, final RestRequest.Method method) { + notImplemented(channel, "Method " + method.name() + " not supported for this action."); + } + + public static void notImplemented(final RestChannel channel, final String message) { + response(channel, RestStatus.NOT_IMPLEMENTED, message); + } + + public static void notFound(final RestChannel channel, final String message) { + response(channel, RestStatus.NOT_FOUND, message); + } + + public static void conflict(final RestChannel channel, final String message) { + response(channel, RestStatus.CONFLICT, message); + } + + public static void internalServerError(final RestChannel channel, final String message) { + response(channel, RestStatus.INTERNAL_SERVER_ERROR, message); + } + + public static void forbidden(final RestChannel channel, final String message) { + response(channel, RestStatus.FORBIDDEN, message); + } + + public static void badRequest(final RestChannel channel, final String message) { + response(channel, RestStatus.BAD_REQUEST, message); + } + + public static void unauthorized(final RestChannel channel) { + response(channel, RestStatus.UNAUTHORIZED, "Unauthorized"); + } + + public static void response(RestChannel channel, RestStatus status, String message) { + response(channel, status, payload(status, message)); + } + + public static void response(final RestChannel channel, final RestStatus status, final ToXContent toXContent) { + try (final var builder = channel.newBuilder()) { + toXContent.toXContent(builder, ToXContent.EMPTY_PARAMS); + channel.sendResponse(new BytesRestResponse(status, builder)); + } catch (final IOException ioe) { + throw ExceptionsHelper.convertToOpenSearchException(ioe); + } + } + + public static ToXContent forbiddenMessage(final String message) { + return payload(RestStatus.FORBIDDEN, message); + } + + public static ToXContent badRequestMessage(final String message) { + return payload(RestStatus.BAD_REQUEST, message); + } + + public static ToXContent methodNotImplementedMessage(final RestRequest.Method method) { + return payload(RestStatus.NOT_FOUND, "Method " + method.name() + " not supported for this action."); + } + + public static ToXContent notFoundMessage(final String message) { + return payload(RestStatus.NOT_FOUND, message); + } + + public static ToXContent conflictMessage(final String message) { + return payload(RestStatus.CONFLICT, message); + } + + public static ToXContent payload(final RestStatus status, final String message) { + return (builder, params) -> builder.startObject().field("status", status.name()).field("message", message).endObject(); + } + +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/CreatedBy.java b/common/src/main/java/org/opensearch/security/common/resources/CreatedBy.java new file mode 100644 index 0000000000..747a5d6565 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/CreatedBy.java @@ -0,0 +1,89 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +/** + * This class is used to store information about the creator of a resource. + * Concrete implementation will be provided by security plugin + * + * @opensearch.experimental + */ +public class CreatedBy implements ToXContentFragment, NamedWriteable { + + private final Enum creatorType; + private final String creator; + + public CreatedBy(Enum creatorType, String creator) { + this.creatorType = creatorType; + this.creator = creator; + } + + public CreatedBy(StreamInput in) throws IOException { + this.creatorType = in.readEnum(Creator.class); + this.creator = in.readString(); + } + + public String getCreator() { + return creator; + } + + public Enum getCreatorType() { + return creatorType; + } + + @Override + public String toString() { + return "CreatedBy {" + this.creatorType + "='" + this.creator + '\'' + '}'; + } + + @Override + public String getWriteableName() { + return "created_by"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeEnum(Creator.valueOf(creatorType.name())); + out.writeString(creator); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().field(String.valueOf(creatorType), creator).endObject(); + } + + public static CreatedBy fromXContent(XContentParser parser) throws IOException { + String creator = null; + Enum creatorType = null; + XContentParser.Token token; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + creatorType = Creator.fromName(parser.currentName()); + } else if (token == XContentParser.Token.VALUE_STRING) { + creator = parser.text(); + } + } + + if (creator == null) { + throw new IllegalArgumentException(creatorType + " is required"); + } + + return new CreatedBy(creatorType, creator); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/Creator.java b/common/src/main/java/org/opensearch/security/common/resources/Creator.java new file mode 100644 index 0000000000..a126f5c557 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/Creator.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.common.resources; + +public enum Creator { + USER("user"); + + private final String name; + + Creator(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static Creator fromName(String name) { + for (Creator creator : values()) { + if (creator.name.equalsIgnoreCase(name)) { // Case-insensitive comparison + return creator; + } + } + throw new IllegalArgumentException("No enum constant for name: " + name); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/Recipient.java b/common/src/main/java/org/opensearch/security/common/resources/Recipient.java new file mode 100644 index 0000000000..d38b8890a1 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/Recipient.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources; + +public enum Recipient { + USERS("users"), + ROLES("roles"), + BACKEND_ROLES("backend_roles"); + + private final String name; + + Recipient(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/RecipientType.java b/common/src/main/java/org/opensearch/security/common/resources/RecipientType.java new file mode 100644 index 0000000000..6d7c09bda4 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/RecipientType.java @@ -0,0 +1,24 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources; + +/** + * This class determines a type of recipient a resource can be shared with. + * An example type would be a user or a role. + * This class is used to determine the type of recipient a resource can be shared with. + * + * @opensearch.experimental + */ +public record RecipientType(String type) { + + @Override + public String toString() { + return type; + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/RecipientTypeRegistry.java b/common/src/main/java/org/opensearch/security/common/resources/RecipientTypeRegistry.java new file mode 100644 index 0000000000..ff9b0e602a --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/RecipientTypeRegistry.java @@ -0,0 +1,33 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class determines a collection of recipient types a resource can be shared with. + * + * @opensearch.experimental + */ +public class RecipientTypeRegistry { + private static final Map REGISTRY = new HashMap<>(); + + public static void registerRecipientType(String key, RecipientType recipientType) { + REGISTRY.put(key, recipientType); + } + + public static RecipientType fromValue(String value) { + RecipientType type = REGISTRY.get(value); + if (type == null) { + throw new IllegalArgumentException("Unknown RecipientType: " + value + ". Must be 1 of these: " + REGISTRY.values()); + } + return type; + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java new file mode 100644 index 0000000000..98b9a4f910 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java @@ -0,0 +1,581 @@ +/* + * 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.common.resources; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.StepListener; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.security.common.auth.UserSubjectImpl; +import org.opensearch.security.common.configuration.AdminDNs; +import org.opensearch.security.common.support.ConfigConstants; +import org.opensearch.security.common.user.User; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.ResourceParser; +import org.opensearch.threadpool.ThreadPool; + +/** + * This class handles resource access permissions for users and roles. + * It provides methods to check if a user has permission to access a resource + * based on the resource sharing configuration. + */ +public class ResourceAccessHandler { + private static final Logger LOGGER = LogManager.getLogger(ResourceAccessHandler.class); + + private final ThreadContext threadContext; + private final ResourceSharingIndexHandler resourceSharingIndexHandler; + private final AdminDNs adminDNs; + + public ResourceAccessHandler( + final ThreadPool threadPool, + final ResourceSharingIndexHandler resourceSharingIndexHandler, + AdminDNs adminDns + ) { + this.threadContext = threadPool.getThreadContext(); + this.resourceSharingIndexHandler = resourceSharingIndexHandler; + this.adminDNs = adminDns; + } + + /** + * Initializes the recipient types for users, roles, and backend roles. + * These recipient types are used to identify the types of recipients for resource sharing. + */ + public void initializeRecipientTypes() { + RecipientTypeRegistry.registerRecipientType(Recipient.USERS.getName(), new RecipientType(Recipient.USERS.getName())); + RecipientTypeRegistry.registerRecipientType(Recipient.ROLES.getName(), new RecipientType(Recipient.ROLES.getName())); + RecipientTypeRegistry.registerRecipientType( + Recipient.BACKEND_ROLES.getName(), + new RecipientType(Recipient.BACKEND_ROLES.getName()) + ); + } + + /** + * Returns a set of accessible resource IDs for the current user within the specified resource index. + * + * @param resourceIndex The resource index to check for accessible resources. + * @param listener The listener to be notified with the set of accessible resource IDs. + */ + public void getAccessibleResourceIdsForCurrentUser(String resourceIndex, ActionListener> listener) { + final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( + ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER + ); + final User user = (userSubject == null) ? null : userSubject.getUser(); + + // If no user is authenticated, return an empty set + if (user == null) { + LOGGER.info("Unable to fetch user details."); + listener.onResponse(Collections.emptySet()); + return; + } + + LOGGER.info("Listing accessible resources within the resource index {} for user: {}", resourceIndex, user.getName()); + + // 2. If the user is admin, simply fetch all resources + if (adminDNs.isAdmin(user)) { + loadAllResources(resourceIndex, ActionListener.wrap(listener::onResponse, listener::onFailure)); + return; + } + + // StepListener for the user’s "own" resources + StepListener> ownResourcesListener = new StepListener<>(); + + // StepListener for resources shared with the user’s name + StepListener> userNameResourcesListener = new StepListener<>(); + + // StepListener for resources shared with the user’s roles + StepListener> rolesResourcesListener = new StepListener<>(); + + // StepListener for resources shared with the user’s backend roles + StepListener> backendRolesResourcesListener = new StepListener<>(); + + // Load own resources for the user. + loadOwnResources(resourceIndex, user.getName(), ownResourcesListener); + + // Load resources shared with the user by its name. + ownResourcesListener.whenComplete( + ownResources -> loadSharedWithResources( + resourceIndex, + Set.of(user.getName()), + Recipient.USERS.getName(), + userNameResourcesListener + ), + listener::onFailure + ); + + // Load resources shared with the user’s roles. + userNameResourcesListener.whenComplete( + userNameResources -> loadSharedWithResources( + resourceIndex, + user.getSecurityRoles(), + Recipient.ROLES.getName(), + rolesResourcesListener + ), + listener::onFailure + ); + + // Load resources shared with the user’s backend roles. + rolesResourcesListener.whenComplete( + rolesResources -> loadSharedWithResources( + resourceIndex, + user.getRoles(), + Recipient.BACKEND_ROLES.getName(), + backendRolesResourcesListener + ), + listener::onFailure + ); + + // Combine all results and pass them back to the original listener. + backendRolesResourcesListener.whenComplete(backendRolesResources -> { + Set allResources = new HashSet<>(); + + // Retrieve results from each StepListener + allResources.addAll(ownResourcesListener.result()); + allResources.addAll(userNameResourcesListener.result()); + allResources.addAll(rolesResourcesListener.result()); + allResources.addAll(backendRolesResourcesListener.result()); + + LOGGER.debug("Found {} accessible resources for user {}", allResources.size(), user.getName()); + listener.onResponse(allResources); + }, listener::onFailure); + } + + /** + * Returns a set of accessible resources for the current user within the specified resource index. + * + * @param resourceIndex The resource index to check for accessible resources. + * @param listener The listener to be notified with the set of accessible resources. + */ + @SuppressWarnings("unchecked") + public void getAccessibleResourcesForCurrentUser(String resourceIndex, ActionListener> listener) { + try { + validateArguments(resourceIndex); + + ResourceParser parser = ResourcePluginInfo.getInstance().getResourceProviders().get(resourceIndex).getResourceParser(); + + StepListener> resourceIdsListener = new StepListener<>(); + StepListener> resourcesListener = new StepListener<>(); + + // Fetch resource IDs + getAccessibleResourceIdsForCurrentUser(resourceIndex, resourceIdsListener); + + // Fetch docs + resourceIdsListener.whenComplete(resourceIds -> { + if (resourceIds.isEmpty()) { + // No accessible resources => immediately respond with empty set + listener.onResponse(Collections.emptySet()); + } else { + // Fetch the resource documents asynchronously + this.resourceSharingIndexHandler.getResourceDocumentsFromIds(resourceIds, resourceIndex, parser, resourcesListener); + } + }, listener::onFailure); + + // Send final response + resourcesListener.whenComplete( + listener::onResponse, + ex -> listener.onFailure(new ResourceSharingException("Failed to get accessible resources: " + ex.getMessage(), ex)) + ); + } catch (Exception e) { + listener.onFailure(new ResourceSharingException("Failed to process accessible resources request: " + e.getMessage(), e)); + } + } + + /** + * Checks whether current user has given permission (scope) to access given resource. + * + * @param resourceId The resource ID to check access for. + * @param resourceIndex The resource index containing the resource. + * @param scope The permission scope to check. + * @param listener The listener to be notified with the permission check result. + */ + public void hasPermission(String resourceId, String resourceIndex, String scope, ActionListener listener) { + validateArguments(resourceId, resourceIndex, scope); + + final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( + ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER + ); + final User user = (userSubject == null) ? null : userSubject.getUser(); + + if (user == null) { + LOGGER.warn("No authenticated user found in ThreadContext"); + listener.onResponse(false); + return; + } + + LOGGER.info("Checking if user '{}' has '{}' permission to resource '{}'", user.getName(), scope, resourceId); + + if (adminDNs.isAdmin(user)) { + LOGGER.info("User '{}' is admin, automatically granted '{}' permission on '{}'", user.getName(), scope, resourceId); + listener.onResponse(true); + return; + } + + Set userRoles = user.getSecurityRoles(); + Set userBackendRoles = user.getRoles(); + + this.resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId, ActionListener.wrap(document -> { + if (document == null) { + LOGGER.warn("Resource '{}' not found in index '{}'", resourceId, resourceIndex); + listener.onResponse(false); + return; + } + + if (isSharedWithEveryone(document) + || isOwnerOfResource(document, user.getName()) + || isSharedWithEntity(document, Recipient.USERS, Set.of(user.getName()), scope) + || isSharedWithEntity(document, Recipient.ROLES, userRoles, scope) + || isSharedWithEntity(document, Recipient.BACKEND_ROLES, userBackendRoles, scope)) { + + LOGGER.info("User '{}' has '{}' permission to resource '{}'", user.getName(), scope, resourceId); + listener.onResponse(true); + } else { + LOGGER.info("User '{}' does not have '{}' permission to resource '{}'", user.getName(), scope, resourceId); + listener.onResponse(false); + } + }, exception -> { + LOGGER.error( + "Failed to fetch resource sharing document for resource '{}' in index '{}': {}", + resourceId, + resourceIndex, + exception.getMessage() + ); + listener.onFailure(exception); + })); + } + + /** + * Shares a resource with the specified users, roles, and backend roles. + * + * @param resourceId The resource ID to share. + * @param resourceIndex The index where resource is store + * @param shareWith The users, roles, and backend roles as well as scope to share the resource with. + * @param listener The listener to be notified with the updated ResourceSharing document. + */ + public void shareWith(String resourceId, String resourceIndex, ShareWith shareWith, ActionListener listener) { + validateArguments(resourceId, resourceIndex, shareWith); + + final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( + ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER + ); + final User user = (userSubject == null) ? null : userSubject.getUser(); + + if (user == null) { + LOGGER.warn("No authenticated user found in the ThreadContext."); + listener.onFailure(new ResourceSharingException("No authenticated user found.")); + return; + } + + LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith.toString()); + + boolean isAdmin = adminDNs.isAdmin(user); + + this.resourceSharingIndexHandler.updateResourceSharingInfo( + resourceId, + resourceIndex, + user.getName(), + shareWith, + isAdmin, + ActionListener.wrap( + // On success, return the updated ResourceSharing + updatedResourceSharing -> { + LOGGER.info("Successfully shared resource {} with {}", resourceId, shareWith.toString()); + listener.onResponse(updatedResourceSharing); + }, + // On failure, log and pass the exception along + e -> { + LOGGER.error("Failed to share resource {} with {}: {}", resourceId, shareWith.toString(), e.getMessage()); + listener.onFailure(e); + } + ) + ); + } + + /** + * Revokes access to a resource for the specified users, roles, and backend roles. + * + * @param resourceId The resource ID to revoke access from. + * @param resourceIndex The index where resource is store + * @param revokeAccess The users, roles, and backend roles to revoke access for. + * @param scopes The permission scopes to revoke access for. + * @param listener The listener to be notified with the updated ResourceSharing document. + */ + public void revokeAccess( + String resourceId, + String resourceIndex, + Map> revokeAccess, + Set scopes, + ActionListener listener + ) { + // Validate input + validateArguments(resourceId, resourceIndex, revokeAccess, scopes); + + // Retrieve user + final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( + ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER + ); + final User user = (userSubject == null) ? null : userSubject.getUser(); + + if (user != null) { + LOGGER.info("User {} revoking access to resource {} for {} for scopes {} ", user.getName(), resourceId, revokeAccess, scopes); + } else { + listener.onFailure( + new ResourceSharingException( + "Failed to revoke access to resource {} for {} for scopes {} with no authenticated user", + resourceId, + revokeAccess, + scopes + ) + ); + } + + boolean isAdmin = (user != null) && adminDNs.isAdmin(user); + + this.resourceSharingIndexHandler.revokeAccess( + resourceId, + resourceIndex, + revokeAccess, + scopes, + (user != null ? user.getName() : null), + isAdmin, + ActionListener.wrap(listener::onResponse, exception -> { + LOGGER.error("Failed to revoke access to resource {} in index {}: {}", resourceId, resourceIndex, exception.getMessage()); + listener.onFailure(exception); + }) + ); + } + + public void checkDeletePermission(String resourceId, String resourceIndex, ActionListener listener) { + try { + validateArguments(resourceId, resourceIndex); + + final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( + ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER + ); + final User user = (userSubject == null) ? null : userSubject.getUser(); + + if (user == null) { + listener.onFailure(new ResourceSharingException("No authenticated user available.")); + return; + } + + StepListener fetchDocListener = new StepListener<>(); + resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId, fetchDocListener); + + fetchDocListener.whenComplete(document -> { + if (document == null) { + LOGGER.info("Document {} does not exist in index {}", resourceId, resourceIndex); + listener.onResponse(false); + return; + } + + boolean isAdmin = adminDNs.isAdmin(user); + boolean isOwner = isOwnerOfResource(document, user.getName()); + + if (!isAdmin && !isOwner) { + LOGGER.info("User {} does not have access to delete the record {}", user.getName(), resourceId); + listener.onResponse(false); + } else { + listener.onResponse(true); + } + }, listener::onFailure); + } catch (Exception e) { + LOGGER.error("Failed to check delete permission for resource {}", resourceId, e); + listener.onFailure(e); + } + } + + /** + * Deletes a resource sharing record by its ID and the resource index it belongs to. + * + * @param resourceId The resource ID to delete. + * @param resourceIndex The resource index containing the resource. + * @param listener The listener to be notified with the deletion result. + */ + public void deleteResourceSharingRecord(String resourceId, String resourceIndex, ActionListener listener) { + try { + validateArguments(resourceId, resourceIndex); + + LOGGER.info("Deleting resource sharing record for resource {} in {}", resourceId, resourceIndex); + + StepListener deleteDocListener = new StepListener<>(); + resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex, deleteDocListener); + deleteDocListener.whenComplete(listener::onResponse, listener::onFailure); + + } catch (Exception e) { + LOGGER.error("Failed to delete resource sharing record for resource {}", resourceId, e); + listener.onFailure(e); + } + } + + /** + * Deletes all resource sharing records for the current user. + * + * @param listener The listener to be notified with the deletion result. + */ + public void deleteAllResourceSharingRecordsForCurrentUser(ActionListener listener) { + final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( + ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER + ); + final User user = (userSubject == null) ? null : userSubject.getUser(); + + if (user == null) { + listener.onFailure(new ResourceSharingException("No authenticated user available.")); + return; + } + + LOGGER.info("Deleting all resource sharing records for user {}", user.getName()); + + resourceSharingIndexHandler.deleteAllRecordsForUser(user.getName(), ActionListener.wrap(listener::onResponse, exception -> { + LOGGER.error( + "Failed to delete all resource sharing records for user {}: {}", + user.getName(), + exception.getMessage(), + exception + ); + listener.onFailure(exception); + })); + } + + /** + * Loads all resources within the specified resource index. + * + * @param resourceIndex The resource index to load resources from. + * @param listener The listener to be notified with the set of resource IDs. + */ + private void loadAllResources(String resourceIndex, ActionListener> listener) { + this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex, listener); + } + + /** + * Loads resources owned by the specified user within the given resource index. + * + * @param resourceIndex The resource index to load resources from. + * @param userName The username of the owner. + * @param listener The listener to be notified with the set of resource IDs. + */ + private void loadOwnResources(String resourceIndex, String userName, ActionListener> listener) { + this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName, listener); + } + + /** + * Loads resources shared with the specified entities within the given resource index, including public resources. + * + * @param resourceIndex The resource index to load resources from. + * @param entities The set of entities to check for shared resources. + * @param recipientType The type of entity (e.g., users, roles, backend_roles). + * @param listener The listener to be notified with the set of resource IDs. + */ + private void loadSharedWithResources( + String resourceIndex, + Set entities, + String recipientType, + ActionListener> listener + ) { + Set entitiesCopy = new HashSet<>(entities); + // To allow "public" resources to be matched for any user, role, backend_role + entitiesCopy.add("*"); + this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entitiesCopy, recipientType, listener); + } + + /** + * Checks if the given resource is owned by the specified user. + * + * @param document The ResourceSharing document to check. + * @param userName The username to check ownership against. + * @return True if the resource is owned by the user, false otherwise. + */ + private boolean isOwnerOfResource(ResourceSharing document, String userName) { + return document.getCreatedBy() != null && document.getCreatedBy().getCreator().equals(userName); + } + + /** + * Checks if the given resource is shared with the specified entities and scope. + * + * @param document The ResourceSharing document to check. + * @param recipient The recipient entity + * @param entities The set of entities to check for sharing. + * @param scope The permission scope to check. + * @return True if the resource is shared with the entities and scope, false otherwise. + */ + private boolean isSharedWithEntity(ResourceSharing document, Recipient recipient, Set entities, String scope) { + for (String entity : entities) { + if (checkSharing(document, recipient, entity, scope)) { + return true; + } + } + return false; + } + + /** + * Checks if the given resource is shared with everyone. + * + * @param document The ResourceSharing document to check. + * @return True if the resource is shared with everyone, false otherwise. + */ + private boolean isSharedWithEveryone(ResourceSharing document) { + return document.getShareWith() != null + && document.getShareWith().getSharedWithScopes().stream().anyMatch(sharedWithScope -> sharedWithScope.getScope().equals("*")); + } + + /** + * Checks if the given resource is shared with the specified entity and scope. + * + * @param document The ResourceSharing document to check. + * @param recipient The recipient entity + * @param identifier The identifier of the entity to check for sharing. + * @param scope The permission scope to check. + * @return True if the resource is shared with the entity and scope, false otherwise. + */ + private boolean checkSharing(ResourceSharing document, Recipient recipient, String identifier, String scope) { + if (document.getShareWith() == null) { + return false; + } + + return document.getShareWith() + .getSharedWithScopes() + .stream() + .filter(sharedWithScope -> sharedWithScope.getScope().equals(scope)) + .findFirst() + .map(sharedWithScope -> { + SharedWithScope.ScopeRecipients scopePermissions = sharedWithScope.getSharedWithPerScope(); + Map> recipients = scopePermissions.getRecipients(); + + return switch (recipient) { + case Recipient.USERS, Recipient.ROLES, Recipient.BACKEND_ROLES -> recipients.get( + RecipientTypeRegistry.fromValue(recipient.getName()) + ).contains(identifier); + }; + }) + .orElse(false); // Return false if no matching scope is found + } + + private void validateArguments(Object... args) { + if (args == null) { + throw new IllegalArgumentException("Arguments cannot be null"); + } + for (Object arg : args) { + if (arg == null) { + throw new IllegalArgumentException("Argument cannot be null"); + } + // Additional check for String type arguments + if (arg instanceof String && ((String) arg).trim().isEmpty()) { + throw new IllegalArgumentException("Arguments cannot be empty"); + } + } + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java b/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java new file mode 100644 index 0000000000..7b7f6e23b1 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java @@ -0,0 +1,54 @@ +package org.opensearch.security.common.resources; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import org.opensearch.security.spi.resources.ResourceProvider; + +public class ResourcePluginInfo { + private static ResourcePluginInfo INSTANCE; + + private final Map resourceProviderMap = new HashMap<>(); + private final Set resourceIndices = new HashSet<>(); + + private ResourcePluginInfo() {} + + public static ResourcePluginInfo getInstance() { + if (INSTANCE == null) { + INSTANCE = new ResourcePluginInfo(); + } + return INSTANCE; + } + + public void setResourceProviders(Map providerMap) { + resourceProviderMap.clear(); + resourceProviderMap.putAll(providerMap); + } + + public void setResourceIndices(Set indices) { + resourceIndices.clear(); + resourceIndices.addAll(indices); + } + + public Map getResourceProviders() { + return ImmutableMap.copyOf(resourceProviderMap); + } + + public Set getResourceIndices() { + return ImmutableSet.copyOf(resourceIndices); + } + + // TODO following should be removed once core test framework allows loading extended classes + public Map getResourceProvidersMutable() { + return resourceProviderMap; + } + + public Set getResourceIndicesMutable() { + return resourceIndices; + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharing.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharing.java new file mode 100644 index 0000000000..c267c12bb5 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharing.java @@ -0,0 +1,206 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources; + +import java.io.IOException; +import java.util.Objects; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +/** + * Represents a resource sharing configuration that manages access control for OpenSearch resources. + * This class holds information about shared resources including their source, creator, and sharing permissions. + * + *

          This class implements {@link ToXContentFragment} for JSON serialization and {@link NamedWriteable} + * for stream-based serialization.

          + *

          + * The class maintains information about: + *

            + *
          • The source index where the resource is defined
          • + *
          • The unique identifier of the resource
          • + *
          • The creator's information
          • + *
          • The sharing permissions and recipients
          • + *
          + * + * @opensearch.experimental + * @see org.opensearch.security.common.resources.CreatedBy + * @see org.opensearch.security.common.resources.ShareWith + */ +public class ResourceSharing implements ToXContentFragment, NamedWriteable { + + /** + * The index where the resource is defined + */ + private String sourceIdx; + + /** + * The unique identifier of the resource + */ + private String resourceId; + + /** + * Information about who created the resource + */ + private CreatedBy createdBy; + + /** + * Information about with whom the resource is shared with + */ + private ShareWith shareWith; + + public ResourceSharing(String sourceIdx, String resourceId, CreatedBy createdBy, ShareWith shareWith) { + this.sourceIdx = sourceIdx; + this.resourceId = resourceId; + this.createdBy = createdBy; + this.shareWith = shareWith; + } + + public String getSourceIdx() { + return sourceIdx; + } + + public void setSourceIdx(String sourceIdx) { + this.sourceIdx = sourceIdx; + } + + public String getResourceId() { + return resourceId; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + public CreatedBy getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(CreatedBy createdBy) { + this.createdBy = createdBy; + } + + public ShareWith getShareWith() { + return shareWith; + } + + public void setShareWith(ShareWith shareWith) { + this.shareWith = shareWith; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ResourceSharing resourceSharing = (ResourceSharing) o; + return Objects.equals(getSourceIdx(), resourceSharing.getSourceIdx()) + && Objects.equals(getResourceId(), resourceSharing.getResourceId()) + && Objects.equals(getCreatedBy(), resourceSharing.getCreatedBy()) + && Objects.equals(getShareWith(), resourceSharing.getShareWith()); + } + + @Override + public int hashCode() { + return Objects.hash(getSourceIdx(), getResourceId(), getCreatedBy(), getShareWith()); + } + + @Override + public String toString() { + return "Resource {" + + "sourceIdx='" + + sourceIdx + + '\'' + + ", resourceId='" + + resourceId + + '\'' + + ", createdBy=" + + createdBy + + ", sharedWith=" + + shareWith + + '}'; + } + + @Override + public String getWriteableName() { + return "resource_sharing"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(sourceIdx); + out.writeString(resourceId); + createdBy.writeTo(out); + if (shareWith != null) { + out.writeBoolean(true); + shareWith.writeTo(out); + } else { + out.writeBoolean(false); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject().field("source_idx", sourceIdx).field("resource_id", resourceId).field("created_by"); + createdBy.toXContent(builder, params); + if (shareWith != null && !shareWith.getSharedWithScopes().isEmpty()) { + builder.field("share_with"); + shareWith.toXContent(builder, params); + } + return builder.endObject(); + } + + public static ResourceSharing fromXContent(XContentParser parser) throws IOException { + String sourceIdx = null; + String resourceId = null; + CreatedBy createdBy = null; + ShareWith shareWith = null; + + String currentFieldName = null; + XContentParser.Token token; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else { + switch (Objects.requireNonNull(currentFieldName)) { + case "source_idx": + sourceIdx = parser.text(); + break; + case "resource_id": + resourceId = parser.text(); + break; + case "created_by": + createdBy = CreatedBy.fromXContent(parser); + break; + case "share_with": + shareWith = ShareWith.fromXContent(parser); + break; + default: + parser.skipChildren(); + break; + } + } + } + + validateRequiredField("source_idx", sourceIdx); + validateRequiredField("resource_id", resourceId); + validateRequiredField("created_by", createdBy); + + return new ResourceSharing(sourceIdx, resourceId, createdBy, shareWith); + } + + private static void validateRequiredField(String field, T value) { + if (value == null) { + throw new IllegalArgumentException(field + " is required"); + } + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java new file mode 100644 index 0000000000..387254cbf7 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java @@ -0,0 +1,16 @@ +/* + * 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.common.resources; + +public class ResourceSharingConstants { + // Resource sharing index + public static final String OPENSEARCH_RESOURCE_SHARING_INDEX = ".opensearch_resource_sharing"; +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingException.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingException.java new file mode 100644 index 0000000000..e95d4b51ee --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingException.java @@ -0,0 +1,39 @@ +/* + * 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.common.resources; + +import java.io.IOException; + +import org.opensearch.OpenSearchException; +import org.opensearch.core.common.io.stream.StreamInput; + +/** + * This class represents an exception that occurs during resource sharing operations. + * It extends the OpenSearchException class. + */ +public class ResourceSharingException extends OpenSearchException { + public ResourceSharingException(Throwable cause) { + super(cause); + } + + public ResourceSharingException(String msg, Object... args) { + super(msg, args); + } + + public ResourceSharingException(String msg, Throwable cause, Object... args) { + super(msg, cause, args); + } + + public ResourceSharingException(StreamInput in) throws IOException { + super(in); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java new file mode 100644 index 0000000000..ede8985e68 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java @@ -0,0 +1,1393 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security.common.resources; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.DocWriteRequest; +import org.opensearch.action.StepListener; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.action.get.MultiGetItemResponse; +import org.opensearch.action.get.MultiGetRequest; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.index.IndexResponse; +import org.opensearch.action.search.ClearScrollRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.search.SearchScrollRequest; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentHelper; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.index.IndexNotFoundException; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.MultiMatchQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.index.reindex.BulkByScrollResponse; +import org.opensearch.index.reindex.DeleteByQueryAction; +import org.opensearch.index.reindex.DeleteByQueryRequest; +import org.opensearch.index.reindex.UpdateByQueryAction; +import org.opensearch.index.reindex.UpdateByQueryRequest; +import org.opensearch.script.Script; +import org.opensearch.script.ScriptType; +import org.opensearch.search.Scroll; +import org.opensearch.search.SearchHit; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.security.common.DefaultObjectMapper; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.ResourceParser; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.client.Client; + +import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; + +/** + * This class handles the creation and management of the resource sharing index. + * It provides methods to create the index, index resource sharing entries along with updates and deletion, retrieve shared resources. + */ +public class ResourceSharingIndexHandler { + + private static final Logger LOGGER = LogManager.getLogger(ResourceSharingIndexHandler.class); + + private final Client client; + + private final String resourceSharingIndex; + + private final ThreadPool threadPool; + + public ResourceSharingIndexHandler(final String indexName, final Client client, final ThreadPool threadPool) { + this.resourceSharingIndex = indexName; + this.client = client; + this.threadPool = threadPool; + } + + public final static Map INDEX_SETTINGS = Map.of( + "index.number_of_shards", + 1, + "index.auto_expand_replicas", + "0-all", + "index.hidden", + "true" + ); + + /** + * Creates the resource sharing index if it doesn't already exist. + * This method initializes the index with predefined mappings and settings + * for storing resource sharing information. + * The index will be created with the following structure: + * - source_idx (keyword): The source index containing the original document + * - resource_id (keyword): The ID of the shared resource + * - created_by (object): Information about the user who created the sharing + * - user (keyword): Username of the creator + * - share_with (object): Access control configuration for shared resources + * - [group_name] (object): Name of the access group + * - users (array): List of users with access + * - roles (array): List of roles with access + * - backend_roles (array): List of backend roles with access + * + * @throws RuntimeException if there are issues reading/writing index settings + * or communicating with the cluster + */ + + public void createResourceSharingIndexIfAbsent(Callable callable) { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + + CreateIndexRequest cir = new CreateIndexRequest(resourceSharingIndex).settings(INDEX_SETTINGS).waitForActiveShards(1); + ActionListener cirListener = ActionListener.wrap(response -> { + LOGGER.info("Resource sharing index {} created.", resourceSharingIndex); + if (callable != null) { + callable.call(); + } + }, (failResponse) -> { + /* Index already exists, ignore and continue */ + LOGGER.info("Index {} already exists.", resourceSharingIndex); + try { + if (callable != null) { + callable.call(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + this.client.admin().indices().create(cir, cirListener); + } + } + + /** + * Creates or updates a resource sharing record in the dedicated resource sharing index. + * This method handles the persistence of sharing metadata for resources, including + * the creator information and sharing permissions. + * + * @param resourceId The unique identifier of the resource being shared + * @param resourceIndex The source index where the original resource is stored + * @param createdBy Object containing information about the user creating/updating the sharing + * @param shareWith Object containing the sharing permissions' configuration. Can be null for initial creation. + * When provided, it should contain the access control settings for different groups: + * { + * "group_name": { + * "users": ["user1", "user2"], + * "roles": ["role1", "role2"], + * "backend_roles": ["backend_role1"] + * } + * } + * @return ResourceSharing Returns resourceSharing object if the operation was successful, null otherwise + * @throws IOException if there are issues with index operations or JSON processing + */ + public ResourceSharing indexResourceSharing(String resourceId, String resourceIndex, CreatedBy createdBy, ShareWith shareWith) + throws IOException { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + ResourceSharing entry = new ResourceSharing(resourceIndex, resourceId, createdBy, shareWith); + + IndexRequest ir = client.prepareIndex(resourceSharingIndex) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .setSource(entry.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) + .setOpType(DocWriteRequest.OpType.CREATE) // only create if an entry doesn't exist + .request(); + + ActionListener irListener = ActionListener.wrap( + idxResponse -> LOGGER.info("Successfully created {} entry.", resourceSharingIndex), + (failResponse) -> { + LOGGER.error(failResponse.getMessage()); + LOGGER.info("Failed to create {} entry.", resourceSharingIndex); + } + ); + client.index(ir, irListener); + return entry; + } catch (Exception e) { + LOGGER.info("Failed to create {} entry.", resourceSharingIndex, e); + throw new ResourceSharingException("Failed to create " + resourceSharingIndex + " entry.", e); + } + } + + /** + * Fetches all resource sharing records that match the specified system index. This method retrieves + * a get of resource IDs associated with the given system index from the resource sharing index. + * + *

          The method executes the following steps: + *

            + *
          1. Creates a search request with term query matching the system index
          2. + *
          3. Applies source filtering to only fetch resource_id field
          4. + *
          5. Executes the search with a limit of 10000 documents
          6. + *
          7. Processes the results to extract resource IDs
          8. + *
          + * + *

          Example query structure: + *

          +     * {
          +     *   "query": {
          +     *     "term": {
          +     *       "source_idx": "resource_index_name"
          +     *     }
          +     *   },
          +     *   "_source": ["resource_id"],
          +     *   "size": 10000
          +     * }
          +     * 
          + * + * @param pluginIndex The source index to match against the source_idx field + * @param listener The listener to be notified when the operation completes. + * The listener receives a set of resource IDs as a result. + * @apiNote This method: + *
            + *
          • Uses source filtering for optimal performance
          • + *
          • Performs exact matching on the source_idx field
          • + *
          • Returns an empty get instead of throwing exceptions
          • + *
          + */ + public void fetchAllDocuments(String pluginIndex, ActionListener> listener) { + LOGGER.debug("Fetching all documents asynchronously from {} where source_idx = {}", resourceSharingIndex, pluginIndex); + + try (final ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query( + QueryBuilders.termQuery("source_idx.keyword", pluginIndex) + ).size(10000).fetchSource(new String[] { "resource_id" }, null); + + searchRequest.source(searchSourceBuilder); + + client.search(searchRequest, new ActionListener<>() { + @Override + public void onResponse(SearchResponse searchResponse) { + try { + Set resourceIds = new HashSet<>(); + + SearchHit[] hits = searchResponse.getHits().getHits(); + for (SearchHit hit : hits) { + Map sourceAsMap = hit.getSourceAsMap(); + if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { + resourceIds.add(sourceAsMap.get("resource_id").toString()); + } + } + + LOGGER.debug("Found {} documents in {} for source_idx: {}", resourceIds.size(), resourceSharingIndex, pluginIndex); + + listener.onResponse(resourceIds); + } catch (Exception e) { + LOGGER.error( + "Error while processing search response from {} for source_idx: {}", + resourceSharingIndex, + pluginIndex, + e + ); + listener.onFailure(e); + } + } + + @Override + public void onFailure(Exception e) { + LOGGER.error("Failed to fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); + listener.onFailure(e); + } + }); + } catch (Exception e) { + LOGGER.error("Failed to initiate fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); + listener.onFailure(e); + } + } + + /** + * Fetches documents that match the specified system index and have specific access type values. + * This method uses scroll API to handle large result sets efficiently. + * + *

          The method executes the following steps: + *

            + *
          1. Validates the RecipientType parameter
          2. + *
          3. Creates a scrolling search request with a compound query
          4. + *
          5. Processes results in batches using scroll API
          6. + *
          7. Collects all matching resource IDs
          8. + *
          9. Cleans up scroll context
          10. + *
          + * + *

          Example query structure: + *

          +     * {
          +     *   "query": {
          +     *     "bool": {
          +     *       "must": [
          +     *         { "term": { "source_idx": "resource_index_name" } },
          +     *         {
          +     *           "bool": {
          +     *             "should": [
          +     *               {
          +     *                 "nested": {
          +     *                   "path": "share_with.*.RecipientType",
          +     *                   "query": {
          +     *                     "term": { "share_with.*.RecipientType": "entity_value" }
          +     *                   }
          +     *                 }
          +     *               }
          +     *             ],
          +     *             "minimum_should_match": 1
          +     *           }
          +     *         }
          +     *       ]
          +     *     }
          +     *   },
          +     *   "_source": ["resource_id"],
          +     *   "size": 1000
          +     * }
          +     * 
          + * + * @param pluginIndex The source index to match against the source_idx field + * @param entities Set of values to match in the specified RecipientType field + * @param recipientType The type of association with the resource. Must be one of: + *
            + *
          • "users" - for user-based access
          • + *
          • "roles" - for role-based access
          • + *
          • "backend_roles" - for backend role-based access
          • + *
          + * @param listener The listener to be notified when the operation completes. + * The listener receives a set of resource IDs as a result. + * @throws RuntimeException if the search operation fails + * @apiNote This method: + *
            + *
          • Uses scroll API with 1-minute timeout
          • + *
          • Processes results in batches of 1000 documents
          • + *
          • Performs source filtering for optimization
          • + *
          • Uses nested queries for accessing array elements
          • + *
          • Properly cleans up scroll context after use
          • + *
          + */ + + public void fetchDocumentsForAllScopes( + String pluginIndex, + Set entities, + String recipientType, + ActionListener> listener + ) { + // "*" must match all scopes + fetchDocumentsForAGivenScope(pluginIndex, entities, recipientType, "*", listener); + } + + /** + * Fetches documents that match the specified system index and have specific access type values for a given scope. + * This method uses scroll API to handle large result sets efficiently. + * + *

          The method executes the following steps: + *

            + *
          1. Validates the RecipientType parameter
          2. + *
          3. Creates a scrolling search request with a compound query
          4. + *
          5. Processes results in batches using scroll API
          6. + *
          7. Collects all matching resource IDs
          8. + *
          9. Cleans up scroll context
          10. + *
          + * + *

          Example query structure: + *

          +     * {
          +     *   "query": {
          +     *     "bool": {
          +     *       "must": [
          +     *         { "term": { "source_idx": "resource_index_name" } },
          +     *         {
          +     *           "bool": {
          +     *             "should": [
          +     *               {
          +     *                 "nested": {
          +     *                   "path": "share_with.scope.RecipientType",
          +     *                   "query": {
          +     *                     "term": { "share_with.scope.RecipientType": "entity_value" }
          +     *                   }
          +     *                 }
          +     *               }
          +     *             ],
          +     *             "minimum_should_match": 1
          +     *           }
          +     *         }
          +     *       ]
          +     *     }
          +     *   },
          +     *   "_source": ["resource_id"],
          +     *   "size": 1000
          +     * }
          +     * 
          + * + * @param pluginIndex The source index to match against the source_idx field + * @param entities Set of values to match in the specified RecipientType field + * @param recipientType The type of association with the resource. Must be one of: + *
            + *
          • "users" - for user-based access
          • + *
          • "roles" - for role-based access
          • + *
          • "backend_roles" - for backend role-based access
          • + *
          + * @param scope The scope of the access. Should be implementation of {@link org.opensearch.security.spi.resources.ResourceAccessScope} + * @param listener The listener to be notified when the operation completes. + * The listener receives a set of resource IDs as a result. + * @throws RuntimeException if the search operation fails + * @apiNote This method: + *
            + *
          • Uses scroll API with 1-minute timeout
          • + *
          • Processes results in batches of 1000 documents
          • + *
          • Performs source filtering for optimization
          • + *
          • Uses nested queries for accessing array elements
          • + *
          • Properly cleans up scroll context after use
          • + *
          + */ + public void fetchDocumentsForAGivenScope( + String pluginIndex, + Set entities, + String recipientType, + String scope, + ActionListener> listener + ) { + LOGGER.debug( + "Fetching documents asynchronously from index: {}, where share_with.{}.{} contains any of {}", + pluginIndex, + scope, + recipientType, + entities + ); + + final Set resourceIds = new HashSet<>(); + final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); + + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); + searchRequest.scroll(scroll); + + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery().must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)); + + BoolQueryBuilder shouldQuery = QueryBuilders.boolQuery(); + if ("*".equals(scope)) { + for (String entity : entities) { + shouldQuery.should( + QueryBuilders.multiMatchQuery(entity, "share_with.*." + recipientType + ".keyword") + .type(MultiMatchQueryBuilder.Type.BEST_FIELDS) + ); + } + } else { + for (String entity : entities) { + shouldQuery.should(QueryBuilders.termQuery("share_with." + scope + "." + recipientType + ".keyword", entity)); + } + } + shouldQuery.minimumShouldMatch(1); + + boolQuery.must(QueryBuilders.existsQuery("share_with")).must(shouldQuery); + + executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery, ActionListener.wrap(success -> { + LOGGER.debug("Found {} documents matching the criteria in {}", resourceIds.size(), resourceSharingIndex); + listener.onResponse(resourceIds); + + }, exception -> { + LOGGER.error( + "Search failed for pluginIndex={}, scope={}, recipientType={}, entities={}", + pluginIndex, + scope, + recipientType, + entities, + exception + ); + listener.onFailure(exception); + + })); + } catch (Exception e) { + LOGGER.error( + "Failed to initiate fetch from {} for criteria - pluginIndex: {}, scope: {}, RecipientType: {}, entities: {}", + resourceSharingIndex, + pluginIndex, + scope, + recipientType, + entities, + e + ); + listener.onFailure(new RuntimeException("Failed to fetch documents: " + e.getMessage(), e)); + } + } + + /** + * Fetches documents from the resource sharing index that match a specific field value. + * This method uses scroll API to efficiently handle large result sets and performs exact + * matching on both system index and the specified field. + * + *

          The method executes the following steps: + *

            + *
          1. Validates input parameters for null/empty values
          2. + *
          3. Creates a scrolling search request with a bool query
          4. + *
          5. Processes results in batches using scroll API
          6. + *
          7. Extracts resource IDs from matching documents
          8. + *
          9. Cleans up scroll context after completion
          10. + *
          + * + *

          Example query structure: + *

          +     * {
          +     *   "query": {
          +     *     "bool": {
          +     *       "must": [
          +     *         { "term": { "source_idx": "system_index_value" } },
          +     *         { "term": { "field_name": "field_value" } }
          +     *       ]
          +     *     }
          +     *   },
          +     *   "_source": ["resource_id"],
          +     *   "size": 1000
          +     * }
          +     * 
          + * + * @param pluginIndex The source index to match against the source_idx field + * @param field The field name to search in. Must be a valid field in the index mapping + * @param value The value to match for the specified field. Performs exact term matching + * @param listener The listener to be notified when the operation completes. + * The listener receives a set of resource IDs as a result. + * @throws IllegalArgumentException if any parameter is null or empty + * @throws RuntimeException if the search operation fails, wrapping the underlying exception + * @apiNote This method: + *
            + *
          • Uses scroll API with 1-minute timeout for handling large result sets
          • + *
          • Performs exact term matching (not analyzed) on field values
          • + *
          • Processes results in batches of 1000 documents
          • + *
          • Uses source filtering to only fetch resource_id field
          • + *
          • Automatically cleans up scroll context after use
          • + *
          + *

          + * Example usage: + *

          +     * Set resources = fetchDocumentsByField("myIndex", "status", "active");
          +     * 
          + */ + public void fetchDocumentsByField(String pluginIndex, String field, String value, ActionListener> listener) { + if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(field) || StringUtils.isBlank(value)) { + listener.onFailure(new IllegalArgumentException("pluginIndex, field, and value must not be null or empty")); + return; + } + + LOGGER.debug("Fetching documents from index: {}, where {} = {}", pluginIndex, field, value); + + Set resourceIds = new HashSet<>(); + final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); + + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); + searchRequest.scroll(scroll); + + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)) + .must(QueryBuilders.termQuery(field + ".keyword", value)); + + executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery, ActionListener.wrap(success -> { + LOGGER.info("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); + listener.onResponse(resourceIds); + }, exception -> { + LOGGER.error("Failed to fetch documents from {} where {} = {}", resourceSharingIndex, field, value, exception); + listener.onFailure(new RuntimeException("Failed to fetch documents: " + exception.getMessage(), exception)); + })); + } catch (Exception e) { + LOGGER.error("Failed to initiate fetch from {} where {} = {}", resourceSharingIndex, field, value, e); + listener.onFailure(new RuntimeException("Failed to initiate fetch: " + e.getMessage(), e)); + } + + } + + /** + * Fetches a specific resource sharing document by its resource ID and system index. + * This method performs an exact match search and parses the result into a ResourceSharing object. + * + *

          The method executes the following steps: + *

            + *
          1. Validates input parameters for null/empty values
          2. + *
          3. Creates a search request with a bool query for exact matching
          4. + *
          5. Executes the search with a limit of 1 document
          6. + *
          7. Parses the result using XContent parser if found
          8. + *
          9. Returns null if no matching document exists
          10. + *
          + * + *

          Example query structure: + *

          +     * {
          +     *   "query": {
          +     *     "bool": {
          +     *       "must": [
          +     *         { "term": { "source_idx": "resource_index_name" } },
          +     *         { "term": { "resource_id": "resource_id_value" } }
          +     *       ]
          +     *     }
          +     *   },
          +     *   "size": 1
          +     * }
          +     * 
          + * + * @param pluginIndex The source index to match against the source_idx field + * @param resourceId The resource ID to fetch. Must exactly match the resource_id field + * @param listener The listener to be notified when the operation completes. + * The listener receives the parsed ResourceSharing object or null if not found + * @throws IllegalArgumentException if pluginIndexName or resourceId is null or empty + * @throws RuntimeException if the search operation fails or parsing errors occur, + * wrapping the underlying exception + * @apiNote This method: + *
            + *
          • Uses term queries for exact matching
          • + *
          • Expects only one matching document per resource ID
          • + *
          • Uses XContent parsing for consistent object creation
          • + *
          • Returns null instead of throwing exceptions for non-existent documents
          • + *
          • Provides detailed logging for troubleshooting
          • + *
          + *

          + * Example usage: + *

          +     * ResourceSharing sharing = fetchDocumentById("myIndex", "resource123");
          +     * if (sharing != null) {
          +     *     // Process the resource sharing object
          +     * }
          +     * 
          + */ + public void fetchDocumentById(String pluginIndex, String resourceId, ActionListener listener) { + if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(resourceId)) { + listener.onFailure(new IllegalArgumentException("pluginIndex and resourceId must not be null or empty")); + return; + } + LOGGER.debug("Fetching document from index: {}, resourceId: {}", pluginIndex, resourceId); + + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)) + .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); + + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery).size(1); // There is only one document for + // a single resource + + SearchRequest searchRequest = new SearchRequest(resourceSharingIndex).source(searchSourceBuilder); + + client.search(searchRequest, new ActionListener<>() { + @Override + public void onResponse(SearchResponse searchResponse) { + try { + SearchHit[] hits = searchResponse.getHits().getHits(); + if (hits.length == 0) { + LOGGER.debug("No document found for resourceId: {} in index: {}", resourceId, pluginIndex); + listener.onResponse(null); + return; + } + + SearchHit hit = hits[0]; + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, hit.getSourceAsString()) + ) { + parser.nextToken(); + ResourceSharing resourceSharing = ResourceSharing.fromXContent(parser); + + LOGGER.debug("Successfully fetched document for resourceId: {} from index: {}", resourceId, pluginIndex); + + listener.onResponse(resourceSharing); + } + } catch (Exception e) { + LOGGER.error("Failed to parse document for resourceId: {} from index: {}", resourceId, pluginIndex, e); + listener.onFailure( + new ResourceSharingException( + "Failed to parse document for resourceId: " + resourceId + " from index: " + pluginIndex, + e + ) + ); + } + } + + @Override + public void onFailure(Exception e) { + + LOGGER.error("Failed to fetch document for resourceId: {} from index: {}", resourceId, pluginIndex, e); + listener.onFailure( + new ResourceSharingException( + "Failed to fetch document for resourceId: " + resourceId + " from index: " + pluginIndex, + e + ) + ); + + } + }); + } catch (Exception e) { + LOGGER.error("Failed to fetch document for resourceId: {} from index: {}", resourceId, pluginIndex, e); + listener.onFailure( + new ResourceSharingException("Failed to fetch document for resourceId: " + resourceId + " from index: " + pluginIndex, e) + ); + } + } + + /** + * Helper method to execute a search request and collect resource IDs from the results. + * + * @param resourceIds List to collect resource IDs + * @param scroll Search Scroll + * @param searchRequest Request to execute + * @param boolQuery Query to execute with the request + * @param listener Listener to be notified when the operation completes + */ + private void executeSearchRequest( + Set resourceIds, + Scroll scroll, + SearchRequest searchRequest, + BoolQueryBuilder boolQuery, + ActionListener listener + ) { + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery) + .size(1000) + .fetchSource(new String[] { "resource_id" }, null); + + searchRequest.source(searchSourceBuilder); + + StepListener searchStep = new StepListener<>(); + + client.search(searchRequest, searchStep); + + searchStep.whenComplete(initialResponse -> { + String scrollId = initialResponse.getScrollId(); + processScrollResults(resourceIds, scroll, scrollId, initialResponse.getHits().getHits(), listener); + }, listener::onFailure); + } + + /** + * Helper method to process scroll results recursively. + * + * @param resourceIds List to collect resource IDs + * @param scroll Search Scroll + * @param scrollId Scroll ID + * @param hits Search hits + * @param listener Listener to be notified when the operation completes + */ + private void processScrollResults( + Set resourceIds, + Scroll scroll, + String scrollId, + SearchHit[] hits, + ActionListener listener + ) { + // If no hits, clean up and complete + if (hits == null || hits.length == 0) { + clearScroll(scrollId, listener); + return; + } + + // Process current batch of hits + for (SearchHit hit : hits) { + Map sourceAsMap = hit.getSourceAsMap(); + if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { + resourceIds.add(sourceAsMap.get("resource_id").toString()); + } + } + + // Prepare next scroll request + SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); + scrollRequest.scroll(scroll); + + // Execute next scroll + client.searchScroll(scrollRequest, ActionListener.wrap(scrollResponse -> { + // Process next batch recursively + processScrollResults(resourceIds, scroll, scrollResponse.getScrollId(), scrollResponse.getHits().getHits(), listener); + }, e -> { + // Clean up scroll context on failure + clearScroll(scrollId, ActionListener.wrap(r -> listener.onFailure(e), ex -> { + e.addSuppressed(ex); + listener.onFailure(e); + })); + })); + } + + /** + * Helper method to clear scroll context. + * + * @param scrollId Scroll ID + * @param listener Listener to be notified when the operation completes + */ + private void clearScroll(String scrollId, ActionListener listener) { + if (scrollId == null) { + listener.onResponse(null); + return; + } + + ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); + clearScrollRequest.addScrollId(scrollId); + + client.clearScroll(clearScrollRequest, ActionListener.wrap(r -> listener.onResponse(null), e -> { + LOGGER.warn("Failed to clear scroll context", e); + listener.onResponse(null); + })); + } + + /** + * Updates the sharing configuration for an existing resource in the resource sharing index. + * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map, Set, String, boolean, ActionListener)} + * This method modifies the sharing permissions for a specific resource identified by its + * resource ID and source index. + * + * @param resourceId The unique identifier of the resource whose sharing configuration needs to be updated + * @param sourceIdx The source index where the original resource is stored + * @param requestUserName The user requesting to share the resource + * @param shareWith Updated sharing configuration object containing access control settings: + * { + * "scope": { + * "users": ["user1", "user2"], + * "roles": ["role1", "role2"], + * "backend_roles": ["backend_role1"] + * } + * } + * @param isAdmin Boolean indicating whether the user requesting to share is an admin or not + * @param listener Listener to be notified when the operation completes + * @throws RuntimeException if there's an error during the update operation + */ + public void updateResourceSharingInfo( + String resourceId, + String sourceIdx, + String requestUserName, + ShareWith shareWith, + boolean isAdmin, + ActionListener listener + ) { + XContentBuilder builder; + Map shareWithMap; + try { + builder = XContentFactory.jsonBuilder(); + shareWith.toXContent(builder, ToXContent.EMPTY_PARAMS); + String json = builder.toString(); + shareWithMap = DefaultObjectMapper.readValue(json, new TypeReference<>() { + }); + } catch (IOException e) { + LOGGER.error("Failed to build json content", e); + listener.onFailure(new ResourceSharingException("Failed to build json content", e)); + return; + } + + StepListener fetchDocListener = new StepListener<>(); + StepListener updateScriptListener = new StepListener<>(); + StepListener updatedSharingListener = new StepListener<>(); + + // Fetch resource sharing doc + fetchDocumentById(sourceIdx, resourceId, fetchDocListener); + + // build update script + fetchDocListener.whenComplete(currentSharingInfo -> { + // Check if user can share. At present only the resource creator and admin is allowed to share the resource + if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { + + LOGGER.error("User {} is not authorized to share resource {}", requestUserName, resourceId); + listener.onFailure( + new ResourceSharingException("User " + requestUserName + " is not authorized to share resource " + resourceId) + ); + } + + Script updateScript = new Script(ScriptType.INLINE, "painless", """ + if (ctx._source.share_with == null) { + ctx._source.share_with = [:]; + } + + for (def entry : params.shareWith.entrySet()) { + def scopeName = entry.getKey(); + def newScope = entry.getValue(); + + if (!ctx._source.share_with.containsKey(scopeName)) { + def newScopeEntry = [:]; + for (def field : newScope.entrySet()) { + if (field.getValue() != null && !field.getValue().isEmpty()) { + newScopeEntry[field.getKey()] = new HashSet(field.getValue()); + } + } + ctx._source.share_with[scopeName] = newScopeEntry; + } else { + def existingScope = ctx._source.share_with[scopeName]; + + for (def field : newScope.entrySet()) { + def fieldName = field.getKey(); + def newValues = field.getValue(); + + if (newValues != null && !newValues.isEmpty()) { + if (!existingScope.containsKey(fieldName)) { + existingScope[fieldName] = new HashSet(); + } + + for (def value : newValues) { + if (!existingScope[fieldName].contains(value)) { + existingScope[fieldName].add(value); + } + } + } + } + } + } + """, Collections.singletonMap("shareWith", shareWithMap)); + + updateByQueryResourceSharing(sourceIdx, resourceId, updateScript, updateScriptListener); + + }, listener::onFailure); + + // Build & return the updated ResourceSharing + updateScriptListener.whenComplete(success -> { + if (!success) { + LOGGER.error("Failed to update resource sharing info for resource {}", resourceId); + listener.onResponse(null); + return; + } + // TODO check if this should be replaced by Java in-memory computation (current intuition is that it will be more memory + // intensive to do it in java) + fetchDocumentById(sourceIdx, resourceId, updatedSharingListener); + }, listener::onFailure); + + updatedSharingListener.whenComplete(listener::onResponse, listener::onFailure); + } + + /** + * Updates resource sharing entries that match the specified source index and resource ID + * using the provided update script. This method performs an update-by-query operation + * in the resource sharing index. + * + *

          The method executes the following steps: + *

            + *
          1. Creates a bool query to match exact source index and resource ID
          2. + *
          3. Constructs an update-by-query request with the query and update script
          4. + *
          5. Executes the update operation
          6. + *
          7. Returns success/failure status based on update results
          8. + *
          + * + *

          Example document matching structure: + *

          +     * {
          +     *   "source_idx": "source_index_name",
          +     *   "resource_id": "resource_id_value",
          +     *   "share_with": {
          +     *     // sharing configuration to be updated
          +     *   }
          +     * }
          +     * 
          + * + * @param sourceIdx The source index to match in the query (exact match) + * @param resourceId The resource ID to match in the query (exact match) + * @param updateScript The script containing the update operations to be performed. + * This script defines how the matching documents should be modified + * @param listener Listener to be notified when the operation completes + * @apiNote This method: + *
            + *
          • Uses term queries for exact matching of source_idx and resource_id
          • + *
          • Returns false for both "no matching documents" and "operation failure" cases
          • + *
          • Logs the complete update request for debugging purposes
          • + *
          • Provides detailed logging for success and failure scenarios
          • + *
          + * @implNote The update operation uses a bool query with two must clauses: + *
          +     * {
          +     *   "query": {
          +     *     "bool": {
          +     *       "must": [
          +     *         { "term": { "source_idx.keyword": sourceIdx } },
          +     *         { "term": { "resource_id.keyword": resourceId } }
          +     *       ]
          +     *     }
          +     *   }
          +     * }
          +     * 
          + */ + private void updateByQueryResourceSharing(String sourceIdx, String resourceId, Script updateScript, ActionListener listener) { + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + BoolQueryBuilder query = QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) + .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); + + UpdateByQueryRequest ubq = new UpdateByQueryRequest(resourceSharingIndex).setQuery(query) + .setScript(updateScript) + .setRefresh(true); + + client.execute(UpdateByQueryAction.INSTANCE, ubq, new ActionListener<>() { + @Override + public void onResponse(BulkByScrollResponse response) { + long updated = response.getUpdated(); + if (updated > 0) { + LOGGER.info("Successfully updated {} documents in {}.", updated, resourceSharingIndex); + listener.onResponse(true); + } else { + LOGGER.info( + "No documents found to update in {} for source_idx: {} and resource_id: {}", + resourceSharingIndex, + sourceIdx, + resourceId + ); + listener.onResponse(false); + } + + } + + @Override + public void onFailure(Exception e) { + + LOGGER.error("Failed to update documents in {}.", resourceSharingIndex, e); + listener.onFailure(e); + + } + }); + } catch (Exception e) { + LOGGER.error("Failed to update documents in {} before request submission.", resourceSharingIndex, e); + listener.onFailure(e); + } + } + + /** + * Revokes access for specified entities from a resource sharing document. This method removes the specified + * entities (users, roles, or backend roles) from the existing sharing configuration while preserving other + * sharing settings. + * + *

          The method performs the following steps: + *

            + *
          1. Fetches the existing document
          2. + *
          3. Removes specified entities from their respective lists in all sharing groups
          4. + *
          5. Updates the document if modifications were made
          6. + *
          7. Returns the updated resource sharing configuration
          8. + *
          + * + *

          Example document structure: + *

          +     * {
          +     *   "source_idx": "resource_index_name",
          +     *   "resource_id": "resource_id",
          +     *   "share_with": {
          +     *     "scope": {
          +     *       "users": ["user1", "user2"],
          +     *       "roles": ["role1", "role2"],
          +     *       "backend_roles": ["backend_role1"]
          +     *     }
          +     *   }
          +     * }
          +     * 
          + * + * @param resourceId The ID of the resource from which to revoke access + * @param sourceIdx The name of the system index where the resource exists + * @param revokeAccess A map containing entity types (USER, ROLE, BACKEND_ROLE) and their corresponding + * values to be removed from the sharing configuration + * @param scopes A get of scopes to revoke access from. If null or empty, access is revoked from all scopes + * @param requestUserName The user trying to revoke the accesses + * @param isAdmin Boolean indicating whether the user is an admin or not + * @param listener Listener to be notified when the operation completes + * @throws IllegalArgumentException if resourceId, sourceIdx is null/empty, or if revokeAccess is null/empty + * @throws RuntimeException if the update operation fails or encounters an error + * @apiNote This method modifies the existing document. If no modifications are needed (i.e., specified + * entities don't exist in the current configuration), the original document is returned unchanged. + * @example + *
          +     * Map> revokeAccess = new HashMap<>();
          +     * revokeAccess.put(RecipientType.USER, Set.of("user1", "user2"));
          +     * revokeAccess.put(RecipientType.ROLE, Set.of("role1"));
          +     * ResourceSharing updated = revokeAccess("resourceId", "pluginIndex", revokeAccess);
          +     * 
          + * @see RecipientType + * @see ResourceSharing + */ + public void revokeAccess( + String resourceId, + String sourceIdx, + Map> revokeAccess, + Set scopes, + String requestUserName, + boolean isAdmin, + ActionListener listener + ) { + if (StringUtils.isBlank(resourceId) || StringUtils.isBlank(sourceIdx) || revokeAccess == null || revokeAccess.isEmpty()) { + listener.onFailure(new IllegalArgumentException("resourceId, sourceIdx, and revokeAccess must not be null or empty")); + return; + } + + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + + LOGGER.debug( + "Revoking access for resource {} in {} for entities: {} and scopes: {}", + resourceId, + sourceIdx, + revokeAccess, + scopes + ); + + StepListener currentSharingListener = new StepListener<>(); + StepListener revokeUpdateListener = new StepListener<>(); + StepListener updatedSharingListener = new StepListener<>(); + + // Fetch the current ResourceSharing document + fetchDocumentById(sourceIdx, resourceId, currentSharingListener); + + // Check permissions & build revoke script + currentSharingListener.whenComplete(currentSharingInfo -> { + // Only admin or the creator of the resource is currently allowed to revoke access + if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { + listener.onFailure( + new ResourceSharingException( + "User " + requestUserName + " is not authorized to revoke access to resource " + resourceId + ) + ); + } + + Map revoke = new HashMap<>(); + for (Map.Entry> entry : revokeAccess.entrySet()) { + revoke.put(entry.getKey().type().toLowerCase(), new ArrayList<>(entry.getValue())); + } + List scopesToUse = (scopes != null) ? new ArrayList<>(scopes) : new ArrayList<>(); + + Script revokeScript = new Script(ScriptType.INLINE, "painless", """ + if (ctx._source.share_with != null) { + Set scopesToProcess = new HashSet(params.scopes.isEmpty() ? ctx._source.share_with.keySet() : params.scopes); + + for (def scopeName : scopesToProcess) { + if (ctx._source.share_with.containsKey(scopeName)) { + def existingScope = ctx._source.share_with.get(scopeName); + + for (def entry : params.revokeAccess.entrySet()) { + def RecipientType = entry.getKey(); + def entitiesToRemove = entry.getValue(); + + if (existingScope.containsKey(RecipientType) && existingScope[RecipientType] != null) { + if (!(existingScope[RecipientType] instanceof HashSet)) { + existingScope[RecipientType] = new HashSet(existingScope[RecipientType]); + } + + existingScope[RecipientType].removeAll(entitiesToRemove); + + if (existingScope[RecipientType].isEmpty()) { + existingScope.remove(RecipientType); + } + } + } + + if (existingScope.isEmpty()) { + ctx._source.share_with.remove(scopeName); + } + } + } + } + """, Map.of("revokeAccess", revoke, "scopes", scopesToUse)); + updateByQueryResourceSharing(sourceIdx, resourceId, revokeScript, revokeUpdateListener); + + }, listener::onFailure); + + // Return doc or null based on successful result, fail otherwise + revokeUpdateListener.whenComplete(success -> { + if (!success) { + LOGGER.error("Failed to revoke access for resource {} in index {} (no docs updated).", resourceId, sourceIdx); + listener.onResponse(null); + return; + } + // TODO check if this should be replaced by Java in-memory computation (current intuition is that it will be more memory + // intensive to do it in java) + fetchDocumentById(sourceIdx, resourceId, updatedSharingListener); + }, listener::onFailure); + + updatedSharingListener.whenComplete(listener::onResponse, listener::onFailure); + } + } + + /** + * Deletes resource sharing records that match the specified source index and resource ID. + * This method performs a delete-by-query operation in the resource sharing index. + * + *

          The method executes the following steps: + *

            + *
          1. Creates a delete-by-query request with a bool query
          2. + *
          3. Matches documents based on exact source index and resource ID
          4. + *
          5. Executes the delete operation with immediate refresh
          6. + *
          7. Returns the success/failure status based on deletion results
          8. + *
          + * + *

          Example document structure that will be deleted: + *

          +     * {
          +     *   "source_idx": "source_index_name",
          +     *   "resource_id": "resource_id_value",
          +     *   "share_with": {
          +     *     // sharing configuration
          +     *   }
          +     * }
          +     * 
          + * + * @param sourceIdx The source index to match in the query (exact match) + * @param resourceId The resource ID to match in the query (exact match) + * @param listener The listener to be notified when the operation completes + * @throws IllegalArgumentException if sourceIdx or resourceId is null/empty + * @throws RuntimeException if the delete operation fails or encounters an error + * @implNote The delete operation uses a bool query with two must clauses to ensure exact matching: + *
          +     * {
          +     *   "query": {
          +     *     "bool": {
          +     *       "must": [
          +     *         { "term": { "source_idx": sourceIdx } },
          +     *         { "term": { "resource_id": resourceId } }
          +     *       ]
          +     *     }
          +     *   }
          +     * }
          +     * 
          + */ + public void deleteResourceSharingRecord(String resourceId, String sourceIdx, ActionListener listener) { + LOGGER.debug( + "Deleting documents asynchronously from {} where source_idx = {} and resource_id = {}", + resourceSharingIndex, + sourceIdx, + resourceId + ); + + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + DeleteByQueryRequest dbq = new DeleteByQueryRequest(resourceSharingIndex).setQuery( + QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) + .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)) + ).setRefresh(true); + + client.execute(DeleteByQueryAction.INSTANCE, dbq, new ActionListener<>() { + @Override + public void onResponse(BulkByScrollResponse response) { + + long deleted = response.getDeleted(); + if (deleted > 0) { + LOGGER.info("Successfully deleted {} documents from {}", deleted, resourceSharingIndex); + listener.onResponse(true); + } else { + LOGGER.info( + "No documents found to delete in {} for source_idx: {} and resource_id: {}", + resourceSharingIndex, + sourceIdx, + resourceId + ); + // No documents were deleted + listener.onResponse(false); + } + } + + @Override + public void onFailure(Exception e) { + LOGGER.error("Failed to delete documents from {}", resourceSharingIndex, e); + listener.onFailure(e); + + } + }); + } catch (Exception e) { + LOGGER.error("Failed to delete documents from {} before request submission", resourceSharingIndex, e); + listener.onFailure(e); + } + } + + /** + * Deletes all resource sharing records that were created by a specific user. + * This method performs a delete-by-query operation to remove all documents where + * the created_by.user field matches the specified username. + * + *

          The method executes the following steps: + *

            + *
          1. Validates the input username parameter
          2. + *
          3. Creates a delete-by-query request with term query matching
          4. + *
          5. Executes the delete operation with immediate refresh
          6. + *
          7. Returns the operation status based on number of deleted documents
          8. + *
          + * + *

          Example query structure: + *

          +     * {
          +     *   "query": {
          +     *     "term": {
          +     *       "created_by.user": "username"
          +     *     }
          +     *   }
          +     * }
          +     * 
          + * + * @param name The username to match against the created_by.user field + * @param listener The listener to be notified when the operation completes + * @throws IllegalArgumentException if name is null or empty + * @implNote Implementation details: + *
            + *
          • Uses DeleteByQueryRequest for efficient bulk deletion
          • + *
          • Sets refresh=true for immediate consistency
          • + *
          • Uses term query for exact username matching
          • + *
          • Implements comprehensive error handling and logging
          • + *
          + *

          + * Example usage: + *

          +     * boolean success = deleteAllRecordsForUser("john.doe");
          +     * if (success) {
          +     *     // Records were successfully deleted
          +     * } else {
          +     *     // No matching records found or operation failed
          +     * }
          +     * 
          + */ + public void deleteAllRecordsForUser(String name, ActionListener listener) { + if (StringUtils.isBlank(name)) { + listener.onFailure(new IllegalArgumentException("Username must not be null or empty")); + return; + } + + LOGGER.debug("Deleting all records for user {} asynchronously", name); + + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + DeleteByQueryRequest deleteRequest = new DeleteByQueryRequest(resourceSharingIndex).setQuery( + QueryBuilders.termQuery("created_by.user", name) + ).setRefresh(true); + + client.execute(DeleteByQueryAction.INSTANCE, deleteRequest, new ActionListener<>() { + @Override + public void onResponse(BulkByScrollResponse response) { + long deletedDocs = response.getDeleted(); + if (deletedDocs > 0) { + LOGGER.info("Successfully deleted {} documents created by user {}", deletedDocs, name); + listener.onResponse(true); + } else { + LOGGER.info("No documents found for user {}", name); + // No documents matched => success = false + listener.onResponse(false); + } + } + + @Override + public void onFailure(Exception e) { + LOGGER.error("Failed to delete documents for user {}", name, e); + listener.onFailure(e); + } + }); + } catch (Exception e) { + LOGGER.error("Failed to delete documents for user {} before request submission", name, e); + listener.onFailure(e); + } + } + + /** + * Fetches all documents from the specified resource index and deserializes them into the specified class. + * + * @param resourceIndex The resource index to fetch documents from. + * @param parser The class to deserialize the documents into a specified type defined by the parser. + * @param listener The listener to be notified with the set of deserialized documents. + * @param The type of the deserialized documents. + */ + public void getResourceDocumentsFromIds( + Set resourceIds, + String resourceIndex, + ResourceParser parser, + ActionListener> listener + ) { + if (resourceIds.isEmpty()) { + listener.onResponse(new HashSet<>()); + return; + } + + // stashing Context to avoid permission issues in-case resourceIndex is a system index + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + MultiGetRequest request = new MultiGetRequest(); + for (String id : resourceIds) { + request.add(new MultiGetRequest.Item(resourceIndex, id)); + } + + client.multiGet(request, ActionListener.wrap(response -> { + Set result = new HashSet<>(); + try { + for (MultiGetItemResponse itemResponse : response.getResponses()) { + if (!itemResponse.isFailed() && itemResponse.getResponse().isExists()) { + BytesReference sourceAsString = itemResponse.getResponse().getSourceAsBytesRef(); + XContentParser xContentParser = XContentHelper.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + sourceAsString, + XContentType.JSON + ); + T resource = parser.parseXContent(xContentParser); + result.add(resource); + } + } + listener.onResponse(result); + } catch (Exception e) { + listener.onFailure(new ResourceSharingException("Failed to parse resources: " + e.getMessage(), e)); + } + }, e -> { + if (e instanceof IndexNotFoundException) { + LOGGER.error("Index {} does not exist", resourceIndex, e); + listener.onFailure(e); + } else { + LOGGER.error("Failed to fetch resources with ids {} from index {}", resourceIds, resourceIndex, e); + listener.onFailure(new ResourceSharingException("Failed to fetch resources: " + e.getMessage(), e)); + } + })); + } + } + +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexListener.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexListener.java new file mode 100644 index 0000000000..c4a47b7fad --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexListener.java @@ -0,0 +1,168 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources; + +import java.io.IOException; +import java.util.Objects; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.index.shard.ShardId; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.index.engine.Engine; +import org.opensearch.index.shard.IndexingOperationListener; +import org.opensearch.security.common.auth.UserSubjectImpl; +import org.opensearch.security.common.configuration.AdminDNs; +import org.opensearch.security.common.support.ConfigConstants; +import org.opensearch.security.common.user.User; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.client.Client; + +/** + * This class implements an index operation listener for operations performed on resources stored in plugin's indices + * These indices are defined on bootstrap and configured to listen in OpenSearchSecurityPlugin.java + */ +public class ResourceSharingIndexListener implements IndexingOperationListener { + + private final static Logger log = LogManager.getLogger(ResourceSharingIndexListener.class); + + private static final ResourceSharingIndexListener INSTANCE = new ResourceSharingIndexListener(); + private ResourceSharingIndexHandler resourceSharingIndexHandler; + private ResourceAccessHandler resourceAccessHandler; + + private boolean initialized; + + private ThreadPool threadPool; + + private ResourceSharingIndexListener() {} + + public static ResourceSharingIndexListener getInstance() { + return ResourceSharingIndexListener.INSTANCE; + } + + /** + * Initializes the ResourceSharingIndexListener with the provided ThreadPool and Client. + * This method is called during the plugin's initialization process. + * + * @param threadPool The ThreadPool instance to be used for executing operations. + * @param client The Client instance to be used for interacting with OpenSearch. + * @param adminDns The AdminDNs instance to be used for checking admin privileges. + */ + public void initialize(ThreadPool threadPool, Client client, AdminDNs adminDns) { + + if (initialized) { + return; + } + + initialized = true; + this.threadPool = threadPool; + this.resourceSharingIndexHandler = new ResourceSharingIndexHandler( + ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX, + client, + threadPool + ); + + resourceAccessHandler = new ResourceAccessHandler(threadPool, this.resourceSharingIndexHandler, adminDns); + + } + + public boolean isInitialized() { + return initialized; + } + + /** + * This method is called after an index operation is performed. + * It creates a resource sharing entry in the dedicated resource sharing index. + * + * @param shardId The shard ID of the index where the operation was performed. + * @param index The index where the operation was performed. + * @param result The result of the index operation. + */ + @Override + public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { + + String resourceIndex = shardId.getIndexName(); + log.debug("postIndex called on {}", resourceIndex); + + String resourceId = index.id(); + + final UserSubjectImpl userSubject = (UserSubjectImpl) threadPool.getThreadContext() + .getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); + final User user = userSubject.getUser(); + try { + Objects.requireNonNull(user); + ResourceSharing sharing = this.resourceSharingIndexHandler.indexResourceSharing( + resourceId, + resourceIndex, + new CreatedBy(Creator.USER, user.getName()), + null + ); + log.info("Successfully created a resource sharing entry {}", sharing); + } catch (IOException e) { + log.info("Failed to create a resource sharing entry for resource: {}", resourceId); + } + } + + /** + * This method is called after a delete operation is performed. + * It deletes the corresponding resource sharing entry from the dedicated resource sharing index. + * + * @param shardId The shard ID of the index where the delete operation was performed. + * @param delete The delete operation that was performed. + * @return The delete operation to be performed. + */ + @Override + public Engine.Delete preDelete(ShardId shardId, Engine.Delete delete) { + + String resourceIndex = shardId.getIndexName(); + log.debug("preDelete called on {}", resourceIndex); + + String resourceId = delete.id(); + + this.resourceAccessHandler.checkDeletePermission(resourceId, resourceIndex, ActionListener.wrap((canDelete) -> { + if (canDelete) { + log.debug("Proceeding with delete operation for resource {}", resourceId); + } else { + throw new OpenSearchSecurityException( + "Delete operation not permitted for resource " + resourceId + " in index " + resourceIndex, + RestStatus.FORBIDDEN + ); + } + }, exception -> log.error("Failed to check delete permission for resource {}", resourceId, exception))); + return delete; + } + + /** + * This method is called after a delete operation is performed. + * It deletes the corresponding resource sharing entry from the dedicated resource sharing index. + * + * @param shardId The shard ID of the index where the delete operation was performed. + * @param delete The delete operation that was performed. + * @param result The result of the delete operation. + */ + @Override + public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { + + String resourceIndex = shardId.getIndexName(); + log.debug("postDelete called on {}", resourceIndex); + + String resourceId = delete.id(); + + this.resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex, ActionListener.wrap(deleted -> { + if (deleted) { + log.info("Successfully deleted resource sharing entry for resource {}", resourceId); + } else { + log.info("No resource sharing entry found for resource {}", resourceId); + } + }, exception -> log.error("Failed to delete resource sharing entry for resource {}", resourceId, exception))); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java new file mode 100644 index 0000000000..eb3d5b3fa2 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java @@ -0,0 +1,53 @@ +/* + * 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.common.resources; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class ResourceSharingIndexManagementRepository { + + private static final Logger log = LogManager.getLogger(ResourceSharingIndexManagementRepository.class); + + private final ResourceSharingIndexHandler resourceSharingIndexHandler; + private final boolean resourceSharingEnabled; + + protected ResourceSharingIndexManagementRepository( + final ResourceSharingIndexHandler resourceSharingIndexHandler, + boolean isResourceSharingEnabled + ) { + this.resourceSharingIndexHandler = resourceSharingIndexHandler; + this.resourceSharingEnabled = isResourceSharingEnabled; + } + + public static ResourceSharingIndexManagementRepository create( + ResourceSharingIndexHandler resourceSharingIndexHandler, + boolean isResourceSharingEnabled + ) { + return new ResourceSharingIndexManagementRepository(resourceSharingIndexHandler, isResourceSharingEnabled); + } + + /** + * Creates the resource sharing index if it doesn't already exist. + * This method is called during the initialization phase of the repository. + * It ensures that the index is set up with the necessary mappings and settings + * before any operations are performed on the index. + */ + public void createResourceSharingIndexIfAbsent() { + // TODO check if this should be wrapped in an atomic completable future + if (resourceSharingEnabled) { + log.info("Attempting to create Resource Sharing index"); + this.resourceSharingIndexHandler.createResourceSharingIndexIfAbsent(() -> null); + } + + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ShareWith.java b/common/src/main/java/org/opensearch/security/common/resources/ShareWith.java new file mode 100644 index 0000000000..2deface76c --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ShareWith.java @@ -0,0 +1,103 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +/** + * This class contains information about whom a resource is shared with and at what scope. + * Example: + * "share_with": { + * "read_only": { + * "users": [], + * "roles": [], + * "backend_roles": [] + * }, + * "read_write": { + * "users": [], + * "roles": [], + * "backend_roles": [] + * } + * } + * + * @opensearch.experimental + */ +public class ShareWith implements ToXContentFragment, NamedWriteable { + + /** + * A set of objects representing the scopes and their associated users, roles, and backend roles. + */ + private final Set sharedWithScopes; + + public ShareWith(Set sharedWithScopes) { + this.sharedWithScopes = sharedWithScopes; + } + + public ShareWith(StreamInput in) throws IOException { + this.sharedWithScopes = in.readSet(SharedWithScope::new); + } + + public Set getSharedWithScopes() { + return sharedWithScopes; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + + for (SharedWithScope scope : sharedWithScopes) { + scope.toXContent(builder, params); + } + + return builder.endObject(); + } + + public static ShareWith fromXContent(XContentParser parser) throws IOException { + Set sharedWithScopes = new HashSet<>(); + + if (parser.currentToken() != XContentParser.Token.START_OBJECT) { + parser.nextToken(); + } + + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + // Each field in the object represents a SharedWithScope + if (token == XContentParser.Token.FIELD_NAME) { + SharedWithScope scope = SharedWithScope.fromXContent(parser); + sharedWithScopes.add(scope); + } + } + + return new ShareWith(sharedWithScopes); + } + + @Override + public String getWriteableName() { + return "share_with"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeCollection(sharedWithScopes); + } + + @Override + public String toString() { + return "ShareWith " + sharedWithScopes; + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/SharedWithScope.java b/common/src/main/java/org/opensearch/security/common/resources/SharedWithScope.java new file mode 100644 index 0000000000..b8a16e56f7 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/SharedWithScope.java @@ -0,0 +1,169 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +/** + * This class represents the scope at which a resource is shared with. + * Example: + * "read_only": { + * "users": [], + * "roles": [], + * "backend_roles": [] + * } + * where "users", "roles" and "backend_roles" are the recipient entities + * + * @opensearch.experimental + */ +public class SharedWithScope implements ToXContentFragment, NamedWriteable { + + private final String scope; + + private final ScopeRecipients scopeRecipients; + + public SharedWithScope(String scope, ScopeRecipients scopeRecipients) { + this.scope = scope; + this.scopeRecipients = scopeRecipients; + } + + public SharedWithScope(StreamInput in) throws IOException { + this.scope = in.readString(); + this.scopeRecipients = new ScopeRecipients(in); + } + + public String getScope() { + return scope; + } + + public ScopeRecipients getSharedWithPerScope() { + return scopeRecipients; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field(scope); + builder.startObject(); + + scopeRecipients.toXContent(builder, params); + + return builder.endObject(); + } + + public static SharedWithScope fromXContent(XContentParser parser) throws IOException { + String scope = parser.currentName(); + + parser.nextToken(); + + ScopeRecipients scopeRecipients = ScopeRecipients.fromXContent(parser); + + return new SharedWithScope(scope, scopeRecipients); + } + + @Override + public String toString() { + return "{" + scope + ": " + scopeRecipients + '}'; + } + + @Override + public String getWriteableName() { + return "shared_with_scope"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(scope); + out.writeNamedWriteable(scopeRecipients); + } + + /** + * This class represents the entities with whom a resource is shared with for a given scope. + * + * @opensearch.experimental + */ + public static class ScopeRecipients implements ToXContentFragment, NamedWriteable { + + private final Map> recipients; + + public ScopeRecipients(Map> recipients) { + if (recipients == null) { + throw new IllegalArgumentException("Recipients map cannot be null"); + } + this.recipients = recipients; + } + + public ScopeRecipients(StreamInput in) throws IOException { + this.recipients = in.readMap( + key -> RecipientTypeRegistry.fromValue(key.readString()), + input -> input.readSet(StreamInput::readString) + ); + } + + public Map> getRecipients() { + return recipients; + } + + @Override + public String getWriteableName() { + return "scope_recipients"; + } + + public static ScopeRecipients fromXContent(XContentParser parser) throws IOException { + Map> recipients = new HashMap<>(); + + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + String fieldName = parser.currentName(); + RecipientType recipientType = RecipientTypeRegistry.fromValue(fieldName); + + parser.nextToken(); + Set values = new HashSet<>(); + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + values.add(parser.text()); + } + recipients.put(recipientType, values); + } + } + + return new ScopeRecipients(recipients); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeMap( + recipients, + (streamOutput, recipientType) -> streamOutput.writeString(recipientType.type()), + (streamOutput, strings) -> streamOutput.writeCollection(strings, StreamOutput::writeString) + ); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (recipients.isEmpty()) { + return builder; + } + for (Map.Entry> entry : recipients.entrySet()) { + builder.array(entry.getKey().type(), entry.getValue().toArray()); + } + return builder; + } + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/package-info.java b/common/src/main/java/org/opensearch/security/common/resources/package-info.java new file mode 100644 index 0000000000..afb8d92761 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/package-info.java @@ -0,0 +1,14 @@ +/* + * 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. + */ + +/** + * This package defines class required to implement resource access control in OpenSearch. + * + * @opensearch.experimental + */ +package org.opensearch.security.common.resources; diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java new file mode 100644 index 0000000000..31c7038113 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources.rest; + +import org.opensearch.action.ActionType; + +public class ResourceAccessAction extends ActionType { + + public static final ResourceAccessAction INSTANCE = new ResourceAccessAction(); + + public static final String NAME = "cluster:admin/security/resource_access"; + + private ResourceAccessAction() { + super(NAME, ResourceAccessResponse::new); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java new file mode 100644 index 0000000000..4bae7fd430 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java @@ -0,0 +1,154 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources.rest; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.security.common.resources.ShareWith; + +public class ResourceAccessRequest extends ActionRequest { + + public enum Operation { + LIST, + SHARE, + REVOKE, + VERIFY + } + + private final Operation operation; + private final String resourceId; + private final String resourceIndex; + private final String scope; + private ShareWith shareWith; + private Map> revokedEntities; + private Set scopes; + + /** + * New Constructor: Initialize request from a `Map` + */ + @SuppressWarnings("unchecked") + public ResourceAccessRequest(Map source, Map params) throws IOException { + if (source.containsKey("operation")) { + this.operation = (Operation) source.get("operation"); + } else { + throw new IllegalArgumentException("Missing required field: operation"); + } + + this.resourceId = (String) source.get("resource_id"); + this.resourceIndex = params.containsKey("resource_index") ? params.get("resource_index") : (String) (source.get("resource_index")); + this.scope = (String) source.get("scope"); + + if (source.containsKey("share_with")) { + this.shareWith = parseShareWith(source); + } + + if (source.containsKey("entities_to_revoke")) { + this.revokedEntities = ((Map>) source.get("entities_to_revoke")); + } + + if (source.containsKey("scopes")) { + this.scopes = Set.copyOf((List) source.get("scopes")); + } + } + + public ResourceAccessRequest(StreamInput in) throws IOException { + super(in); + this.operation = in.readEnum(Operation.class); + this.resourceId = in.readOptionalString(); + this.resourceIndex = in.readOptionalString(); + this.scope = in.readOptionalString(); + this.shareWith = in.readOptionalWriteable(ShareWith::new); + this.revokedEntities = in.readMap(StreamInput::readString, valIn -> valIn.readSet(StreamInput::readString)); + + this.scopes = in.readSet(StreamInput::readString); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeEnum(operation); + out.writeOptionalString(resourceId); + out.writeOptionalString(resourceIndex); + out.writeOptionalString(scope); + out.writeOptionalWriteable(shareWith); + out.writeMap(revokedEntities, StreamOutput::writeString, StreamOutput::writeStringCollection); + out.writeStringCollection(scopes); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + /** + * Parse the share with structure from the request body. + * + * @param source the request body + * @return the parsed ShareWith object + * @throws IOException if an I/O error occurs + */ + @SuppressWarnings("unchecked") + private ShareWith parseShareWith(Map source) throws IOException { + Map shareWithMap = (Map) source.get("share_with"); + if (shareWithMap == null || shareWithMap.isEmpty()) { + throw new IllegalArgumentException("share_with is required and cannot be empty"); + } + + String jsonString = XContentFactory.jsonBuilder().map(shareWithMap).toString(); + + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, jsonString) + ) { + return ShareWith.fromXContent(parser); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); + } + } + + public Operation getOperation() { + return operation; + } + + public String getResourceId() { + return resourceId; + } + + public String getResourceIndex() { + return resourceIndex; + } + + public String getScope() { + return scope; + } + + public ShareWith getShareWith() { + return shareWith; + } + + public Map> getRevokedEntities() { + return revokedEntities; + } + + public Set getScopes() { + return scopes; + } + +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java new file mode 100644 index 0000000000..ed45d34cf5 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.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. + */ + +package org.opensearch.security.common.resources.rest; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamOutput; + +public class ResourceAccessRequestParams implements NamedWriteable { + @Override + public String getWriteableName() { + return "resource_access_request_params"; + } + + @Override + public void writeTo(StreamOutput streamOutput) throws IOException { + + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java new file mode 100644 index 0000000000..97f2fb7c44 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java @@ -0,0 +1,96 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources.rest; + +import java.io.IOException; +import java.util.Collections; +import java.util.Set; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.security.common.resources.ResourceSharing; +import org.opensearch.security.spi.resources.Resource; + +public class ResourceAccessResponse extends ActionResponse implements ToXContentObject { + public enum ResponseType { + RESOURCES, + RESOURCE_SHARING, + BOOLEAN + } + + private final ResponseType responseType; + private final Object responseData; + + public ResourceAccessResponse(final StreamInput in) throws IOException { + this.responseType = in.readEnum(ResponseType.class); + this.responseData = null; + } + + public ResourceAccessResponse(Set resources) { + this.responseType = ResponseType.RESOURCES; + this.responseData = resources; + } + + public ResourceAccessResponse(ResourceSharing resourceSharing) { + this.responseType = ResponseType.RESOURCE_SHARING; + this.responseData = resourceSharing; + } + + public ResourceAccessResponse(boolean hasPermission) { + this.responseType = ResponseType.BOOLEAN; + this.responseData = hasPermission; + } + + @SuppressWarnings("unchecked") + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeEnum(responseType); + switch (responseType) { + case RESOURCES -> out.writeCollection((Set) responseData); + case RESOURCE_SHARING -> ((ResourceSharing) responseData).writeTo(out); + case BOOLEAN -> out.writeBoolean((Boolean) responseData); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + switch (responseType) { + case RESOURCES -> builder.field("resources", responseData); + case RESOURCE_SHARING -> builder.field("sharing_info", responseData); + case BOOLEAN -> builder.field("has_permission", responseData); + } + return builder.endObject(); + } + + @SuppressWarnings("unchecked") + public Set getResources() { + return responseType == ResponseType.RESOURCES ? (Set) responseData : Collections.emptySet(); + } + + public ResourceSharing getResourceSharing() { + return responseType == ResponseType.RESOURCE_SHARING ? (ResourceSharing) responseData : null; + } + + public Boolean getHasPermission() { + return responseType == ResponseType.BOOLEAN ? (Boolean) responseData : null; + } + + @Override + public String toString() { + if (responseData == null) { + return "ResourceAccessResponse{" + "responseType=" + responseType + ", responseData=null}"; + } + return "ResourceAccessResponse{" + "responseType=" + responseType + ", responseData=" + responseData.toString() + "}"; + + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java new file mode 100644 index 0000000000..84523c94ed --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java @@ -0,0 +1,146 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources.rest; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.transport.client.node.NodeClient; + +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.common.dlic.rest.api.Responses.badRequest; +import static org.opensearch.security.common.dlic.rest.api.Responses.forbidden; +import static org.opensearch.security.common.dlic.rest.api.Responses.ok; +import static org.opensearch.security.common.dlic.rest.api.Responses.unauthorized; +import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.LIST; +import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.REVOKE; +import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.SHARE; +import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.VERIFY; +import static org.opensearch.security.common.support.Utils.PLUGIN_RESOURCE_ROUTE_PREFIX; +import static org.opensearch.security.common.support.Utils.addRoutesPrefix; + +/** + * This class handles the REST API for resource access management. + */ +public class ResourceAccessRestAction extends BaseRestHandler { + private static final Logger LOGGER = LogManager.getLogger(ResourceAccessRestAction.class); + + public ResourceAccessRestAction() {} + + @Override + public List routes() { + return addRoutesPrefix( + ImmutableList.of( + new Route(GET, "/list/{resource_index}"), + new Route(POST, "/revoke"), + new Route(POST, "/share"), + new Route(POST, "/verify_access") + ), + PLUGIN_RESOURCE_ROUTE_PREFIX + ); + } + + @Override + public String getName() { + return "resource_api_action"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + consumeParams(request); // early consume params to avoid 400s + + Map source = new HashMap<>(); + if (request.hasContent()) { + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + } + + String path = request.path().split(PLUGIN_RESOURCE_ROUTE_PREFIX)[1].split("/")[1]; + switch (path) { + case "list" -> source.put("operation", LIST); + case "revoke" -> source.put("operation", REVOKE); + case "share" -> source.put("operation", SHARE); + case "verify_access" -> source.put("operation", VERIFY); + default -> { + return channel -> badRequest(channel, "Unknown route: " + path); + } + } + + ResourceAccessRequest resourceAccessRequest = new ResourceAccessRequest(source, request.params()); + return channel -> { + client.executeLocally(ResourceAccessAction.INSTANCE, resourceAccessRequest, new ActionListener<>() { + + @Override + public void onResponse(ResourceAccessResponse response) { + try { + sendResponse(channel, response); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void onFailure(Exception e) { + handleError(channel, e); + } + + }); + }; + } + + /** + * Consume params early to avoid 400s. + * + * @param request from which the params must be consumed + */ + private void consumeParams(RestRequest request) { + request.param("resource_index", ""); + } + + /** + * Send the appropriate response to the channel. + * @param channel the channel to send the response to + * @param response the response to send + * @throws IOException if an I/O error occurs + */ + private void sendResponse(RestChannel channel, ResourceAccessResponse response) throws IOException { + ok(channel, response::toXContent); + } + + /** + * Handle errors that occur during request processing. + * @param channel the channel to send the error response to + * @param e the exception that caused the error + */ + private void handleError(RestChannel channel, Exception e) { + String message = e.getMessage(); + LOGGER.error(message, e); + if (message.contains("not authorized")) { + forbidden(channel, message); + } else if (message.contains("no authenticated")) { + unauthorized(channel); + } + channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, message)); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java new file mode 100644 index 0000000000..3c548512ee --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java @@ -0,0 +1,101 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources.rest; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.security.common.resources.RecipientType; +import org.opensearch.security.common.resources.RecipientTypeRegistry; +import org.opensearch.security.common.resources.ResourceAccessHandler; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +public class ResourceAccessTransportAction extends HandledTransportAction { + private final ResourceAccessHandler resourceAccessHandler; + + @Inject + public ResourceAccessTransportAction( + TransportService transportService, + ActionFilters actionFilters, + ResourceAccessHandler resourceAccessHandler + ) { + super(ResourceAccessAction.NAME, transportService, actionFilters, ResourceAccessRequest::new); + this.resourceAccessHandler = resourceAccessHandler; + } + + @Override + protected void doExecute(Task task, ResourceAccessRequest request, ActionListener actionListener) { + switch (request.getOperation()) { + case LIST: + handleListResources(request, actionListener); + break; + case SHARE: + handleGrantAccess(request, actionListener); + break; + case REVOKE: + handleRevokeAccess(request, actionListener); + break; + case VERIFY: + handleVerifyAccess(request, actionListener); + break; + default: + actionListener.onFailure(new IllegalArgumentException("Unknown action type: " + request.getOperation())); + } + } + + private void handleListResources(ResourceAccessRequest request, ActionListener listener) { + resourceAccessHandler.getAccessibleResourcesForCurrentUser( + request.getResourceIndex(), + ActionListener.wrap(resources -> listener.onResponse(new ResourceAccessResponse(resources)), listener::onFailure) + ); + } + + private void handleGrantAccess(ResourceAccessRequest request, ActionListener listener) { + resourceAccessHandler.shareWith( + request.getResourceId(), + request.getResourceIndex(), + request.getShareWith(), + ActionListener.wrap(response -> listener.onResponse(new ResourceAccessResponse(response)), listener::onFailure) + ); + } + + private void handleRevokeAccess(ResourceAccessRequest request, ActionListener listener) { + resourceAccessHandler.revokeAccess( + request.getResourceId(), + request.getResourceIndex(), + parseRevokedEntities(request.getRevokedEntities()), + request.getScopes(), + ActionListener.wrap(success -> listener.onResponse(new ResourceAccessResponse(success)), listener::onFailure) + ); + } + + private void handleVerifyAccess(ResourceAccessRequest request, ActionListener listener) { + resourceAccessHandler.hasPermission( + request.getResourceId(), + request.getResourceIndex(), + request.getScope(), + ActionListener.wrap(hasPermission -> listener.onResponse(new ResourceAccessResponse(hasPermission)), listener::onFailure) + ); + } + + /** + * Helper method to parse revoked entities from a generic Map + */ + private Map> parseRevokedEntities(Map> revokeSource) { + return revokeSource.entrySet() + .stream() + .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/support/ConfigConstants.java b/common/src/main/java/org/opensearch/security/common/support/ConfigConstants.java new file mode 100644 index 0000000000..beb4fa2722 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/support/ConfigConstants.java @@ -0,0 +1,399 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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.common.support; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.common.auditlog.impl.AuditCategory; + +import com.password4j.types.Hmac; + +public class ConfigConstants { + + public static final String OPENDISTRO_SECURITY_CONFIG_PREFIX = "_opendistro_security_"; + public static final String SECURITY_SETTINGS_PREFIX = "plugins.security."; + + public static final String OPENDISTRO_SECURITY_CHANNEL_TYPE = OPENDISTRO_SECURITY_CONFIG_PREFIX + "channel_type"; + + public static final String OPENDISTRO_SECURITY_ORIGIN = OPENDISTRO_SECURITY_CONFIG_PREFIX + "origin"; + public static final String OPENDISTRO_SECURITY_ORIGIN_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "origin_header"; + + public static final String OPENDISTRO_SECURITY_DLS_QUERY_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "dls_query"; + + public static final String OPENDISTRO_SECURITY_DLS_FILTER_LEVEL_QUERY_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + + "dls_filter_level_query"; + public static final String OPENDISTRO_SECURITY_DLS_FILTER_LEVEL_QUERY_TRANSIENT = OPENDISTRO_SECURITY_CONFIG_PREFIX + + "dls_filter_level_query_t"; + + public static final String OPENDISTRO_SECURITY_DLS_MODE_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "dls_mode"; + public static final String OPENDISTRO_SECURITY_DLS_MODE_TRANSIENT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "dls_mode_t"; + + public static final String OPENDISTRO_SECURITY_FLS_FIELDS_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "fls_fields"; + + public static final String OPENDISTRO_SECURITY_MASKED_FIELD_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "masked_fields"; + + public static final String OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "doc_allowlist"; + public static final String OPENDISTRO_SECURITY_DOC_ALLOWLIST_TRANSIENT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "doc_allowlist_t"; + + public static final String OPENDISTRO_SECURITY_FILTER_LEVEL_DLS_DONE = OPENDISTRO_SECURITY_CONFIG_PREFIX + "filter_level_dls_done"; + + public static final String OPENDISTRO_SECURITY_DLS_QUERY_CCS = OPENDISTRO_SECURITY_CONFIG_PREFIX + "dls_query_ccs"; + + public static final String OPENDISTRO_SECURITY_FLS_FIELDS_CCS = OPENDISTRO_SECURITY_CONFIG_PREFIX + "fls_fields_ccs"; + + public static final String OPENDISTRO_SECURITY_MASKED_FIELD_CCS = OPENDISTRO_SECURITY_CONFIG_PREFIX + "masked_fields_ccs"; + + public static final String OPENDISTRO_SECURITY_CONF_REQUEST_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "conf_request"; + + public static final String OPENDISTRO_SECURITY_REMOTE_ADDRESS = OPENDISTRO_SECURITY_CONFIG_PREFIX + "remote_address"; + public static final String OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "remote_address_header"; + + public static final String OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + + "initial_action_class_header"; + + /** + * Set by SSL plugin for https requests only + */ + public static final String OPENDISTRO_SECURITY_SSL_PEER_CERTIFICATES = OPENDISTRO_SECURITY_CONFIG_PREFIX + "ssl_peer_certificates"; + + /** + * Set by SSL plugin for https requests only + */ + public static final String OPENDISTRO_SECURITY_SSL_PRINCIPAL = OPENDISTRO_SECURITY_CONFIG_PREFIX + "ssl_principal"; + + /** + * If this is set to TRUE then the request comes from a Server Node (fully trust) + * Its expected that there is a _opendistro_security_user attached as header + */ + public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_INTERCLUSTER_REQUEST = OPENDISTRO_SECURITY_CONFIG_PREFIX + + "ssl_transport_intercluster_request"; + + public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_TRUSTED_CLUSTER_REQUEST = OPENDISTRO_SECURITY_CONFIG_PREFIX + + "ssl_transport_trustedcluster_request"; + + // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions manager used to allow/disallow TLS connections to extensions + public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_EXTENSION_REQUEST = OPENDISTRO_SECURITY_CONFIG_PREFIX + + "ssl_transport_extension_request"; + // CS-ENFORCE-SINGLE + + /** + * Set by the SSL plugin, this is the peer node certificate on the transport layer + */ + public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_PRINCIPAL = OPENDISTRO_SECURITY_CONFIG_PREFIX + "ssl_transport_principal"; + + public static final String OPENDISTRO_SECURITY_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user"; + public static final String OPENDISTRO_SECURITY_USER_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_header"; + + // persistent header. This header is set once and cannot be stashed + public static final String OPENDISTRO_SECURITY_AUTHENTICATED_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "authenticated_user"; + + public static final String OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_info"; + + public static final String OPENDISTRO_SECURITY_INJECTED_USER = "injected_user"; + public static final String OPENDISTRO_SECURITY_INJECTED_USER_HEADER = "injected_user_header"; + + public static final String OPENDISTRO_SECURITY_XFF_DONE = OPENDISTRO_SECURITY_CONFIG_PREFIX + "xff_done"; + + public static final String SSO_LOGOUT_URL = OPENDISTRO_SECURITY_CONFIG_PREFIX + "sso_logout_url"; + + public static final String OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX = ".opendistro_security"; + + public static final String SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE = SECURITY_SETTINGS_PREFIX + "enable_snapshot_restore_privilege"; + public static final boolean SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE = true; + + public static final String SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES = SECURITY_SETTINGS_PREFIX + + "check_snapshot_restore_write_privileges"; + public static final boolean SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES = true; + public static final Set SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES = Collections.unmodifiableSet( + new HashSet(Arrays.asList("indices:admin/create", "indices:data/write/index" + // "indices:data/write/bulk" + )) + ); + + public static final String SECURITY_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = SECURITY_SETTINGS_PREFIX + + "cert.intercluster_request_evaluator_class"; + public static final String OPENDISTRO_SECURITY_ACTION_NAME = OPENDISTRO_SECURITY_CONFIG_PREFIX + "action_name"; + + public static final String SECURITY_AUTHCZ_ADMIN_DN = SECURITY_SETTINGS_PREFIX + "authcz.admin_dn"; + public static final String SECURITY_CONFIG_INDEX_NAME = SECURITY_SETTINGS_PREFIX + "config_index_name"; + public static final String SECURITY_AUTHCZ_IMPERSONATION_DN = SECURITY_SETTINGS_PREFIX + "authcz.impersonation_dn"; + public static final String SECURITY_AUTHCZ_REST_IMPERSONATION_USERS = SECURITY_SETTINGS_PREFIX + "authcz.rest_impersonation_user"; + + public static final String BCRYPT = "bcrypt"; + public static final String PBKDF2 = "pbkdf2"; + + public static final String SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS = SECURITY_SETTINGS_PREFIX + "password.hashing.bcrypt.rounds"; + public static final int SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS_DEFAULT = 12; + public static final String SECURITY_PASSWORD_HASHING_BCRYPT_MINOR = SECURITY_SETTINGS_PREFIX + "password.hashing.bcrypt.minor"; + public static final String SECURITY_PASSWORD_HASHING_BCRYPT_MINOR_DEFAULT = "Y"; + + public static final String SECURITY_PASSWORD_HASHING_ALGORITHM = SECURITY_SETTINGS_PREFIX + "password.hashing.algorithm"; + public static final String SECURITY_PASSWORD_HASHING_ALGORITHM_DEFAULT = BCRYPT; + public static final String SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS = SECURITY_SETTINGS_PREFIX + + "password.hashing.pbkdf2.iterations"; + public static final int SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS_DEFAULT = 600_000; + public static final String SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH = SECURITY_SETTINGS_PREFIX + "password.hashing.pbkdf2.length"; + public static final int SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH_DEFAULT = 256; + public static final String SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION = SECURITY_SETTINGS_PREFIX + "password.hashing.pbkdf2.function"; + public static final String SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION_DEFAULT = Hmac.SHA256.name(); + + public static final String SECURITY_AUDIT_TYPE_DEFAULT = SECURITY_SETTINGS_PREFIX + "audit.type"; + public static final String SECURITY_AUDIT_CONFIG_DEFAULT = SECURITY_SETTINGS_PREFIX + "audit.config"; + public static final String SECURITY_AUDIT_CONFIG_ROUTES = SECURITY_SETTINGS_PREFIX + "audit.routes"; + public static final String SECURITY_AUDIT_CONFIG_ENDPOINTS = SECURITY_SETTINGS_PREFIX + "audit.endpoints"; + public static final String SECURITY_AUDIT_THREADPOOL_SIZE = SECURITY_SETTINGS_PREFIX + "audit.threadpool.size"; + public static final String SECURITY_AUDIT_THREADPOOL_MAX_QUEUE_LEN = SECURITY_SETTINGS_PREFIX + "audit.threadpool.max_queue_len"; + public static final String OPENDISTRO_SECURITY_AUDIT_LOG_REQUEST_BODY = "opendistro_security.audit.log_request_body"; + public static final String OPENDISTRO_SECURITY_AUDIT_RESOLVE_INDICES = "opendistro_security.audit.resolve_indices"; + public static final String OPENDISTRO_SECURITY_AUDIT_ENABLE_REST = "opendistro_security.audit.enable_rest"; + public static final String OPENDISTRO_SECURITY_AUDIT_ENABLE_TRANSPORT = "opendistro_security.audit.enable_transport"; + public static final String OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_TRANSPORT_CATEGORIES = + "opendistro_security.audit.config.disabled_transport_categories"; + public static final String OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES = + "opendistro_security.audit.config.disabled_rest_categories"; + public static final List OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT = ImmutableList.of( + AuditCategory.AUTHENTICATED.toString(), + AuditCategory.GRANTED_PRIVILEGES.toString() + ); + public static final String OPENDISTRO_SECURITY_AUDIT_IGNORE_USERS = "opendistro_security.audit.ignore_users"; + public static final String OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS = "opendistro_security.audit.ignore_requests"; + public static final String SECURITY_AUDIT_IGNORE_HEADERS = SECURITY_SETTINGS_PREFIX + "audit.ignore_headers"; + public static final String OPENDISTRO_SECURITY_AUDIT_RESOLVE_BULK_REQUESTS = "opendistro_security.audit.resolve_bulk_requests"; + public static final boolean OPENDISTRO_SECURITY_AUDIT_SSL_VERIFY_HOSTNAMES_DEFAULT = true; + public static final boolean OPENDISTRO_SECURITY_AUDIT_SSL_ENABLE_SSL_CLIENT_AUTH_DEFAULT = false; + public static final String OPENDISTRO_SECURITY_AUDIT_EXCLUDE_SENSITIVE_HEADERS = "opendistro_security.audit.exclude_sensitive_headers"; + + public static final String SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX = SECURITY_SETTINGS_PREFIX + "audit.config."; + + // Internal Opensearch data_stream + public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_NAME = "data_stream.name"; + public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_TEMPLATE_MANAGE = "data_stream.template.manage"; + public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_TEMPLATE_NAME = "data_stream.template.name"; + public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_TEMPLATE_NUMBER_OF_REPLICAS = "data_stream.template.number_of_replicas"; + public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_TEMPLATE_NUMBER_OF_SHARDS = "data_stream.template.number_of_shards"; + + // Internal / External OpenSearch + public static final String SECURITY_AUDIT_OPENSEARCH_INDEX = "index"; + public static final String SECURITY_AUDIT_OPENSEARCH_TYPE = "type"; + + // External OpenSearch + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_HTTP_ENDPOINTS = "http_endpoints"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME = "username"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD = "password"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL = "enable_ssl"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_VERIFY_HOSTNAMES = "verify_hostnames"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL_CLIENT_AUTH = "enable_ssl_client_auth"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_FILEPATH = "pemkey_filepath"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_CONTENT = "pemkey_content"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_PASSWORD = "pemkey_password"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_FILEPATH = "pemcert_filepath"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_CONTENT = "pemcert_content"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_FILEPATH = "pemtrustedcas_filepath"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_CONTENT = "pemtrustedcas_content"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_JKS_CERT_ALIAS = "cert_alias"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_CIPHERS = "enabled_ssl_ciphers"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_PROTOCOLS = "enabled_ssl_protocols"; + + // Webhooks + public static final String SECURITY_AUDIT_WEBHOOK_URL = "webhook.url"; + public static final String SECURITY_AUDIT_WEBHOOK_FORMAT = "webhook.format"; + public static final String SECURITY_AUDIT_WEBHOOK_SSL_VERIFY = "webhook.ssl.verify"; + public static final String SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_FILEPATH = "webhook.ssl.pemtrustedcas_filepath"; + public static final String SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_CONTENT = "webhook.ssl.pemtrustedcas_content"; + + // Log4j + public static final String SECURITY_AUDIT_LOG4J_LOGGER_NAME = "log4j.logger_name"; + public static final String SECURITY_AUDIT_LOG4J_LEVEL = "log4j.level"; + + // retry + public static final String SECURITY_AUDIT_RETRY_COUNT = SECURITY_SETTINGS_PREFIX + "audit.config.retry_count"; + public static final String SECURITY_AUDIT_RETRY_DELAY_MS = SECURITY_SETTINGS_PREFIX + "audit.config.retry_delay_ms"; + + public static final String SECURITY_KERBEROS_KRB5_FILEPATH = SECURITY_SETTINGS_PREFIX + "kerberos.krb5_filepath"; + public static final String SECURITY_KERBEROS_ACCEPTOR_KEYTAB_FILEPATH = SECURITY_SETTINGS_PREFIX + "kerberos.acceptor_keytab_filepath"; + public static final String SECURITY_KERBEROS_ACCEPTOR_PRINCIPAL = SECURITY_SETTINGS_PREFIX + "kerberos.acceptor_principal"; + public static final String SECURITY_CERT_OID = SECURITY_SETTINGS_PREFIX + "cert.oid"; + public static final String SECURITY_CERT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = SECURITY_SETTINGS_PREFIX + + "cert.intercluster_request_evaluator_class"; + public static final String SECURITY_ADVANCED_MODULES_ENABLED = SECURITY_SETTINGS_PREFIX + "advanced_modules_enabled"; + public static final String SECURITY_NODES_DN = SECURITY_SETTINGS_PREFIX + "nodes_dn"; + public static final String SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED = SECURITY_SETTINGS_PREFIX + "nodes_dn_dynamic_config_enabled"; + public static final String SECURITY_DISABLED = SECURITY_SETTINGS_PREFIX + "disabled"; + + public static final String SECURITY_CACHE_TTL_MINUTES = SECURITY_SETTINGS_PREFIX + "cache.ttl_minutes"; + public static final String SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES = SECURITY_SETTINGS_PREFIX + "allow_unsafe_democertificates"; + public static final String SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX = SECURITY_SETTINGS_PREFIX + "allow_default_init_securityindex"; + + public static final String SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE = SECURITY_SETTINGS_PREFIX + + "allow_default_init_securityindex.use_cluster_state"; + + public static final String SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST = SECURITY_SETTINGS_PREFIX + + "background_init_if_securityindex_not_exist"; + + public static final String SECURITY_ROLES_MAPPING_RESOLUTION = SECURITY_SETTINGS_PREFIX + "roles_mapping_resolution"; + + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_METADATA_ONLY = + "opendistro_security.compliance.history.write.metadata_only"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_METADATA_ONLY = + "opendistro_security.compliance.history.read.metadata_only"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS = + "opendistro_security.compliance.history.read.watched_fields"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES = + "opendistro_security.compliance.history.write.watched_indices"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_LOG_DIFFS = + "opendistro_security.compliance.history.write.log_diffs"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_IGNORE_USERS = + "opendistro_security.compliance.history.read.ignore_users"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_IGNORE_USERS = + "opendistro_security.compliance.history.write.ignore_users"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED = + "opendistro_security.compliance.history.external_config_enabled"; + public static final String OPENDISTRO_SECURITY_SOURCE_FIELD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "source_field_context"; + public static final String SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION = SECURITY_SETTINGS_PREFIX + + "compliance.disable_anonymous_authentication"; + public static final String SECURITY_COMPLIANCE_IMMUTABLE_INDICES = SECURITY_SETTINGS_PREFIX + "compliance.immutable_indices"; + public static final String SECURITY_COMPLIANCE_SALT = SECURITY_SETTINGS_PREFIX + "compliance.salt"; + public static final String SECURITY_COMPLIANCE_SALT_DEFAULT = "e1ukloTsQlOgPquJ";// 16 chars + public static final String SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED = + "opendistro_security.compliance.history.internal_config_enabled"; + public static final String SECURITY_SSL_ONLY = SECURITY_SETTINGS_PREFIX + "ssl_only"; + public static final String SECURITY_CONFIG_SSL_DUAL_MODE_ENABLED = "plugins.security_config.ssl_dual_mode_enabled"; + public static final String SECURITY_SSL_DUAL_MODE_SKIP_SECURITY = OPENDISTRO_SECURITY_CONFIG_PREFIX + "passive_security"; + public static final String LEGACY_OPENDISTRO_SECURITY_CONFIG_SSL_DUAL_MODE_ENABLED = "opendistro_security_config.ssl_dual_mode_enabled"; + public static final String SECURITY_SSL_CERT_RELOAD_ENABLED = SECURITY_SETTINGS_PREFIX + "ssl_cert_reload_enabled"; + public static final String SECURITY_SSL_CERTIFICATES_HOT_RELOAD_ENABLED = SECURITY_SETTINGS_PREFIX + + "ssl.certificates_hot_reload.enabled"; + public static final String SECURITY_DISABLE_ENVVAR_REPLACEMENT = SECURITY_SETTINGS_PREFIX + "disable_envvar_replacement"; + public static final String SECURITY_DFM_EMPTY_OVERRIDES_ALL = SECURITY_SETTINGS_PREFIX + "dfm_empty_overrides_all"; + + public enum RolesMappingResolution { + MAPPING_ONLY, + BACKENDROLES_ONLY, + BOTH + } + + public static final String SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS = SECURITY_SETTINGS_PREFIX + + "filter_securityindex_from_all_requests"; + public static final String SECURITY_DLS_MODE = SECURITY_SETTINGS_PREFIX + "dls.mode"; + // REST API + public static final String SECURITY_RESTAPI_ROLES_ENABLED = SECURITY_SETTINGS_PREFIX + "restapi.roles_enabled"; + public static final String SECURITY_RESTAPI_ADMIN_ENABLED = SECURITY_SETTINGS_PREFIX + "restapi.admin.enabled"; + public static final String SECURITY_RESTAPI_ENDPOINTS_DISABLED = SECURITY_SETTINGS_PREFIX + "restapi.endpoints_disabled"; + public static final String SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX = SECURITY_SETTINGS_PREFIX + "restapi.password_validation_regex"; + public static final String SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE = SECURITY_SETTINGS_PREFIX + + "restapi.password_validation_error_message"; + public static final String SECURITY_RESTAPI_PASSWORD_MIN_LENGTH = SECURITY_SETTINGS_PREFIX + "restapi.password_min_length"; + public static final String SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH = SECURITY_SETTINGS_PREFIX + + "restapi.password_score_based_validation_strength"; + // Illegal Opcodes from here on + public static final String SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY = SECURITY_SETTINGS_PREFIX + + "unsupported.disable_rest_auth_initially"; + public static final String SECURITY_UNSUPPORTED_DELAY_INITIALIZATION_SECONDS = SECURITY_SETTINGS_PREFIX + + "unsupported.delay_initialization_seconds"; + public static final String SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY = SECURITY_SETTINGS_PREFIX + + "unsupported.disable_intertransport_auth_initially"; + public static final String SECURITY_UNSUPPORTED_PASSIVE_INTERTRANSPORT_AUTH_INITIALLY = SECURITY_SETTINGS_PREFIX + + "unsupported.passive_intertransport_auth_initially"; + public static final String SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED = SECURITY_SETTINGS_PREFIX + + "unsupported.restore.securityindex.enabled"; + public static final String SECURITY_UNSUPPORTED_INJECT_USER_ENABLED = SECURITY_SETTINGS_PREFIX + "unsupported.inject_user.enabled"; + public static final String SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED = SECURITY_SETTINGS_PREFIX + + "unsupported.inject_user.admin.enabled"; + public static final String SECURITY_UNSUPPORTED_ALLOW_NOW_IN_DLS = SECURITY_SETTINGS_PREFIX + "unsupported.allow_now_in_dls"; + + public static final String SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION = SECURITY_SETTINGS_PREFIX + + "unsupported.restapi.allow_securityconfig_modification"; + public static final String SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES = SECURITY_SETTINGS_PREFIX + "unsupported.load_static_resources"; + public static final String SECURITY_UNSUPPORTED_ACCEPT_INVALID_CONFIG = SECURITY_SETTINGS_PREFIX + "unsupported.accept_invalid_config"; + + public static final String SECURITY_PROTECTED_INDICES_ENABLED_KEY = SECURITY_SETTINGS_PREFIX + "protected_indices.enabled"; + public static final Boolean SECURITY_PROTECTED_INDICES_ENABLED_DEFAULT = false; + public static final String SECURITY_PROTECTED_INDICES_KEY = SECURITY_SETTINGS_PREFIX + "protected_indices.indices"; + public static final List SECURITY_PROTECTED_INDICES_DEFAULT = Collections.emptyList(); + public static final String SECURITY_PROTECTED_INDICES_ROLES_KEY = SECURITY_SETTINGS_PREFIX + "protected_indices.roles"; + public static final List SECURITY_PROTECTED_INDICES_ROLES_DEFAULT = Collections.emptyList(); + + // Roles injection for plugins + public static final String OPENDISTRO_SECURITY_INJECTED_ROLES = "opendistro_security_injected_roles"; + public static final String OPENDISTRO_SECURITY_INJECTED_ROLES_HEADER = "opendistro_security_injected_roles_header"; + + // Roles validation for the plugins + public static final String OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION = "opendistro_security_injected_roles_validation"; + public static final String OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION_HEADER = + "opendistro_security_injected_roles_validation_header"; + + // System indices settings + public static final String SYSTEM_INDEX_PERMISSION = "system:admin/system_index"; + public static final String SECURITY_SYSTEM_INDICES_ENABLED_KEY = SECURITY_SETTINGS_PREFIX + "system_indices.enabled"; + public static final Boolean SECURITY_SYSTEM_INDICES_ENABLED_DEFAULT = false; + public static final String SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY = SECURITY_SETTINGS_PREFIX + + "system_indices.permission.enabled"; + public static final Boolean SECURITY_SYSTEM_INDICES_PERMISSIONS_DEFAULT = false; + public static final String SECURITY_SYSTEM_INDICES_KEY = SECURITY_SETTINGS_PREFIX + "system_indices.indices"; + public static final List SECURITY_SYSTEM_INDICES_DEFAULT = Collections.emptyList(); + public static final String SECURITY_MASKED_FIELDS_ALGORITHM_DEFAULT = SECURITY_SETTINGS_PREFIX + "masked_fields.algorithm.default"; + + public static final String TENANCY_PRIVATE_TENANT_NAME = "private"; + public static final String TENANCY_GLOBAL_TENANT_NAME = "global"; + public static final String TENANCY_GLOBAL_TENANT_DEFAULT_NAME = ""; + + public static final String USE_JDK_SERIALIZATION = SECURITY_SETTINGS_PREFIX + "use_jdk_serialization"; + + // On-behalf-of endpoints settings + // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings + public static final String EXTENSIONS_BWC_PLUGIN_MODE = "bwcPluginMode"; + public static final boolean EXTENSIONS_BWC_PLUGIN_MODE_DEFAULT = false; + // CS-ENFORCE-SINGLE + + // Variable for initial admin password support + public static final String OPENSEARCH_INITIAL_ADMIN_PASSWORD = "OPENSEARCH_INITIAL_ADMIN_PASSWORD"; + + // Resource sharing feature-flag + public static final String OPENSEARCH_RESOURCE_SHARING_ENABLED = SECURITY_SETTINGS_PREFIX + "resource_sharing.enabled"; + public static final boolean OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT = true; + + public static Set getSettingAsSet( + final Settings settings, + final String key, + final List defaultList, + final boolean ignoreCaseForNone + ) { + final List list = settings.getAsList(key, defaultList); + if (list.size() == 1 && "NONE".equals(ignoreCaseForNone ? list.get(0).toUpperCase() : list.get(0))) { + return Collections.emptySet(); + } + return ImmutableSet.copyOf(list); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/support/Utils.java b/common/src/main/java/org/opensearch/security/common/support/Utils.java new file mode 100644 index 0000000000..ffdc8d9390 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/support/Utils.java @@ -0,0 +1,285 @@ +/* + * 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.common.support; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.tuple.Pair; + +import org.opensearch.ExceptionsHelper; +import org.opensearch.OpenSearchParseException; +import org.opensearch.SpecialPermission; +import org.opensearch.common.CheckedSupplier; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.xcontent.XContentHelper; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.Strings; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.NamedRoute; +import org.opensearch.rest.RestHandler.DeprecatedRoute; +import org.opensearch.rest.RestHandler.Route; +import org.opensearch.security.common.DefaultObjectMapper; +import org.opensearch.security.common.user.User; + +import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; + +public class Utils { + @Deprecated + public static final String LEGACY_OPENDISTRO_PREFIX = "_opendistro/_security"; + public static final String PLUGINS_PREFIX = "_plugins/_security"; + + public final static String PLUGIN_ROUTE_PREFIX = "/" + PLUGINS_PREFIX; + + @Deprecated + public final static String LEGACY_PLUGIN_ROUTE_PREFIX = "/" + LEGACY_OPENDISTRO_PREFIX; + + public final static String PLUGIN_API_ROUTE_PREFIX = PLUGIN_ROUTE_PREFIX + "/api"; + + @Deprecated + public final static String LEGACY_PLUGIN_API_ROUTE_PREFIX = LEGACY_PLUGIN_ROUTE_PREFIX + "/api"; + + public final static String OPENDISTRO_API_DEPRECATION_MESSAGE = + "[_opendistro/_security] is a deprecated endpoint path. Please use _plugins/_security instead."; + + public final static String PLUGIN_RESOURCE_ROUTE_PREFIX = PLUGIN_ROUTE_PREFIX + "/resources"; + + private static final ObjectMapper internalMapper = new ObjectMapper(); + + public static Map convertJsonToxToStructuredMap(ToXContent jsonContent) { + Map map = null; + try { + final BytesReference bytes = XContentHelper.toXContent(jsonContent, XContentType.JSON, false); + map = XContentHelper.convertToMap(bytes, false, XContentType.JSON).v2(); + } catch (IOException e1) { + throw ExceptionsHelper.convertToOpenSearchException(e1); + } + + return map; + } + + public static Map convertJsonToxToStructuredMap(String jsonContent) { + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, jsonContent) + ) { + return parser.map(); + } catch (IOException e1) { + throw ExceptionsHelper.convertToOpenSearchException(e1); + } + } + + private static BytesReference convertStructuredMapToBytes(Map structuredMap) { + try { + return BytesReference.bytes(JsonXContent.contentBuilder().map(structuredMap)); + } catch (IOException e) { + throw new OpenSearchParseException("Failed to convert map", e); + } + } + + public static String convertStructuredMapToJson(Map structuredMap) { + try { + return XContentHelper.convertToJson(convertStructuredMapToBytes(structuredMap), false, XContentType.JSON); + } catch (IOException e) { + throw new OpenSearchParseException("Failed to convert map", e); + } + } + + public static JsonNode convertJsonToJackson(BytesReference jsonContent) { + try { + return DefaultObjectMapper.readTree(jsonContent.utf8ToString()); + } catch (IOException e1) { + throw ExceptionsHelper.convertToOpenSearchException(e1); + } + + } + + public static JsonNode toJsonNode(final String content) throws IOException { + return DefaultObjectMapper.readTree(content); + } + + public static Object toConfigObject(final JsonNode content, final Class clazz) throws IOException { + return DefaultObjectMapper.readTree(content, clazz); + } + + public static JsonNode convertJsonToJackson(ToXContent jsonContent, boolean omitDefaults) { + try { + return DefaultObjectMapper.readTree( + Strings.toString( + XContentType.JSON, + jsonContent, + new ToXContent.MapParams(Map.of("omit_defaults", String.valueOf(omitDefaults))) + ) + ); + } catch (IOException e1) { + throw ExceptionsHelper.convertToOpenSearchException(e1); + } + + } + + @SuppressWarnings("removal") + public static byte[] jsonMapToByteArray(Map jsonAsMap) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> internalMapper.writeValueAsBytes(jsonAsMap)); + } catch (final PrivilegedActionException e) { + if (e.getCause() instanceof JsonProcessingException) { + throw (JsonProcessingException) e.getCause(); + } else if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new RuntimeException(e.getCause()); + } + } + } + + @SuppressWarnings("removal") + public static Map byteArrayToMutableJsonMap(byte[] jsonBytes) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged( + (PrivilegedExceptionAction>) () -> internalMapper.readValue( + jsonBytes, + new TypeReference>() { + } + ) + ); + } catch (final PrivilegedActionException e) { + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } else if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new RuntimeException(e.getCause()); + } + } + } + + /** + * Generate field resource paths + * @param fields fields + * @param prefix prefix path + * @return new set of fields resource paths + */ + public static Set generateFieldResourcePaths(final Set fields, final String prefix) { + return fields.stream().map(field -> prefix + field).collect(ImmutableSet.toImmutableSet()); + } + + /** + * Add prefixes(_plugins/_security/api) to rest API routes + * @param routes routes + * @return new list of API routes prefixed with and _plugins/_security/api + */ + public static List addRoutesPrefix(List routes) { + return addRoutesPrefix(routes, PLUGIN_API_ROUTE_PREFIX); + } + + /** + * Add prefixes(_opendistro/_security/api) to rest API routes + * Deprecated in favor of addRoutesPrefix(List routes) + * @param routes routes + * @return new list of API routes prefixed with and _opendistro/_security/api + */ + @Deprecated + public static List addLegacyRoutesPrefix(List routes) { + return addDeprecatedRoutesPrefix(routes, LEGACY_PLUGIN_API_ROUTE_PREFIX); + } + + /** + * Add customized prefix(_opendistro... and _plugins...)to API rest routes + * @param routes routes + * @param prefixes all api prefix + * @return new list of API routes prefixed with the strings listed in prefixes + * Total number of routes will be expanded len(prefixes) as much comparing to the list passed in + */ + public static List addRoutesPrefix(List routes, final String... prefixes) { + return routes.stream().flatMap(r -> Arrays.stream(prefixes).map(p -> { + if (r instanceof NamedRoute) { + NamedRoute nr = (NamedRoute) r; + return new NamedRoute.Builder().method(nr.getMethod()) + .path(p + nr.getPath()) + .uniqueName(nr.name()) + .legacyActionNames(nr.actionNames()) + .build(); + } + return new Route(r.getMethod(), p + r.getPath()); + })).collect(ImmutableList.toImmutableList()); + } + + /** + * Add prefixes(_plugins...) to rest API routes + * @param deprecatedRoutes Routes being deprecated + * @return new list of API routes prefixed with _opendistro... and _plugins... + *Total number of routes is expanded as twice as the number of routes passed in + */ + public static List addDeprecatedRoutesPrefix(List deprecatedRoutes) { + return addDeprecatedRoutesPrefix(deprecatedRoutes, LEGACY_PLUGIN_API_ROUTE_PREFIX, PLUGIN_API_ROUTE_PREFIX); + } + + /** + * Add customized prefix(_opendistro... and _plugins...)to API rest routes + * @param deprecatedRoutes Routes being deprecated + * @param prefixes all api prefix + * @return new list of API routes prefixed with the strings listed in prefixes + * Total number of routes will be expanded len(prefixes) as much comparing to the list passed in + */ + public static List addDeprecatedRoutesPrefix(List deprecatedRoutes, final String... prefixes) { + return deprecatedRoutes.stream() + .flatMap(r -> Arrays.stream(prefixes).map(p -> new DeprecatedRoute(r.getMethod(), p + r.getPath(), r.getDeprecationMessage()))) + .collect(ImmutableList.toImmutableList()); + } + + public static Pair userAndRemoteAddressFrom(final ThreadContext threadContext) { + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final TransportAddress remoteAddress = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + return Pair.of(user, remoteAddress); + } + + public static T withIOException(final CheckedSupplier action) { + try { + return action.get(); + } catch (final IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + +} diff --git a/common/src/main/java/org/opensearch/security/common/support/WildcardMatcher.java b/common/src/main/java/org/opensearch/security/common/support/WildcardMatcher.java new file mode 100644 index 0000000000..4e5ab5b29b --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/support/WildcardMatcher.java @@ -0,0 +1,556 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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.common.support; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; + +public abstract class WildcardMatcher implements Predicate { + + public static final WildcardMatcher ANY = new WildcardMatcher() { + + @Override + public boolean matchAny(Stream candidates) { + return true; + } + + @Override + public boolean matchAny(Collection candidates) { + return true; + } + + @Override + public boolean matchAny(String... candidates) { + return true; + } + + @Override + public boolean matchAll(Stream candidates) { + return true; + } + + @Override + public boolean matchAll(Collection candidates) { + return true; + } + + @Override + public boolean matchAll(String[] candidates) { + return true; + } + + @Override + public > T getMatchAny(Stream candidates, Collector collector) { + return candidates.collect(collector); + } + + @Override + public boolean test(String candidate) { + return true; + } + + @Override + public String toString() { + return "*"; + } + }; + + public static final WildcardMatcher NONE = new WildcardMatcher() { + + @Override + public boolean matchAny(Stream candidates) { + return false; + } + + @Override + public boolean matchAny(Collection candidates) { + return false; + } + + @Override + public boolean matchAny(String... candidates) { + return false; + } + + @Override + public boolean matchAll(Stream candidates) { + return false; + } + + @Override + public boolean matchAll(Collection candidates) { + return false; + } + + @Override + public boolean matchAll(String[] candidates) { + return false; + } + + @Override + public > T getMatchAny(Stream candidates, Collector collector) { + return Stream.empty().collect(collector); + } + + @Override + public > T getMatchAny(Collection candidate, Collector collector) { + return Stream.empty().collect(collector); + } + + @Override + public > T getMatchAny(String[] candidate, Collector collector) { + return Stream.empty().collect(collector); + } + + @Override + public boolean test(String candidate) { + return false; + } + + @Override + public String toString() { + return ""; + } + }; + + public static WildcardMatcher from(String pattern, boolean caseSensitive) { + if (pattern == null) { + return NONE; + } else if (pattern.equals("*")) { + return ANY; + } else if (pattern.startsWith("/") && pattern.endsWith("/")) { + return new RegexMatcher(pattern, caseSensitive); + } else if (pattern.indexOf('?') >= 0 || pattern.indexOf('*') >= 0) { + return caseSensitive ? new SimpleMatcher(pattern) : new CasefoldingMatcher(pattern, SimpleMatcher::new); + } else { + return caseSensitive ? new Exact(pattern) : new CasefoldingMatcher(pattern, Exact::new); + } + } + + public static WildcardMatcher from(String pattern) { + return from(pattern, true); + } + + // This may in future use more optimized techniques to combine multiple WildcardMatchers in a single automaton + public static WildcardMatcher from(Stream stream, boolean caseSensitive) { + Collection matchers = stream.map(t -> { + if (t == null) { + return NONE; + } else if (t instanceof String) { + return WildcardMatcher.from(((String) t), caseSensitive); + } else if (t instanceof WildcardMatcher) { + return ((WildcardMatcher) t); + } + throw new UnsupportedOperationException("WildcardMatcher can't be constructed from " + t.getClass().getSimpleName()); + }).collect(ImmutableSet.toImmutableSet()); + + if (matchers.isEmpty()) { + return NONE; + } else if (matchers.size() == 1) { + return matchers.stream().findFirst().get(); + } + return new MatcherCombiner(matchers); + } + + public static WildcardMatcher from(Collection collection, boolean caseSensitive) { + if (collection == null || collection.isEmpty()) { + return NONE; + } else if (collection.size() == 1) { + T t = collection.stream().findFirst().get(); + if (t instanceof String) { + return from(((String) t), caseSensitive); + } else if (t instanceof WildcardMatcher) { + return ((WildcardMatcher) t); + } + throw new UnsupportedOperationException("WildcardMatcher can't be constructed from " + t.getClass().getSimpleName()); + } + return from(collection.stream(), caseSensitive); + } + + public static WildcardMatcher from(String[] patterns, boolean caseSensitive) { + if (patterns == null || patterns.length == 0) { + return NONE; + } else if (patterns.length == 1) { + return from(patterns[0], caseSensitive); + } + return from(Arrays.stream(patterns), caseSensitive); + } + + public static WildcardMatcher from(Stream patterns) { + return from(patterns, true); + } + + public static WildcardMatcher from(Collection patterns) { + return from(patterns, true); + } + + public static WildcardMatcher from(String... patterns) { + return from(patterns, true); + } + + public WildcardMatcher concat(Stream matchers) { + return new MatcherCombiner(Stream.concat(matchers, Stream.of(this)).collect(ImmutableSet.toImmutableSet())); + } + + public WildcardMatcher concat(Collection matchers) { + if (matchers.isEmpty()) { + return this; + } + return concat(matchers.stream()); + } + + public WildcardMatcher concat(WildcardMatcher... matchers) { + if (matchers.length == 0) { + return this; + } + return concat(Arrays.stream(matchers)); + } + + public boolean matchAny(Stream candidates) { + return candidates.anyMatch(this); + } + + public boolean matchAny(Collection candidates) { + return matchAny(candidates.stream()); + } + + public boolean matchAny(String... candidates) { + return matchAny(Arrays.stream(candidates)); + } + + public boolean matchAll(Stream candidates) { + return candidates.allMatch(this); + } + + public boolean matchAll(Collection candidates) { + return matchAll(candidates.stream()); + } + + public boolean matchAll(String[] candidates) { + return matchAll(Arrays.stream(candidates)); + } + + public > T getMatchAny(Stream candidates, Collector collector) { + return candidates.filter(this).collect(collector); + } + + public > T getMatchAny(Collection candidate, Collector collector) { + return getMatchAny(candidate.stream(), collector); + } + + public > T getMatchAny(final String[] candidate, Collector collector) { + return getMatchAny(Arrays.stream(candidate), collector); + } + + public Optional findFirst(final String candidate) { + return Optional.ofNullable(test(candidate) ? this : null); + } + + public Iterable iterateMatching(Iterable candidates) { + return iterateMatching(candidates, Function.identity()); + } + + public Iterable iterateMatching(Iterable candidates, Function toStringFunction) { + return new Iterable() { + + @Override + public Iterator iterator() { + Iterator delegate = candidates.iterator(); + + return new Iterator() { + private E next; + + @Override + public boolean hasNext() { + if (next == null) { + init(); + } + + return next != null; + } + + @Override + public E next() { + if (next == null) { + init(); + } + + E result = next; + next = null; + return result; + } + + private void init() { + while (delegate.hasNext()) { + E candidate = delegate.next(); + + if (test(toStringFunction.apply(candidate))) { + next = candidate; + break; + } + } + } + }; + } + }; + } + + public static List matchers(Collection patterns) { + return patterns.stream().map(p -> WildcardMatcher.from(p, true)).collect(Collectors.toList()); + } + + public static List getAllMatchingPatterns(final Collection matchers, final String candidate) { + return matchers.stream().filter(p -> p.test(candidate)).map(Objects::toString).collect(Collectors.toList()); + } + + public static List getAllMatchingPatterns(final Collection pattern, final Collection candidates) { + return pattern.stream().filter(p -> p.matchAny(candidates)).map(Objects::toString).collect(Collectors.toList()); + } + + public static boolean isExact(String pattern) { + return pattern == null || !(pattern.contains("*") || pattern.contains("?") || (pattern.startsWith("/") && pattern.endsWith("/"))); + } + + // + // --- Implementation specializations --- + // + // Casefolding matcher - sits on top of case-sensitive matcher + // and proxies toLower() of input string to the wrapped matcher + private static final class CasefoldingMatcher extends WildcardMatcher { + + private final WildcardMatcher inner; + + public CasefoldingMatcher(String pattern, Function simpleWildcardMatcher) { + this.inner = simpleWildcardMatcher.apply(pattern.toLowerCase()); + } + + @Override + public boolean test(String candidate) { + return inner.test(candidate.toLowerCase()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CasefoldingMatcher that = (CasefoldingMatcher) o; + return inner.equals(that.inner); + } + + @Override + public int hashCode() { + return inner.hashCode(); + } + + @Override + public String toString() { + return inner.toString(); + } + } + + public static final class Exact extends WildcardMatcher { + + private final String pattern; + + private Exact(String pattern) { + this.pattern = pattern; + } + + @Override + public boolean test(String candidate) { + return pattern.equals(candidate); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Exact that = (Exact) o; + return pattern.equals(that.pattern); + } + + @Override + public int hashCode() { + return pattern.hashCode(); + } + + @Override + public String toString() { + return pattern; + } + } + + // RegexMatcher uses JDK Pattern to test for matching, + // assumes "//" strings as input pattern + private static final class RegexMatcher extends WildcardMatcher { + + private final Pattern pattern; + + private RegexMatcher(String pattern, boolean caseSensitive) { + Preconditions.checkArgument(pattern.length() > 1 && pattern.startsWith("/") && pattern.endsWith("/")); + final String stripSlashesPattern = pattern.substring(1, pattern.length() - 1); + this.pattern = caseSensitive + ? Pattern.compile(stripSlashesPattern) + : Pattern.compile(stripSlashesPattern, Pattern.CASE_INSENSITIVE); + } + + @Override + public boolean test(String candidate) { + return pattern.matcher(candidate).matches(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RegexMatcher that = (RegexMatcher) o; + return pattern.pattern().equals(that.pattern.pattern()); + } + + @Override + public int hashCode() { + return pattern.pattern().hashCode(); + } + + @Override + public String toString() { + return "/" + pattern.pattern() + "/"; + } + } + + // Simple implementation of WildcardMatcher matcher with * and ? without + // using exlicit stack or recursion (as long as we don't need sub-matches it does work) + // allows us to save on resources and heap allocations unless Regex is required + private static final class SimpleMatcher extends WildcardMatcher { + + private final String pattern; + + SimpleMatcher(String pattern) { + this.pattern = pattern; + } + + @Override + public boolean test(String candidate) { + int i = 0; + int j = 0; + int n = candidate.length(); + int m = pattern.length(); + int text_backup = -1; + int wild_backup = -1; + while (i < n) { + if (j < m && pattern.charAt(j) == '*') { + text_backup = i; + wild_backup = ++j; + } else if (j < m && (pattern.charAt(j) == '?' || pattern.charAt(j) == candidate.charAt(i))) { + i++; + j++; + } else { + if (wild_backup == -1) return false; + i = ++text_backup; + j = wild_backup; + } + } + while (j < m && pattern.charAt(j) == '*') + j++; + return j >= m; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SimpleMatcher that = (SimpleMatcher) o; + return pattern.equals(that.pattern); + } + + @Override + public int hashCode() { + return pattern.hashCode(); + } + + @Override + public String toString() { + return pattern; + } + } + + // MatcherCombiner is a combination of a set of matchers + // matches if any of the set do + // Empty MultiMatcher always returns false + private static final class MatcherCombiner extends WildcardMatcher { + + private final Collection wildcardMatchers; + private final int hashCode; + + MatcherCombiner(Collection wildcardMatchers) { + Preconditions.checkArgument(wildcardMatchers.size() > 1); + this.wildcardMatchers = wildcardMatchers; + hashCode = wildcardMatchers.hashCode(); + } + + @Override + public boolean test(String candidate) { + return wildcardMatchers.stream().anyMatch(m -> m.test(candidate)); + } + + @Override + public Optional findFirst(final String candidate) { + return wildcardMatchers.stream().filter(m -> m.test(candidate)).findFirst(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MatcherCombiner that = (MatcherCombiner) o; + return wildcardMatchers.equals(that.wildcardMatchers); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public String toString() { + return wildcardMatchers.toString(); + } + } +} diff --git a/common/src/main/java/org/opensearch/security/common/user/AuthCredentials.java b/common/src/main/java/org/opensearch/security/common/user/AuthCredentials.java new file mode 100644 index 0000000000..9255b63dba --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/user/AuthCredentials.java @@ -0,0 +1,254 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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.common.user; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.opensearch.OpenSearchSecurityException; + +/** + * AuthCredentials are an abstraction to encapsulate credentials like passwords or generic + * native credentials like GSS tokens. + * + */ +public final class AuthCredentials { + + private static final String DIGEST_ALGORITHM = "SHA-256"; + private final String username; + private byte[] password; + private Object nativeCredentials; + private final Set securityRoles = new HashSet(); + private final Set backendRoles = new HashSet(); + private boolean complete; + private final byte[] internalPasswordHash; + private final Map attributes = new HashMap<>(); + + /** + * Create new credentials with a username and native credentials + * + * @param username The username, must not be null or empty + * @param nativeCredentials Arbitrary credentials (like GSS tokens), must not be null + * @throws IllegalArgumentException if username or nativeCredentials are null or empty + */ + public AuthCredentials(final String username, final Object nativeCredentials) { + this(username, null, nativeCredentials); + + if (nativeCredentials == null) { + throw new IllegalArgumentException("nativeCredentials must not be null or empty"); + } + } + + /** + * Create new credentials with a username and password + * + * @param username The username, must not be null or empty + * @param password The password, must not be null or empty + * @throws IllegalArgumentException if username or password is null or empty + */ + public AuthCredentials(final String username, final byte[] password) { + this(username, password, null); + + if (password == null || password.length == 0) { + throw new IllegalArgumentException("password must not be null or empty"); + } + } + + /** + * Create new credentials with a username, a initial optional set of roles and empty password/native credentials + + * @param username The username, must not be null or empty + * @param backendRoles set of roles this user is a member of + * @throws IllegalArgumentException if username is null or empty + */ + public AuthCredentials(final String username, String... backendRoles) { + this(username, null, null, backendRoles); + } + + /** + * Create new credentials with a username, a initial optional set of roles and empty password/native credentials + * @param username The username, must not be null or empty + * @param securityRoles The internal roles the user has been mapped to + * @param backendRoles set of roles this user is a member of + * @throws IllegalArgumentException if username is null or empty + */ + public AuthCredentials(final String username, List securityRoles, String... backendRoles) { + this(username, null, null, backendRoles); + this.securityRoles.addAll(securityRoles); + } + + private AuthCredentials(final String username, byte[] password, Object nativeCredentials, String... backendRoles) { + super(); + + if (username == null || username.isEmpty()) { + throw new IllegalArgumentException("username must not be null or empty"); + } + + this.username = username; + // make defensive copy + this.password = password == null ? null : Arrays.copyOf(password, password.length); + + if (this.password != null) { + try { + MessageDigest digester = MessageDigest.getInstance(DIGEST_ALGORITHM); + internalPasswordHash = digester.digest(this.password); + } catch (NoSuchAlgorithmException e) { + throw new OpenSearchSecurityException("Unable to digest password", e); + } + } else { + internalPasswordHash = null; + } + + if (password != null) { + Arrays.fill(password, (byte) '\0'); + password = null; + } + + this.nativeCredentials = nativeCredentials; + nativeCredentials = null; + + if (backendRoles != null && backendRoles.length > 0) { + this.backendRoles.addAll(Arrays.asList(backendRoles)); + } + } + + /** + * Wipe password and native credentials + */ + public void clearSecrets() { + if (password != null) { + Arrays.fill(password, (byte) '\0'); + password = null; + } + + nativeCredentials = null; + } + + public String getUsername() { + return username; + } + + /** + * + * @return Defensive copy of the password + */ + public byte[] getPassword() { + // make defensive copy + return password == null ? null : Arrays.copyOf(password, password.length); + } + + public Object getNativeCredentials() { + return nativeCredentials; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(internalPasswordHash); + result = prime * result + ((username == null) ? 0 : username.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + AuthCredentials other = (AuthCredentials) obj; + if (internalPasswordHash == null + || other.internalPasswordHash == null + || !MessageDigest.isEqual(internalPasswordHash, other.internalPasswordHash)) return false; + if (username == null) { + if (other.username != null) return false; + } else if (!username.equals(other.username)) return false; + return true; + } + + @Override + public String toString() { + return "AuthCredentials [username=" + + username + + ", password empty=" + + (password == null) + + ", nativeCredentials empty=" + + (nativeCredentials == null) + + ",backendRoles=" + + backendRoles + + "]"; + } + + /** + * + * @return Defensive copy of the roles this user is member of. + */ + public Set getBackendRoles() { + return new HashSet(backendRoles); + } + + /** + * + * @return Defensive copy of the security roles this user is member of. + */ + public Set getSecurityRoles() { + return Set.copyOf(securityRoles); + } + + public boolean isComplete() { + return complete; + } + + /** + * If the credentials are complete and no further roundtrips with the originator are due + * then this method must be called so that the authentication flow can proceed. + *

          + * If this credentials are already marked a complete then a call to this method does nothing. + * + * @return this + */ + public AuthCredentials markComplete() { + this.complete = true; + return this; + } + + public void addAttribute(String name, String value) { + if (name != null && !name.isEmpty()) { + this.attributes.put(name, value); + } + } + + public Map getAttributes() { + return Collections.unmodifiableMap(this.attributes); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/user/CustomAttributesAware.java b/common/src/main/java/org/opensearch/security/common/user/CustomAttributesAware.java new file mode 100644 index 0000000000..144bb04002 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/user/CustomAttributesAware.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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.common.user; + +import java.util.Map; + +public interface CustomAttributesAware { + + Map getCustomAttributesMap(); +} diff --git a/common/src/main/java/org/opensearch/security/common/user/User.java b/common/src/main/java/org/opensearch/security/common/user/User.java new file mode 100644 index 0000000000..015ddf7fb1 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/user/User.java @@ -0,0 +1,312 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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.common.user; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.Lists; + +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.common.io.stream.Writeable; + +/** + * A authenticated user and attributes associated to them (like roles, tenant, custom attributes) + *

          + * Do not subclass from this class! + */ +public class User implements Serializable, Writeable, CustomAttributesAware { + + public static final User ANONYMOUS = new User( + "opendistro_security_anonymous", + Lists.newArrayList("opendistro_security_anonymous_backendrole"), + null + ); + + // This is a default user that is injected into a transport request when a user info is not present and passive_intertransport_auth is + // enabled. + // This is to be used in scenarios where some of the nodes do not have security enabled, and therefore do not pass any user information + // in threadcontext, yet we need the communication to not break between the nodes. + // Attach the required permissions to either the user or the backend role. + public static final User DEFAULT_TRANSPORT_USER = new User( + "opendistro_security_default_transport_user", + Lists.newArrayList("opendistro_security_default_transport_backendrole"), + null + ); + + private static final long serialVersionUID = -5500938501822658596L; + private final String name; + /** + * roles == backend_roles + */ + private final Set roles = Collections.synchronizedSet(new HashSet()); + private final Set securityRoles = Collections.synchronizedSet(new HashSet()); + private String requestedTenant; + private Map attributes = Collections.synchronizedMap(new HashMap<>()); + private boolean isInjected = false; + + public User(final StreamInput in) throws IOException { + super(); + name = in.readString(); + roles.addAll(in.readList(StreamInput::readString)); + requestedTenant = in.readString(); + if (requestedTenant.isEmpty()) { + requestedTenant = null; + } + attributes = Collections.synchronizedMap(in.readMap(StreamInput::readString, StreamInput::readString)); + securityRoles.addAll(in.readList(StreamInput::readString)); + } + + /** + * Create a new authenticated user + * + * @param name The username (must not be null or empty) + * @param roles Roles of which the user is a member off (maybe null) + * @param customAttributes Custom attributes associated with this (maybe null) + * @throws IllegalArgumentException if name is null or empty + */ + public User(final String name, final Collection roles, final AuthCredentials customAttributes) { + super(); + + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("name must not be null or empty"); + } + + this.name = name; + + if (roles != null) { + this.addRoles(roles); + } + + if (customAttributes != null) { + this.attributes.putAll(customAttributes.getAttributes()); + } + + } + + /** + * Create a new authenticated user without roles and attributes + * + * @param name The username (must not be null or empty) + * @throws IllegalArgumentException if name is null or empty + */ + public User(final String name) { + this(name, null, null); + } + + public final String getName() { + return name; + } + + /** + * @return A unmodifiable set of the backend roles this user is a member of + */ + public final Set getRoles() { + return Collections.unmodifiableSet(roles); + } + + /** + * Associate this user with a backend role + * + * @param role The backend role + */ + public final void addRole(final String role) { + this.roles.add(role); + } + + /** + * Associate this user with a set of backend roles + * + * @param roles The backend roles + */ + public final void addRoles(final Collection roles) { + if (roles != null) { + this.roles.addAll(roles); + } + } + + /** + * Check if this user is a member of a backend role + * + * @param role The backend role + * @return true if this user is a member of the backend role, false otherwise + */ + public final boolean isUserInRole(final String role) { + return this.roles.contains(role); + } + + /** + * Associate this user with a set of custom attributes + * + * @param attributes custom attributes + */ + public final void addAttributes(final Map attributes) { + if (attributes != null) { + this.attributes.putAll(attributes); + } + } + + public final String getRequestedTenant() { + return requestedTenant; + } + + public final void setRequestedTenant(String requestedTenant) { + this.requestedTenant = requestedTenant; + } + + public boolean isInjected() { + return isInjected; + } + + public void setInjected(boolean isInjected) { + this.isInjected = isInjected; + } + + public final String toStringWithAttributes() { + return "User [name=" + + name + + ", backend_roles=" + + roles + + ", requestedTenant=" + + requestedTenant + + ", attributes=" + + attributes + + "]"; + } + + @Override + public final String toString() { + return "User [name=" + name + ", backend_roles=" + roles + ", requestedTenant=" + requestedTenant + "]"; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (name == null ? 0 : name.hashCode()); + return result; + } + + @Override + public final boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof User)) { + return false; + } + final User other = (User) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return true; + } + + /** + * Copy all backend roles from another user + * + * @param user The user from which the backend roles should be copied over + */ + public final void copyRolesFrom(final User user) { + if (user != null) { + this.addRoles(user.getRoles()); + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeStringCollection(new ArrayList(roles)); + out.writeString(requestedTenant == null ? "" : requestedTenant); + out.writeMap(attributes, StreamOutput::writeString, StreamOutput::writeString); + out.writeStringCollection(securityRoles == null ? Collections.emptyList() : new ArrayList(securityRoles)); + } + + /** + * Get the custom attributes associated with this user + * + * @return A modifiable map with all the current custom attributes associated with this user + */ + public synchronized final Map getCustomAttributesMap() { + if (attributes == null) { + attributes = Collections.synchronizedMap(new HashMap<>()); + } + return attributes; + } + + public final void addSecurityRoles(final Collection securityRoles) { + if (securityRoles != null && this.securityRoles != null) { + this.securityRoles.addAll(securityRoles); + } + } + + public final Set getSecurityRoles() { + return this.securityRoles == null + ? Collections.synchronizedSet(Collections.emptySet()) + : Collections.unmodifiableSet(this.securityRoles); + } + + /** + * Check the custom attributes associated with this user + * + * @return true if it has a service account attributes, otherwise false + */ + public boolean isServiceAccount() { + Map userAttributesMap = this.getCustomAttributesMap(); + return userAttributesMap != null && "true".equals(userAttributesMap.get("attr.internal.service")); + } + + /** + * Check the custom attributes associated with this user + * + * @return true if it has a plugin account attributes, otherwise false + */ + public boolean isPluginUser() { + return name != null && name.startsWith("plugin:"); + } + + public void setAttributes(Map attributes) { + if (attributes == null) { + this.attributes = Collections.synchronizedMap(new HashMap<>()); + } + } +} diff --git a/common/test/java/org/opensearch/security/common/auth/UserSubjectImpl.java b/common/test/java/org/opensearch/security/common/auth/UserSubjectImpl.java new file mode 100644 index 0000000000..a28ed8dd63 --- /dev/null +++ b/common/test/java/org/opensearch/security/common/auth/UserSubjectImpl.java @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * 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. + * + */ +package org.opensearch.security.auth; + +import java.security.Principal; +import java.util.concurrent.Callable; + +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.identity.NamedPrincipal; +import org.opensearch.identity.UserSubject; +import org.opensearch.identity.tokens.AuthToken; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + +public class UserSubjectImpl implements UserSubject { + private final NamedPrincipal userPrincipal; + private final ThreadPool threadPool; + private final User user; + + UserSubjectImpl(ThreadPool threadPool, User user) { + this.threadPool = threadPool; + this.user = user; + this.userPrincipal = new NamedPrincipal(user.getName()); + } + + @Override + public void authenticate(AuthToken authToken) { + // not implemented + } + + @Override + public Principal getPrincipal() { + return userPrincipal; + } + + @Override + public T runAs(Callable callable) throws Exception { + try (ThreadContext.StoredContext ctx = threadPool.getThreadContext().stashContext()) { + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); + return callable.call(); + } + } + + public User getUser() { + return user; + } +} From 8b21b50ed69b5e5474f9300ee52d28a87b3826f1 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 15:50:57 -0500 Subject: [PATCH 139/201] Addresses changes around common library Signed-off-by: Darshit Chanpura --- build.gradle | 5 +- .../resources/rest/ResourceAccessRequest.java | 31 +++++-- .../rest/ResourceAccessResponse.java | 11 +-- .../rest/ResourceAccessRestAction.java | 83 ++++++++++--------- .../rest/ResourceAccessTransportAction.java | 17 +--- .../security/OpenSearchSecurityPlugin.java | 68 +++++++-------- .../security/auth/BackendRegistry.java | 24 +++++- .../security/auth/UserSubjectImplTests.java | 3 +- .../security/resources/CreatedByTests.java | 2 + .../resources/RecipientTypeRegistryTests.java | 2 + .../security/resources/ShareWithTests.java | 4 + 11 files changed, 129 insertions(+), 121 deletions(-) diff --git a/build.gradle b/build.gradle index 47ef65db09..eb2c369a9f 100644 --- a/build.gradle +++ b/build.gradle @@ -561,6 +561,8 @@ allprojects { integrationTestImplementation 'org.slf4j:slf4j-api:2.0.12' integrationTestImplementation 'com.selectivem.collections:special-collections-complete:1.4.0' integrationTestImplementation "org.opensearch.plugin:lang-painless:${opensearch_version}" + integrationTestImplementation project(path:":opensearch-security-common") + integrationTestImplementation project(path:":opensearch-resource-sharing-spi") } } @@ -636,7 +638,7 @@ check.dependsOn integrationTest dependencies { implementation project(path: ":opensearch-resource-sharing-spi") - compileOnly "org.opensearch.plugin:lang-painless:${opensearch_version}" + implementation project(path: ":opensearch-security-common") implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" implementation "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}" implementation "org.apache.httpcomponents.client5:httpclient5-cache:${versions.httpclient5}" @@ -743,7 +745,6 @@ dependencies { testImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}" testImplementation 'com.unboundid:unboundid-ldapsdk:4.0.14' testImplementation 'com.github.stephenc.jcip:jcip-annotations:1.0-1' - testImplementation 'com.unboundid:unboundid-ldapsdk:4.0.14' testImplementation 'org.apache.httpcomponents:fluent-hc:4.5.14' testImplementation "org.apache.httpcomponents.client5:httpclient5-fluent:${versions.httpclient5}" testImplementation "org.apache.kafka:kafka_2.13:${kafka_version}" diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java index 4bae7fd430..97e31c2769 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java @@ -9,9 +9,9 @@ package org.opensearch.security.common.resources.rest; import java.io.IOException; -import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionRequestValidationException; @@ -22,8 +22,11 @@ import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.security.common.resources.RecipientType; +import org.opensearch.security.common.resources.RecipientTypeRegistry; import org.opensearch.security.common.resources.ShareWith; +// TODO: Fix revoked entries public class ResourceAccessRequest extends ActionRequest { public enum Operation { @@ -38,7 +41,7 @@ public enum Operation { private final String resourceIndex; private final String scope; private ShareWith shareWith; - private Map> revokedEntities; + private Map> revokedEntities; private Set scopes; /** @@ -60,12 +63,12 @@ public ResourceAccessRequest(Map source, Map par this.shareWith = parseShareWith(source); } - if (source.containsKey("entities_to_revoke")) { - this.revokedEntities = ((Map>) source.get("entities_to_revoke")); + if (source.containsKey("revoked_entities")) { + this.revokedEntities = parseRevokedEntities(source); } if (source.containsKey("scopes")) { - this.scopes = Set.copyOf((List) source.get("scopes")); + this.scopes = Set.copyOf((Set) source.get("scopes")); } } @@ -76,8 +79,7 @@ public ResourceAccessRequest(StreamInput in) throws IOException { this.resourceIndex = in.readOptionalString(); this.scope = in.readOptionalString(); this.shareWith = in.readOptionalWriteable(ShareWith::new); - this.revokedEntities = in.readMap(StreamInput::readString, valIn -> valIn.readSet(StreamInput::readString)); - + // this.revokedEntities = in.readMap(StreamInput::readEnum, StreamInput::readSet); this.scopes = in.readSet(StreamInput::readString); } @@ -88,7 +90,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(resourceIndex); out.writeOptionalString(scope); out.writeOptionalWriteable(shareWith); - out.writeMap(revokedEntities, StreamOutput::writeString, StreamOutput::writeStringCollection); + // out.writeMap(revokedEntities, StreamOutput::writeEnum, StreamOutput::writeStringCollection); out.writeStringCollection(scopes); } @@ -123,6 +125,17 @@ private ShareWith parseShareWith(Map source) throws IOException } } + /** + * Helper method to parse revoked entities from a generic Map + */ + @SuppressWarnings("unchecked") + private Map> parseRevokedEntities(Map source) { + Map> revokeSource = (Map>) source.get("entities"); + return revokeSource.entrySet() + .stream() + .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); + } + public Operation getOperation() { return operation; } @@ -143,7 +156,7 @@ public ShareWith getShareWith() { return shareWith; } - public Map> getRevokedEntities() { + public Map> getRevokedEntities() { return revokedEntities; } diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java index 97f2fb7c44..35dbdecef7 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java @@ -66,7 +66,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.startObject(); switch (responseType) { case RESOURCES -> builder.field("resources", responseData); - case RESOURCE_SHARING -> builder.field("sharing_info", responseData); + case RESOURCE_SHARING -> builder.field("resource_sharing", responseData); case BOOLEAN -> builder.field("has_permission", responseData); } return builder.endObject(); @@ -84,13 +84,4 @@ public ResourceSharing getResourceSharing() { public Boolean getHasPermission() { return responseType == ResponseType.BOOLEAN ? (Boolean) responseData : null; } - - @Override - public String toString() { - if (responseData == null) { - return "ResourceAccessResponse{" + "responseType=" + responseType + ", responseData=null}"; - } - return "ResourceAccessResponse{" + "responseType=" + responseType + ", responseData=" + responseData.toString() + "}"; - - } } diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java index 84523c94ed..99d4392a22 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java @@ -17,21 +17,19 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestResponse; +import org.opensearch.rest.action.RestToXContentListener; import org.opensearch.transport.client.node.NodeClient; import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; import static org.opensearch.security.common.dlic.rest.api.Responses.badRequest; -import static org.opensearch.security.common.dlic.rest.api.Responses.forbidden; -import static org.opensearch.security.common.dlic.rest.api.Responses.ok; -import static org.opensearch.security.common.dlic.rest.api.Responses.unauthorized; import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.LIST; import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.REVOKE; import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.SHARE; @@ -89,20 +87,17 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli ResourceAccessRequest resourceAccessRequest = new ResourceAccessRequest(source, request.params()); return channel -> { - client.executeLocally(ResourceAccessAction.INSTANCE, resourceAccessRequest, new ActionListener<>() { - + client.executeLocally(ResourceAccessAction.INSTANCE, resourceAccessRequest, new RestToXContentListener<>(channel) { @Override - public void onResponse(ResourceAccessResponse response) { - try { - sendResponse(channel, response); - } catch (IOException e) { - throw new RuntimeException(e); - } + public RestResponse buildResponse(ResourceAccessResponse response, XContentBuilder builder) throws Exception { + assert !response.isFragment(); // would be nice if we could make default methods final + response.toXContent(builder, channel.request()); + return new BytesRestResponse(getStatus(response), builder); } @Override - public void onFailure(Exception e) { - handleError(channel, e); + protected RestStatus getStatus(ResourceAccessResponse response) { + return RestStatus.OK; } }); @@ -118,29 +113,37 @@ private void consumeParams(RestRequest request) { request.param("resource_index", ""); } - /** - * Send the appropriate response to the channel. - * @param channel the channel to send the response to - * @param response the response to send - * @throws IOException if an I/O error occurs - */ - private void sendResponse(RestChannel channel, ResourceAccessResponse response) throws IOException { - ok(channel, response::toXContent); - } - - /** - * Handle errors that occur during request processing. - * @param channel the channel to send the error response to - * @param e the exception that caused the error - */ - private void handleError(RestChannel channel, Exception e) { - String message = e.getMessage(); - LOGGER.error(message, e); - if (message.contains("not authorized")) { - forbidden(channel, message); - } else if (message.contains("no authenticated")) { - unauthorized(channel); - } - channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, message)); - } + // /** + // * Send the appropriate response to the channel. + // * @param channel the channel to send the response to + // * @param response the response to send + // * @throws IOException if an I/O error occurs + // */ + // @SuppressWarnings("unchecked") + // private void sendResponse(RestChannel channel, Object response) throws IOException { + // if (response instanceof Set) { // get + // Set resources = (Set) response; + // ok(channel, (builder, params) -> builder.startObject().field("resources", resources).endObject()); + // } else if (response instanceof ResourceSharing resourceSharing) { // share & revoke + // ok(channel, (resourceSharing::toXContent)); + // } else if (response instanceof Boolean) { // verify_access + // ok(channel, (builder, params) -> builder.startObject().field("has_permission", String.valueOf(response)).endObject()); + // } + // } + // + // /** + // * Handle errors that occur during request processing. + // * @param channel the channel to send the error response to + // * @param message the error message + // * @param e the exception that caused the error + // */ + // private void handleError(RestChannel channel, String message, Exception e) { + // LOGGER.error(message, e); + // if (message.contains("not authorized")) { + // forbidden(channel, message); + // } else if (message.contains("no authenticated")) { + // unauthorized(channel); + // } + // channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, message)); + // } } diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java index 3c548512ee..bcd4c2ed55 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java @@ -8,16 +8,10 @@ package org.opensearch.security.common.resources.rest; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; -import org.opensearch.security.common.resources.RecipientType; -import org.opensearch.security.common.resources.RecipientTypeRegistry; import org.opensearch.security.common.resources.ResourceAccessHandler; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; @@ -75,7 +69,7 @@ private void handleRevokeAccess(ResourceAccessRequest request, ActionListener listener.onResponse(new ResourceAccessResponse(success)), listener::onFailure) ); @@ -89,13 +83,4 @@ private void handleVerifyAccess(ResourceAccessRequest request, ActionListener listener.onResponse(new ResourceAccessResponse(hasPermission)), listener::onFailure) ); } - - /** - * Helper method to parse revoked entities from a generic Map - */ - private Map> parseRevokedEntities(Map> revokeSource) { - return revokeSource.entrySet() - .stream() - .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); - } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index fc8e75a6fb..c076596a5c 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -58,8 +58,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -146,6 +144,15 @@ import org.opensearch.security.auditlog.config.AuditConfig.Filter.FilterEntries; import org.opensearch.security.auditlog.impl.AuditLogImpl; import org.opensearch.security.auth.BackendRegistry; +import org.opensearch.security.common.resources.ResourceAccessHandler; +import org.opensearch.security.common.resources.ResourcePluginInfo; +import org.opensearch.security.common.resources.ResourceSharingConstants; +import org.opensearch.security.common.resources.ResourceSharingIndexHandler; +import org.opensearch.security.common.resources.ResourceSharingIndexListener; +import org.opensearch.security.common.resources.ResourceSharingIndexManagementRepository; +import org.opensearch.security.common.resources.rest.ResourceAccessAction; +import org.opensearch.security.common.resources.rest.ResourceAccessRestAction; +import org.opensearch.security.common.resources.rest.ResourceAccessTransportAction; import org.opensearch.security.compliance.ComplianceIndexingOperationListener; import org.opensearch.security.compliance.ComplianceIndexingOperationListenerImpl; import org.opensearch.security.configuration.AdminDNs; @@ -176,18 +183,12 @@ import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.resources.ResourceAccessHandler; -import org.opensearch.security.resources.ResourceSharingConstants; -import org.opensearch.security.resources.ResourceSharingIndexHandler; -import org.opensearch.security.resources.ResourceSharingIndexListener; -import org.opensearch.security.resources.ResourceSharingIndexManagementRepository; import org.opensearch.security.rest.DashboardsInfoAction; import org.opensearch.security.rest.SecurityConfigUpdateAction; import org.opensearch.security.rest.SecurityHealthAction; import org.opensearch.security.rest.SecurityInfoAction; import org.opensearch.security.rest.SecurityWhoAmIAction; import org.opensearch.security.rest.TenantInfoAction; -import org.opensearch.security.rest.resources.access.ResourceAccessRestAction; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.setting.OpensearchDynamicSetting; @@ -271,6 +272,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile RestLayerPrivilegesEvaluator restLayerEvaluator; private volatile ConfigurationRepository cr; private volatile AdminDNs adminDns; + private volatile org.opensearch.security.common.configuration.AdminDNs adminDNsCommon; private volatile ClusterService cs; private volatile AtomicReference localNode = new AtomicReference<>(); private volatile AuditLog auditLog; @@ -290,8 +292,6 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile DlsFlsBaseContext dlsFlsBaseContext; private ResourceSharingIndexManagementRepository rmr; private ResourceAccessHandler resourceAccessHandler; - private static final Map RESOURCE_PROVIDERS = new HashMap<>(); - private static final Set RESOURCE_INDICES = new HashSet<>(); public static boolean isActionTraceEnabled() { @@ -688,7 +688,7 @@ public List getRestHandlers( ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT )) { - handlers.add(new ResourceAccessRestAction(resourceAccessHandler)); + handlers.add(new ResourceAccessRestAction()); } log.debug("Added {} rest handler(s)", handlers.size()); } @@ -717,6 +717,12 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre actions.add(new ActionHandler<>(CertificatesActionType.INSTANCE, TransportCertificatesInfoNodesAction.class)); } actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class)); + if (settings.getAsBoolean( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT + )) { + actions.add(new ActionHandler<>(ResourceAccessAction.INSTANCE, ResourceAccessTransportAction.class)); + } } return actions; } @@ -747,11 +753,11 @@ public void onIndexModule(IndexModule indexModule) { // Listening on POST and DELETE operations in resource indices ResourceSharingIndexListener resourceSharingIndexListener = ResourceSharingIndexListener.getInstance(); - resourceSharingIndexListener.initialize(threadPool, localClient, auditLog); + resourceSharingIndexListener.initialize(threadPool, localClient, adminDNsCommon); if (settings.getAsBoolean( ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - ) && RESOURCE_INDICES.contains(indexModule.getIndex().getName())) { + ) && ResourcePluginInfo.getInstance().getResourceIndices().contains(indexModule.getIndex().getName())) { indexModule.addIndexOperationListener(resourceSharingIndexListener); log.info("Security plugin started listening to operations on resource-index {}", indexModule.getIndex().getName()); } @@ -1132,6 +1138,7 @@ public Collection createComponents( sslExceptionHandler = new AuditLogSslExceptionHandler(auditLog); adminDns = new AdminDNs(settings); + adminDNsCommon = new org.opensearch.security.common.configuration.AdminDNs(settings); cr = ConfigurationRepository.create(settings, this.configPath, threadPool, localClient, clusterService, auditLog); @@ -1161,13 +1168,8 @@ public Collection createComponents( ); final var resourceSharingIndex = ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; - ResourceSharingIndexHandler rsIndexHandler = new ResourceSharingIndexHandler( - resourceSharingIndex, - localClient, - threadPool, - auditLog - ); - resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns); + ResourceSharingIndexHandler rsIndexHandler = new ResourceSharingIndexHandler(resourceSharingIndex, localClient, threadPool); + resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDNsCommon); resourceAccessHandler.initializeRecipientTypes(); // Resource Sharing index is enabled by default boolean isResourceSharingEnabled = settings.getAsBoolean( @@ -1256,6 +1258,7 @@ public Collection createComponents( } components.add(adminDns); + components.add(adminDNsCommon); components.add(cr); components.add(xffResolver); components.add(backendRegistry); @@ -2281,23 +2284,6 @@ private void tryAddSecurityProvider() { }); } - public static Map getResourceProviders() { - return ImmutableMap.copyOf(RESOURCE_PROVIDERS); - } - - public static Set getResourceIndices() { - return ImmutableSet.copyOf(RESOURCE_INDICES); - } - - // TODO following should be removed once core test framework allows loading extended classes - public static Map getResourceProvidersMutable() { - return RESOURCE_PROVIDERS; - } - - public static Set getResourceIndicesMutable() { - return RESOURCE_INDICES; - } - // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings @Override public void loadExtensions(ExtensiblePlugin.ExtensionLoader loader) { @@ -2306,17 +2292,21 @@ public void loadExtensions(ExtensiblePlugin.ExtensionLoader loader) { ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT )) { + Set resourceIndices = new HashSet<>(); + Map resourceProviders = new HashMap<>(); for (ResourceSharingExtension extension : loader.loadExtensions(ResourceSharingExtension.class)) { String resourceType = extension.getResourceType(); String resourceIndexName = extension.getResourceIndex(); ResourceParser resourceParser = extension.getResourceParser(); - RESOURCE_INDICES.add(resourceIndexName); + resourceIndices.add(resourceIndexName); ResourceProvider resourceProvider = new ResourceProvider(resourceType, resourceIndexName, resourceParser); - RESOURCE_PROVIDERS.put(resourceIndexName, resourceProvider); + resourceProviders.put(resourceIndexName, resourceProvider); log.info("Loaded resource sharing extension: {}, index: {}", resourceType, resourceIndexName); } + ResourcePluginInfo.getInstance().setResourceIndices(resourceIndices); + ResourcePluginInfo.getInstance().setResourceProviders(resourceProviders); } } // CS-ENFORCE-SINGLE diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index b527b8fca2..ddee3b7021 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -59,6 +59,7 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; +import org.opensearch.security.common.auth.UserSubjectImpl; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.filter.SecurityRequestChannel; @@ -198,7 +199,7 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { * @param request * @return The authenticated user, null means another roundtrip * @throws OpenSearchSecurityException - */ + */ public boolean authenticate(final SecurityRequestChannel request) { final boolean isDebugEnabled = log.isDebugEnabled(); final boolean isBlockedBasedOnAddress = request.getRemoteAddress() @@ -225,7 +226,7 @@ public boolean authenticate(final SecurityRequestChannel request) { if (adminDns.isAdminDN(sslPrincipal)) { // PKI authenticated REST call User superuser = new User(sslPrincipal); - UserSubject subject = new UserSubjectImpl(threadPool, superuser); + UserSubject subject = new UserSubjectImpl(threadPool, new org.opensearch.security.common.user.User(sslPrincipal)); threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); auditLog.logSucceededLogin(sslPrincipal, true, null, request); @@ -393,7 +394,15 @@ public boolean authenticate(final SecurityRequestChannel request) { final User impersonatedUser = impersonate(request, authenticatedUser); final User effectiveUser = impersonatedUser == null ? authenticatedUser : impersonatedUser; threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, effectiveUser); - UserSubject subject = new UserSubjectImpl(threadPool, effectiveUser); + + // TODO: The following artistry must be reverted when User class is completely moved to :opensearch-security-common + org.opensearch.security.common.user.User effUser = new org.opensearch.security.common.user.User( + effectiveUser.getName(), + effectiveUser.getRoles(), + null + ); + effUser.setAttributes(effectiveUser.getCustomAttributesMap()); + UserSubject subject = new UserSubjectImpl(threadPool, effUser); threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); auditLog.logSucceededLogin(effectiveUser.getName(), false, authenticatedUser.getName(), request); } else { @@ -422,7 +431,14 @@ public boolean authenticate(final SecurityRequestChannel request) { User anonymousUser = new User(User.ANONYMOUS.getName(), new HashSet(User.ANONYMOUS.getRoles()), null); anonymousUser.setRequestedTenant(tenant); - UserSubject subject = new UserSubjectImpl(threadPool, anonymousUser); + org.opensearch.security.common.user.User anonymousUserCommon = new org.opensearch.security.common.user.User( + User.ANONYMOUS.getName(), + new HashSet<>(User.ANONYMOUS.getRoles()), + null + ); + anonymousUserCommon.setRequestedTenant(tenant); + + UserSubject subject = new UserSubjectImpl(threadPool, anonymousUserCommon); threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, anonymousUser); threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); diff --git a/src/test/java/org/opensearch/security/auth/UserSubjectImplTests.java b/src/test/java/org/opensearch/security/auth/UserSubjectImplTests.java index 9e630ef750..07bac9e349 100644 --- a/src/test/java/org/opensearch/security/auth/UserSubjectImplTests.java +++ b/src/test/java/org/opensearch/security/auth/UserSubjectImplTests.java @@ -15,7 +15,8 @@ import org.junit.Test; -import org.opensearch.security.user.User; +import org.opensearch.security.common.auth.UserSubjectImpl; +import org.opensearch.security.common.user.User; import org.opensearch.threadpool.TestThreadPool; import org.opensearch.threadpool.ThreadPool; diff --git a/src/test/java/org/opensearch/security/resources/CreatedByTests.java b/src/test/java/org/opensearch/security/resources/CreatedByTests.java index 0bc651b4d5..55bcdfe68f 100644 --- a/src/test/java/org/opensearch/security/resources/CreatedByTests.java +++ b/src/test/java/org/opensearch/security/resources/CreatedByTests.java @@ -19,6 +19,8 @@ import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.security.common.resources.CreatedBy; +import org.opensearch.security.common.resources.Creator; import org.opensearch.security.test.SingleClusterTest; import static org.hamcrest.Matchers.equalTo; diff --git a/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java b/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java index d1a6854c3e..c569c55803 100644 --- a/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java +++ b/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java @@ -10,6 +10,8 @@ import org.hamcrest.MatcherAssert; +import org.opensearch.security.common.resources.RecipientType; +import org.opensearch.security.common.resources.RecipientTypeRegistry; import org.opensearch.security.test.SingleClusterTest; import static org.hamcrest.Matchers.equalTo; diff --git a/src/test/java/org/opensearch/security/resources/ShareWithTests.java b/src/test/java/org/opensearch/security/resources/ShareWithTests.java index cec50a8198..7350241de2 100644 --- a/src/test/java/org/opensearch/security/resources/ShareWithTests.java +++ b/src/test/java/org/opensearch/security/resources/ShareWithTests.java @@ -25,6 +25,10 @@ import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.security.common.resources.RecipientType; +import org.opensearch.security.common.resources.RecipientTypeRegistry; +import org.opensearch.security.common.resources.ShareWith; +import org.opensearch.security.common.resources.SharedWithScope; import org.opensearch.security.spi.resources.ResourceAccessScope; import org.opensearch.security.test.SingleClusterTest; From d32a2c2d12e9b7d92ca0a8cf025e2085dd9d78e7 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 15:54:23 -0500 Subject: [PATCH 140/201] Adds get resource endpoint to sample plugin Signed-off-by: Darshit Chanpura --- sample-resource-plugin/build.gradle | 1 + .../AbstractSampleResourcePluginTests.java | 1 + ...rcePluginResourceSharingDisabledTests.java | 2 +- .../sample/SampleResourcePluginTests.java | 32 ++++-- .../sample/SampleResourcePlugin.java | 6 +- .../actions/rest/get/GetResourceAction.java | 29 +++++ .../actions/rest/get/GetResourceRequest.java | 49 +++++++++ .../actions/rest/get/GetResourceResponse.java | 53 +++++++++ .../rest/get/GetResourceRestAction.java | 49 +++++++++ .../transport/GetResourceTransportAction.java | 101 ++++++++++++++++++ 10 files changed, 314 insertions(+), 9 deletions(-) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle index 5b4447239e..ac49f05709 100644 --- a/sample-resource-plugin/build.gradle +++ b/sample-resource-plugin/build.gradle @@ -80,6 +80,7 @@ dependencies { integrationTestImplementation rootProject.sourceSets.integrationTest.output integrationTestImplementation rootProject.sourceSets.main.output integrationTestImplementation "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" + integrationTestImplementation "org.opensearch:opensearch-security-common:${opensearch_build}" } sourceSets { diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java index 4127e6abc8..cb363d0704 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java @@ -30,6 +30,7 @@ public class AbstractSampleResourcePluginTests { ); static final String SAMPLE_RESOURCE_CREATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/create"; + static final String SAMPLE_RESOURCE_GET_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/get"; static final String SAMPLE_RESOURCE_UPDATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/update"; static final String SAMPLE_RESOURCE_DELETE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/delete"; private static final String PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH = PLUGIN_RESOURCE_ROUTE_PREFIX.replaceFirst("/", ""); diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java index e1a86684ed..e8a01ff486 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java @@ -27,7 +27,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; -import static org.opensearch.security.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; +import static org.opensearch.security.common.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; import static org.opensearch.security.support.ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index 27845bef52..38005551ad 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -16,7 +16,7 @@ import org.junit.runner.RunWith; import org.opensearch.painless.PainlessModulePlugin; -import org.opensearch.security.OpenSearchSecurityPlugin; +import org.opensearch.security.common.resources.ResourcePluginInfo; import org.opensearch.security.spi.resources.ResourceAccessScope; import org.opensearch.security.spi.resources.ResourceProvider; import org.opensearch.test.framework.cluster.ClusterManager; @@ -29,7 +29,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; -import static org.opensearch.security.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; +import static org.opensearch.security.common.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; @@ -53,8 +53,8 @@ public void clearIndices() { try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { client.delete(RESOURCE_INDEX_NAME); client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX); - OpenSearchSecurityPlugin.getResourceIndicesMutable().remove(RESOURCE_INDEX_NAME); - OpenSearchSecurityPlugin.getResourceProvidersMutable().remove(RESOURCE_INDEX_NAME); + ResourcePluginInfo.getInstance().getResourceIndicesMutable().remove(RESOURCE_INDEX_NAME); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().remove(RESOURCE_INDEX_NAME); } } @@ -96,14 +96,14 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); assertThat(response.getStatusReason(), containsString("Created")); resourceSharingDocId = response.bodyAsJsonNode().get("_id").asText(); - // Also update the in-memory map and list - OpenSearchSecurityPlugin.getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); + // Also update the in-memory map and get + ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); ResourceProvider provider = new ResourceProvider( SampleResource.class.getCanonicalName(), RESOURCE_INDEX_NAME, new SampleResourceParser() ); - OpenSearchSecurityPlugin.getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); Thread.sleep(1000); response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); @@ -199,6 +199,12 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { ); } + // get sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + // revoke share_with_user's access try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { Thread.sleep(1000); @@ -221,6 +227,18 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { assertThat(response.bodyAsJsonNode().get("has_permission").asBoolean(), equalTo(false)); } + // get sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // delete sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.delete(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + // delete sample resource try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index 70472f0b6d..a522bd7396 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -41,8 +41,11 @@ import org.opensearch.sample.resource.actions.rest.create.UpdateResourceAction; import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceAction; import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceRestAction; +import org.opensearch.sample.resource.actions.rest.get.GetResourceAction; +import org.opensearch.sample.resource.actions.rest.get.GetResourceRestAction; import org.opensearch.sample.resource.actions.transport.CreateResourceTransportAction; import org.opensearch.sample.resource.actions.transport.DeleteResourceTransportAction; +import org.opensearch.sample.resource.actions.transport.GetResourceTransportAction; import org.opensearch.sample.resource.actions.transport.UpdateResourceTransportAction; import org.opensearch.script.ScriptService; import org.opensearch.security.spi.resources.ResourceParser; @@ -89,13 +92,14 @@ public List getRestHandlers( IndexNameExpressionResolver indexNameExpressionResolver, Supplier nodesInCluster ) { - return List.of(new CreateResourceRestAction(), new DeleteResourceRestAction()); + return List.of(new CreateResourceRestAction(), new GetResourceRestAction(), new DeleteResourceRestAction()); } @Override public List> getActions() { return List.of( new ActionHandler<>(CreateResourceAction.INSTANCE, CreateResourceTransportAction.class), + new ActionHandler<>(GetResourceAction.INSTANCE, GetResourceTransportAction.class), new ActionHandler<>(UpdateResourceAction.INSTANCE, UpdateResourceTransportAction.class), new ActionHandler<>(DeleteResourceAction.INSTANCE, DeleteResourceTransportAction.class) ); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceAction.java new file mode 100644 index 0000000000..0249a06501 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceAction.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.get; + +import org.opensearch.action.ActionType; + +/** + * Action to get a sample resource + */ +public class GetResourceAction extends ActionType { + /** + * Get sample resource action instance + */ + public static final GetResourceAction INSTANCE = new GetResourceAction(); + /** + * Get sample resource action name + */ + public static final String NAME = "cluster:admin/sample-resource-plugin/get"; + + private GetResourceAction() { + super(NAME, GetResourceResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRequest.java new file mode 100644 index 0000000000..eb8d8abb1f --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRequest.java @@ -0,0 +1,49 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.get; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +/** + * Request object for GetSampleResource transport action + */ +public class GetResourceRequest extends ActionRequest { + + private final String resourceId; + + /** + * Default constructor + */ + public GetResourceRequest(String resourceId) { + this.resourceId = resourceId; + } + + public GetResourceRequest(StreamInput in) throws IOException { + this.resourceId = in.readString(); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(this.resourceId); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceId() { + return this.resourceId; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceResponse.java new file mode 100644 index 0000000000..b6d986e257 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceResponse.java @@ -0,0 +1,53 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.get; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.sample.SampleResource; + +public class GetResourceResponse extends ActionResponse implements ToXContentObject { + private final SampleResource resource; + + /** + * Default constructor + * + * @param resource The resource + */ + public GetResourceResponse(SampleResource resource) { + this.resource = resource; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeNamedWriteable(resource); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public GetResourceResponse(final StreamInput in) throws IOException { + resource = in.readNamedWriteable(SampleResource.class); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("resource", resource); + builder.endObject(); + return builder; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java new file mode 100644 index 0000000000..3f94613124 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java @@ -0,0 +1,49 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.get; + +import java.util.List; + +import org.opensearch.core.common.Strings; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.transport.client.node.NodeClient; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; + +public class GetResourceRestAction extends BaseRestHandler { + + public GetResourceRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(GET, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/get/{resource_id}")); + } + + @Override + public String getName() { + return "get_sample_resource"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + String resourceId = request.param("resource_id"); + if (Strings.isNullOrEmpty(resourceId)) { + throw new IllegalArgumentException("resource_id parameter is required"); + } + + // verify access + + final GetResourceRequest getResourceRequest = new GetResourceRequest(resourceId); + return channel -> client.executeLocally(GetResourceAction.INSTANCE, getResourceRequest, new RestToXContentListener<>(channel)); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java new file mode 100644 index 0000000000..ab84ed5748 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java @@ -0,0 +1,101 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.transport; + +import java.io.IOException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.ResourceNotFoundException; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.get.GetResponse; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.sample.SampleResource; +import org.opensearch.sample.resource.actions.rest.get.GetResourceAction; +import org.opensearch.sample.resource.actions.rest.get.GetResourceRequest; +import org.opensearch.sample.resource.actions.rest.get.GetResourceResponse; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; +import org.opensearch.transport.client.Client; + +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +public class GetResourceTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(GetResourceTransportAction.class); + + private final TransportService transportService; + private final Client nodeClient; + + @Inject + public GetResourceTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { + super(GetResourceAction.NAME, transportService, actionFilters, GetResourceRequest::new); + this.transportService = transportService; + this.nodeClient = nodeClient; + } + + @Override + protected void doExecute(Task task, GetResourceRequest request, ActionListener listener) { + if (request.getResourceId() == null || request.getResourceId().isEmpty()) { + listener.onFailure(new IllegalArgumentException("Resource ID cannot be null or empty")); + return; + } + + ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + getResource(request, ActionListener.wrap(getResponse -> { + if (getResponse.isSourceEmpty()) { + listener.onFailure(new ResourceNotFoundException("Resource " + request.getResourceId() + " not found.")); + } else { + // String jsonString = XContentFactory.jsonBuilder().map(getResponse.getSourceAsMap()).toString(); + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, getResponse.getSourceAsString()) + ) { + listener.onResponse(new GetResourceResponse(SampleResource.fromXContent(parser))); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); + } + } + }, exception -> { + log.error("Failed to fetch resource: " + request.getResourceId(), exception); + listener.onFailure(exception); + })); + } + } + + private void getResource(GetResourceRequest request, ActionListener listener) { + XContentBuilder builder; + try { + builder = JsonXContent.contentBuilder() + .startObject() + .field("resource_id", request.getResourceId()) + .field("resource_index", RESOURCE_INDEX_NAME) + .field("scope", "string_value") // Modify as needed + .endObject(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + GetRequest getRequest = new GetRequest(RESOURCE_INDEX_NAME, request.getResourceId()); + + nodeClient.get(getRequest, listener); + } + +} From a1533a1cf436643429c33bd988e6fe04a83240c2 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 15:54:48 -0500 Subject: [PATCH 141/201] Adds client for resource plugins Signed-off-by: Darshit Chanpura --- build.gradle | 1 + client/build.gradle | 97 +++++++++++++++++++ .../resources/ResourceSharingClient.java | 30 ++++++ .../resources/ResourceSharingNodeClient.java | 52 ++++++++++ .../client/resources/package-info.java | 14 +++ settings.gradle | 6 ++ 6 files changed, 200 insertions(+) create mode 100644 client/build.gradle create mode 100644 client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java create mode 100644 client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java create mode 100644 client/src/main/java/org/opensearch/security/client/resources/package-info.java diff --git a/build.gradle b/build.gradle index eb2c369a9f..38efef16e8 100644 --- a/build.gradle +++ b/build.gradle @@ -562,6 +562,7 @@ allprojects { integrationTestImplementation 'com.selectivem.collections:special-collections-complete:1.4.0' integrationTestImplementation "org.opensearch.plugin:lang-painless:${opensearch_version}" integrationTestImplementation project(path:":opensearch-security-common") + integrationTestImplementation project(path:":opensearch-security-client") integrationTestImplementation project(path:":opensearch-resource-sharing-spi") } } diff --git a/client/build.gradle b/client/build.gradle new file mode 100644 index 0000000000..763edc6b4b --- /dev/null +++ b/client/build.gradle @@ -0,0 +1,97 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + id 'java' + id 'maven-publish' +} + +ext { + opensearch_version = System.getProperty("opensearch.version", "3.0.0-alpha1-SNAPSHOT") + isSnapshot = "true" == System.getProperty("build.snapshot", "true") + buildVersionQualifier = System.getProperty("build.version_qualifier", "alpha1") + + // 2.0.0-rc1-SNAPSHOT -> 2.0.0.0-rc1-SNAPSHOT + version_tokens = opensearch_version.tokenize('-') + opensearch_build = version_tokens[0] + '.0' + + common_utils_version = System.getProperty("common_utils.version", '3.0.0.0-alpha1-SNAPSHOT') + + if (buildVersionQualifier) { + opensearch_build += "-${buildVersionQualifier}" + } + if (isSnapshot) { + opensearch_build += "-SNAPSHOT" + } +} + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } +} + +dependencies { + // Main implementation dependencies + compileOnly "org.opensearch:opensearch:${opensearch_version}" + compileOnly "org.opensearch:opensearch-resource-sharing-common:${opensearch_build}" +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +task sourcesJar(type: Jar) { + archiveClassifier.set 'sources' + from sourceSets.main.allJava +} + +task javadocJar(type: Jar) { + archiveClassifier.set 'javadoc' + from tasks.javadoc +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar + pom { + name.set("OpenSearch Security Client") + description.set("OpenSearch Security Client") + url.set("https://github.com/opensearch-project/security") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + scm { + connection.set("scm:git@github.com:opensearch-project/security.git") + developerConnection.set("scm:git@github.com:opensearch-project/security.git") + url.set("https://github.com/opensearch-project/security.git") + } + developers { + developer { + name.set("OpenSearch Contributors") + url.set("https://github.com/opensearch-project") + } + } + } + } + } + repositories { + maven { + name = "Snapshots" + url = "https://aws.oss.sonatype.org/content/repositories/snapshots" + credentials { + username "$System.env.SONATYPE_USERNAME" + password "$System.env.SONATYPE_PASSWORD" + } + } + } +} diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java new file mode 100644 index 0000000000..7397ba4713 --- /dev/null +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java @@ -0,0 +1,30 @@ +/* + * 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. + */ + +package org.opensearch.security.client.resources; + +import org.opensearch.core.action.ActionListener; + +import java.util.List; + +public interface ResourceSharingClient { + + void verifyResourceAccess(String resourceId, String resourceIndex, String scope, ActionListener listener); + + void grantResourceAccess( + String resourceId, + String resourceIndex, + String userOrRole, + String accessLevel, + ActionListener listener + ); + + void revokeResourceAccess(String resourceId, String resourceIndex, String userOrRole, ActionListener listener); + + void listAccessibleResources(String userOrRole, ActionListener> listener); +} diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java new file mode 100644 index 0000000000..dd379cbe1a --- /dev/null +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java @@ -0,0 +1,52 @@ +package org.opensearch.security.client.resources;// package org.opensearch.security.spi.resources.client; +// +// import org.opensearch.core.action.ActionListener; +// import org.opensearch.transport.client.node.NodeClient; +// +// import java.util.List; +// +// public class ResourceSharingNodeClient { +// +// private final NodeClient nodeClient; +// +// public ResourceSharingClient(NodeClient nodeClient) { +// this.nodeClient = nodeClient; +// } +// +// public void verifyResourceAccess(String resourceId, String resourceIndex, String scope, ActionListener listener) { +// ResourceAccessRequest request = new ResourceAccessRequest(ResourceAccessRequest.OperationType.VERIFY, resourceId, resourceIndex, scope); +// execute(ResourceAccessAction.INSTANCE, request, wrapBooleanResponse(listener)); +// } +// +// public void grantResourceAccess(String resourceId, String resourceIndex, String userOrRole, String accessLevel, ActionListener +// listener) { +// ResourceAccessRequest request = new ResourceAccessRequest(ResourceAccessRequest.OperationType.GRANT, resourceId, resourceIndex, +// userOrRole, accessLevel); +// execute(ResourceAccessAction.INSTANCE, request, wrapBooleanResponse(listener)); +// } +// +// public void revokeResourceAccess(String resourceId, String resourceIndex, String userOrRole, ActionListener listener) { +// ResourceAccessRequest request = new ResourceAccessRequest(ResourceAccessRequest.OperationType.REVOKE, resourceId, resourceIndex, +// userOrRole); +// execute(ResourceAccessAction.INSTANCE, request, wrapBooleanResponse(listener)); +// } +// +// public void listAccessibleResources(String userOrRole, ActionListener> listener) { +// ResourceAccessRequest request = new ResourceAccessRequest(ResourceAccessRequest.OperationType.LIST, userOrRole); +// execute(ResourceAccessAction.INSTANCE, request, wrapListResponse(listener)); +// } +// +// private ActionListener wrapBooleanResponse(ActionListener listener) { +// return ActionListener.wrap( +// response -> listener.onResponse(response.getHasPermission()), +// listener::onFailure +// ); +// } +// +// private ActionListener wrapListResponse(ActionListener> listener) { +// return ActionListener.wrap( +// response -> listener.onResponse(response.getAccessibleResources()), +// listener::onFailure +// ); +// } +// } diff --git a/client/src/main/java/org/opensearch/security/client/resources/package-info.java b/client/src/main/java/org/opensearch/security/client/resources/package-info.java new file mode 100644 index 0000000000..72b5b51a99 --- /dev/null +++ b/client/src/main/java/org/opensearch/security/client/resources/package-info.java @@ -0,0 +1,14 @@ +/* + * 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. + */ + +/** + * This package defines class required to implement resource access control in OpenSearch. + * + * @opensearch.experimental + */ +package org.opensearch.security.client.resources; diff --git a/settings.gradle b/settings.gradle index 647daa1a47..02aa91f8ee 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,5 +9,11 @@ rootProject.name = 'opensearch-security' include "spi" project(":spi").name = "opensearch-resource-sharing-spi" +include 'common' +project(":common").name = rootProject.name + "-common" + +include 'client' +project(":client").name = rootProject.name + "-client" + include "sample-resource-plugin" project(":sample-resource-plugin").name = "opensearch-sample-resource-plugin" From d4301430c3c6987d69d2b1e93df939a9d2353aab Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 16:37:54 -0500 Subject: [PATCH 142/201] Fixes sample plugin tests and add builder pattern to ResourceAccessRequest Signed-off-by: Darshit Chanpura --- .../resources/rest/ResourceAccessRequest.java | 106 +++++++++++++----- .../rest/ResourceAccessResponse.java | 7 +- .../rest/ResourceAccessRestAction.java | 83 +++++++------- .../rest/ResourceAccessTransportAction.java | 15 ++- .../AbstractSampleResourcePluginTests.java | 2 +- .../sample/SampleResourcePluginTests.java | 33 ++++-- 6 files changed, 167 insertions(+), 79 deletions(-) diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java index 97e31c2769..1272757e45 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java @@ -9,9 +9,9 @@ package org.opensearch.security.common.resources.rest; import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionRequestValidationException; @@ -22,11 +22,8 @@ import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.security.common.resources.RecipientType; -import org.opensearch.security.common.resources.RecipientTypeRegistry; import org.opensearch.security.common.resources.ShareWith; -// TODO: Fix revoked entries public class ResourceAccessRequest extends ActionRequest { public enum Operation { @@ -40,9 +37,22 @@ public enum Operation { private final String resourceId; private final String resourceIndex; private final String scope; - private ShareWith shareWith; - private Map> revokedEntities; - private Set scopes; + private final ShareWith shareWith; + private final Map> revokedEntities; + private final Set scopes; + + /** + * Private constructor to enforce usage of Builder + */ + private ResourceAccessRequest(Builder builder) { + this.operation = builder.operation; + this.resourceId = builder.resourceId; + this.resourceIndex = builder.resourceIndex; + this.scope = builder.scope; + this.shareWith = builder.shareWith; + this.revokedEntities = builder.revokedEntities; + this.scopes = builder.scopes; + } /** * New Constructor: Initialize request from a `Map` @@ -56,19 +66,25 @@ public ResourceAccessRequest(Map source, Map par } this.resourceId = (String) source.get("resource_id"); - this.resourceIndex = params.containsKey("resource_index") ? params.get("resource_index") : (String) (source.get("resource_index")); + this.resourceIndex = params.containsKey("resource_index") ? params.get("resource_index") : (String) source.get("resource_index"); this.scope = (String) source.get("scope"); if (source.containsKey("share_with")) { this.shareWith = parseShareWith(source); + } else { + this.shareWith = null; } - if (source.containsKey("revoked_entities")) { - this.revokedEntities = parseRevokedEntities(source); + if (source.containsKey("entities_to_revoke")) { + this.revokedEntities = ((Map>) source.get("entities_to_revoke")); + } else { + this.revokedEntities = null; } if (source.containsKey("scopes")) { - this.scopes = Set.copyOf((Set) source.get("scopes")); + this.scopes = Set.copyOf((List) source.get("scopes")); + } else { + this.scopes = null; } } @@ -79,7 +95,7 @@ public ResourceAccessRequest(StreamInput in) throws IOException { this.resourceIndex = in.readOptionalString(); this.scope = in.readOptionalString(); this.shareWith = in.readOptionalWriteable(ShareWith::new); - // this.revokedEntities = in.readMap(StreamInput::readEnum, StreamInput::readSet); + this.revokedEntities = in.readMap(StreamInput::readString, valIn -> valIn.readSet(StreamInput::readString)); this.scopes = in.readSet(StreamInput::readString); } @@ -90,7 +106,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(resourceIndex); out.writeOptionalString(scope); out.writeOptionalWriteable(shareWith); - // out.writeMap(revokedEntities, StreamOutput::writeEnum, StreamOutput::writeStringCollection); + out.writeMap(revokedEntities, StreamOutput::writeString, StreamOutput::writeStringCollection); out.writeStringCollection(scopes); } @@ -125,17 +141,6 @@ private ShareWith parseShareWith(Map source) throws IOException } } - /** - * Helper method to parse revoked entities from a generic Map - */ - @SuppressWarnings("unchecked") - private Map> parseRevokedEntities(Map source) { - Map> revokeSource = (Map>) source.get("entities"); - return revokeSource.entrySet() - .stream() - .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); - } - public Operation getOperation() { return operation; } @@ -156,7 +161,7 @@ public ShareWith getShareWith() { return shareWith; } - public Map> getRevokedEntities() { + public Map> getRevokedEntities() { return revokedEntities; } @@ -164,4 +169,55 @@ public Set getScopes() { return scopes; } + /** + * Builder for ResourceAccessRequest + */ + public static class Builder { + private Operation operation; + private String resourceId; + private String resourceIndex; + private String scope; + private ShareWith shareWith; + private Map> revokedEntities; + private Set scopes; + + public Builder setOperation(Operation operation) { + this.operation = operation; + return this; + } + + public Builder setResourceId(String resourceId) { + this.resourceId = resourceId; + return this; + } + + public Builder setResourceIndex(String resourceIndex) { + this.resourceIndex = resourceIndex; + return this; + } + + public Builder setScope(String scope) { + this.scope = scope; + return this; + } + + public Builder setShareWith(ShareWith shareWith) { + this.shareWith = shareWith; + return this; + } + + public Builder setRevokedEntities(Map> revokedEntities) { + this.revokedEntities = revokedEntities; + return this; + } + + public Builder setScopes(Set scopes) { + this.scopes = scopes; + return this; + } + + public ResourceAccessRequest build() { + return new ResourceAccessRequest(this); + } + } } diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java index 35dbdecef7..5cbb6aec7a 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java @@ -66,7 +66,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.startObject(); switch (responseType) { case RESOURCES -> builder.field("resources", responseData); - case RESOURCE_SHARING -> builder.field("resource_sharing", responseData); + case RESOURCE_SHARING -> builder.field("sharing_info", responseData); case BOOLEAN -> builder.field("has_permission", responseData); } return builder.endObject(); @@ -84,4 +84,9 @@ public ResourceSharing getResourceSharing() { public Boolean getHasPermission() { return responseType == ResponseType.BOOLEAN ? (Boolean) responseData : null; } + + @Override + public String toString() { + return "ResourceAccessResponse [responseType=" + responseType + ", responseData=" + responseData + "]"; + } } diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java index 99d4392a22..8df9bcf5c3 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java @@ -17,19 +17,21 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; -import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; -import org.opensearch.rest.RestResponse; -import org.opensearch.rest.action.RestToXContentListener; import org.opensearch.transport.client.node.NodeClient; import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; import static org.opensearch.security.common.dlic.rest.api.Responses.badRequest; +import static org.opensearch.security.common.dlic.rest.api.Responses.forbidden; +import static org.opensearch.security.common.dlic.rest.api.Responses.ok; +import static org.opensearch.security.common.dlic.rest.api.Responses.unauthorized; import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.LIST; import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.REVOKE; import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.SHARE; @@ -87,17 +89,20 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli ResourceAccessRequest resourceAccessRequest = new ResourceAccessRequest(source, request.params()); return channel -> { - client.executeLocally(ResourceAccessAction.INSTANCE, resourceAccessRequest, new RestToXContentListener<>(channel) { + client.executeLocally(ResourceAccessAction.INSTANCE, resourceAccessRequest, new ActionListener<>() { + @Override - public RestResponse buildResponse(ResourceAccessResponse response, XContentBuilder builder) throws Exception { - assert !response.isFragment(); // would be nice if we could make default methods final - response.toXContent(builder, channel.request()); - return new BytesRestResponse(getStatus(response), builder); + public void onResponse(ResourceAccessResponse response) { + try { + sendResponse(channel, response); + } catch (IOException e) { + throw new RuntimeException(e); + } } @Override - protected RestStatus getStatus(ResourceAccessResponse response) { - return RestStatus.OK; + public void onFailure(Exception e) { + handleError(channel, e); } }); @@ -113,37 +118,29 @@ private void consumeParams(RestRequest request) { request.param("resource_index", ""); } - // /** - // * Send the appropriate response to the channel. - // * @param channel the channel to send the response to - // * @param response the response to send - // * @throws IOException if an I/O error occurs - // */ - // @SuppressWarnings("unchecked") - // private void sendResponse(RestChannel channel, Object response) throws IOException { - // if (response instanceof Set) { // get - // Set resources = (Set) response; - // ok(channel, (builder, params) -> builder.startObject().field("resources", resources).endObject()); - // } else if (response instanceof ResourceSharing resourceSharing) { // share & revoke - // ok(channel, (resourceSharing::toXContent)); - // } else if (response instanceof Boolean) { // verify_access - // ok(channel, (builder, params) -> builder.startObject().field("has_permission", String.valueOf(response)).endObject()); - // } - // } - // - // /** - // * Handle errors that occur during request processing. - // * @param channel the channel to send the error response to - // * @param message the error message - // * @param e the exception that caused the error - // */ - // private void handleError(RestChannel channel, String message, Exception e) { - // LOGGER.error(message, e); - // if (message.contains("not authorized")) { - // forbidden(channel, message); - // } else if (message.contains("no authenticated")) { - // unauthorized(channel); - // } - // channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, message)); - // } + /** + * Send the appropriate response to the channel. + * @param channel the channel to send the response to + * @param response the response to send + * @throws IOException if an I/O error occurs + */ + private void sendResponse(RestChannel channel, ResourceAccessResponse response) throws IOException { + ok(channel, response::toXContent); + } + + /** + * Handle errors that occur during request processing. + * @param channel the channel to send the error response to + * @param e the exception that caused the error + */ + private void handleError(RestChannel channel, Exception e) { + String message = e.getMessage(); + LOGGER.error(message, e); + if (message.contains("not authorized")) { + forbidden(channel, message); + } else if (message.contains("no authenticated")) { + unauthorized(channel); + } + channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, message)); + } } diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java index bcd4c2ed55..6043baebbf 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java @@ -8,10 +8,16 @@ package org.opensearch.security.common.resources.rest; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; +import org.opensearch.security.common.resources.RecipientType; +import org.opensearch.security.common.resources.RecipientTypeRegistry; import org.opensearch.security.common.resources.ResourceAccessHandler; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; @@ -69,7 +75,7 @@ private void handleRevokeAccess(ResourceAccessRequest request, ActionListener listener.onResponse(new ResourceAccessResponse(success)), listener::onFailure) ); @@ -83,4 +89,11 @@ private void handleVerifyAccess(ResourceAccessRequest request, ActionListener listener.onResponse(new ResourceAccessResponse(hasPermission)), listener::onFailure) ); } + + @SuppressWarnings("unchecked") + private Map> parseRevokedEntities(Map> revokeSource) { + return revokeSource.entrySet() + .stream() + .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); + } } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java index cb363d0704..ac420cec43 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java @@ -67,7 +67,7 @@ static String revokeAccessPayload(String resourceId) { + "\"resource_index\": \"" + RESOURCE_INDEX_NAME + "\"," - + "\"entities\": {" + + "\"entities_to_revoke\": {" + "\"users\": [\"" + SHARED_WITH_USER.getName() + "\"]" diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index 38005551ad..ffb34396d7 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -154,7 +154,13 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload(resourceId)); response.assertStatusCode(HttpStatus.SC_OK); assertThat( - response.bodyAsJsonNode().get("share_with").get(SampleResourceScope.PUBLIC.value()).get("users").get(0).asText(), + response.bodyAsJsonNode() + .get("sharing_info") + .get("share_with") + .get(SampleResourceScope.PUBLIC.value()) + .get("users") + .get(0) + .asText(), containsString(SHARED_WITH_USER.getName()) ); } @@ -230,21 +236,24 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { // get sample resource with shared_with_user try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + // TODO change this to forbidden once client has been implemented + response.assertStatusCode(HttpStatus.SC_OK); } // delete sample resource with shared_with_user try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.delete(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - } - - // delete sample resource - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + // TODO change this to forbidden once client has been implemented response.assertStatusCode(HttpStatus.SC_OK); } + // delete sample resource + // TODO uncomment once client has been implemented + // try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + // HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + // response.assertStatusCode(HttpStatus.SC_OK); + // } + // corresponding entry should be removed from resource-sharing index try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { // Since test framework doesn't yet allow loading ex tensions we need to delete the resource sharing entry manually @@ -256,6 +265,14 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString("hits\":[]")); } + + // get sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } } + // TODO: similar to above, add test case to test sample plugin apis using security client + } From f3e34b6baff06e088cd7a4ebc2a5689be6c9fbdb Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 16:52:01 -0500 Subject: [PATCH 143/201] Fixes builder pattern for ResourceAccessRequest Signed-off-by: Darshit Chanpura --- .../resources/rest/ResourceAccessRequest.java | 101 +++++++++--------- .../rest/ResourceAccessRestAction.java | 2 +- 2 files changed, 52 insertions(+), 51 deletions(-) diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java index 1272757e45..66dc62343b 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java @@ -9,9 +9,11 @@ package org.opensearch.security.common.resources.rest; import java.io.IOException; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionRequestValidationException; @@ -55,37 +57,35 @@ private ResourceAccessRequest(Builder builder) { } /** - * New Constructor: Initialize request from a `Map` + * Static factory method to initialize ResourceAccessRequest from a Map. */ @SuppressWarnings("unchecked") - public ResourceAccessRequest(Map source, Map params) throws IOException { + public static ResourceAccessRequest from(Map source, Map params) throws IOException { + Builder builder = new Builder(); + if (source.containsKey("operation")) { - this.operation = (Operation) source.get("operation"); + builder.operation(Operation.valueOf((String) source.get("operation"))); } else { throw new IllegalArgumentException("Missing required field: operation"); } - this.resourceId = (String) source.get("resource_id"); - this.resourceIndex = params.containsKey("resource_index") ? params.get("resource_index") : (String) source.get("resource_index"); - this.scope = (String) source.get("scope"); + builder.resourceId((String) source.get("resource_id")); + builder.resourceIndex(params.getOrDefault("resource_index", (String) source.get("resource_index"))); + builder.scope((String) source.get("scope")); if (source.containsKey("share_with")) { - this.shareWith = parseShareWith(source); - } else { - this.shareWith = null; + builder.shareWith(source); } if (source.containsKey("entities_to_revoke")) { - this.revokedEntities = ((Map>) source.get("entities_to_revoke")); - } else { - this.revokedEntities = null; + builder.revokedEntities(source); } if (source.containsKey("scopes")) { - this.scopes = Set.copyOf((List) source.get("scopes")); - } else { - this.scopes = null; + builder.scopes(Set.copyOf((List) source.get("scopes"))); // Ensuring Set type } + + return builder.build(); } public ResourceAccessRequest(StreamInput in) throws IOException { @@ -115,32 +115,6 @@ public ActionRequestValidationException validate() { return null; } - /** - * Parse the share with structure from the request body. - * - * @param source the request body - * @return the parsed ShareWith object - * @throws IOException if an I/O error occurs - */ - @SuppressWarnings("unchecked") - private ShareWith parseShareWith(Map source) throws IOException { - Map shareWithMap = (Map) source.get("share_with"); - if (shareWithMap == null || shareWithMap.isEmpty()) { - throw new IllegalArgumentException("share_with is required and cannot be empty"); - } - - String jsonString = XContentFactory.jsonBuilder().map(shareWithMap).toString(); - - try ( - XContentParser parser = XContentType.JSON.xContent() - .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, jsonString) - ) { - return ShareWith.fromXContent(parser); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); - } - } - public Operation getOperation() { return operation; } @@ -181,37 +155,37 @@ public static class Builder { private Map> revokedEntities; private Set scopes; - public Builder setOperation(Operation operation) { + public Builder operation(Operation operation) { this.operation = operation; return this; } - public Builder setResourceId(String resourceId) { + public Builder resourceId(String resourceId) { this.resourceId = resourceId; return this; } - public Builder setResourceIndex(String resourceIndex) { + public Builder resourceIndex(String resourceIndex) { this.resourceIndex = resourceIndex; return this; } - public Builder setScope(String scope) { + public Builder scope(String scope) { this.scope = scope; return this; } - public Builder setShareWith(ShareWith shareWith) { - this.shareWith = shareWith; + public Builder shareWith(Map source) throws IOException { + this.shareWith = parseShareWith(source); return this; } - public Builder setRevokedEntities(Map> revokedEntities) { - this.revokedEntities = revokedEntities; + public Builder revokedEntities(Map source) throws IOException { + this.revokedEntities = parseRevokedEntities(source); return this; } - public Builder setScopes(Set scopes) { + public Builder scopes(Set scopes) { this.scopes = scopes; return this; } @@ -219,5 +193,32 @@ public Builder setScopes(Set scopes) { public ResourceAccessRequest build() { return new ResourceAccessRequest(this); } + + @SuppressWarnings("unchecked") + private ShareWith parseShareWith(Map source) throws IOException { + Map shareWithMap = (Map) source.get("share_with"); + if (shareWithMap == null || shareWithMap.isEmpty()) { + throw new IllegalArgumentException("share_with is required and cannot be empty"); + } + + String jsonString = XContentFactory.jsonBuilder().map(shareWithMap).toString(); + + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, jsonString) + ) { + return ShareWith.fromXContent(parser); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); + } + } + + @SuppressWarnings("unchecked") + private Map> parseRevokedEntities(Map source) throws IOException { + + return ((Map>) source.get("entities_to_revoke")).entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> new HashSet<>(e.getValue()))); + } } } diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java index 8df9bcf5c3..fd55eeab2e 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java @@ -87,7 +87,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli } } - ResourceAccessRequest resourceAccessRequest = new ResourceAccessRequest(source, request.params()); + ResourceAccessRequest resourceAccessRequest = ResourceAccessRequest.from(source, request.params()); return channel -> { client.executeLocally(ResourceAccessAction.INSTANCE, resourceAccessRequest, new ActionListener<>() { From 4ba3f219025df797af199ad6f4ed3b04110a15dd Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 19:02:31 -0500 Subject: [PATCH 144/201] Refactors ResourceSharingException Signed-off-by: Darshit Chanpura --- common/build.gradle | 2 -- .../resources/ResourceAccessHandler.java | 3 ++- .../resources/ResourceSharingIndexHandler.java | 1 + .../resources/rest/ResourceAccessRequest.java | 18 +++++++++++++----- .../exceptions}/ResourceSharingException.java | 17 ++++++++++++++++- 5 files changed, 32 insertions(+), 9 deletions(-) rename {common/src/main/java/org/opensearch/security/common/resources => spi/src/main/java/org/opensearch/security/spi/resources/exceptions}/ResourceSharingException.java (65%) diff --git a/common/build.gradle b/common/build.gradle index ecbbffd75a..5dcb58e3fb 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -43,10 +43,8 @@ repositories { } dependencies { - // Main implementation dependencies compileOnly "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" compileOnly "org.opensearch.plugin:lang-painless:${opensearch_version}" -// compileOnly "org.opensearch:opensearch:${opensearch_version}" compileOnly "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" compileOnly "com.google.guava:guava:${guava_version}" compileOnly "org.apache.commons:commons-lang3:${versions.commonslang}" diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java index 98b9a4f910..5005f1c671 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java @@ -28,6 +28,7 @@ import org.opensearch.security.common.user.User; import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.ResourceParser; +import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; import org.opensearch.threadpool.ThreadPool; /** @@ -231,7 +232,7 @@ public void hasPermission(String resourceId, String resourceIndex, String scope, this.resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId, ActionListener.wrap(document -> { if (document == null) { LOGGER.warn("Resource '{}' not found in index '{}'", resourceId, resourceIndex); - listener.onResponse(false); + listener.onFailure(new ResourceSharingException("Resource " + resourceId + " not found in index " + resourceIndex)); return; } diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java index ede8985e68..4b39820123 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java @@ -66,6 +66,7 @@ import org.opensearch.security.common.DefaultObjectMapper; import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.ResourceParser; +import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java index 66dc62343b..57f0456959 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java @@ -64,7 +64,7 @@ public static ResourceAccessRequest from(Map source, Map source) throws IOException { - this.shareWith = parseShareWith(source); + public Builder shareWith(Map source) { + try { + this.shareWith = parseShareWith(source); + } catch (Exception e) { + this.shareWith = null; + } return this; } - public Builder revokedEntities(Map source) throws IOException { - this.revokedEntities = parseRevokedEntities(source); + public Builder revokedEntities(Map source) { + try { + this.revokedEntities = parseRevokedEntities(source); + } catch (Exception e) { + this.revokedEntities = null; + } return this; } diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingException.java b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java similarity index 65% rename from common/src/main/java/org/opensearch/security/common/resources/ResourceSharingException.java rename to spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java index e95d4b51ee..31c19fc2db 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingException.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java @@ -9,12 +9,13 @@ * GitHub history for details. */ -package org.opensearch.security.common.resources; +package org.opensearch.security.spi.resources.exceptions; import java.io.IOException; import org.opensearch.OpenSearchException; import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.rest.RestStatus; /** * This class represents an exception that occurs during resource sharing operations. @@ -36,4 +37,18 @@ public ResourceSharingException(String msg, Throwable cause, Object... args) { public ResourceSharingException(StreamInput in) throws IOException { super(in); } + + @Override + public RestStatus status() { + String message = getMessage(); + if (message.contains("not authorized")) { + return RestStatus.FORBIDDEN; + } else if (message.contains("no authenticated")) { + return RestStatus.UNAUTHORIZED; + } else if (message.contains("not found")) { + return RestStatus.NOT_FOUND; + } + + return RestStatus.INTERNAL_SERVER_ERROR; + } } From c558dd9f2b9ae84b316090de0c6fe039da2d4469 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 19:03:05 -0500 Subject: [PATCH 145/201] Completes client implementation Signed-off-by: Darshit Chanpura --- client/build.gradle | 3 +- .../resources/ResourceSharingClient.java | 21 +-- .../resources/ResourceSharingNodeClient.java | 139 +++++++++++------- 3 files changed, 101 insertions(+), 62 deletions(-) diff --git a/client/build.gradle b/client/build.gradle index 763edc6b4b..a8dfbf9dbf 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -36,7 +36,8 @@ repositories { dependencies { // Main implementation dependencies compileOnly "org.opensearch:opensearch:${opensearch_version}" - compileOnly "org.opensearch:opensearch-resource-sharing-common:${opensearch_build}" + compileOnly "org.opensearch:opensearch-security-common:${opensearch_build}" + compileOnly "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" } java { diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java index 7397ba4713..8c98903978 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java @@ -8,23 +8,26 @@ package org.opensearch.security.client.resources; -import org.opensearch.core.action.ActionListener; +import java.util.Map; +import java.util.Set; -import java.util.List; +import org.opensearch.core.action.ActionListener; +import org.opensearch.security.common.resources.ResourceSharing; +import org.opensearch.security.spi.resources.Resource; public interface ResourceSharingClient { void verifyResourceAccess(String resourceId, String resourceIndex, String scope, ActionListener listener); - void grantResourceAccess( + void shareResource(String resourceId, String resourceIndex, Map shareWith, ActionListener listener); + + void revokeResourceAccess( String resourceId, String resourceIndex, - String userOrRole, - String accessLevel, - ActionListener listener + Map entitiesToRevoke, + Set scopes, + ActionListener listener ); - void revokeResourceAccess(String resourceId, String resourceIndex, String userOrRole, ActionListener listener); - - void listAccessibleResources(String userOrRole, ActionListener> listener); + void listAccessibleResourcesForCurrentUser(String resourceIndex, ActionListener> listener); } diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java index dd379cbe1a..9c3237c098 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java @@ -1,52 +1,87 @@ -package org.opensearch.security.client.resources;// package org.opensearch.security.spi.resources.client; -// -// import org.opensearch.core.action.ActionListener; -// import org.opensearch.transport.client.node.NodeClient; -// -// import java.util.List; -// -// public class ResourceSharingNodeClient { -// -// private final NodeClient nodeClient; -// -// public ResourceSharingClient(NodeClient nodeClient) { -// this.nodeClient = nodeClient; -// } -// -// public void verifyResourceAccess(String resourceId, String resourceIndex, String scope, ActionListener listener) { -// ResourceAccessRequest request = new ResourceAccessRequest(ResourceAccessRequest.OperationType.VERIFY, resourceId, resourceIndex, scope); -// execute(ResourceAccessAction.INSTANCE, request, wrapBooleanResponse(listener)); -// } -// -// public void grantResourceAccess(String resourceId, String resourceIndex, String userOrRole, String accessLevel, ActionListener -// listener) { -// ResourceAccessRequest request = new ResourceAccessRequest(ResourceAccessRequest.OperationType.GRANT, resourceId, resourceIndex, -// userOrRole, accessLevel); -// execute(ResourceAccessAction.INSTANCE, request, wrapBooleanResponse(listener)); -// } -// -// public void revokeResourceAccess(String resourceId, String resourceIndex, String userOrRole, ActionListener listener) { -// ResourceAccessRequest request = new ResourceAccessRequest(ResourceAccessRequest.OperationType.REVOKE, resourceId, resourceIndex, -// userOrRole); -// execute(ResourceAccessAction.INSTANCE, request, wrapBooleanResponse(listener)); -// } -// -// public void listAccessibleResources(String userOrRole, ActionListener> listener) { -// ResourceAccessRequest request = new ResourceAccessRequest(ResourceAccessRequest.OperationType.LIST, userOrRole); -// execute(ResourceAccessAction.INSTANCE, request, wrapListResponse(listener)); -// } -// -// private ActionListener wrapBooleanResponse(ActionListener listener) { -// return ActionListener.wrap( -// response -> listener.onResponse(response.getHasPermission()), -// listener::onFailure -// ); -// } -// -// private ActionListener wrapListResponse(ActionListener> listener) { -// return ActionListener.wrap( -// response -> listener.onResponse(response.getAccessibleResources()), -// listener::onFailure -// ); -// } -// } +/* + * 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. + */ + +package org.opensearch.security.client.resources; + +import java.util.Map; +import java.util.Set; + +import org.opensearch.core.action.ActionListener; +import org.opensearch.security.common.resources.ResourceSharing; +import org.opensearch.security.common.resources.rest.ResourceAccessAction; +import org.opensearch.security.common.resources.rest.ResourceAccessRequest; +import org.opensearch.security.common.resources.rest.ResourceAccessResponse; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.transport.client.Client; + +public final class ResourceSharingNodeClient implements ResourceSharingClient { + + private final Client client; + + public ResourceSharingNodeClient(Client client) { + this.client = client; + } + + public void verifyResourceAccess(String resourceId, String resourceIndex, String scope, ActionListener listener) { + ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.VERIFY) + .resourceId(resourceId) + .resourceIndex(resourceIndex) + .scope(scope) + .build(); + client.execute(ResourceAccessAction.INSTANCE, request, verifyAccessResponseListener(listener)); + } + + public void shareResource( + String resourceId, + String resourceIndex, + Map shareWith, + ActionListener listener + ) { + ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.SHARE) + .resourceId(resourceId) + .resourceIndex(resourceIndex) + .shareWith(shareWith) + .build(); + client.execute(ResourceAccessAction.INSTANCE, request, sharingInfoResponseListener(listener)); + } + + public void revokeResourceAccess( + String resourceId, + String resourceIndex, + Map entitiesToRevoke, + Set scopes, + ActionListener listener + ) { + ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.REVOKE) + .resourceId(resourceId) + .resourceIndex(resourceIndex) + .revokedEntities(entitiesToRevoke) + .scopes(scopes) + .build(); + client.execute(ResourceAccessAction.INSTANCE, request, sharingInfoResponseListener(listener)); + } + + public void listAccessibleResourcesForCurrentUser(String resourceIndex, ActionListener> listener) { + ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.LIST) + .resourceIndex(resourceIndex) + .build(); + client.execute(ResourceAccessAction.INSTANCE, request, listResourcesResponseListener(listener)); + } + + private ActionListener verifyAccessResponseListener(ActionListener listener) { + return ActionListener.wrap(response -> listener.onResponse(response.getHasPermission()), listener::onFailure); + } + + private ActionListener sharingInfoResponseListener(ActionListener listener) { + return ActionListener.wrap(response -> listener.onResponse(response.getResourceSharing()), listener::onFailure); + } + + private ActionListener listResourcesResponseListener(ActionListener> listener) { + return ActionListener.wrap(response -> listener.onResponse(response.getResources()), listener::onFailure); + } +} From 0df9a24a51676e281558d2fe1f2029e854519296 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 19:03:38 -0500 Subject: [PATCH 146/201] Adds client usage to sample resource plugin Signed-off-by: Darshit Chanpura --- sample-resource-plugin/build.gradle | 2 + .../sample/SampleResourcePluginTests.java | 25 ++++---- .../DeleteResourceTransportAction.java | 56 +++++++++++----- .../transport/GetResourceTransportAction.java | 64 ++++++++++++------- .../client/ResourceSharingClientAccessor.java | 23 +++++++ 5 files changed, 120 insertions(+), 50 deletions(-) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle index ac49f05709..2962a10f1c 100644 --- a/sample-resource-plugin/build.gradle +++ b/sample-resource-plugin/build.gradle @@ -74,6 +74,7 @@ configurations.all { dependencies { // Main implementation dependencies compileOnly "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" + compileOnly "org.opensearch:opensearch-security-client:${opensearch_build}" compileOnly "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" // Integration test dependencies @@ -81,6 +82,7 @@ dependencies { integrationTestImplementation rootProject.sourceSets.main.output integrationTestImplementation "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" integrationTestImplementation "org.opensearch:opensearch-security-common:${opensearch_build}" + integrationTestImplementation "org.opensearch:opensearch-security-client:${opensearch_build}" } sourceSets { diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index ffb34396d7..b211121ca9 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -236,34 +236,31 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { // get sample resource with shared_with_user try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - // TODO change this to forbidden once client has been implemented - response.assertStatusCode(HttpStatus.SC_OK); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); } // delete sample resource with shared_with_user try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); - // TODO change this to forbidden once client has been implemented - response.assertStatusCode(HttpStatus.SC_OK); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); } // delete sample resource - // TODO uncomment once client has been implemented - // try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - // HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); - // response.assertStatusCode(HttpStatus.SC_OK); - // } + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } // corresponding entry should be removed from resource-sharing index try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { // Since test framework doesn't yet allow loading ex tensions we need to delete the resource sharing entry manually HttpResponse response = client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc/" + resourceSharingDocId); - assertThat(response.getStatusReason(), containsString("OK")); + response.assertStatusCode(HttpStatus.SC_OK); Thread.sleep(1000); response = client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search"); response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), containsString("hits\":[]")); + assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(0)); } // get sample resource with shared_with_user @@ -271,6 +268,12 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); response.assertStatusCode(HttpStatus.SC_NOT_FOUND); } + + // get sample resource with admin + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } } // TODO: similar to above, add test case to test sample plugin apis using security client diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java index 39265d49cd..47f5e80bb0 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java @@ -21,12 +21,16 @@ import org.opensearch.common.inject.Inject; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.SampleResourceScope; import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceAction; import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceRequest; import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceResponse; +import org.opensearch.sample.resource.client.ResourceSharingClientAccessor; +import org.opensearch.security.client.resources.ResourceSharingClient; +import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; -import org.opensearch.transport.client.Client; +import org.opensearch.transport.client.node.NodeClient; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; @@ -34,10 +38,10 @@ public class DeleteResourceTransportAction extends HandledTransportAction listener) { - if (request.getResourceId() == null || request.getResourceId().isEmpty()) { + + String resourceId = request.getResourceId(); + if (resourceId == null || resourceId.isEmpty()) { listener.onFailure(new IllegalArgumentException("Resource ID cannot be null or empty")); return; } - ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); - try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { - deleteResource(request, ActionListener.wrap(deleteResponse -> { - if (deleteResponse.getResult() == DocWriteResponse.Result.NOT_FOUND) { - listener.onFailure(new ResourceNotFoundException("Resource " + request.getResourceId() + " not found.")); - } else { - listener.onResponse(new DeleteResourceResponse("Resource " + request.getResourceId() + " deleted successfully.")); + // Check permission to resource + ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient); + resourceSharingClient.verifyResourceAccess( + resourceId, + RESOURCE_INDEX_NAME, + SampleResourceScope.PUBLIC.value(), + ActionListener.wrap(isAuthorized -> { + if (!isAuthorized) { + listener.onFailure(new ResourceSharingException("Current user is not authorized to delete resource: " + resourceId)); + return; + } + + // Authorization successful, proceed with deletion + ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); + try (ThreadContext.StoredContext ignored = threadContext.stashContext()) { + deleteResource(resourceId, ActionListener.wrap(deleteResponse -> { + if (deleteResponse.getResult() == DocWriteResponse.Result.NOT_FOUND) { + listener.onFailure(new ResourceNotFoundException("Resource " + resourceId + " not found.")); + } else { + listener.onResponse(new DeleteResourceResponse("Resource " + resourceId + " deleted successfully.")); + } + }, exception -> { + log.error("Failed to delete resource: " + resourceId, exception); + listener.onFailure(exception); + })); } }, exception -> { - log.error("Failed to delete resource: " + request.getResourceId(), exception); + log.error("Failed to verify resource access: " + resourceId, exception); listener.onFailure(exception); - })); - } + }) + ); } - private void deleteResource(DeleteResourceRequest request, ActionListener listener) { - DeleteRequest deleteRequest = new DeleteRequest(RESOURCE_INDEX_NAME, request.getResourceId()).setRefreshPolicy( + private void deleteResource(String resourceId, ActionListener listener) { + DeleteRequest deleteRequest = new DeleteRequest(RESOURCE_INDEX_NAME, resourceId).setRefreshPolicy( WriteRequest.RefreshPolicy.IMMEDIATE ); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java index ab84ed5748..f6cfbcc36d 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java @@ -28,12 +28,16 @@ import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.sample.SampleResource; +import org.opensearch.sample.SampleResourceScope; import org.opensearch.sample.resource.actions.rest.get.GetResourceAction; import org.opensearch.sample.resource.actions.rest.get.GetResourceRequest; import org.opensearch.sample.resource.actions.rest.get.GetResourceResponse; +import org.opensearch.sample.resource.client.ResourceSharingClientAccessor; +import org.opensearch.security.client.resources.ResourceSharingClient; +import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; -import org.opensearch.transport.client.Client; +import org.opensearch.transport.client.node.NodeClient; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; @@ -41,10 +45,10 @@ public class GetResourceTransportAction extends HandledTransportAction { - if (getResponse.isSourceEmpty()) { - listener.onFailure(new ResourceNotFoundException("Resource " + request.getResourceId() + " not found.")); - } else { - // String jsonString = XContentFactory.jsonBuilder().map(getResponse.getSourceAsMap()).toString(); - try ( - XContentParser parser = XContentType.JSON.xContent() - .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, getResponse.getSourceAsString()) - ) { - listener.onResponse(new GetResourceResponse(SampleResource.fromXContent(parser))); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); - } + // Check permission to resource + ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient); + resourceSharingClient.verifyResourceAccess( + request.getResourceId(), + RESOURCE_INDEX_NAME, + SampleResourceScope.PUBLIC.value(), + ActionListener.wrap(isAuthorized -> { + if (!isAuthorized) { + listener.onFailure( + new ResourceSharingException("Current user is not authorized to access resource: " + request.getResourceId()) + ); + return; } - }, exception -> { - log.error("Failed to fetch resource: " + request.getResourceId(), exception); - listener.onFailure(exception); - })); - } + + ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); + try (ThreadContext.StoredContext ignored = threadContext.stashContext()) { + getResource(request, ActionListener.wrap(getResponse -> { + if (getResponse.isSourceEmpty()) { + listener.onFailure(new ResourceNotFoundException("Resource " + request.getResourceId() + " not found.")); + } else { + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + getResponse.getSourceAsString() + ) + ) { + listener.onResponse(new GetResourceResponse(SampleResource.fromXContent(parser))); + } + } + }, listener::onFailure)); + } + }, listener::onFailure) + ); } private void getResource(GetResourceRequest request, ActionListener listener) { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java new file mode 100644 index 0000000000..ef8ecd977f --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java @@ -0,0 +1,23 @@ +package org.opensearch.sample.resource.client; + +import org.opensearch.security.client.resources.ResourceSharingNodeClient; +import org.opensearch.transport.client.node.NodeClient; + +public class ResourceSharingClientAccessor { + private static ResourceSharingNodeClient INSTANCE; + + private ResourceSharingClientAccessor() {} + + /** + * get machine learning client. + * + * @param nodeClient node client + * @return machine learning client + */ + public static ResourceSharingNodeClient getResourceSharingClient(NodeClient nodeClient) { + if (INSTANCE == null) { + INSTANCE = new ResourceSharingNodeClient(nodeClient); + } + return INSTANCE; + } +} From 6433fbb78a772631c8fbd8a4bfbeeda67c4e422b Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 22:24:52 -0500 Subject: [PATCH 147/201] Updates client methods Signed-off-by: Darshit Chanpura --- .../security/client/resources/ResourceSharingClient.java | 2 +- .../client/resources/ResourceSharingNodeClient.java | 2 +- .../security/spi/resources/sharing}/CreatedBy.java | 2 +- .../opensearch/security/spi/resources/sharing}/Creator.java | 2 +- .../security/spi/resources/sharing}/Recipient.java | 0 .../security/spi/resources/sharing}/RecipientType.java | 2 +- .../spi/resources/sharing}/RecipientTypeRegistry.java | 4 +++- .../security/spi/resources/sharing}/ResourceSharing.java | 6 ++++-- .../security/spi/resources/sharing}/ShareWith.java | 2 +- .../security/spi/resources/sharing}/SharedWithScope.java | 2 +- 10 files changed, 14 insertions(+), 10 deletions(-) rename {common/src/main/java/org/opensearch/security/common/resources => spi/src/main/java/org/opensearch/security/spi/resources/sharing}/CreatedBy.java (98%) rename {common/src/main/java/org/opensearch/security/common/resources => spi/src/main/java/org/opensearch/security/spi/resources/sharing}/Creator.java (93%) rename {common/src/main/java/org/opensearch/security/common/resources => spi/src/main/java/org/opensearch/security/spi/resources/sharing}/Recipient.java (100%) rename {common/src/main/java/org/opensearch/security/common/resources => spi/src/main/java/org/opensearch/security/spi/resources/sharing}/RecipientType.java (91%) rename {common/src/main/java/org/opensearch/security/common/resources => spi/src/main/java/org/opensearch/security/spi/resources/sharing}/RecipientTypeRegistry.java (89%) rename {common/src/main/java/org/opensearch/security/common/resources => spi/src/main/java/org/opensearch/security/spi/resources/sharing}/ResourceSharing.java (96%) rename {common/src/main/java/org/opensearch/security/common/resources => spi/src/main/java/org/opensearch/security/spi/resources/sharing}/ShareWith.java (98%) rename {common/src/main/java/org/opensearch/security/common/resources => spi/src/main/java/org/opensearch/security/spi/resources/sharing}/SharedWithScope.java (99%) diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java index 8c98903978..015896eb46 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java @@ -12,8 +12,8 @@ import java.util.Set; import org.opensearch.core.action.ActionListener; -import org.opensearch.security.common.resources.ResourceSharing; import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.sharing.ResourceSharing; public interface ResourceSharingClient { diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java index 9c3237c098..021c331d1b 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java @@ -12,11 +12,11 @@ import java.util.Set; import org.opensearch.core.action.ActionListener; -import org.opensearch.security.common.resources.ResourceSharing; import org.opensearch.security.common.resources.rest.ResourceAccessAction; import org.opensearch.security.common.resources.rest.ResourceAccessRequest; import org.opensearch.security.common.resources.rest.ResourceAccessResponse; import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.sharing.ResourceSharing; import org.opensearch.transport.client.Client; public final class ResourceSharingNodeClient implements ResourceSharingClient { diff --git a/common/src/main/java/org/opensearch/security/common/resources/CreatedBy.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java similarity index 98% rename from common/src/main/java/org/opensearch/security/common/resources/CreatedBy.java rename to spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java index 747a5d6565..904818e9ac 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/CreatedBy.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources; +package org.opensearch.security.spi.resources; import java.io.IOException; diff --git a/common/src/main/java/org/opensearch/security/common/resources/Creator.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java similarity index 93% rename from common/src/main/java/org/opensearch/security/common/resources/Creator.java rename to spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java index a126f5c557..8baa747d6d 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/Creator.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources; +package org.opensearch.security.spi.resources; public enum Creator { USER("user"); diff --git a/common/src/main/java/org/opensearch/security/common/resources/Recipient.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java similarity index 100% rename from common/src/main/java/org/opensearch/security/common/resources/Recipient.java rename to spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java diff --git a/common/src/main/java/org/opensearch/security/common/resources/RecipientType.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientType.java similarity index 91% rename from common/src/main/java/org/opensearch/security/common/resources/RecipientType.java rename to spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientType.java index 6d7c09bda4..adcf029e38 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/RecipientType.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientType.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources; +package org.opensearch.security.spi.resources; /** * This class determines a type of recipient a resource can be shared with. diff --git a/common/src/main/java/org/opensearch/security/common/resources/RecipientTypeRegistry.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java similarity index 89% rename from common/src/main/java/org/opensearch/security/common/resources/RecipientTypeRegistry.java rename to spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java index ff9b0e602a..3008e32191 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/RecipientTypeRegistry.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java @@ -8,6 +8,8 @@ package org.opensearch.security.common.resources; +import org.opensearch.security.spi.resources.sharing.RecipientType; + import java.util.HashMap; import java.util.Map; @@ -16,7 +18,7 @@ * * @opensearch.experimental */ -public class RecipientTypeRegistry { +public final class RecipientTypeRegistry { private static final Map REGISTRY = new HashMap<>(); public static void registerRecipientType(String key, RecipientType recipientType) { diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharing.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java similarity index 96% rename from common/src/main/java/org/opensearch/security/common/resources/ResourceSharing.java rename to spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java index c267c12bb5..311a8a2823 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharing.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java @@ -16,6 +16,8 @@ import org.opensearch.core.xcontent.ToXContentFragment; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.security.spi.resources.sharing.CreatedBy; +import org.opensearch.security.spi.resources.sharing.ShareWith; /** * Represents a resource sharing configuration that manages access control for OpenSearch resources. @@ -33,8 +35,8 @@ * * * @opensearch.experimental - * @see org.opensearch.security.common.resources.CreatedBy - * @see org.opensearch.security.common.resources.ShareWith + * @see org.opensearch.security.spi.resources.sharing.CreatedBy + * @see org.opensearch.security.spi.resources.sharing.ShareWith */ public class ResourceSharing implements ToXContentFragment, NamedWriteable { diff --git a/common/src/main/java/org/opensearch/security/common/resources/ShareWith.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ShareWith.java similarity index 98% rename from common/src/main/java/org/opensearch/security/common/resources/ShareWith.java rename to spi/src/main/java/org/opensearch/security/spi/resources/sharing/ShareWith.java index 2deface76c..7625fe9f39 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ShareWith.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ShareWith.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources; +package org.opensearch.security.spi.resources; import java.io.IOException; import java.util.HashSet; diff --git a/common/src/main/java/org/opensearch/security/common/resources/SharedWithScope.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java similarity index 99% rename from common/src/main/java/org/opensearch/security/common/resources/SharedWithScope.java rename to spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java index b8a16e56f7..1430863fcc 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/SharedWithScope.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources; +package org.opensearch.security.spi.resources; import java.io.IOException; import java.util.HashMap; From a48c274e2dcdab9a8bf0dd01b6397fe23160c674 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 22:25:29 -0500 Subject: [PATCH 148/201] Moves some files to spi from common and updates references Signed-off-by: Darshit Chanpura --- .../common/resources/ResourceAccessHandler.java | 6 ++++++ .../common/resources/ResourceSharingIndexHandler.java | 8 ++++++-- .../resources/ResourceSharingIndexListener.java | 3 +++ .../common/resources/rest/ResourceAccessRequest.java | 2 +- .../common/resources/rest/ResourceAccessResponse.java | 2 +- .../resources/rest/ResourceAccessTransportAction.java | 5 ++--- spi/build.gradle | 1 + .../security/spi/resources/sharing/CreatedBy.java | 2 +- .../security/spi/resources/sharing/Creator.java | 2 +- .../security/spi/resources/sharing/Recipient.java | 2 +- .../security/spi/resources/sharing/RecipientType.java | 2 +- .../spi/resources/sharing/RecipientTypeRegistry.java | 11 +++++++---- .../spi/resources/sharing/ResourceSharing.java | 4 +--- .../security/spi/resources/sharing/ShareWith.java | 2 +- .../spi/resources/sharing/SharedWithScope.java | 2 +- 15 files changed, 34 insertions(+), 20 deletions(-) diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java index 5005f1c671..764a72fd72 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java @@ -29,6 +29,12 @@ import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.ResourceParser; import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.RecipientType; +import org.opensearch.security.spi.resources.sharing.RecipientTypeRegistry; +import org.opensearch.security.spi.resources.sharing.ResourceSharing; +import org.opensearch.security.spi.resources.sharing.ShareWith; +import org.opensearch.security.spi.resources.sharing.SharedWithScope; import org.opensearch.threadpool.ThreadPool; /** diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java index 4b39820123..cdec3b7ffe 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java @@ -67,6 +67,10 @@ import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.ResourceParser; import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; +import org.opensearch.security.spi.resources.sharing.CreatedBy; +import org.opensearch.security.spi.resources.sharing.RecipientType; +import org.opensearch.security.spi.resources.sharing.ResourceSharing; +import org.opensearch.security.spi.resources.sharing.ShareWith; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; @@ -804,7 +808,7 @@ private void clearScroll(String scrollId, ActionListener listener) { * * @param resourceId The unique identifier of the resource whose sharing configuration needs to be updated * @param sourceIdx The source index where the original resource is stored - * @param requestUserName The user requesting to share the resource + * @param requestUserName The user requesting to revoke the resource * @param shareWith Updated sharing configuration object containing access control settings: * { * "scope": { @@ -813,7 +817,7 @@ private void clearScroll(String scrollId, ActionListener listener) { * "backend_roles": ["backend_role1"] * } * } - * @param isAdmin Boolean indicating whether the user requesting to share is an admin or not + * @param isAdmin Boolean indicating whether the user requesting to revoke is an admin or not * @param listener Listener to be notified when the operation completes * @throws RuntimeException if there's an error during the update operation */ diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexListener.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexListener.java index c4a47b7fad..617f45c87c 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexListener.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexListener.java @@ -24,6 +24,9 @@ import org.opensearch.security.common.configuration.AdminDNs; import org.opensearch.security.common.support.ConfigConstants; import org.opensearch.security.common.user.User; +import org.opensearch.security.spi.resources.sharing.CreatedBy; +import org.opensearch.security.spi.resources.sharing.Creator; +import org.opensearch.security.spi.resources.sharing.ResourceSharing; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java index 57f0456959..9748a51194 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java @@ -24,7 +24,7 @@ import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.security.common.resources.ShareWith; +import org.opensearch.security.spi.resources.sharing.ShareWith; public class ResourceAccessRequest extends ActionRequest { diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java index 5cbb6aec7a..b9ba76ff4d 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java @@ -17,8 +17,8 @@ import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.security.common.resources.ResourceSharing; import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.sharing.ResourceSharing; public class ResourceAccessResponse extends ActionResponse implements ToXContentObject { public enum ResponseType { diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java index 6043baebbf..0d3ea6ee44 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java @@ -16,9 +16,9 @@ import org.opensearch.action.support.HandledTransportAction; import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; -import org.opensearch.security.common.resources.RecipientType; -import org.opensearch.security.common.resources.RecipientTypeRegistry; import org.opensearch.security.common.resources.ResourceAccessHandler; +import org.opensearch.security.spi.resources.sharing.RecipientType; +import org.opensearch.security.spi.resources.sharing.RecipientTypeRegistry; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; @@ -90,7 +90,6 @@ private void handleVerifyAccess(ResourceAccessRequest request, ActionListener> parseRevokedEntities(Map> revokeSource) { return revokeSource.entrySet() .stream() diff --git a/spi/build.gradle b/spi/build.gradle index ee79bc0785..78f7fdfddf 100644 --- a/spi/build.gradle +++ b/spi/build.gradle @@ -20,6 +20,7 @@ repositories { dependencies { compileOnly "org.opensearch:opensearch:${opensearch_version}" + compileOnly "org.opensearch:opensearch-resource-sharing-spi:${opensearch_version}" } java { diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java index 904818e9ac..5146d2f026 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.spi.resources; +package org.opensearch.security.spi.resources.sharing; import java.io.IOException; diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java index 8baa747d6d..6ca338488e 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.spi.resources; +package org.opensearch.security.spi.resources.sharing; public enum Creator { USER("user"); diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java index d38b8890a1..7fdd4bf30c 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources; +package org.opensearch.security.spi.resources.sharing; public enum Recipient { USERS("users"), diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientType.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientType.java index adcf029e38..d3b916abc2 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientType.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientType.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.spi.resources; +package org.opensearch.security.spi.resources.sharing; /** * This class determines a type of recipient a resource can be shared with. diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java index 3008e32191..bb10b677f6 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java @@ -6,9 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources; - -import org.opensearch.security.spi.resources.sharing.RecipientType; +package org.opensearch.security.spi.resources.sharing; import java.util.HashMap; import java.util.Map; @@ -19,9 +17,14 @@ * @opensearch.experimental */ public final class RecipientTypeRegistry { - private static final Map REGISTRY = new HashMap<>(); + // TODO: Check what size should this be. A cap should be added to avoid infinite addition of objects + private static final Integer REGISTRY_MAX_SIZE = 20; + private static final Map REGISTRY = new HashMap<>(10); public static void registerRecipientType(String key, RecipientType recipientType) { + if (REGISTRY.size() == REGISTRY_MAX_SIZE) { + throw new IllegalArgumentException("RecipientTypeRegistry is full. Cannot register more recipient types."); + } REGISTRY.put(key, recipientType); } diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java index 311a8a2823..731e589fbb 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources; +package org.opensearch.security.spi.resources.sharing; import java.io.IOException; import java.util.Objects; @@ -16,8 +16,6 @@ import org.opensearch.core.xcontent.ToXContentFragment; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.security.spi.resources.sharing.CreatedBy; -import org.opensearch.security.spi.resources.sharing.ShareWith; /** * Represents a resource sharing configuration that manages access control for OpenSearch resources. diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ShareWith.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ShareWith.java index 7625fe9f39..267bb7ece0 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ShareWith.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ShareWith.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.spi.resources; +package org.opensearch.security.spi.resources.sharing; import java.io.IOException; import java.util.HashSet; diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java index 1430863fcc..81386da422 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.spi.resources; +package org.opensearch.security.spi.resources.sharing; import java.io.IOException; import java.util.HashMap; From da4a60e0efac0400d82cd5f3e64390c4bcff33a6 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 22:26:04 -0500 Subject: [PATCH 149/201] Updates references in main packages Signed-off-by: Darshit Chanpura --- .../org/opensearch/security/resources/CreatedByTests.java | 4 ++-- .../security/resources/RecipientTypeRegistryTests.java | 4 ++-- .../org/opensearch/security/resources/ShareWithTests.java | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/java/org/opensearch/security/resources/CreatedByTests.java b/src/test/java/org/opensearch/security/resources/CreatedByTests.java index 55bcdfe68f..7682251401 100644 --- a/src/test/java/org/opensearch/security/resources/CreatedByTests.java +++ b/src/test/java/org/opensearch/security/resources/CreatedByTests.java @@ -19,8 +19,8 @@ import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.security.common.resources.CreatedBy; -import org.opensearch.security.common.resources.Creator; +import org.opensearch.security.spi.resources.sharing.CreatedBy; +import org.opensearch.security.spi.resources.sharing.Creator; import org.opensearch.security.test.SingleClusterTest; import static org.hamcrest.Matchers.equalTo; diff --git a/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java b/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java index c569c55803..8238797cb0 100644 --- a/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java +++ b/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java @@ -10,8 +10,8 @@ import org.hamcrest.MatcherAssert; -import org.opensearch.security.common.resources.RecipientType; -import org.opensearch.security.common.resources.RecipientTypeRegistry; +import org.opensearch.security.spi.resources.sharing.RecipientType; +import org.opensearch.security.spi.resources.sharing.RecipientTypeRegistry; import org.opensearch.security.test.SingleClusterTest; import static org.hamcrest.Matchers.equalTo; diff --git a/src/test/java/org/opensearch/security/resources/ShareWithTests.java b/src/test/java/org/opensearch/security/resources/ShareWithTests.java index 7350241de2..9b25aa1fbb 100644 --- a/src/test/java/org/opensearch/security/resources/ShareWithTests.java +++ b/src/test/java/org/opensearch/security/resources/ShareWithTests.java @@ -25,11 +25,11 @@ import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.security.common.resources.RecipientType; -import org.opensearch.security.common.resources.RecipientTypeRegistry; -import org.opensearch.security.common.resources.ShareWith; -import org.opensearch.security.common.resources.SharedWithScope; import org.opensearch.security.spi.resources.ResourceAccessScope; +import org.opensearch.security.spi.resources.sharing.RecipientType; +import org.opensearch.security.spi.resources.sharing.RecipientTypeRegistry; +import org.opensearch.security.spi.resources.sharing.ShareWith; +import org.opensearch.security.spi.resources.sharing.SharedWithScope; import org.opensearch.security.test.SingleClusterTest; import org.mockito.Mockito; From 5386fc8c67a157a085d9a8a844b091983804d0fd Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 22:27:09 -0500 Subject: [PATCH 150/201] Adds share and revoke REST APIs in sample resource plugin Signed-off-by: Darshit Chanpura --- .../sample/SampleResourcePlugin.java | 18 ++++- .../rest/get/GetResourceRestAction.java | 2 - .../revoke/RevokeResourceAccessAction.java | 29 ++++++++ .../revoke/RevokeResourceAccessRequest.java | 67 +++++++++++++++++++ .../revoke/RevokeResourceAccessResponse.java | 43 ++++++++++++ .../RevokeResourceAccessRestAction.java | 65 ++++++++++++++++++ .../rest/share/ShareResourceAction.java | 29 ++++++++ .../rest/share/ShareResourceRequest.java | 56 ++++++++++++++++ .../rest/share/ShareResourceResponse.java | 43 ++++++++++++ .../rest/share/ShareResourceRestAction.java | 60 +++++++++++++++++ .../RevokeResourceAccessTransportAction.java | 56 ++++++++++++++++ .../ShareResourceTransportAction.java | 60 +++++++++++++++++ .../client/ResourceSharingClientAccessor.java | 8 +++ 13 files changed, 532 insertions(+), 4 deletions(-) create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index a522bd7396..9d92bb43ad 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -43,9 +43,15 @@ import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceRestAction; import org.opensearch.sample.resource.actions.rest.get.GetResourceAction; import org.opensearch.sample.resource.actions.rest.get.GetResourceRestAction; +import org.opensearch.sample.resource.actions.rest.revoke.RevokeResourceAccessAction; +import org.opensearch.sample.resource.actions.rest.revoke.RevokeResourceAccessRestAction; +import org.opensearch.sample.resource.actions.rest.share.ShareResourceAction; +import org.opensearch.sample.resource.actions.rest.share.ShareResourceRestAction; import org.opensearch.sample.resource.actions.transport.CreateResourceTransportAction; import org.opensearch.sample.resource.actions.transport.DeleteResourceTransportAction; import org.opensearch.sample.resource.actions.transport.GetResourceTransportAction; +import org.opensearch.sample.resource.actions.transport.RevokeResourceAccessTransportAction; +import org.opensearch.sample.resource.actions.transport.ShareResourceTransportAction; import org.opensearch.sample.resource.actions.transport.UpdateResourceTransportAction; import org.opensearch.script.ScriptService; import org.opensearch.security.spi.resources.ResourceParser; @@ -92,7 +98,13 @@ public List getRestHandlers( IndexNameExpressionResolver indexNameExpressionResolver, Supplier nodesInCluster ) { - return List.of(new CreateResourceRestAction(), new GetResourceRestAction(), new DeleteResourceRestAction()); + return List.of( + new CreateResourceRestAction(), + new GetResourceRestAction(), + new DeleteResourceRestAction(), + new ShareResourceRestAction(), + new RevokeResourceAccessRestAction() + ); } @Override @@ -101,7 +113,9 @@ public List getRestHandlers( new ActionHandler<>(CreateResourceAction.INSTANCE, CreateResourceTransportAction.class), new ActionHandler<>(GetResourceAction.INSTANCE, GetResourceTransportAction.class), new ActionHandler<>(UpdateResourceAction.INSTANCE, UpdateResourceTransportAction.class), - new ActionHandler<>(DeleteResourceAction.INSTANCE, DeleteResourceTransportAction.class) + new ActionHandler<>(DeleteResourceAction.INSTANCE, DeleteResourceTransportAction.class), + new ActionHandler<>(ShareResourceAction.INSTANCE, ShareResourceTransportAction.class), + new ActionHandler<>(RevokeResourceAccessAction.INSTANCE, RevokeResourceAccessTransportAction.class) ); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java index 3f94613124..13ea45c9f0 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java @@ -41,8 +41,6 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli throw new IllegalArgumentException("resource_id parameter is required"); } - // verify access - final GetResourceRequest getResourceRequest = new GetResourceRequest(resourceId); return channel -> client.executeLocally(GetResourceAction.INSTANCE, getResourceRequest, new RestToXContentListener<>(channel)); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessAction.java new file mode 100644 index 0000000000..6f6a308797 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessAction.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.revoke; + +import org.opensearch.action.ActionType; + +/** + * Action to revoke a sample resource + */ +public class RevokeResourceAccessAction extends ActionType { + /** + * Share sample resource action instance + */ + public static final RevokeResourceAccessAction INSTANCE = new RevokeResourceAccessAction(); + /** + * Share sample resource action name + */ + public static final String NAME = "cluster:admin/sample-resource-plugin/revoke"; + + private RevokeResourceAccessAction() { + super(NAME, RevokeResourceAccessResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRequest.java new file mode 100644 index 0000000000..6038b4c996 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRequest.java @@ -0,0 +1,67 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.revoke; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +/** + * Request object for revoking access to a sample resource transport action + */ +public class RevokeResourceAccessRequest extends ActionRequest { + + String resourceId; + Map entitiesToRevoke; + Set scopes; + + public RevokeResourceAccessRequest(String resourceId, Map entitiesToRevoke, List scopes) { + this.resourceId = resourceId; + this.entitiesToRevoke = entitiesToRevoke; + this.scopes = new HashSet<>(scopes); + } + + public RevokeResourceAccessRequest(StreamInput in) throws IOException { + resourceId = in.readString(); + entitiesToRevoke = in.readMap(); + scopes = in.readSet(StreamInput::readString); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(resourceId); + out.writeMap(entitiesToRevoke); + out.writeStringCollection(scopes); + + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceId() { + return resourceId; + } + + public Map getEntitiesToRevoke() { + return entitiesToRevoke; + } + + public Set getScopes() { + return scopes; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessResponse.java new file mode 100644 index 0000000000..18b8d78a3e --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessResponse.java @@ -0,0 +1,43 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.revoke; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.security.spi.resources.sharing.ShareWith; + +public class RevokeResourceAccessResponse extends ActionResponse implements ToXContentObject { + private final ShareWith shareWith; + + public RevokeResourceAccessResponse(ShareWith shareWith) { + this.shareWith = shareWith; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeNamedWriteable(shareWith); + } + + public RevokeResourceAccessResponse(final StreamInput in) throws IOException { + shareWith = new ShareWith(in); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("share_with", shareWith); + builder.endObject(); + return builder; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java new file mode 100644 index 0000000000..0669481540 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java @@ -0,0 +1,65 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.revoke; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.core.common.Strings; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.transport.client.node.NodeClient; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; + +public class RevokeResourceAccessRestAction extends BaseRestHandler { + + public RevokeResourceAccessRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(GET, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/revoke/{resource_id}")); + } + + @Override + public String getName() { + return "get_sample_resource"; + } + + @SuppressWarnings("unchecked") + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + String resourceId = request.param("resource_id"); + if (Strings.isNullOrEmpty(resourceId)) { + throw new IllegalArgumentException("resource_id parameter is required"); + } + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + final RevokeResourceAccessRequest getResourceRequest = new RevokeResourceAccessRequest( + resourceId, + (Map) source.get("entities_to_revoke"), + (List) source.get("scopes") + ); + return channel -> client.executeLocally( + RevokeResourceAccessAction.INSTANCE, + getResourceRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceAction.java new file mode 100644 index 0000000000..1c924a7f62 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceAction.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.share; + +import org.opensearch.action.ActionType; + +/** + * Action to share a sample resource + */ +public class ShareResourceAction extends ActionType { + /** + * Share sample resource action instance + */ + public static final ShareResourceAction INSTANCE = new ShareResourceAction(); + /** + * Share sample resource action name + */ + public static final String NAME = "cluster:admin/sample-resource-plugin/revoke"; + + private ShareResourceAction() { + super(NAME, ShareResourceResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRequest.java new file mode 100644 index 0000000000..7cca2bddee --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRequest.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.share; + +import java.io.IOException; +import java.util.Map; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +/** + * Request object for sharing sample resource transport action + */ +public class ShareResourceRequest extends ActionRequest { + + private final String resourceId; + + private final Map shareWith; + + public ShareResourceRequest(String resourceId, Map shareWith) { + this.resourceId = resourceId; + this.shareWith = shareWith; + } + + public ShareResourceRequest(StreamInput in) throws IOException { + this.resourceId = in.readString(); + this.shareWith = in.readMap(); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(this.resourceId); + out.writeMap(shareWith); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceId() { + return this.resourceId; + } + + public Map getShareWith() { + return shareWith; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceResponse.java new file mode 100644 index 0000000000..abadf88b49 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceResponse.java @@ -0,0 +1,43 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.share; + +import java.io.IOException; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.security.spi.resources.sharing.ShareWith; + +public class ShareResourceResponse extends ActionResponse implements ToXContentObject { + private final ShareWith shareWith; + + public ShareResourceResponse(ShareWith shareWith) { + this.shareWith = shareWith; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeNamedWriteable(shareWith); + } + + public ShareResourceResponse(final StreamInput in) throws IOException { + shareWith = new ShareWith(in); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("share_with", shareWith); + builder.endObject(); + return builder; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java new file mode 100644 index 0000000000..9d7a8303e1 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java @@ -0,0 +1,60 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.rest.share; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.core.common.Strings; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.transport.client.node.NodeClient; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; + +public class ShareResourceRestAction extends BaseRestHandler { + + public ShareResourceRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(GET, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/share/{resource_id}")); + } + + @Override + public String getName() { + return "get_sample_resource"; + } + + @SuppressWarnings("unchecked") + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + String resourceId = request.param("resource_id"); + if (Strings.isNullOrEmpty(resourceId)) { + throw new IllegalArgumentException("resource_id parameter is required"); + } + + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + Map shareWith = (Map) source.get("share_with"); + + final ShareResourceRequest getResourceRequest = new ShareResourceRequest(resourceId, shareWith); + return channel -> client.executeLocally(ShareResourceAction.INSTANCE, getResourceRequest, new RestToXContentListener<>(channel)); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java new file mode 100644 index 0000000000..bda950e1c5 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.resource.actions.rest.revoke.RevokeResourceAccessAction; +import org.opensearch.sample.resource.actions.rest.revoke.RevokeResourceAccessRequest; +import org.opensearch.sample.resource.actions.rest.revoke.RevokeResourceAccessResponse; +import org.opensearch.sample.resource.client.ResourceSharingClientAccessor; +import org.opensearch.security.client.resources.ResourceSharingClient; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; +import org.opensearch.transport.client.node.NodeClient; + +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +public class RevokeResourceAccessTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(RevokeResourceAccessTransportAction.class); + + private final NodeClient nodeClient; + + @Inject + public RevokeResourceAccessTransportAction(TransportService transportService, ActionFilters actionFilters, NodeClient nodeClient) { + super(RevokeResourceAccessAction.NAME, transportService, actionFilters, RevokeResourceAccessRequest::new); + this.nodeClient = nodeClient; + } + + @Override + protected void doExecute(Task task, RevokeResourceAccessRequest request, ActionListener listener) { + // Check permission to resource + ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient); + resourceSharingClient.revokeResourceAccess( + request.getResourceId(), + RESOURCE_INDEX_NAME, + request.getEntitiesToRevoke(), + request.getScopes(), + ActionListener.wrap(success -> { + RevokeResourceAccessResponse response = new RevokeResourceAccessResponse(success.getShareWith()); + listener.onResponse(response); + }, listener::onFailure) + ); + } + +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java new file mode 100644 index 0000000000..fb611c6c49 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java @@ -0,0 +1,60 @@ +/* + * 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. + */ + +package org.opensearch.sample.resource.actions.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.resource.actions.rest.get.GetResourceAction; +import org.opensearch.sample.resource.actions.rest.share.ShareResourceRequest; +import org.opensearch.sample.resource.actions.rest.share.ShareResourceResponse; +import org.opensearch.sample.resource.client.ResourceSharingClientAccessor; +import org.opensearch.security.client.resources.ResourceSharingClient; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; +import org.opensearch.transport.client.node.NodeClient; + +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +public class ShareResourceTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(ShareResourceTransportAction.class); + + private final NodeClient nodeClient; + + @Inject + public ShareResourceTransportAction(TransportService transportService, ActionFilters actionFilters, NodeClient nodeClient) { + super(GetResourceAction.NAME, transportService, actionFilters, ShareResourceRequest::new); + this.nodeClient = nodeClient; + } + + @Override + protected void doExecute(Task task, ShareResourceRequest request, ActionListener listener) { + if (request.getResourceId() == null || request.getResourceId().isEmpty()) { + listener.onFailure(new IllegalArgumentException("Resource ID cannot be null or empty")); + return; + } + + // Check permission to resource + ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient); + resourceSharingClient.shareResource( + request.getResourceId(), + RESOURCE_INDEX_NAME, + request.getShareWith(), + ActionListener.wrap(sharing -> { + ShareResourceResponse response = new ShareResourceResponse(sharing.getShareWith()); + listener.onResponse(response); + }, listener::onFailure) + ); + } + +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java index ef8ecd977f..abb27d21cb 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java @@ -1,3 +1,11 @@ +/* + * 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. + */ + package org.opensearch.sample.resource.client; import org.opensearch.security.client.resources.ResourceSharingNodeClient; From 1a8d56ea9f07c89512424bc320dcbbb25b3ab3ce Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sun, 2 Mar 2025 22:41:42 -0500 Subject: [PATCH 151/201] Fixes newly added rest apis Signed-off-by: Darshit Chanpura --- .../actions/rest/revoke/RevokeResourceAccessRestAction.java | 2 +- .../resource/actions/rest/share/ShareResourceAction.java | 2 +- .../actions/rest/share/ShareResourceRestAction.java | 6 +++--- .../actions/transport/ShareResourceTransportAction.java | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java index 0669481540..7f5e17c763 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java @@ -34,7 +34,7 @@ public List routes() { @Override public String getName() { - return "get_sample_resource"; + return "revoke_sample_resource"; } @SuppressWarnings("unchecked") diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceAction.java index 1c924a7f62..52de757b1b 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceAction.java @@ -21,7 +21,7 @@ public class ShareResourceAction extends ActionType { /** * Share sample resource action name */ - public static final String NAME = "cluster:admin/sample-resource-plugin/revoke"; + public static final String NAME = "cluster:admin/sample-resource-plugin/share"; private ShareResourceAction() { super(NAME, ShareResourceResponse::new); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java index 9d7a8303e1..800ae9b4b5 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java @@ -34,7 +34,7 @@ public List routes() { @Override public String getName() { - return "get_sample_resource"; + return "share_sample_resource"; } @SuppressWarnings("unchecked") @@ -54,7 +54,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli Map shareWith = (Map) source.get("share_with"); - final ShareResourceRequest getResourceRequest = new ShareResourceRequest(resourceId, shareWith); - return channel -> client.executeLocally(ShareResourceAction.INSTANCE, getResourceRequest, new RestToXContentListener<>(channel)); + final ShareResourceRequest shareResourceRequest = new ShareResourceRequest(resourceId, shareWith); + return channel -> client.executeLocally(ShareResourceAction.INSTANCE, shareResourceRequest, new RestToXContentListener<>(channel)); } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java index fb611c6c49..51ad19ed41 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java @@ -15,7 +15,7 @@ import org.opensearch.action.support.HandledTransportAction; import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; -import org.opensearch.sample.resource.actions.rest.get.GetResourceAction; +import org.opensearch.sample.resource.actions.rest.share.ShareResourceAction; import org.opensearch.sample.resource.actions.rest.share.ShareResourceRequest; import org.opensearch.sample.resource.actions.rest.share.ShareResourceResponse; import org.opensearch.sample.resource.client.ResourceSharingClientAccessor; @@ -33,7 +33,7 @@ public class ShareResourceTransportAction extends HandledTransportAction Date: Mon, 3 Mar 2025 00:03:08 -0500 Subject: [PATCH 152/201] Fixes ResourceAccessRequest body parsers Signed-off-by: Darshit Chanpura --- .../resources/rest/ResourceAccessRequest.java | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java index 9748a51194..0116f54bf4 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java @@ -9,7 +9,7 @@ package org.opensearch.security.common.resources.rest; import java.io.IOException; -import java.util.HashSet; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -74,11 +74,11 @@ public static ResourceAccessRequest from(Map source, Map) source.get("share_with")); } if (source.containsKey("entities_to_revoke")) { - builder.revokedEntities(source); + builder.revokedEntities((Map) source.get("entities_to_revoke")); } if (source.containsKey("scopes")) { @@ -202,31 +202,38 @@ public ResourceAccessRequest build() { return new ResourceAccessRequest(this); } - @SuppressWarnings("unchecked") private ShareWith parseShareWith(Map source) throws IOException { - Map shareWithMap = (Map) source.get("share_with"); - if (shareWithMap == null || shareWithMap.isEmpty()) { + if (source == null || source.isEmpty()) { throw new IllegalArgumentException("share_with is required and cannot be empty"); } - String jsonString = XContentFactory.jsonBuilder().map(shareWithMap).toString(); + String jsonString = XContentFactory.jsonBuilder().map(source).toString(); try ( XContentParser parser = XContentType.JSON.xContent() .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, jsonString) ) { + return ShareWith.fromXContent(parser); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); } } - @SuppressWarnings("unchecked") - private Map> parseRevokedEntities(Map source) throws IOException { + private Map> parseRevokedEntities(Map source) { - return ((Map>) source.get("entities_to_revoke")).entrySet() + return source.entrySet() .stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> new HashSet<>(e.getValue()))); + .filter(entry -> entry.getValue() instanceof Collection) + .collect( + Collectors.toMap( + Map.Entry::getKey, + entry -> ((Collection) entry.getValue()).stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .collect(Collectors.toSet()) + ) + ); } } } From e2d1fe6bc9a690a48c68c8688541ded7afef7d0a Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 00:03:28 -0500 Subject: [PATCH 153/201] Fixes share and revoke routes Signed-off-by: Darshit Chanpura --- .../actions/rest/revoke/RevokeResourceAccessRestAction.java | 4 ++-- .../resource/actions/rest/share/ShareResourceRestAction.java | 4 ++-- .../transport/RevokeResourceAccessTransportAction.java | 2 +- .../actions/transport/ShareResourceTransportAction.java | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java index 7f5e17c763..06aefe0f46 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java @@ -20,7 +20,7 @@ import org.opensearch.transport.client.node.NodeClient; import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.POST; import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; public class RevokeResourceAccessRestAction extends BaseRestHandler { @@ -29,7 +29,7 @@ public RevokeResourceAccessRestAction() {} @Override public List routes() { - return singletonList(new Route(GET, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/revoke/{resource_id}")); + return singletonList(new Route(POST, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/revoke/{resource_id}")); } @Override diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java index 800ae9b4b5..4ce5ee2f69 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java @@ -20,7 +20,7 @@ import org.opensearch.transport.client.node.NodeClient; import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.POST; import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; public class ShareResourceRestAction extends BaseRestHandler { @@ -29,7 +29,7 @@ public ShareResourceRestAction() {} @Override public List routes() { - return singletonList(new Route(GET, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/share/{resource_id}")); + return singletonList(new Route(POST, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/share/{resource_id}")); } @Override diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java index bda950e1c5..738d26f234 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java @@ -39,7 +39,6 @@ public RevokeResourceAccessTransportAction(TransportService transportService, Ac @Override protected void doExecute(Task task, RevokeResourceAccessRequest request, ActionListener listener) { - // Check permission to resource ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient); resourceSharingClient.revokeResourceAccess( request.getResourceId(), @@ -47,6 +46,7 @@ protected void doExecute(Task task, RevokeResourceAccessRequest request, ActionL request.getEntitiesToRevoke(), request.getScopes(), ActionListener.wrap(success -> { + RevokeResourceAccessResponse response = new RevokeResourceAccessResponse(success.getShareWith()); listener.onResponse(response); }, listener::onFailure) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java index 51ad19ed41..9ea744b8f6 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java @@ -44,7 +44,6 @@ protected void doExecute(Task task, ShareResourceRequest request, ActionListener return; } - // Check permission to resource ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient); resourceSharingClient.shareResource( request.getResourceId(), From c10e87774e0cfc7c82d8697ff935328e30b6f443 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 00:04:03 -0500 Subject: [PATCH 154/201] Adds test in Sample plugin to utilize plugin routes Signed-off-by: Darshit Chanpura --- .../AbstractSampleResourcePluginTests.java | 33 +++- .../sample/SampleResourcePluginTests.java | 183 +++++++++++++++++- 2 files changed, 208 insertions(+), 8 deletions(-) diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java index ac420cec43..0379fc3faa 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java @@ -33,13 +33,15 @@ public class AbstractSampleResourcePluginTests { static final String SAMPLE_RESOURCE_GET_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/get"; static final String SAMPLE_RESOURCE_UPDATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/update"; static final String SAMPLE_RESOURCE_DELETE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/delete"; + static final String SAMPLE_RESOURCE_SHARE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/share"; + static final String SAMPLE_RESOURCE_REVOKE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/revoke"; private static final String PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH = PLUGIN_RESOURCE_ROUTE_PREFIX.replaceFirst("/", ""); static final String SECURITY_RESOURCE_LIST_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/list"; static final String SECURITY_RESOURCE_SHARE_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/share"; static final String SECURITY_RESOURCE_VERIFY_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/verify_access"; static final String SECURITY_RESOURCE_REVOKE_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/revoke"; - static String shareWithPayload(String resourceId) { + static String shareWithPayloadSecurityApi(String resourceId) { return "{" + "\"resource_id\":\"" + resourceId @@ -59,7 +61,21 @@ static String shareWithPayload(String resourceId) { + "}"; } - static String revokeAccessPayload(String resourceId) { + static String shareWithPayload() { + return "{" + + "\"share_with\":{" + + "\"" + + SampleResourceScope.PUBLIC.value() + + "\":{" + + "\"users\": [\"" + + SHARED_WITH_USER.getName() + + "\"]" + + "}" + + "}" + + "}"; + } + + static String revokeAccessPayloadSecurityApi(String resourceId) { return "{" + "\"resource_id\": \"" + resourceId @@ -77,4 +93,17 @@ static String revokeAccessPayload(String resourceId) { + "\"]" + "}"; } + + static String revokeAccessPayload() { + return "{" + + "\"entities_to_revoke\": {" + + "\"users\": [\"" + + SHARED_WITH_USER.getName() + + "\"]" + + "}," + + "\"scopes\": [\"" + + ResourceAccessScope.PUBLIC + + "\"]" + + "}"; + } } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index b211121ca9..9e86c467ac 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -68,7 +68,7 @@ public void testPluginInstalledCorrectly() { } @Test - public void testCreateUpdateDeleteSampleResource() throws Exception { + public void testCreateUpdateDeleteSampleResourceWithSecurityAPIs() throws Exception { String resourceId; String resourceSharingDocId; // create sample resource @@ -137,9 +137,10 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { } // shared_with_user should not be able to share admin's resource with itself + // Only admins and owners can share/revoke access at the moment try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload(resourceId)); + HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayloadSecurityApi(resourceId)); response.assertStatusCode(HttpStatus.SC_FORBIDDEN); assertThat( response.bodyAsJsonNode().get("message").asText(), @@ -151,7 +152,7 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { Thread.sleep(1000); - HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayload(resourceId)); + HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayloadSecurityApi(resourceId)); response.assertStatusCode(HttpStatus.SC_OK); assertThat( response.bodyAsJsonNode() @@ -196,8 +197,9 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { } // shared_with user should not be able to revoke access to admin's resource + // Only admins and owners can share/revoke access at the moment try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayload(resourceId)); + HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayloadSecurityApi(resourceId)); response.assertStatusCode(HttpStatus.SC_FORBIDDEN); assertThat( response.bodyAsJsonNode().get("message").asText(), @@ -214,7 +216,7 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { // revoke share_with_user's access try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { Thread.sleep(1000); - HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayload(resourceId)); + HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayloadSecurityApi(resourceId)); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.bodyAsJsonNode().get("share_with"), nullValue()); } @@ -276,6 +278,175 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { } } - // TODO: similar to above, add test case to test sample plugin apis using security client + @Test + public void testCreateUpdateDeleteSampleResource() throws Exception { + String resourceId; + String resourceSharingDocId; + // create sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResource = "{\"name\":\"sample\"}"; + HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + response.assertStatusCode(HttpStatus.SC_OK); + + resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + } + + // Create an entry in resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually + String json = String.format( + "{" + + " \"source_idx\": \".sample_resource_sharing_plugin\"," + + " \"resource_id\": \"%s\"," + + " \"created_by\": {" + + " \"user\": \"admin\"" + + " }" + + "}", + resourceId + ); + HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); + assertThat(response.getStatusReason(), containsString("Created")); + resourceSharingDocId = response.bodyAsJsonNode().get("_id").asText(); + // Also update the in-memory map and get + ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); + ResourceProvider provider = new ResourceProvider( + SampleResource.class.getCanonicalName(), + RESOURCE_INDEX_NAME, + new SampleResourceParser() + ); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); + + Thread.sleep(1000); + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sample")); + } + + // Update sample resource (admin should be able to update resource) + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); + updateResponse.assertStatusCode(HttpStatus.SC_OK); + } + + // resource should be visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // resource should not be visible to shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // shared_with_user should not be able to share admin's resource with itself + // Only admins and owners can share/revoke access at the moment + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + + HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + assertThat( + response.bodyAsJsonNode().get("error").get("root_cause").get(0).get("reason").asText(), + containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") + ); + } + + // share resource with shared_with user + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + + HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat( + response.bodyAsJsonNode().get("share_with").get(SampleResourceScope.PUBLIC.value()).get("users").get(0).asText(), + containsString(SHARED_WITH_USER.getName()) + ); + } + + // resource should now be visible to shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // resource is still visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // shared_with user should not be able to revoke access to admin's resource + // Only admins and owners can share/revoke access at the moment + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.postJson(SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, revokeAccessPayload()); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + assertThat( + response.bodyAsJsonNode().get("error").get("root_cause").get(0).get("reason").asText(), + containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") + ); + } + + // get sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // revoke share_with_user's access + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + HttpResponse response = client.postJson(SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, revokeAccessPayload()); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("share_with").size(), equalTo(0)); + } + // get sample resource with shared_with_user, user no longer has access to resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // delete sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // delete sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // corresponding entry should be removed from resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to delete the resource sharing entry manually + HttpResponse response = client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc/" + resourceSharingDocId); + response.assertStatusCode(HttpStatus.SC_OK); + + Thread.sleep(1000); + response = client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search"); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(0)); + } + + // get sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } + + // get sample resource with admin + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } + } } From f76e87c324126cf6c30a7f95685131c166ca1e57 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 00:37:31 -0500 Subject: [PATCH 155/201] Updates package info and readmes Signed-off-by: Darshit Chanpura --- client/README.md | 11 ++++ .../client/resources/package-info.java | 2 +- .../common/{resources => }/package-info.java | 4 +- sample-resource-plugin/README.md | 61 ++++++++++++++++++- 4 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 client/README.md rename common/src/main/java/org/opensearch/security/common/{resources => }/package-info.java (63%) diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000000..5325c6e747 --- /dev/null +++ b/client/README.md @@ -0,0 +1,11 @@ +# Resource Sharing and Access Control SPI + +This Client package provides ResourceSharing client to be utilized by resource plugins to implement access control by communicating with security plugins. + +## License + +This code is licensed under the Apache 2.0 License. + +## Copyright + +Copyright OpenSearch Contributors. diff --git a/client/src/main/java/org/opensearch/security/client/resources/package-info.java b/client/src/main/java/org/opensearch/security/client/resources/package-info.java index 72b5b51a99..606d8affae 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/package-info.java +++ b/client/src/main/java/org/opensearch/security/client/resources/package-info.java @@ -7,7 +7,7 @@ */ /** - * This package defines class required to implement resource access control in OpenSearch. + * This package defines a resource sharing client that will be utilized by resource plugins to call security plugin's APIs * * @opensearch.experimental */ diff --git a/common/src/main/java/org/opensearch/security/common/resources/package-info.java b/common/src/main/java/org/opensearch/security/common/package-info.java similarity index 63% rename from common/src/main/java/org/opensearch/security/common/resources/package-info.java rename to common/src/main/java/org/opensearch/security/common/package-info.java index afb8d92761..d6651ffd42 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/package-info.java +++ b/common/src/main/java/org/opensearch/security/common/package-info.java @@ -7,8 +7,8 @@ */ /** - * This package defines class required to implement resource access control in OpenSearch. + * This package defines common classes required to implement resource access control in OpenSearch. * * @opensearch.experimental */ -package org.opensearch.security.common.resources; +package org.opensearch.security.common; diff --git a/sample-resource-plugin/README.md b/sample-resource-plugin/README.md index d27c2cd0f2..970c92aaf3 100644 --- a/sample-resource-plugin/README.md +++ b/sample-resource-plugin/README.md @@ -29,9 +29,10 @@ The plugin exposes the following six API endpoints: - **Response:** ```json { - "message": "Resource created successfully." + "message": "Created resource: 9UdrWpUB99GNznAOkx43" } ``` + ### 2. Update Resource - **Endpoint:** `POST /_plugins/sample_resource_sharing/update/{resourceId}` - **Description:** Updates a resource. @@ -58,6 +59,64 @@ The plugin exposes the following six API endpoints: } ``` + +### 4. Get Resource +- **Endpoint:** `GET /_plugins/sample_resource_sharing/get/{resource_id}` +- **Description:** Get a specified resource owned by the requesting user, if the user has access to the resource, else fails. +- **Response:** + ```json + { + "resource" : { + "name" : "", + "description" : null, + "attributes" : null + } + } + ``` + +### 5. Share Resource +- **Endpoint:** `POST /_plugins/sample_resource_sharing/share/{resource_id}` +- **Description:** Shares a resource with the intended entities. At present, only admin and resource owners can share the resource. +- **Request Body:** + ```json + { + "share_with": { + "public": { + "users": [ "sample_user" ] + } + } + } + ``` +- **Response:** + ```json + { + "share_with": { + "public": { + "users": [ "sample_user" ] + } + } + } + ``` + +### 6. Revoke Resource Access +- **Endpoint:** `POST /_plugins/sample_resource_sharing/revoke/{resource_id}` +- **Description:** Shares a resource with the intended entities for given scopes. At present, only admin and resource owners can share the resource. +- **Request Body:** + ```json + { + "entities_to_revoke": { + "users": [ "sample_user" ] + }, + "scopes": [ "public" ] + } + ``` +- **Response:** + ```json + { + "share_with" : { } + } + ``` + ## Installation 1. Clone the repository: From 8801a3c8b3ec106ef020f2e380cea8e247bb0eba Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 00:39:40 -0500 Subject: [PATCH 156/201] Adds new components to maven publish workflow Signed-off-by: Darshit Chanpura --- .github/workflows/maven-publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index 2d4e7e1df0..d3d1ba84eb 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -34,3 +34,5 @@ jobs: echo "::add-mask::$SONATYPE_PASSWORD" ./gradlew --no-daemon publishPluginZipPublicationToSnapshotsRepository ./gradlew --no-daemon :opensearch-resource-sharing-spi:publishMavenJavaPublicationToSnapshotsRepository + ./gradlew --no-daemon :opensearch-security-client:publishMavenJavaPublicationToSnapshotsRepository + ./gradlew --no-daemon :opensearch-security-common:publishMavenJavaPublicationToSnapshotsRepository From 3bd5f19dc8202e0e6f48d721d928364792227a2c Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 00:47:43 -0500 Subject: [PATCH 157/201] Updates CI workflow Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca9088387f..21ef6aa3fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,6 +117,18 @@ jobs: cache-disabled: true arguments: :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false + - name: Publish Common to Local Maven + uses: gradle/gradle-build-action@v3 + with: + cache-disabled: true + arguments: :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false + + - name: Publish Client to Local Maven + uses: gradle/gradle-build-action@v3 + with: + cache-disabled: true + arguments: :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false + - name: Run Integration Tests uses: gradle/gradle-build-action@v3 with: @@ -256,6 +268,18 @@ jobs: ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier.jar ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar + # Publish Common + ./gradlew :opensearch-security-common:publishToMavenLocal && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version.jar + ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_no_snapshot.jar + ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier.jar + ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar + + # Publish Client + ./gradlew :opensearch-security-client:publishToMavenLocal && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version.jar + ./gradlew :opensearch-security-client-spi:publishToMavenLocal -Dbuild.snapshot=false && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_no_snapshot.jar + ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier.jar + ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar + # Build artifacts ./gradlew clean assemble && \ test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ From 103787819f226cb119478cac369b6ef444c05d1a Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 00:57:50 -0500 Subject: [PATCH 158/201] Updates compileOnly to implementation for common package dependency Signed-off-by: Darshit Chanpura --- common/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/build.gradle b/common/build.gradle index 5dcb58e3fb..430dbab4e3 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -45,7 +45,7 @@ repositories { dependencies { compileOnly "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" compileOnly "org.opensearch.plugin:lang-painless:${opensearch_version}" - compileOnly "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" + implementation "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" compileOnly "com.google.guava:guava:${guava_version}" compileOnly "org.apache.commons:commons-lang3:${versions.commonslang}" compileOnly 'com.password4j:password4j:1.8.2' From 0697648248f05786ed57637857b60026e3255965 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 20:56:57 -0500 Subject: [PATCH 159/201] Updates client to consider feature flag Signed-off-by: Darshit Chanpura --- .../resources/ResourceSharingClient.java | 6 +- .../resources/ResourceSharingNodeClient.java | 56 ++++++++++++++----- ...leResourcePluginFeatureDisabledTests.java} | 36 +++++++----- 3 files changed, 69 insertions(+), 29 deletions(-) rename sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/{SampleResourcePluginResourceSharingDisabledTests.java => SampleResourcePluginFeatureDisabledTests.java} (80%) diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java index 015896eb46..282dc741f3 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java @@ -12,9 +12,11 @@ import java.util.Set; import org.opensearch.core.action.ActionListener; -import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.sharing.ResourceSharing; +/** + * Interface for resource sharing client operations. + */ public interface ResourceSharingClient { void verifyResourceAccess(String resourceId, String resourceIndex, String scope, ActionListener listener); @@ -28,6 +30,4 @@ void revokeResourceAccess( Set scopes, ActionListener listener ); - - void listAccessibleResourcesForCurrentUser(String resourceIndex, ActionListener> listener); } diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java index 021c331d1b..759914d80e 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java @@ -11,23 +11,44 @@ import java.util.Map; import java.util.Set; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.settings.Settings; import org.opensearch.core.action.ActionListener; +import org.opensearch.core.rest.RestStatus; import org.opensearch.security.common.resources.rest.ResourceAccessAction; import org.opensearch.security.common.resources.rest.ResourceAccessRequest; import org.opensearch.security.common.resources.rest.ResourceAccessResponse; -import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.common.support.ConfigConstants; import org.opensearch.security.spi.resources.sharing.ResourceSharing; import org.opensearch.transport.client.Client; +/** + * Client for resource sharing operations. + */ public final class ResourceSharingNodeClient implements ResourceSharingClient { + private static final Logger log = LogManager.getLogger(ResourceSharingNodeClient.class); + private final Client client; + private final boolean resourceSharingEnabled; - public ResourceSharingNodeClient(Client client) { + public ResourceSharingNodeClient(Client client, Settings settings) { this.client = client; + this.resourceSharingEnabled = settings.getAsBoolean( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT + ); } public void verifyResourceAccess(String resourceId, String resourceIndex, String scope, ActionListener listener) { + if (!resourceSharingEnabled) { + log.warn("Resource Access Control feature is disabled. Access to resource is automatically granted."); + listener.onResponse(true); + return; + } ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.VERIFY) .resourceId(resourceId) .resourceIndex(resourceIndex) @@ -42,6 +63,16 @@ public void shareResource( Map shareWith, ActionListener listener ) { + if (!resourceSharingEnabled) { + log.warn("Resource Access Control feature is disabled. Resource is not shareable."); + listener.onFailure( + new OpenSearchException( + "Resource Access Control feature is disabled. Resource is not shareable.", + RestStatus.NOT_IMPLEMENTED + ) + ); + return; + } ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.SHARE) .resourceId(resourceId) .resourceIndex(resourceIndex) @@ -57,6 +88,16 @@ public void revokeResourceAccess( Set scopes, ActionListener listener ) { + if (!resourceSharingEnabled) { + log.warn("Resource Access Control feature is disabled. Resource access is not revoked."); + listener.onFailure( + new OpenSearchException( + "Resource Access Control feature is disabled. Resource access is not revoked.", + RestStatus.NOT_IMPLEMENTED + ) + ); + return; + } ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.REVOKE) .resourceId(resourceId) .resourceIndex(resourceIndex) @@ -66,13 +107,6 @@ public void revokeResourceAccess( client.execute(ResourceAccessAction.INSTANCE, request, sharingInfoResponseListener(listener)); } - public void listAccessibleResourcesForCurrentUser(String resourceIndex, ActionListener> listener) { - ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.LIST) - .resourceIndex(resourceIndex) - .build(); - client.execute(ResourceAccessAction.INSTANCE, request, listResourcesResponseListener(listener)); - } - private ActionListener verifyAccessResponseListener(ActionListener listener) { return ActionListener.wrap(response -> listener.onResponse(response.getHasPermission()), listener::onFailure); } @@ -80,8 +114,4 @@ private ActionListener verifyAccessResponseListener(Acti private ActionListener sharingInfoResponseListener(ActionListener listener) { return ActionListener.wrap(response -> listener.onResponse(response.getResourceSharing()), listener::onFailure); } - - private ActionListener listResourcesResponseListener(ActionListener> listener) { - return ActionListener.wrap(response -> listener.onResponse(response.getResources()), listener::onFailure); - } } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java similarity index 80% rename from sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java rename to sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java index e8a01ff486..fa7fcbc938 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginResourceSharingDisabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java @@ -25,7 +25,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; import static org.opensearch.security.common.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; import static org.opensearch.security.support.ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED; @@ -34,10 +33,11 @@ /** * These tests run with security disabled + * */ @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class SampleResourcePluginResourceSharingDisabledTests extends AbstractSampleResourcePluginTests { +public class SampleResourcePluginSecurityDisabledTests extends AbstractSampleResourcePluginTests { @ClassRule public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) @@ -45,7 +45,7 @@ public class SampleResourcePluginResourceSharingDisabledTests extends AbstractSa .anonymousAuth(true) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(USER_ADMIN, SHARED_WITH_USER) - .nodeSettings(Map.of(OPENSEARCH_RESOURCE_SHARING_ENABLED, false)) + .nodeSettings(Map.of(OPENSEARCH_RESOURCE_SHARING_ENABLED, false, "plugins.security.disabled", true)) .build(); @After @@ -100,18 +100,16 @@ public void testNoResourceRestrictions() throws Exception { // resource should be visible to super-admin try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - - HttpResponse response = client.postJson(RESOURCE_INDEX_NAME + "/_search", "{\"query\" : {\"match_all\" : {}}}"); + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(1)); assertThat(response.getBody(), containsString("sample")); } // resource should be visible to shared_with_user since there is no restriction and this user has * permission try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.postJson(RESOURCE_INDEX_NAME + "/_search", "{\"query\" : {\"match_all\" : {}}}"); + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sample")); } // shared_with_user is able to update admin's resource @@ -123,9 +121,21 @@ public void testNoResourceRestrictions() throws Exception { // admin can see updated value try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse getResponse = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); - getResponse.assertStatusCode(HttpStatus.SC_OK); - assertThat(getResponse.getBody(), containsString("sampleUpdated")); + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // shared_with_user is able to call sample share api + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); + updateResponse.assertStatusCode(HttpStatus.SC_OK); + } + + // shared_with_user is able to call sample revoke api + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); + updateResponse.assertStatusCode(HttpStatus.SC_OK); } // delete sample resource - share_with user delete admin user's resource @@ -136,8 +146,8 @@ public void testNoResourceRestrictions() throws Exception { // admin can no longer see the resource try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse getResponse = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); - getResponse.assertStatusCode(HttpStatus.SC_NOT_FOUND); + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); } } From 8ae532fa40908c67b18704beabd76d9fa03e5714 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 20:57:23 -0500 Subject: [PATCH 160/201] Fixes createdby toXContent Signed-off-by: Darshit Chanpura --- .../security/spi/resources/sharing/CreatedBy.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java index 5146d2f026..b4b44f3770 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java @@ -25,10 +25,10 @@ */ public class CreatedBy implements ToXContentFragment, NamedWriteable { - private final Enum creatorType; + private final Creator creatorType; private final String creator; - public CreatedBy(Enum creatorType, String creator) { + public CreatedBy(Creator creatorType, String creator) { this.creatorType = creatorType; this.creator = creator; } @@ -42,7 +42,7 @@ public String getCreator() { return creator; } - public Enum getCreatorType() { + public Creator getCreatorType() { return creatorType; } @@ -64,12 +64,12 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return builder.startObject().field(String.valueOf(creatorType), creator).endObject(); + return builder.startObject().field(creatorType.getName(), creator).endObject(); } public static CreatedBy fromXContent(XContentParser parser) throws IOException { String creator = null; - Enum creatorType = null; + Creator creatorType = null; XContentParser.Token token; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { From 88206e1062d89b9039041e05fe396c0fc1e3a250 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 21:03:31 -0500 Subject: [PATCH 161/201] Renames resource index listener and adds pre index and delete operation checks to verify resource access Signed-off-by: Darshit Chanpura --- .../resources/ResourceAccessHandler.java | 2 +- ...stener.java => ResourceIndexListener.java} | 171 +++++++++++------- .../security/OpenSearchSecurityPlugin.java | 8 +- 3 files changed, 106 insertions(+), 75 deletions(-) rename common/src/main/java/org/opensearch/security/common/resources/{ResourceSharingIndexListener.java => ResourceIndexListener.java} (50%) diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java index 764a72fd72..1bb48bb77d 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java @@ -366,7 +366,7 @@ public void revokeAccess( ); } - public void checkDeletePermission(String resourceId, String resourceIndex, ActionListener listener) { + public void checkRawAccessPermission(String resourceId, String resourceIndex, ActionListener listener) { try { validateArguments(resourceId, resourceIndex); diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexListener.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java similarity index 50% rename from common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexListener.java rename to common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java index 617f45c87c..3222a1e607 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexListener.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java @@ -10,6 +10,8 @@ import java.io.IOException; import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -31,41 +33,29 @@ import org.opensearch.transport.client.Client; /** - * This class implements an index operation listener for operations performed on resources stored in plugin's indices - * These indices are defined on bootstrap and configured to listen in OpenSearchSecurityPlugin.java + * This class implements an index operation listener for operations performed on resources stored in plugin's indices. + * It verifies permissions before allowing update/delete operations. */ -public class ResourceSharingIndexListener implements IndexingOperationListener { +public class ResourceIndexListener implements IndexingOperationListener { - private final static Logger log = LogManager.getLogger(ResourceSharingIndexListener.class); - - private static final ResourceSharingIndexListener INSTANCE = new ResourceSharingIndexListener(); + private static final Logger log = LogManager.getLogger(ResourceIndexListener.class); + private static final ResourceIndexListener INSTANCE = new ResourceIndexListener(); private ResourceSharingIndexHandler resourceSharingIndexHandler; private ResourceAccessHandler resourceAccessHandler; private boolean initialized; - private ThreadPool threadPool; - private ResourceSharingIndexListener() {} + private ResourceIndexListener() {} - public static ResourceSharingIndexListener getInstance() { - return ResourceSharingIndexListener.INSTANCE; + public static ResourceIndexListener getInstance() { + return ResourceIndexListener.INSTANCE; } - /** - * Initializes the ResourceSharingIndexListener with the provided ThreadPool and Client. - * This method is called during the plugin's initialization process. - * - * @param threadPool The ThreadPool instance to be used for executing operations. - * @param client The Client instance to be used for interacting with OpenSearch. - * @param adminDns The AdminDNs instance to be used for checking admin privileges. - */ public void initialize(ThreadPool threadPool, Client client, AdminDNs adminDns) { - if (initialized) { return; } - initialized = true; this.threadPool = threadPool; this.resourceSharingIndexHandler = new ResourceSharingIndexHandler( @@ -73,9 +63,7 @@ public void initialize(ThreadPool threadPool, Client client, AdminDNs adminDns) client, threadPool ); - - resourceAccessHandler = new ResourceAccessHandler(threadPool, this.resourceSharingIndexHandler, adminDns); - + this.resourceAccessHandler = new ResourceAccessHandler(threadPool, this.resourceSharingIndexHandler, adminDns); } public boolean isInitialized() { @@ -83,24 +71,61 @@ public boolean isInitialized() { } /** - * This method is called after an index operation is performed. - * It creates a resource sharing entry in the dedicated resource sharing index. - * - * @param shardId The shard ID of the index where the operation was performed. - * @param index The index where the operation was performed. - * @param result The result of the index operation. + * Ensures that the user has permission to update before proceeding. */ @Override - public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { + public Engine.Index preIndex(ShardId shardId, Engine.Index index) { + String resourceIndex = shardId.getIndexName(); + log.debug("preIndex called on {}", resourceIndex); + String resourceId = index.id(); + + // Validate permissions + if (checkPermission(resourceId, resourceIndex, index.operationType().name())) { + return index; + } + + throw new OpenSearchSecurityException( + "Index operation not permitted for resource " + resourceId + " in index " + resourceIndex + "for current user", + RestStatus.FORBIDDEN + ); + } + + /** + * Ensures that the user has permission to delete before proceeding. + */ + @Override + public Engine.Delete preDelete(ShardId shardId, Engine.Delete delete) { + String resourceIndex = shardId.getIndexName(); + log.debug("preDelete called on {}", resourceIndex); + String resourceId = delete.id(); + + if (checkPermission(resourceId, resourceIndex, delete.operationType().name())) { + return delete; + } + throw new OpenSearchSecurityException( + "Delete operation not permitted for resource " + resourceId + " in index " + resourceIndex + "for current user", + RestStatus.FORBIDDEN + ); + } + + @Override + public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { String resourceIndex = shardId.getIndexName(); log.debug("postIndex called on {}", resourceIndex); String resourceId = index.id(); + // Only proceed if this was a create operation + if (!result.isCreated()) { + log.debug("Skipping resource sharing entry creation as this was an update operation for resource {}", resourceId); + return; + } + final UserSubjectImpl userSubject = (UserSubjectImpl) threadPool.getThreadContext() .getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); final User user = userSubject.getUser(); + try { Objects.requireNonNull(user); ResourceSharing sharing = this.resourceSharingIndexHandler.indexResourceSharing( @@ -111,55 +136,16 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re ); log.info("Successfully created a resource sharing entry {}", sharing); } catch (IOException e) { - log.info("Failed to create a resource sharing entry for resource: {}", resourceId); + log.error("Failed to create a resource sharing entry for resource: {}", resourceId, e); } } - /** - * This method is called after a delete operation is performed. - * It deletes the corresponding resource sharing entry from the dedicated resource sharing index. - * - * @param shardId The shard ID of the index where the delete operation was performed. - * @param delete The delete operation that was performed. - * @return The delete operation to be performed. - */ - @Override - public Engine.Delete preDelete(ShardId shardId, Engine.Delete delete) { - - String resourceIndex = shardId.getIndexName(); - log.debug("preDelete called on {}", resourceIndex); - - String resourceId = delete.id(); - - this.resourceAccessHandler.checkDeletePermission(resourceId, resourceIndex, ActionListener.wrap((canDelete) -> { - if (canDelete) { - log.debug("Proceeding with delete operation for resource {}", resourceId); - } else { - throw new OpenSearchSecurityException( - "Delete operation not permitted for resource " + resourceId + " in index " + resourceIndex, - RestStatus.FORBIDDEN - ); - } - }, exception -> log.error("Failed to check delete permission for resource {}", resourceId, exception))); - return delete; - } - - /** - * This method is called after a delete operation is performed. - * It deletes the corresponding resource sharing entry from the dedicated resource sharing index. - * - * @param shardId The shard ID of the index where the delete operation was performed. - * @param delete The delete operation that was performed. - * @param result The result of the delete operation. - */ @Override public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { - String resourceIndex = shardId.getIndexName(); log.debug("postDelete called on {}", resourceIndex); String resourceId = delete.id(); - this.resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex, ActionListener.wrap(deleted -> { if (deleted) { log.info("Successfully deleted resource sharing entry for resource {}", resourceId); @@ -168,4 +154,49 @@ public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResul } }, exception -> log.error("Failed to delete resource sharing entry for resource {}", resourceId, exception))); } + + /** + * Helper method to check permissions synchronously using CountDownLatch. + */ + private boolean checkPermission(String resourceId, String resourceIndex, String operation) { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference permissionGranted = new AtomicReference<>(false); + AtomicReference exceptionRef = new AtomicReference<>(null); + + this.resourceAccessHandler.checkRawAccessPermission(resourceId, resourceIndex, new ActionListener() { + @Override + public void onResponse(Boolean hasPermission) { + permissionGranted.set(hasPermission); + latch.countDown(); + } + + @Override + public void onFailure(Exception e) { + exceptionRef.set(e); + latch.countDown(); + } + }); + + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new OpenSearchSecurityException( + "Interrupted while checking " + operation + " permission for resource " + resourceId, + e, + RestStatus.INTERNAL_SERVER_ERROR + ); + } + + if (exceptionRef.get() != null) { + log.error("Failed to check {} permission for resource {}", operation, resourceId, exceptionRef.get()); + throw new OpenSearchSecurityException( + "Failed to check " + operation + " permission for resource " + resourceId, + exceptionRef.get(), + RestStatus.INTERNAL_SERVER_ERROR + ); + } + + return permissionGranted.get(); + } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index c076596a5c..b24935b9c8 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -145,10 +145,10 @@ import org.opensearch.security.auditlog.impl.AuditLogImpl; import org.opensearch.security.auth.BackendRegistry; import org.opensearch.security.common.resources.ResourceAccessHandler; +import org.opensearch.security.common.resources.ResourceIndexListener; import org.opensearch.security.common.resources.ResourcePluginInfo; import org.opensearch.security.common.resources.ResourceSharingConstants; import org.opensearch.security.common.resources.ResourceSharingIndexHandler; -import org.opensearch.security.common.resources.ResourceSharingIndexListener; import org.opensearch.security.common.resources.ResourceSharingIndexManagementRepository; import org.opensearch.security.common.resources.rest.ResourceAccessAction; import org.opensearch.security.common.resources.rest.ResourceAccessRestAction; @@ -752,13 +752,13 @@ public void onIndexModule(IndexModule indexModule) { ); // Listening on POST and DELETE operations in resource indices - ResourceSharingIndexListener resourceSharingIndexListener = ResourceSharingIndexListener.getInstance(); - resourceSharingIndexListener.initialize(threadPool, localClient, adminDNsCommon); + ResourceIndexListener resourceIndexListener = ResourceIndexListener.getInstance(); + resourceIndexListener.initialize(threadPool, localClient, adminDNsCommon); if (settings.getAsBoolean( ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT ) && ResourcePluginInfo.getInstance().getResourceIndices().contains(indexModule.getIndex().getName())) { - indexModule.addIndexOperationListener(resourceSharingIndexListener); + indexModule.addIndexOperationListener(resourceIndexListener); log.info("Security plugin started listening to operations on resource-index {}", indexModule.getIndex().getName()); } From d62820b32e9ee15f0f65fe1b5fa1be51526727ef Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 21:04:15 -0500 Subject: [PATCH 162/201] Conforms to changes in client Signed-off-by: Darshit Chanpura --- sample-resource-plugin/README.md | 5 ++ .../DeleteResourceTransportAction.java | 12 ++- .../transport/GetResourceTransportAction.java | 64 +++++++-------- .../RevokeResourceAccessTransportAction.java | 12 ++- .../ShareResourceTransportAction.java | 12 ++- .../UpdateResourceTransportAction.java | 81 +++++++++++++------ .../client/ResourceSharingClientAccessor.java | 5 +- 7 files changed, 124 insertions(+), 67 deletions(-) diff --git a/sample-resource-plugin/README.md b/sample-resource-plugin/README.md index 970c92aaf3..ce4593ccb7 100644 --- a/sample-resource-plugin/README.md +++ b/sample-resource-plugin/README.md @@ -9,6 +9,11 @@ Publish SPI to local maven before proceeding: ./gradlew clean :opensearch-resource-sharing-spi:publishToMavenLocal ``` +System index feature must be enabled to prevent direct access to resource. Add the following setting in case it has not already been enabled. +```yml +plugins.security.system_indices.enabled: true +``` + ## Features - Create, update and delete resources. diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java index 47f5e80bb0..fbdb9229ba 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java @@ -19,6 +19,7 @@ import org.opensearch.action.support.HandledTransportAction; import org.opensearch.action.support.WriteRequest; import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; import org.opensearch.sample.SampleResourceScope; @@ -39,12 +40,19 @@ public class DeleteResourceTransportAction extends HandledTransportAction { - if (getResponse.isSourceEmpty()) { - listener.onFailure(new ResourceNotFoundException("Resource " + request.getResourceId() + " not found.")); - } else { - try ( - XContentParser parser = XContentType.JSON.xContent() - .createParser( - NamedXContentRegistry.EMPTY, - LoggingDeprecationHandler.INSTANCE, - getResponse.getSourceAsString() - ) - ) { - listener.onResponse(new GetResourceResponse(SampleResource.fromXContent(parser))); - } - } - }, listener::onFailure)); - } + getResourceAction(request, listener); }, listener::onFailure) ); } - private void getResource(GetResourceRequest request, ActionListener listener) { - XContentBuilder builder; - try { - builder = JsonXContent.contentBuilder() - .startObject() - .field("resource_id", request.getResourceId()) - .field("resource_index", RESOURCE_INDEX_NAME) - .field("scope", "string_value") // Modify as needed - .endObject(); - } catch (IOException e) { - throw new RuntimeException(e); + private void getResourceAction(GetResourceRequest request, ActionListener listener) { + ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); + try (ThreadContext.StoredContext ignored = threadContext.stashContext()) { + getResource(request, ActionListener.wrap(getResponse -> { + if (getResponse.isSourceEmpty()) { + listener.onFailure(new ResourceNotFoundException("Resource " + request.getResourceId() + " not found.")); + } else { + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, getResponse.getSourceAsString()) + ) { + listener.onResponse(new GetResourceResponse(SampleResource.fromXContent(parser))); + } + } + }, listener::onFailure)); } + } + private void getResource(GetResourceRequest request, ActionListener listener) { GetRequest getRequest = new GetRequest(RESOURCE_INDEX_NAME, request.getResourceId()); nodeClient.get(getRequest, listener); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java index 738d26f234..e6c2210718 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java @@ -14,6 +14,7 @@ import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; import org.opensearch.core.action.ActionListener; import org.opensearch.sample.resource.actions.rest.revoke.RevokeResourceAccessAction; import org.opensearch.sample.resource.actions.rest.revoke.RevokeResourceAccessRequest; @@ -30,16 +31,23 @@ public class RevokeResourceAccessTransportAction extends HandledTransportAction< private static final Logger log = LogManager.getLogger(RevokeResourceAccessTransportAction.class); private final NodeClient nodeClient; + private final Settings settings; @Inject - public RevokeResourceAccessTransportAction(TransportService transportService, ActionFilters actionFilters, NodeClient nodeClient) { + public RevokeResourceAccessTransportAction( + Settings settings, + TransportService transportService, + ActionFilters actionFilters, + NodeClient nodeClient + ) { super(RevokeResourceAccessAction.NAME, transportService, actionFilters, RevokeResourceAccessRequest::new); this.nodeClient = nodeClient; + this.settings = settings; } @Override protected void doExecute(Task task, RevokeResourceAccessRequest request, ActionListener listener) { - ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient); + ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient, settings); resourceSharingClient.revokeResourceAccess( request.getResourceId(), RESOURCE_INDEX_NAME, diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java index 9ea744b8f6..21d7571cf4 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java @@ -14,6 +14,7 @@ import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; import org.opensearch.core.action.ActionListener; import org.opensearch.sample.resource.actions.rest.share.ShareResourceAction; import org.opensearch.sample.resource.actions.rest.share.ShareResourceRequest; @@ -30,11 +31,18 @@ public class ShareResourceTransportAction extends HandledTransportAction listener) { - ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); - try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { - updateResource(request, listener); - listener.onResponse( - new CreateResourceResponse("Resource " + request.getResource().getResourceName() + " updated successfully.") - ); - } catch (Exception e) { - log.info("Failed to update resource: {}", request.getResourceId(), e); - listener.onFailure(e); + if (request.getResourceId() == null || request.getResourceId().isEmpty()) { + listener.onFailure(new IllegalArgumentException("Resource ID cannot be null or empty")); + return; } + // Check permission to resource + ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient, settings); + resourceSharingClient.verifyResourceAccess( + request.getResourceId(), + RESOURCE_INDEX_NAME, + SampleResourceScope.PUBLIC.value(), + ActionListener.wrap(isAuthorized -> { + if (!isAuthorized) { + listener.onFailure( + new ResourceSharingException("Current user is not authorized to access resource: " + request.getResourceId()) + ); + return; + } + + updateResource(request, listener); + }, listener::onFailure) + ); } private void updateResource(UpdateResourceRequest request, ActionListener listener) { - String resourceId = request.getResourceId(); - Resource sample = request.getResource(); - try (XContentBuilder builder = jsonBuilder()) { - UpdateRequest ur = new UpdateRequest(RESOURCE_INDEX_NAME, resourceId).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .doc(sample.toXContent(builder, ToXContent.EMPTY_PARAMS)); + ThreadContext threadContext = this.transportService.getThreadPool().getThreadContext(); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + String resourceId = request.getResourceId(); + Resource sample = request.getResource(); + try (XContentBuilder builder = jsonBuilder()) { + UpdateRequest ur = new UpdateRequest(RESOURCE_INDEX_NAME, resourceId).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .doc(sample.toXContent(builder, ToXContent.EMPTY_PARAMS)); - log.info("Update Request: {}", ur.toString()); + log.info("Update Request: {}", ur.toString()); - nodeClient.update( - ur, - ActionListener.wrap(updateResponse -> { log.info("Updated resource: {}", updateResponse.toString()); }, listener::onFailure) + nodeClient.update( + ur, + ActionListener.wrap( + updateResponse -> { log.info("Updated resource: {}", updateResponse.toString()); }, + listener::onFailure + ) + ); + } catch (IOException e) { + listener.onFailure(new RuntimeException(e)); + } + listener.onResponse( + new CreateResourceResponse("Resource " + request.getResource().getResourceName() + " updated successfully.") ); - } catch (IOException e) { - listener.onFailure(new RuntimeException(e)); + } catch (Exception e) { + log.info("Failed to update resource: {}", request.getResourceId(), e); + listener.onFailure(e); } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java index abb27d21cb..83e78f803d 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java @@ -8,6 +8,7 @@ package org.opensearch.sample.resource.client; +import org.opensearch.common.settings.Settings; import org.opensearch.security.client.resources.ResourceSharingNodeClient; import org.opensearch.transport.client.node.NodeClient; @@ -22,9 +23,9 @@ private ResourceSharingClientAccessor() {} * @param nodeClient node client * @return machine learning client */ - public static ResourceSharingNodeClient getResourceSharingClient(NodeClient nodeClient) { + public static ResourceSharingNodeClient getResourceSharingClient(NodeClient nodeClient, Settings settings) { if (INSTANCE == null) { - INSTANCE = new ResourceSharingNodeClient(nodeClient); + INSTANCE = new ResourceSharingNodeClient(nodeClient, settings); } return INSTANCE; } From 4ae5ed682dd82cb1561f05957e11b8dfce5b36fc Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 21:40:08 -0500 Subject: [PATCH 163/201] Adds more tests to verify raw access vs via a sample plugin Signed-off-by: Darshit Chanpura --- sample-resource-plugin/README.md | 2 +- ...pleResourcePluginFeatureDisabledTests.java | 10 +- ...esourcePluginSystemIndexDisabledTests.java | 587 ++++++++++++++++++ .../sample/SampleResourcePluginTests.java | 180 +++++- 4 files changed, 753 insertions(+), 26 deletions(-) create mode 100644 sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java diff --git a/sample-resource-plugin/README.md b/sample-resource-plugin/README.md index ce4593ccb7..7d1724242d 100644 --- a/sample-resource-plugin/README.md +++ b/sample-resource-plugin/README.md @@ -40,7 +40,7 @@ The plugin exposes the following six API endpoints: ### 2. Update Resource - **Endpoint:** `POST /_plugins/sample_resource_sharing/update/{resourceId}` -- **Description:** Updates a resource. +- **Description:** Updates a resource if current user has access to it. - **Request Body:** ```json { diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java index fa7fcbc938..eee8aa7532 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java @@ -37,7 +37,7 @@ */ @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class SampleResourcePluginSecurityDisabledTests extends AbstractSampleResourcePluginTests { +public class SampleResourcePluginFeatureDisabledTests extends AbstractSampleResourcePluginTests { @ClassRule public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) @@ -45,7 +45,7 @@ public class SampleResourcePluginSecurityDisabledTests extends AbstractSampleRes .anonymousAuth(true) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(USER_ADMIN, SHARED_WITH_USER) - .nodeSettings(Map.of(OPENSEARCH_RESOURCE_SHARING_ENABLED, false, "plugins.security.disabled", true)) + .nodeSettings(Map.of(OPENSEARCH_RESOURCE_SHARING_ENABLED, false)) .build(); @After @@ -129,13 +129,13 @@ public void testNoResourceRestrictions() throws Exception { // shared_with_user is able to call sample share api try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); - updateResponse.assertStatusCode(HttpStatus.SC_OK); + updateResponse.assertStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); } // shared_with_user is able to call sample revoke api try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); - updateResponse.assertStatusCode(HttpStatus.SC_OK); + HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, revokeAccessPayload()); + updateResponse.assertStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); } // delete sample resource - share_with user delete admin user's resource diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java new file mode 100644 index 0000000000..2d550bcd39 --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java @@ -0,0 +1,587 @@ +/* + * 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. + */ + +package org.opensearch.sample; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.junit.After; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.painless.PainlessModulePlugin; +import org.opensearch.security.common.resources.ResourcePluginInfo; +import org.opensearch.security.spi.resources.ResourceAccessScope; +import org.opensearch.security.spi.resources.ResourceProvider; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.security.common.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +/** + * These tests run with resource sharing enabled + */ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class SampleResourcePluginSystemIndexDisabledTests extends AbstractSampleResourcePluginTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .plugin(SampleResourcePlugin.class, PainlessModulePlugin.class) + .anonymousAuth(true) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_ADMIN, SHARED_WITH_USER) + .build(); + + @After + public void clearIndices() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + client.delete(RESOURCE_INDEX_NAME); + client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX); + ResourcePluginInfo.getInstance().getResourceIndicesMutable().remove(RESOURCE_INDEX_NAME); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().remove(RESOURCE_INDEX_NAME); + } + } + + @Test + public void testPluginInstalledCorrectly() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse pluginsResponse = client.get("_cat/plugins"); + assertThat(pluginsResponse.getBody(), containsString("org.opensearch.security.OpenSearchSecurityPlugin")); + assertThat(pluginsResponse.getBody(), containsString("org.opensearch.sample.SampleResourcePlugin")); + } + } + + @Test + public void testCreateUpdateDeleteSampleResourceWithSecurityAPIs() throws Exception { + String resourceId; + String resourceSharingDocId; + // create sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResource = "{\"name\":\"sample\"}"; + HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + response.assertStatusCode(HttpStatus.SC_OK); + + resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + } + + // Create an entry in resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually + String json = String.format( + "{" + + " \"source_idx\": \".sample_resource_sharing_plugin\"," + + " \"resource_id\": \"%s\"," + + " \"created_by\": {" + + " \"user\": \"admin\"" + + " }" + + "}", + resourceId + ); + HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); + assertThat(response.getStatusReason(), containsString("Created")); + resourceSharingDocId = response.bodyAsJsonNode().get("_id").asText(); + // Also update the in-memory map and get + ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); + ResourceProvider provider = new ResourceProvider( + SampleResource.class.getCanonicalName(), + RESOURCE_INDEX_NAME, + new SampleResourceParser() + ); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); + + Thread.sleep(1000); + response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sample")); + } + + // Update sample resource (shared_with_user cannot update admin's resource) + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); + updateResponse.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // Update sample resource (admin should be able to update resource) + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); + updateResponse.assertStatusCode(HttpStatus.SC_OK); + } + + // resource should be visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + + HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // resource should no longer be visible to shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + + HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(0)); + } + + // shared_with_user should not be able to share admin's resource with itself + // Only admins and owners can share/revoke access at the moment + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + + HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayloadSecurityApi(resourceId)); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + assertThat( + response.bodyAsJsonNode().get("message").asText(), + containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") + ); + } + + // share resource with shared_with user + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + + HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayloadSecurityApi(resourceId)); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat( + response.bodyAsJsonNode() + .get("sharing_info") + .get("share_with") + .get(SampleResourceScope.PUBLIC.value()) + .get("users") + .get(0) + .asText(), + containsString(SHARED_WITH_USER.getName()) + ); + } + + // resource should now be visible to shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // resource is still visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // verify access + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + String verifyAccessPayload = "{\"resource_id\":\"" + + resourceId + + "\",\"resource_index\":\"" + + RESOURCE_INDEX_NAME + + "\",\"scope\":\"" + + ResourceAccessScope.PUBLIC + + "\"}"; + HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("has_permission").asBoolean(), equalTo(true)); + } + + // shared_with user should not be able to revoke access to admin's resource + // Only admins and owners can share/revoke access at the moment + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayloadSecurityApi(resourceId)); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + assertThat( + response.bodyAsJsonNode().get("message").asText(), + containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") + ); + } + + // get sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // resource should be visible to shared_with_user since the resource is shared with this user and this user has * permission + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // revoke share_with_user's access + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayloadSecurityApi(resourceId)); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("share_with"), nullValue()); + } + + // verify access - share_with_user should no longer have access to admin's resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + String verifyAccessPayload = "{\"resource_id\":\"" + + resourceId + + "\",\"resource_index\":\"" + + RESOURCE_INDEX_NAME + + "\",\"scope\":\"" + + ResourceAccessScope.PUBLIC + + "\"}"; + HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("has_permission").asBoolean(), equalTo(false)); + } + + // get sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // delete sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // delete sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // corresponding entry should be removed from resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to delete the resource sharing entry manually + HttpResponse response = client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc/" + resourceSharingDocId); + response.assertStatusCode(HttpStatus.SC_OK); + + Thread.sleep(2000); + response = client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search"); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(0)); + } + + // get sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } + + // get sample resource with admin + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } + } + + @Test + public void testCreateUpdateDeleteSampleResource() throws Exception { + String resourceId; + String resourceSharingDocId; + // create sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResource = "{\"name\":\"sample\"}"; + HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + response.assertStatusCode(HttpStatus.SC_OK); + + resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + } + + // Create an entry in resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually + String json = String.format( + "{" + + " \"source_idx\": \".sample_resource_sharing_plugin\"," + + " \"resource_id\": \"%s\"," + + " \"created_by\": {" + + " \"user\": \"admin\"" + + " }" + + "}", + resourceId + ); + HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); + assertThat(response.getStatusReason(), containsString("Created")); + resourceSharingDocId = response.bodyAsJsonNode().get("_id").asText(); + // Also update the in-memory map and get + ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); + ResourceProvider provider = new ResourceProvider( + SampleResource.class.getCanonicalName(), + RESOURCE_INDEX_NAME, + new SampleResourceParser() + ); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); + + Thread.sleep(1000); + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sample")); + } + + // Update sample resource (admin should be able to update resource) + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); + updateResponse.assertStatusCode(HttpStatus.SC_OK); + } + + // resource should be visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + Thread.sleep(1000); + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // resource should not be visible to shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // shared_with_user should not be able to share admin's resource with itself + // Only admins and owners can share/revoke access at the moment + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + assertThat( + response.bodyAsJsonNode().get("error").get("root_cause").get(0).get("reason").asText(), + containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") + ); + } + + // share resource with shared_with user + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + + HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat( + response.bodyAsJsonNode().get("share_with").get(SampleResourceScope.PUBLIC.value()).get("users").get(0).asText(), + containsString(SHARED_WITH_USER.getName()) + ); + } + + // resource should now be visible to shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // resource is still visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // revoke share_with_user's access + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + HttpResponse response = client.postJson(SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, revokeAccessPayload()); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("share_with").size(), equalTo(0)); + } + + // get sample resource with shared_with_user, user no longer has access to resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // delete sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // delete sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // corresponding entry should be removed from resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to delete the resource sharing entry manually + HttpResponse response = client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc/" + resourceSharingDocId); + response.assertStatusCode(HttpStatus.SC_OK); + + Thread.sleep(1000); + response = client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search"); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(0)); + } + + // get sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } + + // get sample resource with admin + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } + } + + @Test + public void testRawAccess() throws Exception { + String resourceId; + // create sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResource = "{\"name\":\"sample\"}"; + HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + response.assertStatusCode(HttpStatus.SC_OK); + + resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + Thread.sleep(1000); + } + + // Create an entry in resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually + String json = String.format( + "{" + + " \"source_idx\": \".sample_resource_sharing_plugin\"," + + " \"resource_id\": \"%s\"," + + " \"created_by\": {" + + " \"user\": \"admin\"" + + " }" + + "}", + resourceId + ); + HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); + assertThat(response.getStatusReason(), containsString("Created")); + // Also update the in-memory map and get + ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); + ResourceProvider provider = new ResourceProvider( + SampleResource.class.getCanonicalName(), + RESOURCE_INDEX_NAME, + new SampleResourceParser() + ); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); + + Thread.sleep(1000); + response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sample")); + } + + // admin will be able to access resource directly since system index protection is disabled, and also via sample plugin + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // Create an entry in resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually + String json = String.format( + "{" + + " \"source_idx\": \".sample_resource_sharing_plugin\"," + + " \"resource_id\": \"%s\"," + + " \"created_by\": {" + + " \"user\": \"admin\"" + + " }" + + "}", + resourceId + ); + HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); + assertThat(response.getStatusReason(), containsString("Created")); + // Also update the in-memory map and get + ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); + ResourceProvider provider = new ResourceProvider( + SampleResource.class.getCanonicalName(), + RESOURCE_INDEX_NAME, + new SampleResourceParser() + ); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); + + Thread.sleep(1000); + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sample")); + } + + // shared_with_user will be able to access resource directly since system index protection is disabled even-though resource is not + // shared with this user, but cannot access via sample plugin APIs + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // share resource with shared_with user + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + + HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat( + response.bodyAsJsonNode().get("share_with").get(SampleResourceScope.PUBLIC.value()).get("users").get(0).asText(), + containsString(SHARED_WITH_USER.getName()) + ); + } + + // shared_with_user will still be able to access resource directly since system index protection is enabled, but can also access via + // sample plugin + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // revoke share_with_user's access + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + HttpResponse response = client.postJson(SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, revokeAccessPayload()); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("share_with").size(), equalTo(0)); + } + + // shared_with_user will still be able to access the resource directly but not via sample plugin since access is revoked + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // shared_with_user should be able to delete the resource since system index protection is disabled + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.delete(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + } +} diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index 9e86c467ac..4d4d24403e 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -8,6 +8,8 @@ package org.opensearch.sample; +import java.util.Map; + import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; import org.junit.After; @@ -30,6 +32,7 @@ import static org.hamcrest.Matchers.nullValue; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; import static org.opensearch.security.common.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; @@ -46,6 +49,7 @@ public class SampleResourcePluginTests extends AbstractSampleResourcePluginTests .anonymousAuth(true) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(USER_ADMIN, SHARED_WITH_USER) + .nodeSettings(Map.of(SECURITY_SYSTEM_INDICES_ENABLED_KEY, true)) .build(); @After @@ -112,6 +116,13 @@ public void testCreateUpdateDeleteSampleResourceWithSecurityAPIs() throws Except assertThat(response.getBody(), containsString("sample")); } + // Update sample resource (shared_with_user cannot update admin's resource) + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); + updateResponse.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + // Update sample resource (admin should be able to update resource) try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; @@ -213,6 +224,12 @@ public void testCreateUpdateDeleteSampleResourceWithSecurityAPIs() throws Except response.assertStatusCode(HttpStatus.SC_OK); } + // resource should be visible to shared_with_user since the resource is shared with this user and this user has * permission + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + // revoke share_with_user's access try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { Thread.sleep(1000); @@ -259,7 +276,7 @@ public void testCreateUpdateDeleteSampleResourceWithSecurityAPIs() throws Except HttpResponse response = client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc/" + resourceSharingDocId); response.assertStatusCode(HttpStatus.SC_OK); - Thread.sleep(1000); + Thread.sleep(2000); response = client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search"); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(0)); @@ -331,7 +348,7 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { // resource should be visible to super-admin try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - + Thread.sleep(1000); HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString("sampleUpdated")); @@ -347,7 +364,6 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { // shared_with_user should not be able to share admin's resource with itself // Only admins and owners can share/revoke access at the moment try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); response.assertStatusCode(HttpStatus.SC_FORBIDDEN); assertThat( @@ -382,23 +398,6 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { assertThat(response.getBody(), containsString("sampleUpdated")); } - // shared_with user should not be able to revoke access to admin's resource - // Only admins and owners can share/revoke access at the moment - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.postJson(SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, revokeAccessPayload()); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - assertThat( - response.bodyAsJsonNode().get("error").get("root_cause").get(0).get("reason").asText(), - containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") - ); - } - - // get sample resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - } - // revoke share_with_user's access try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { Thread.sleep(1000); @@ -449,4 +448,145 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { response.assertStatusCode(HttpStatus.SC_NOT_FOUND); } } + + @Test + public void testRawAccess() throws Exception { + String resourceId; + // create sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResource = "{\"name\":\"sample\"}"; + HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + response.assertStatusCode(HttpStatus.SC_OK); + + resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + Thread.sleep(1000); + } + + // Create an entry in resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually + String json = String.format( + "{" + + " \"source_idx\": \".sample_resource_sharing_plugin\"," + + " \"resource_id\": \"%s\"," + + " \"created_by\": {" + + " \"user\": \"admin\"" + + " }" + + "}", + resourceId + ); + HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); + assertThat(response.getStatusReason(), containsString("Created")); + // Also update the in-memory map and get + ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); + ResourceProvider provider = new ResourceProvider( + SampleResource.class.getCanonicalName(), + RESOURCE_INDEX_NAME, + new SampleResourceParser() + ); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); + + Thread.sleep(1000); + response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sample")); + } + + // admin should not be able to access resource directly since system index protection is enabled, but can access via sample plugin + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // Create an entry in resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually + String json = String.format( + "{" + + " \"source_idx\": \".sample_resource_sharing_plugin\"," + + " \"resource_id\": \"%s\"," + + " \"created_by\": {" + + " \"user\": \"admin\"" + + " }" + + "}", + resourceId + ); + HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); + assertThat(response.getStatusReason(), containsString("Created")); + // Also update the in-memory map and get + ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); + ResourceProvider provider = new ResourceProvider( + SampleResource.class.getCanonicalName(), + RESOURCE_INDEX_NAME, + new SampleResourceParser() + ); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); + + Thread.sleep(1000); + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sample")); + } + + // shared_with_user should not be able to delete the resource since system index protection is enabled + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.delete(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // shared_with_user should not be able to access resource directly since system index protection is enabled, and resource is not + // shared with user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // share resource with shared_with user + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + + HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat( + response.bodyAsJsonNode().get("share_with").get(SampleResourceScope.PUBLIC.value()).get("users").get(0).asText(), + containsString(SHARED_WITH_USER.getName()) + ); + } + + // shared_with_user should not be able to access resource directly since system index protection is enabled, but can access via + // sample plugin + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // revoke share_with_user's access + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + HttpResponse response = client.postJson(SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, revokeAccessPayload()); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("share_with").size(), equalTo(0)); + } + + // shared_with_user should not be able to access the resource directly nor via sample plugin since access is revoked + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + HttpResponse response = client.get(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + + response = client.postJson(RESOURCE_INDEX_NAME + "/_search", "{\"query\" : {\"match_all\" : {}}}"); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(0)); + } + + } } From 4adb9a6f04cacf402d8ab96bb02dc3cca855c919 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 22:35:36 -0500 Subject: [PATCH 164/201] Updates dependency scopes Signed-off-by: Darshit Chanpura --- client/build.gradle | 4 ++-- spi/build.gradle | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/client/build.gradle b/client/build.gradle index a8dfbf9dbf..6ec8cd0c8c 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -36,8 +36,8 @@ repositories { dependencies { // Main implementation dependencies compileOnly "org.opensearch:opensearch:${opensearch_version}" - compileOnly "org.opensearch:opensearch-security-common:${opensearch_build}" - compileOnly "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" + implementation "org.opensearch:opensearch-security-common:${opensearch_build}" + implementation "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" } java { diff --git a/spi/build.gradle b/spi/build.gradle index 78f7fdfddf..ee79bc0785 100644 --- a/spi/build.gradle +++ b/spi/build.gradle @@ -20,7 +20,6 @@ repositories { dependencies { compileOnly "org.opensearch:opensearch:${opensearch_version}" - compileOnly "org.opensearch:opensearch-resource-sharing-spi:${opensearch_version}" } java { From 36cca04b1426941e100fb30d2551bd6b2a0c71ae Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 22:50:13 -0500 Subject: [PATCH 165/201] Fixes maven publication Signed-off-by: Darshit Chanpura --- .github/workflows/maven-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index d3d1ba84eb..0ee195f952 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -32,7 +32,7 @@ jobs: export SONATYPE_PASSWORD=$(aws secretsmanager get-secret-value --secret-id maven-snapshots-password --query SecretString --output text) echo "::add-mask::$SONATYPE_USERNAME" echo "::add-mask::$SONATYPE_PASSWORD" - ./gradlew --no-daemon publishPluginZipPublicationToSnapshotsRepository ./gradlew --no-daemon :opensearch-resource-sharing-spi:publishMavenJavaPublicationToSnapshotsRepository - ./gradlew --no-daemon :opensearch-security-client:publishMavenJavaPublicationToSnapshotsRepository ./gradlew --no-daemon :opensearch-security-common:publishMavenJavaPublicationToSnapshotsRepository + ./gradlew --no-daemon :opensearch-security-client:publishMavenJavaPublicationToSnapshotsRepository + ./gradlew --no-daemon publishPluginZipPublicationToSnapshotsRepository From 22bd75da4b854f1b05ce7ccc575244ce581dd72b Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 22:54:48 -0500 Subject: [PATCH 166/201] Fixes createdBy tests Signed-off-by: Darshit Chanpura --- .../java/org/opensearch/security/resources/CreatedByTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/opensearch/security/resources/CreatedByTests.java b/src/test/java/org/opensearch/security/resources/CreatedByTests.java index 7682251401..11f34e24d4 100644 --- a/src/test/java/org/opensearch/security/resources/CreatedByTests.java +++ b/src/test/java/org/opensearch/security/resources/CreatedByTests.java @@ -34,7 +34,7 @@ public class CreatedByTests extends SingleClusterTest { - private static final Enum CREATOR_TYPE = Creator.USER; + private static final Creator CREATOR_TYPE = Creator.USER; public void testCreatedByConstructorWithValidUser() { String expectedUser = "testUser"; From 96b9dfbaf97af0de7812b75520820b73e48884b1 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 23:42:11 -0500 Subject: [PATCH 167/201] Fixes CreatedBy toString Signed-off-by: Darshit Chanpura --- .../opensearch/security/spi/resources/sharing/CreatedBy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java index b4b44f3770..fbe8d1208b 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java @@ -48,7 +48,7 @@ public Creator getCreatorType() { @Override public String toString() { - return "CreatedBy {" + this.creatorType + "='" + this.creator + '\'' + '}'; + return "CreatedBy {" + this.creatorType.getName() + "='" + this.creator + '\'' + '}'; } @Override From c311e8255f84fe79e8d2e5a258a13d5747f39b85 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 23:42:41 -0500 Subject: [PATCH 168/201] Adds a new set of tests called resource sharing tests Signed-off-by: Darshit Chanpura --- build.gradle | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index af4ebb509c..d5b890bd34 100644 --- a/build.gradle +++ b/build.gradle @@ -234,7 +234,18 @@ def splitTestConfig = [ "org.opensearch.security.ssl.OpenSSL*" ] ] - ] + ], + resourceSharingTests: [ + description: "Runs most of the SSL tests.", + filters: [ + includeTestsMatching: [ + "org.opensearch.security.resources.*" + ], + excludeTestsMatching: [ + "org.opensearch.security.ssl.OpenSSL*" + ] + ] + ], ] as ConfigObject List taskNames = splitTestConfig.keySet() as List From 26572a5875ffc2ecd228d9699c18caeda78f4dbc Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 23:43:52 -0500 Subject: [PATCH 169/201] Fixes resource sharing unit tests Signed-off-by: Darshit Chanpura --- .../security/resources/CreatedByTests.java | 32 +++++++++++++++++-- .../resources/RecipientTypeRegistryTests.java | 5 +-- .../security/resources/ShareWithTests.java | 14 ++++++-- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/test/java/org/opensearch/security/resources/CreatedByTests.java b/src/test/java/org/opensearch/security/resources/CreatedByTests.java index 11f34e24d4..c15c4e4ace 100644 --- a/src/test/java/org/opensearch/security/resources/CreatedByTests.java +++ b/src/test/java/org/opensearch/security/resources/CreatedByTests.java @@ -11,6 +11,7 @@ import java.io.IOException; import org.hamcrest.MatcherAssert; +import org.junit.Test; import org.opensearch.common.io.stream.BytesStreamOutput; import org.opensearch.common.xcontent.XContentFactory; @@ -21,7 +22,6 @@ import org.opensearch.core.xcontent.XContentParser; import org.opensearch.security.spi.resources.sharing.CreatedBy; import org.opensearch.security.spi.resources.sharing.Creator; -import org.opensearch.security.test.SingleClusterTest; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -32,10 +32,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class CreatedByTests extends SingleClusterTest { +public class CreatedByTests { private static final Creator CREATOR_TYPE = Creator.USER; + @Test public void testCreatedByConstructorWithValidUser() { String expectedUser = "testUser"; CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, expectedUser); @@ -43,6 +44,7 @@ public void testCreatedByConstructorWithValidUser() { MatcherAssert.assertThat(expectedUser, is(equalTo(createdBy.getCreator()))); } + @Test public void testCreatedByFromStreamInput() throws IOException { String expectedUser = "testUser"; @@ -58,6 +60,7 @@ public void testCreatedByFromStreamInput() throws IOException { } } + @Test public void testCreatedByWithEmptyStreamInput() throws IOException { try (StreamInput mockStreamInput = mock(StreamInput.class)) { @@ -67,12 +70,14 @@ public void testCreatedByWithEmptyStreamInput() throws IOException { } } + @Test public void testCreatedByWithEmptyUser() { CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, ""); MatcherAssert.assertThat("", equalTo(createdBy.getCreator())); } + @Test public void testCreatedByWithIOException() throws IOException { try (StreamInput mockStreamInput = mock(StreamInput.class)) { @@ -82,18 +87,21 @@ public void testCreatedByWithIOException() throws IOException { } } + @Test public void testCreatedByWithLongUsername() { String longUsername = "a".repeat(10000); CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, longUsername); MatcherAssert.assertThat(longUsername, equalTo(createdBy.getCreator())); } + @Test public void testCreatedByWithUnicodeCharacters() { String unicodeUsername = "η”¨ζˆ·γ“γ‚“γ«γ‘γ―"; CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, unicodeUsername); MatcherAssert.assertThat(unicodeUsername, equalTo(createdBy.getCreator())); } + @Test public void testFromXContentThrowsExceptionWhenUserFieldIsMissing() throws IOException { String emptyJson = "{}"; IllegalArgumentException exception; @@ -105,6 +113,7 @@ public void testFromXContentThrowsExceptionWhenUserFieldIsMissing() throws IOExc MatcherAssert.assertThat("null is required", equalTo(exception.getMessage())); } + @Test public void testFromXContentWithEmptyInput() throws IOException { String emptyJson = "{}"; try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, emptyJson)) { @@ -113,13 +122,15 @@ public void testFromXContentWithEmptyInput() throws IOException { } } + @Test public void testFromXContentWithExtraFields() throws IOException { String jsonWithExtraFields = "{\"user\": \"testUser\", \"extraField\": \"value\"}"; XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, jsonWithExtraFields); - CreatedBy.fromXContent(parser); + assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); } + @Test public void testFromXContentWithIncorrectFieldType() throws IOException { String jsonWithIncorrectType = "{\"user\": 12345}"; try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, jsonWithIncorrectType)) { @@ -128,6 +139,7 @@ public void testFromXContentWithIncorrectFieldType() throws IOException { } } + @Test public void testFromXContentWithEmptyUser() throws IOException { String emptyJson = "{\"" + CREATOR_TYPE + "\": \"\" }"; CreatedBy createdBy; @@ -141,6 +153,7 @@ public void testFromXContentWithEmptyUser() throws IOException { MatcherAssert.assertThat("", equalTo(createdBy.getCreator())); } + @Test public void testFromXContentWithNullUserValue() throws IOException { String jsonWithNullUser = "{\"user\": null}"; try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, jsonWithNullUser)) { @@ -149,6 +162,7 @@ public void testFromXContentWithNullUserValue() throws IOException { } } + @Test public void testFromXContentWithValidUser() throws IOException { String json = "{\"user\":\"testUser\"}"; XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, json); @@ -159,6 +173,7 @@ public void testFromXContentWithValidUser() throws IOException { MatcherAssert.assertThat("testUser", equalTo(createdBy.getCreator())); } + @Test public void testGetCreatorReturnsCorrectValue() { String expectedUser = "testUser"; CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, expectedUser); @@ -168,29 +183,34 @@ public void testGetCreatorReturnsCorrectValue() { MatcherAssert.assertThat(expectedUser, equalTo(actualUser)); } + @Test public void testGetCreatorWithNullString() { CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, null); MatcherAssert.assertThat(createdBy.getCreator(), nullValue()); } + @Test public void testGetWriteableNameReturnsCorrectString() { CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, "testUser"); MatcherAssert.assertThat("created_by", equalTo(createdBy.getWriteableName())); } + @Test public void testToStringWithEmptyUser() { CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, ""); String result = createdBy.toString(); MatcherAssert.assertThat("CreatedBy {user=''}", equalTo(result)); } + @Test public void testToStringWithNullUser() { CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, (String) null); String result = createdBy.toString(); MatcherAssert.assertThat("CreatedBy {user='null'}", equalTo(result)); } + @Test public void testToStringWithLongUserName() { String longUserName = "a".repeat(1000); @@ -201,6 +221,7 @@ public void testToStringWithLongUserName() { MatcherAssert.assertThat(1019, equalTo(result.length())); } + @Test public void testToXContentWithEmptyUser() throws IOException { CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, ""); XContentBuilder builder = JsonXContent.contentBuilder(); @@ -210,6 +231,7 @@ public void testToXContentWithEmptyUser() throws IOException { MatcherAssert.assertThat("{\"user\":\"\"}", equalTo(result)); } + @Test public void testWriteToWithExceptionInStreamOutput() throws IOException { CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, "user1"); try (StreamOutput failingOutput = new StreamOutput() { @@ -243,6 +265,7 @@ public void reset() throws IOException { } } + @Test public void testWriteToWithLongUserName() throws IOException { String longUserName = "a".repeat(65536); CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, longUserName); @@ -251,6 +274,7 @@ public void testWriteToWithLongUserName() throws IOException { MatcherAssert.assertThat(out.size(), greaterThan(65536)); } + @Test public void test_createdByToStringReturnsCorrectFormat() { String testUser = "testUser"; CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, testUser); @@ -261,6 +285,7 @@ public void test_createdByToStringReturnsCorrectFormat() { MatcherAssert.assertThat(expected, equalTo(actual)); } + @Test public void test_toXContent_serializesCorrectly() throws IOException { String expectedUser = "testUser"; CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, expectedUser); @@ -272,6 +297,7 @@ public void test_toXContent_serializesCorrectly() throws IOException { MatcherAssert.assertThat(expectedJson, equalTo(builder.toString())); } + @Test public void test_writeTo_writesUserCorrectly() throws IOException { String expectedUser = "testUser"; CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, expectedUser); diff --git a/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java b/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java index 8238797cb0..92334c078c 100644 --- a/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java +++ b/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java @@ -9,18 +9,19 @@ package org.opensearch.security.resources; import org.hamcrest.MatcherAssert; +import org.junit.Test; import org.opensearch.security.spi.resources.sharing.RecipientType; import org.opensearch.security.spi.resources.sharing.RecipientTypeRegistry; -import org.opensearch.security.test.SingleClusterTest; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThrows; -public class RecipientTypeRegistryTests extends SingleClusterTest { +public class RecipientTypeRegistryTests { + @Test public void testFromValue() { RecipientTypeRegistry.registerRecipientType("ble1", new RecipientType("ble1")); RecipientTypeRegistry.registerRecipientType("ble2", new RecipientType("ble2")); diff --git a/src/test/java/org/opensearch/security/resources/ShareWithTests.java b/src/test/java/org/opensearch/security/resources/ShareWithTests.java index 9b25aa1fbb..71e47efae6 100644 --- a/src/test/java/org/opensearch/security/resources/ShareWithTests.java +++ b/src/test/java/org/opensearch/security/resources/ShareWithTests.java @@ -16,6 +16,7 @@ import org.hamcrest.MatcherAssert; import org.junit.Before; +import org.junit.Test; import org.opensearch.common.xcontent.XContentFactory; import org.opensearch.common.xcontent.XContentType; @@ -30,7 +31,6 @@ import org.opensearch.security.spi.resources.sharing.RecipientTypeRegistry; import org.opensearch.security.spi.resources.sharing.ShareWith; import org.opensearch.security.spi.resources.sharing.SharedWithScope; -import org.opensearch.security.test.SingleClusterTest; import org.mockito.Mockito; @@ -47,13 +47,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class ShareWithTests extends SingleClusterTest { +public class ShareWithTests { @Before public void setupResourceRecipientTypes() { initializeRecipientTypes(); } + @Test public void testFromXContentWhenCurrentTokenIsNotStartObject() throws IOException { String json = "{\"read_only\": {\"users\": [\"user1\"], \"roles\": [], \"backend_roles\": []}}"; XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, json); @@ -82,6 +83,7 @@ public void testFromXContentWhenCurrentTokenIsNotStartObject() throws IOExceptio ); } + @Test public void testFromXContentWithEmptyInput() throws IOException { String emptyJson = "{}"; XContentParser parser = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, null, emptyJson); @@ -92,6 +94,7 @@ public void testFromXContentWithEmptyInput() throws IOException { MatcherAssert.assertThat(result.getSharedWithScopes(), is(empty())); } + @Test public void testFromXContentWithStartObject() throws IOException { XContentParser parser; try (XContentBuilder builder = XContentFactory.jsonBuilder()) { @@ -152,6 +155,7 @@ public void testFromXContentWithStartObject() throws IOException { } } + @Test public void testFromXContentWithUnexpectedEndOfInput() throws IOException { XContentParser mockParser = mock(XContentParser.class); when(mockParser.currentToken()).thenReturn(XContentParser.Token.START_OBJECT); @@ -163,6 +167,7 @@ public void testFromXContentWithUnexpectedEndOfInput() throws IOException { MatcherAssert.assertThat(result.getSharedWithScopes(), is(empty())); } + @Test public void testToXContentBuildsCorrectly() throws IOException { SharedWithScope scope = new SharedWithScope( "scope1", @@ -186,6 +191,7 @@ public void testToXContentBuildsCorrectly() throws IOException { MatcherAssert.assertThat(expected, equalTo(result)); } + @Test public void testWriteToWithEmptySet() throws IOException { Set emptySet = Collections.emptySet(); ShareWith shareWith = new ShareWith(emptySet); @@ -196,6 +202,7 @@ public void testWriteToWithEmptySet() throws IOException { verify(mockOutput).writeCollection(emptySet); } + @Test public void testWriteToWithIOException() throws IOException { Set set = new HashSet<>(); set.add(new SharedWithScope("test", new SharedWithScope.ScopeRecipients(Map.of()))); @@ -207,6 +214,7 @@ public void testWriteToWithIOException() throws IOException { assertThrows(IOException.class, () -> shareWith.writeTo(mockOutput)); } + @Test public void testWriteToWithLargeSet() throws IOException { Set largeSet = new HashSet<>(); for (int i = 0; i < 10000; i++) { @@ -220,6 +228,7 @@ public void testWriteToWithLargeSet() throws IOException { verify(mockOutput).writeCollection(largeSet); } + @Test public void test_fromXContent_emptyObject() throws IOException { XContentParser parser; try (XContentBuilder builder = XContentFactory.jsonBuilder()) { @@ -232,6 +241,7 @@ public void test_fromXContent_emptyObject() throws IOException { MatcherAssert.assertThat(shareWith.getSharedWithScopes(), is(empty())); } + @Test public void test_writeSharedWithScopesToStream() throws IOException { StreamOutput mockStreamOutput = Mockito.mock(StreamOutput.class); From c2387b58a25311387cb99cc9f119945127188d6b Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 3 Mar 2025 23:57:03 -0500 Subject: [PATCH 170/201] Remove lang-painless dependency Signed-off-by: Darshit Chanpura --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index d5b890bd34..7fd109030c 100644 --- a/build.gradle +++ b/build.gradle @@ -414,7 +414,6 @@ opensearchplugin { name 'opensearch-security' description 'Provide access control related features for OpenSearch' classname 'org.opensearch.security.OpenSearchSecurityPlugin' - extendedPlugins = ['lang-painless'] } // This requires an additional Jar not published as part of build-tools From ad68951e2eada5bb150c2bf22a2afd1a7a90482f Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 4 Mar 2025 01:23:56 -0500 Subject: [PATCH 171/201] Fix CI workflow Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21ef6aa3fd..623df67816 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -238,17 +238,17 @@ jobs: build-artifact-names: runs-on: ubuntu-latest steps: - - name: Setup Environment - uses: actions/checkout@v4 + - name: Setup Environment + uses: actions/checkout@v4 - - name: Configure Java - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 21 + - name: Configure Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 - - name: Build and Test Artifacts - run: | + - name: Build and Test Artifacts + run: | # Set version variables security_plugin_version=$(./gradlew properties -q | grep -E '^version:' | awk '{print $2}') security_plugin_version_no_snapshot=$(echo $security_plugin_version | sed 's/-SNAPSHOT//g') @@ -269,16 +269,16 @@ jobs: ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar # Publish Common - ./gradlew :opensearch-security-common:publishToMavenLocal && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version.jar - ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_no_snapshot.jar - ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier.jar - ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar + ./gradlew :opensearch-security-common:publishToMavenLocal && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version.jar + ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_no_snapshot.jar + ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier.jar + ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar # Publish Client - ./gradlew :opensearch-security-client:publishToMavenLocal && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version.jar - ./gradlew :opensearch-security-client-spi:publishToMavenLocal -Dbuild.snapshot=false && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_no_snapshot.jar - ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier.jar - ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar + ./gradlew :opensearch-security-client:publishToMavenLocal && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version.jar + ./gradlew :opensearch-security-client-spi:publishToMavenLocal -Dbuild.snapshot=false && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_no_snapshot.jar + ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier.jar + ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar # Build artifacts ./gradlew clean assemble && \ @@ -301,6 +301,6 @@ jobs: test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ test -s ./build/distributions/opensearch-security-$security_plugin_version.pom - - name: List files in build directory on failure - if: failure() - run: ls -al ./build/distributions/ + - name: List files in build directory on failure + if: failure() + run: ls -al ./build/distributions/ From 46457fc2670a1ce6ac83053831a6e0bc1eb6ac79 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 4 Mar 2025 12:47:41 -0500 Subject: [PATCH 172/201] Improve failure logs for build and test artifacts Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 623df67816..0433c76562 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -303,4 +303,4 @@ jobs: - name: List files in build directory on failure if: failure() - run: ls -al ./build/distributions/ + run: ls -al ./build/distributions/ ./*/build/libs From c071b1432026b776c3a08a1d6549f3e493c40c72 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 4 Mar 2025 19:38:17 -0500 Subject: [PATCH 173/201] Updates ResourceSharingExtension contract and adds a clause for bad request in ResourceSharingException Signed-off-by: Darshit Chanpura --- .../security/spi/resources/ResourceSharingExtension.java | 4 +--- .../spi/resources/exceptions/ResourceSharingException.java | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java index f6eb1d35e8..5b46c0bfaf 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java @@ -27,7 +27,5 @@ public interface ResourceSharingExtension { */ String getResourceIndex(); - default ResourceParser getResourceParser() { - return null; - }; + ResourceParser getResourceParser(); } diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java index 31c19fc2db..e966d7fc10 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java @@ -47,6 +47,8 @@ public RestStatus status() { return RestStatus.UNAUTHORIZED; } else if (message.contains("not found")) { return RestStatus.NOT_FOUND; + } else if (message.contains("not a system index")) { + return RestStatus.BAD_REQUEST; } return RestStatus.INTERNAL_SERVER_ERROR; From 0a54de6cf9aa72c03dbc04305c2754999735362e Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 4 Mar 2025 19:39:31 -0500 Subject: [PATCH 174/201] Updates resource access transport handler to fail a request is resource index is not a system index Signed-off-by: Darshit Chanpura --- .../resources/rest/ResourceAccessRequest.java | 9 ++++++++- .../resources/rest/ResourceAccessRestAction.java | 2 ++ .../rest/ResourceAccessTransportAction.java | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java index 0116f54bf4..bf2b79cb42 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java @@ -15,6 +15,8 @@ import java.util.Set; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; + import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionRequestValidationException; import org.opensearch.common.xcontent.LoggingDeprecationHandler; @@ -70,7 +72,12 @@ public static ResourceAccessRequest from(Map source, Map { private final ResourceAccessHandler resourceAccessHandler; + private final SystemIndices systemIndices; + @Inject public ResourceAccessTransportAction( TransportService transportService, ActionFilters actionFilters, + SystemIndices systemIndices, ResourceAccessHandler resourceAccessHandler ) { super(ResourceAccessAction.NAME, transportService, actionFilters, ResourceAccessRequest::new); + this.systemIndices = systemIndices; this.resourceAccessHandler = resourceAccessHandler; } @Override protected void doExecute(Task task, ResourceAccessRequest request, ActionListener actionListener) { + // verify that the request if for a system index + if (!this.systemIndices.isSystemIndex(request.getResourceIndex())) { + actionListener.onFailure( + new ResourceSharingException("Resource index '" + request.getResourceIndex() + "' is not a system index.") + ); + return; + } + switch (request.getOperation()) { case LIST: handleListResources(request, actionListener); From 308e5621d9556a46ae1cd179ab8eee1e03fad722 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 4 Mar 2025 19:40:44 -0500 Subject: [PATCH 175/201] Expands sample plugin integration tests to cover multiple scenarios Signed-off-by: Darshit Chanpura --- ...mpleResourcePluginFeatureEnabledTests.java | 466 ++++++++++++++++++ .../AbstractSampleResourcePluginTests.java | 39 +- ...pleResourcePluginFeatureDisabledTests.java | 7 +- ...esourcePluginSystemIndexDisabledTests.java | 412 +--------------- .../sample/SampleResourcePluginTests.java | 413 +--------------- ...ractResourcePluginNonSystemIndexTests.java | 87 ++++ .../ResourceNonSystemIndexPlugin.java | 36 ++ ...ResourceNonSystemIndexSIDisabledTests.java | 37 ++ .../ResourceNonSystemIndexTests.java | 41 ++ 9 files changed, 706 insertions(+), 832 deletions(-) create mode 100644 sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java create mode 100644 sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/AbstractResourcePluginNonSystemIndexTests.java create mode 100644 sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexPlugin.java create mode 100644 sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexSIDisabledTests.java create mode 100644 sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexTests.java diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java new file mode 100644 index 0000000000..2c32112d08 --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java @@ -0,0 +1,466 @@ +/* + * 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. + */ + +package org.opensearch.sample; + +import org.apache.http.HttpStatus; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.opensearch.security.common.resources.ResourcePluginInfo; +import org.opensearch.security.spi.resources.ResourceAccessScope; +import org.opensearch.security.spi.resources.ResourceProvider; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.security.common.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +/** + * This abstract class defines common tests between different feature flag scenarios + */ +public abstract class AbstractSampleResourcePluginFeatureEnabledTests extends AbstractSampleResourcePluginTests { + + protected abstract LocalCluster getLocalCluster(); + + private LocalCluster cluster; + + @Before + public void setup() { + cluster = getLocalCluster(); + } + + @After + public void clearIndices() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + client.delete(RESOURCE_INDEX_NAME); + client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX); + ResourcePluginInfo.getInstance().getResourceIndicesMutable().remove(RESOURCE_INDEX_NAME); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().remove(RESOURCE_INDEX_NAME); + } + } + + @Test + public void testPluginInstalledCorrectly() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + TestRestClient.HttpResponse pluginsResponse = client.get("_cat/plugins"); + assertThat(pluginsResponse.getBody(), containsString("org.opensearch.security.OpenSearchSecurityPlugin")); + assertThat(pluginsResponse.getBody(), containsString("org.opensearch.sample.SampleResourcePlugin")); + } + } + + @Test + public void testCreateUpdateDeleteSampleResourceWithSecurityAPIs() throws Exception { + String resourceId; + String resourceSharingDocId; + // create sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResource = "{\"name\":\"sample\"}"; + TestRestClient.HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + response.assertStatusCode(HttpStatus.SC_OK); + + resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + } + + // Create an entry in resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually + String json = String.format( + "{" + + " \"source_idx\": \"" + + RESOURCE_INDEX_NAME + + "\"," + + " \"resource_id\": \"%s\"," + + " \"created_by\": {" + + " \"user\": \"admin\"" + + " }" + + "}", + resourceId + ); + TestRestClient.HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); + assertThat(response.getStatusReason(), containsString("Created")); + resourceSharingDocId = response.bodyAsJsonNode().get("_id").asText(); + // Also update the in-memory map and get + ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); + ResourceProvider provider = new ResourceProvider( + SampleResource.class.getCanonicalName(), + RESOURCE_INDEX_NAME, + new SampleResourceParser() + ); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); + + Thread.sleep(1000); + response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sample")); + } + + // Update sample resource (shared_with_user cannot update admin's resource) + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + TestRestClient.HttpResponse updateResponse = client.postJson( + SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, + sampleResourceUpdated + ); + updateResponse.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // Update sample resource (admin should be able to update resource) + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + TestRestClient.HttpResponse updateResponse = client.postJson( + SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, + sampleResourceUpdated + ); + updateResponse.assertStatusCode(HttpStatus.SC_OK); + } + + // resource should be visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + + TestRestClient.HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // resource should no longer be visible to shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + + TestRestClient.HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(0)); + } + + // shared_with_user should not be able to share admin's resource with itself + // Only admins and owners can share/revoke access at the moment + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + + TestRestClient.HttpResponse response = client.postJson( + SECURITY_RESOURCE_SHARE_ENDPOINT, + shareWithPayloadSecurityApi(resourceId) + ); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + assertThat( + response.bodyAsJsonNode().get("message").asText(), + containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") + ); + } + + // share resource with shared_with user + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + + TestRestClient.HttpResponse response = client.postJson( + SECURITY_RESOURCE_SHARE_ENDPOINT, + shareWithPayloadSecurityApi(resourceId) + ); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat( + response.bodyAsJsonNode() + .get("sharing_info") + .get("share_with") + .get(SampleResourceScope.PUBLIC.value()) + .get("users") + .get(0) + .asText(), + containsString(SHARED_WITH_USER.getName()) + ); + } + + // resource should now be visible to shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + TestRestClient.HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // resource is still visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // verify access + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + String verifyAccessPayload = "{\"resource_id\":\"" + + resourceId + + "\",\"resource_index\":\"" + + RESOURCE_INDEX_NAME + + "\",\"scope\":\"" + + ResourceAccessScope.PUBLIC + + "\"}"; + TestRestClient.HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("has_permission").asBoolean(), equalTo(true)); + } + + // shared_with user should not be able to revoke access to admin's resource + // Only admins and owners can share/revoke access at the moment + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + TestRestClient.HttpResponse response = client.postJson( + SECURITY_RESOURCE_REVOKE_ENDPOINT, + revokeAccessPayloadSecurityApi(resourceId) + ); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + assertThat( + response.bodyAsJsonNode().get("message").asText(), + containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") + ); + } + + // get sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // resource should be visible to shared_with_user since the resource is shared with this user and this user has * permission + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // revoke share_with_user's access + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + TestRestClient.HttpResponse response = client.postJson( + SECURITY_RESOURCE_REVOKE_ENDPOINT, + revokeAccessPayloadSecurityApi(resourceId) + ); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("share_with"), nullValue()); + } + + // verify access - share_with_user should no longer have access to admin's resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + String verifyAccessPayload = "{\"resource_id\":\"" + + resourceId + + "\",\"resource_index\":\"" + + RESOURCE_INDEX_NAME + + "\",\"scope\":\"" + + ResourceAccessScope.PUBLIC + + "\"}"; + TestRestClient.HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("has_permission").asBoolean(), equalTo(false)); + } + + // get sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // delete sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + TestRestClient.HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // delete sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + TestRestClient.HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // corresponding entry should be removed from resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to delete the resource sharing entry manually + TestRestClient.HttpResponse response = client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc/" + resourceSharingDocId); + response.assertStatusCode(HttpStatus.SC_OK); + + Thread.sleep(2000); + response = client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search"); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(0)); + } + + // get sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } + + // get sample resource with admin + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } + } + + @Test + public void testCreateUpdateDeleteSampleResource() throws Exception { + String resourceId; + String resourceSharingDocId; + // create sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResource = "{\"name\":\"sample\"}"; + TestRestClient.HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + response.assertStatusCode(HttpStatus.SC_OK); + + resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + } + + // Create an entry in resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually + String json = String.format( + "{" + + " \"source_idx\": \".sample_resource_sharing_plugin\"," + + " \"resource_id\": \"%s\"," + + " \"created_by\": {" + + " \"user\": \"admin\"" + + " }" + + "}", + resourceId + ); + TestRestClient.HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); + assertThat(response.getStatusReason(), containsString("Created")); + resourceSharingDocId = response.bodyAsJsonNode().get("_id").asText(); + // Also update the in-memory map and get + ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); + ResourceProvider provider = new ResourceProvider( + SampleResource.class.getCanonicalName(), + RESOURCE_INDEX_NAME, + new SampleResourceParser() + ); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); + + Thread.sleep(1000); + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sample")); + } + + // Update sample resource (admin should be able to update resource) + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + TestRestClient.HttpResponse updateResponse = client.postJson( + SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, + sampleResourceUpdated + ); + updateResponse.assertStatusCode(HttpStatus.SC_OK); + } + + // resource should be visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + Thread.sleep(1000); + TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // resource should not be visible to shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + + TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // shared_with_user should not be able to share admin's resource with itself + // Only admins and owners can share/revoke access at the moment + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + TestRestClient.HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + assertThat( + response.bodyAsJsonNode().get("error").get("root_cause").get(0).get("reason").asText(), + containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") + ); + } + + // share resource with shared_with user + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + + TestRestClient.HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat( + response.bodyAsJsonNode().get("share_with").get(SampleResourceScope.PUBLIC.value()).get("users").get(0).asText(), + containsString(SHARED_WITH_USER.getName()) + ); + } + + // resource should now be visible to shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // resource is still visible to super-admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.getBody(), containsString("sampleUpdated")); + } + + // revoke share_with_user's access + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + TestRestClient.HttpResponse response = client.postJson( + SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, + revokeAccessPayload() + ); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("share_with").size(), equalTo(0)); + } + + // get sample resource with shared_with_user, user no longer has access to resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // delete sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + TestRestClient.HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // delete sample resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + TestRestClient.HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // corresponding entry should be removed from resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to delete the resource sharing entry manually + TestRestClient.HttpResponse response = client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc/" + resourceSharingDocId); + response.assertStatusCode(HttpStatus.SC_OK); + + Thread.sleep(1000); + response = client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search"); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(0)); + } + + // get sample resource with shared_with_user + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } + + // get sample resource with admin + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_NOT_FOUND); + } + } +} diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java index 0379fc3faa..20ae984444 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java @@ -19,29 +19,34 @@ import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_RESOURCE_ROUTE_PREFIX; /** - * These tests run with security enabled + * Abstract class for sample resource plugin tests. Provides common constants and utility methods for testing. This class is not intended to be + * instantiated directly. It is extended by {@link AbstractSampleResourcePluginFeatureEnabledTests}, {@link SampleResourcePluginFeatureDisabledTests}, {@link org.opensearch.sample.nonsystemindex.AbstractResourcePluginNonSystemIndexTests} */ @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class AbstractSampleResourcePluginTests { +public abstract class AbstractSampleResourcePluginTests { - final static TestSecurityConfig.User SHARED_WITH_USER = new TestSecurityConfig.User("resource_sharing_test_user").roles( + protected final static TestSecurityConfig.User SHARED_WITH_USER = new TestSecurityConfig.User("resource_sharing_test_user").roles( new TestSecurityConfig.Role("shared_role").indexPermissions("*").on("*").clusterPermissions("*") ); - static final String SAMPLE_RESOURCE_CREATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/create"; - static final String SAMPLE_RESOURCE_GET_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/get"; - static final String SAMPLE_RESOURCE_UPDATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/update"; - static final String SAMPLE_RESOURCE_DELETE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/delete"; - static final String SAMPLE_RESOURCE_SHARE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/share"; - static final String SAMPLE_RESOURCE_REVOKE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/revoke"; + protected final static TestSecurityConfig.User SHARED_WITH_USER_LIMITED_PERMISSIONS = new TestSecurityConfig.User( + "resource_sharing_test_user" + ).roles(new TestSecurityConfig.Role("shared_role").indexPermissions("*").on(RESOURCE_INDEX_NAME)); + + protected static final String SAMPLE_RESOURCE_CREATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/create"; + protected static final String SAMPLE_RESOURCE_GET_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/get"; + protected static final String SAMPLE_RESOURCE_UPDATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/update"; + protected static final String SAMPLE_RESOURCE_DELETE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/delete"; + protected static final String SAMPLE_RESOURCE_SHARE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/share"; + protected static final String SAMPLE_RESOURCE_REVOKE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/revoke"; private static final String PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH = PLUGIN_RESOURCE_ROUTE_PREFIX.replaceFirst("/", ""); - static final String SECURITY_RESOURCE_LIST_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/list"; - static final String SECURITY_RESOURCE_SHARE_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/share"; - static final String SECURITY_RESOURCE_VERIFY_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/verify_access"; - static final String SECURITY_RESOURCE_REVOKE_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/revoke"; + protected static final String SECURITY_RESOURCE_LIST_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/list"; + protected static final String SECURITY_RESOURCE_SHARE_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/share"; + protected static final String SECURITY_RESOURCE_VERIFY_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/verify_access"; + protected static final String SECURITY_RESOURCE_REVOKE_ENDPOINT = PLUGIN_RESOURCE_ROUTE_PREFIX_NO_LEADING_SLASH + "/revoke"; - static String shareWithPayloadSecurityApi(String resourceId) { + protected static String shareWithPayloadSecurityApi(String resourceId) { return "{" + "\"resource_id\":\"" + resourceId @@ -61,7 +66,7 @@ static String shareWithPayloadSecurityApi(String resourceId) { + "}"; } - static String shareWithPayload() { + protected static String shareWithPayload() { return "{" + "\"share_with\":{" + "\"" @@ -75,7 +80,7 @@ static String shareWithPayload() { + "}"; } - static String revokeAccessPayloadSecurityApi(String resourceId) { + protected static String revokeAccessPayloadSecurityApi(String resourceId) { return "{" + "\"resource_id\": \"" + resourceId @@ -94,7 +99,7 @@ static String revokeAccessPayloadSecurityApi(String resourceId) { + "}"; } - static String revokeAccessPayload() { + protected static String revokeAccessPayload() { return "{" + "\"entities_to_revoke\": {" + "\"users\": [\"" diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java index eee8aa7532..26c2ca1c31 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java @@ -10,12 +10,10 @@ import java.util.Map; -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; import org.junit.After; import org.junit.ClassRule; import org.junit.Test; -import org.junit.runner.RunWith; import org.opensearch.painless.PainlessModulePlugin; import org.opensearch.test.framework.cluster.ClusterManager; @@ -32,11 +30,8 @@ import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** - * These tests run with security disabled - * + * These tests run with resource sharing feature disabled. */ -@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) -@ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class SampleResourcePluginFeatureDisabledTests extends AbstractSampleResourcePluginTests { @ClassRule diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java index 2d550bcd39..ddf500fffc 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java @@ -8,16 +8,12 @@ package org.opensearch.sample; -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; -import org.junit.After; import org.junit.ClassRule; import org.junit.Test; -import org.junit.runner.RunWith; import org.opensearch.painless.PainlessModulePlugin; import org.opensearch.security.common.resources.ResourcePluginInfo; -import org.opensearch.security.spi.resources.ResourceAccessScope; import org.opensearch.security.spi.resources.ResourceProvider; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -27,7 +23,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.nullValue; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; import static org.opensearch.security.common.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; @@ -36,9 +31,7 @@ /** * These tests run with resource sharing enabled */ -@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) -@ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class SampleResourcePluginSystemIndexDisabledTests extends AbstractSampleResourcePluginTests { +public class SampleResourcePluginSystemIndexDisabledTests extends AbstractSampleResourcePluginFeatureEnabledTests { @ClassRule public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) @@ -48,401 +41,9 @@ public class SampleResourcePluginSystemIndexDisabledTests extends AbstractSample .users(USER_ADMIN, SHARED_WITH_USER) .build(); - @After - public void clearIndices() { - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - client.delete(RESOURCE_INDEX_NAME); - client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX); - ResourcePluginInfo.getInstance().getResourceIndicesMutable().remove(RESOURCE_INDEX_NAME); - ResourcePluginInfo.getInstance().getResourceProvidersMutable().remove(RESOURCE_INDEX_NAME); - } - } - - @Test - public void testPluginInstalledCorrectly() { - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse pluginsResponse = client.get("_cat/plugins"); - assertThat(pluginsResponse.getBody(), containsString("org.opensearch.security.OpenSearchSecurityPlugin")); - assertThat(pluginsResponse.getBody(), containsString("org.opensearch.sample.SampleResourcePlugin")); - } - } - - @Test - public void testCreateUpdateDeleteSampleResourceWithSecurityAPIs() throws Exception { - String resourceId; - String resourceSharingDocId; - // create sample resource - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - String sampleResource = "{\"name\":\"sample\"}"; - HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); - response.assertStatusCode(HttpStatus.SC_OK); - - resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); - } - - // Create an entry in resource-sharing index - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually - String json = String.format( - "{" - + " \"source_idx\": \".sample_resource_sharing_plugin\"," - + " \"resource_id\": \"%s\"," - + " \"created_by\": {" - + " \"user\": \"admin\"" - + " }" - + "}", - resourceId - ); - HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); - assertThat(response.getStatusReason(), containsString("Created")); - resourceSharingDocId = response.bodyAsJsonNode().get("_id").asText(); - // Also update the in-memory map and get - ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); - ResourceProvider provider = new ResourceProvider( - SampleResource.class.getCanonicalName(), - RESOURCE_INDEX_NAME, - new SampleResourceParser() - ); - ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); - - Thread.sleep(1000); - response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); - assertThat(response.getBody(), containsString("sample")); - } - - // Update sample resource (shared_with_user cannot update admin's resource) - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; - HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); - updateResponse.assertStatusCode(HttpStatus.SC_FORBIDDEN); - } - - // Update sample resource (admin should be able to update resource) - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; - HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); - updateResponse.assertStatusCode(HttpStatus.SC_OK); - } - - // resource should be visible to super-admin - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - - HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); - assertThat(response.getBody(), containsString("sampleUpdated")); - } - - // resource should no longer be visible to shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - - HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(0)); - } - - // shared_with_user should not be able to share admin's resource with itself - // Only admins and owners can share/revoke access at the moment - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - - HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayloadSecurityApi(resourceId)); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - assertThat( - response.bodyAsJsonNode().get("message").asText(), - containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") - ); - } - - // share resource with shared_with user - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - Thread.sleep(1000); - - HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayloadSecurityApi(resourceId)); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat( - response.bodyAsJsonNode() - .get("sharing_info") - .get("share_with") - .get(SampleResourceScope.PUBLIC.value()) - .get("users") - .get(0) - .asText(), - containsString(SHARED_WITH_USER.getName()) - ); - } - - // resource should now be visible to shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); - assertThat(response.getBody(), containsString("sampleUpdated")); - } - - // resource is still visible to super-admin - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); - assertThat(response.getBody(), containsString("sampleUpdated")); - } - - // verify access - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - String verifyAccessPayload = "{\"resource_id\":\"" - + resourceId - + "\",\"resource_index\":\"" - + RESOURCE_INDEX_NAME - + "\",\"scope\":\"" - + ResourceAccessScope.PUBLIC - + "\"}"; - HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("has_permission").asBoolean(), equalTo(true)); - } - - // shared_with user should not be able to revoke access to admin's resource - // Only admins and owners can share/revoke access at the moment - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayloadSecurityApi(resourceId)); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - assertThat( - response.bodyAsJsonNode().get("message").asText(), - containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") - ); - } - - // get sample resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - } - - // resource should be visible to shared_with_user since the resource is shared with this user and this user has * permission - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - } - - // revoke share_with_user's access - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - Thread.sleep(1000); - HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayloadSecurityApi(resourceId)); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("share_with"), nullValue()); - } - - // verify access - share_with_user should no longer have access to admin's resource - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - String verifyAccessPayload = "{\"resource_id\":\"" - + resourceId - + "\",\"resource_index\":\"" - + RESOURCE_INDEX_NAME - + "\",\"scope\":\"" - + ResourceAccessScope.PUBLIC - + "\"}"; - HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("has_permission").asBoolean(), equalTo(false)); - } - - // get sample resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - } - - // delete sample resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - } - - // delete sample resource - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - } - - // corresponding entry should be removed from resource-sharing index - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - // Since test framework doesn't yet allow loading ex tensions we need to delete the resource sharing entry manually - HttpResponse response = client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc/" + resourceSharingDocId); - response.assertStatusCode(HttpStatus.SC_OK); - - Thread.sleep(2000); - response = client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search"); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(0)); - } - - // get sample resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_NOT_FOUND); - } - - // get sample resource with admin - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_NOT_FOUND); - } - } - - @Test - public void testCreateUpdateDeleteSampleResource() throws Exception { - String resourceId; - String resourceSharingDocId; - // create sample resource - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - String sampleResource = "{\"name\":\"sample\"}"; - HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); - response.assertStatusCode(HttpStatus.SC_OK); - - resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); - } - - // Create an entry in resource-sharing index - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually - String json = String.format( - "{" - + " \"source_idx\": \".sample_resource_sharing_plugin\"," - + " \"resource_id\": \"%s\"," - + " \"created_by\": {" - + " \"user\": \"admin\"" - + " }" - + "}", - resourceId - ); - HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); - assertThat(response.getStatusReason(), containsString("Created")); - resourceSharingDocId = response.bodyAsJsonNode().get("_id").asText(); - // Also update the in-memory map and get - ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); - ResourceProvider provider = new ResourceProvider( - SampleResource.class.getCanonicalName(), - RESOURCE_INDEX_NAME, - new SampleResourceParser() - ); - ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); - - Thread.sleep(1000); - response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), containsString("sample")); - } - - // Update sample resource (admin should be able to update resource) - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; - HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); - updateResponse.assertStatusCode(HttpStatus.SC_OK); - } - - // resource should be visible to super-admin - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - Thread.sleep(1000); - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), containsString("sampleUpdated")); - } - - // resource should not be visible to shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - } - - // shared_with_user should not be able to share admin's resource with itself - // Only admins and owners can share/revoke access at the moment - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - assertThat( - response.bodyAsJsonNode().get("error").get("root_cause").get(0).get("reason").asText(), - containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") - ); - } - - // share resource with shared_with user - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - Thread.sleep(1000); - - HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat( - response.bodyAsJsonNode().get("share_with").get(SampleResourceScope.PUBLIC.value()).get("users").get(0).asText(), - containsString(SHARED_WITH_USER.getName()) - ); - } - - // resource should now be visible to shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), containsString("sampleUpdated")); - } - - // resource is still visible to super-admin - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), containsString("sampleUpdated")); - } - - // revoke share_with_user's access - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - Thread.sleep(1000); - HttpResponse response = client.postJson(SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, revokeAccessPayload()); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("share_with").size(), equalTo(0)); - } - - // get sample resource with shared_with_user, user no longer has access to resource - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - } - - // delete sample resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - } - - // delete sample resource - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - } - - // corresponding entry should be removed from resource-sharing index - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - // Since test framework doesn't yet allow loading ex tensions we need to delete the resource sharing entry manually - HttpResponse response = client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc/" + resourceSharingDocId); - response.assertStatusCode(HttpStatus.SC_OK); - - Thread.sleep(1000); - response = client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search"); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(0)); - } - - // get sample resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_NOT_FOUND); - } - - // get sample resource with admin - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_NOT_FOUND); - } + @Override + protected LocalCluster getLocalCluster() { + return cluster; } @Test @@ -463,7 +64,9 @@ public void testRawAccess() throws Exception { // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually String json = String.format( "{" - + " \"source_idx\": \".sample_resource_sharing_plugin\"," + + " \"source_idx\": \"" + + RESOURCE_INDEX_NAME + + "\"," + " \"resource_id\": \"%s\"," + " \"created_by\": {" + " \"user\": \"admin\"" @@ -584,4 +187,5 @@ public void testRawAccess() throws Exception { } } + } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index 4d4d24403e..07314b5e43 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -10,16 +10,12 @@ import java.util.Map; -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; -import org.junit.After; import org.junit.ClassRule; import org.junit.Test; -import org.junit.runner.RunWith; import org.opensearch.painless.PainlessModulePlugin; import org.opensearch.security.common.resources.ResourcePluginInfo; -import org.opensearch.security.spi.resources.ResourceAccessScope; import org.opensearch.security.spi.resources.ResourceProvider; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -29,7 +25,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.nullValue; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; import static org.opensearch.security.common.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY; @@ -37,11 +32,9 @@ import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** - * These tests run with resource sharing enabled + * These tests run with resource sharing enabled and system index enabled */ -@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) -@ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class SampleResourcePluginTests extends AbstractSampleResourcePluginTests { +public class SampleResourcePluginTests extends AbstractSampleResourcePluginFeatureEnabledTests { @ClassRule public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) @@ -52,401 +45,9 @@ public class SampleResourcePluginTests extends AbstractSampleResourcePluginTests .nodeSettings(Map.of(SECURITY_SYSTEM_INDICES_ENABLED_KEY, true)) .build(); - @After - public void clearIndices() { - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - client.delete(RESOURCE_INDEX_NAME); - client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX); - ResourcePluginInfo.getInstance().getResourceIndicesMutable().remove(RESOURCE_INDEX_NAME); - ResourcePluginInfo.getInstance().getResourceProvidersMutable().remove(RESOURCE_INDEX_NAME); - } - } - - @Test - public void testPluginInstalledCorrectly() { - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse pluginsResponse = client.get("_cat/plugins"); - assertThat(pluginsResponse.getBody(), containsString("org.opensearch.security.OpenSearchSecurityPlugin")); - assertThat(pluginsResponse.getBody(), containsString("org.opensearch.sample.SampleResourcePlugin")); - } - } - - @Test - public void testCreateUpdateDeleteSampleResourceWithSecurityAPIs() throws Exception { - String resourceId; - String resourceSharingDocId; - // create sample resource - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - String sampleResource = "{\"name\":\"sample\"}"; - HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); - response.assertStatusCode(HttpStatus.SC_OK); - - resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); - } - - // Create an entry in resource-sharing index - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually - String json = String.format( - "{" - + " \"source_idx\": \".sample_resource_sharing_plugin\"," - + " \"resource_id\": \"%s\"," - + " \"created_by\": {" - + " \"user\": \"admin\"" - + " }" - + "}", - resourceId - ); - HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); - assertThat(response.getStatusReason(), containsString("Created")); - resourceSharingDocId = response.bodyAsJsonNode().get("_id").asText(); - // Also update the in-memory map and get - ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); - ResourceProvider provider = new ResourceProvider( - SampleResource.class.getCanonicalName(), - RESOURCE_INDEX_NAME, - new SampleResourceParser() - ); - ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); - - Thread.sleep(1000); - response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); - assertThat(response.getBody(), containsString("sample")); - } - - // Update sample resource (shared_with_user cannot update admin's resource) - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; - HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); - updateResponse.assertStatusCode(HttpStatus.SC_FORBIDDEN); - } - - // Update sample resource (admin should be able to update resource) - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; - HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); - updateResponse.assertStatusCode(HttpStatus.SC_OK); - } - - // resource should be visible to super-admin - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - - HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); - assertThat(response.getBody(), containsString("sampleUpdated")); - } - - // resource should no longer be visible to shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - - HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(0)); - } - - // shared_with_user should not be able to share admin's resource with itself - // Only admins and owners can share/revoke access at the moment - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - - HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayloadSecurityApi(resourceId)); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - assertThat( - response.bodyAsJsonNode().get("message").asText(), - containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") - ); - } - - // share resource with shared_with user - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - Thread.sleep(1000); - - HttpResponse response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, shareWithPayloadSecurityApi(resourceId)); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat( - response.bodyAsJsonNode() - .get("sharing_info") - .get("share_with") - .get(SampleResourceScope.PUBLIC.value()) - .get("users") - .get(0) - .asText(), - containsString(SHARED_WITH_USER.getName()) - ); - } - - // resource should now be visible to shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); - assertThat(response.getBody(), containsString("sampleUpdated")); - } - - // resource is still visible to super-admin - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); - assertThat(response.getBody(), containsString("sampleUpdated")); - } - - // verify access - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - String verifyAccessPayload = "{\"resource_id\":\"" - + resourceId - + "\",\"resource_index\":\"" - + RESOURCE_INDEX_NAME - + "\",\"scope\":\"" - + ResourceAccessScope.PUBLIC - + "\"}"; - HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("has_permission").asBoolean(), equalTo(true)); - } - - // shared_with user should not be able to revoke access to admin's resource - // Only admins and owners can share/revoke access at the moment - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayloadSecurityApi(resourceId)); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - assertThat( - response.bodyAsJsonNode().get("message").asText(), - containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") - ); - } - - // get sample resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - } - - // resource should be visible to shared_with_user since the resource is shared with this user and this user has * permission - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - } - - // revoke share_with_user's access - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - Thread.sleep(1000); - HttpResponse response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, revokeAccessPayloadSecurityApi(resourceId)); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("share_with"), nullValue()); - } - - // verify access - share_with_user should no longer have access to admin's resource - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - String verifyAccessPayload = "{\"resource_id\":\"" - + resourceId - + "\",\"resource_index\":\"" - + RESOURCE_INDEX_NAME - + "\",\"scope\":\"" - + ResourceAccessScope.PUBLIC - + "\"}"; - HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("has_permission").asBoolean(), equalTo(false)); - } - - // get sample resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - } - - // delete sample resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - } - - // delete sample resource - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - } - - // corresponding entry should be removed from resource-sharing index - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - // Since test framework doesn't yet allow loading ex tensions we need to delete the resource sharing entry manually - HttpResponse response = client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc/" + resourceSharingDocId); - response.assertStatusCode(HttpStatus.SC_OK); - - Thread.sleep(2000); - response = client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search"); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(0)); - } - - // get sample resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_NOT_FOUND); - } - - // get sample resource with admin - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_NOT_FOUND); - } - } - - @Test - public void testCreateUpdateDeleteSampleResource() throws Exception { - String resourceId; - String resourceSharingDocId; - // create sample resource - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - String sampleResource = "{\"name\":\"sample\"}"; - HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); - response.assertStatusCode(HttpStatus.SC_OK); - - resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); - } - - // Create an entry in resource-sharing index - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually - String json = String.format( - "{" - + " \"source_idx\": \".sample_resource_sharing_plugin\"," - + " \"resource_id\": \"%s\"," - + " \"created_by\": {" - + " \"user\": \"admin\"" - + " }" - + "}", - resourceId - ); - HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); - assertThat(response.getStatusReason(), containsString("Created")); - resourceSharingDocId = response.bodyAsJsonNode().get("_id").asText(); - // Also update the in-memory map and get - ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); - ResourceProvider provider = new ResourceProvider( - SampleResource.class.getCanonicalName(), - RESOURCE_INDEX_NAME, - new SampleResourceParser() - ); - ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); - - Thread.sleep(1000); - response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), containsString("sample")); - } - - // Update sample resource (admin should be able to update resource) - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; - HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); - updateResponse.assertStatusCode(HttpStatus.SC_OK); - } - - // resource should be visible to super-admin - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - Thread.sleep(1000); - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), containsString("sampleUpdated")); - } - - // resource should not be visible to shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - } - - // shared_with_user should not be able to share admin's resource with itself - // Only admins and owners can share/revoke access at the moment - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - assertThat( - response.bodyAsJsonNode().get("error").get("root_cause").get(0).get("reason").asText(), - containsString("User " + SHARED_WITH_USER.getName() + " is not authorized") - ); - } - - // share resource with shared_with user - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - Thread.sleep(1000); - - HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload()); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat( - response.bodyAsJsonNode().get("share_with").get(SampleResourceScope.PUBLIC.value()).get("users").get(0).asText(), - containsString(SHARED_WITH_USER.getName()) - ); - } - - // resource should now be visible to shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), containsString("sampleUpdated")); - } - - // resource is still visible to super-admin - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), containsString("sampleUpdated")); - } - - // revoke share_with_user's access - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - Thread.sleep(1000); - HttpResponse response = client.postJson(SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, revokeAccessPayload()); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("share_with").size(), equalTo(0)); - } - - // get sample resource with shared_with_user, user no longer has access to resource - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - } - - // delete sample resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_FORBIDDEN); - } - - // delete sample resource - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - } - - // corresponding entry should be removed from resource-sharing index - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - // Since test framework doesn't yet allow loading ex tensions we need to delete the resource sharing entry manually - HttpResponse response = client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc/" + resourceSharingDocId); - response.assertStatusCode(HttpStatus.SC_OK); - - Thread.sleep(1000); - response = client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search"); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.bodyAsJsonNode().get("hits").get("hits").size(), equalTo(0)); - } - - // get sample resource with shared_with_user - try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_NOT_FOUND); - } - - // get sample resource with admin - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_NOT_FOUND); - } + @Override + protected LocalCluster getLocalCluster() { + return cluster; } @Test @@ -467,7 +68,9 @@ public void testRawAccess() throws Exception { // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually String json = String.format( "{" - + " \"source_idx\": \".sample_resource_sharing_plugin\"," + + " \"source_idx\": \"" + + RESOURCE_INDEX_NAME + + "\"," + " \"resource_id\": \"%s\"," + " \"created_by\": {" + " \"user\": \"admin\"" diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/AbstractResourcePluginNonSystemIndexTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/AbstractResourcePluginNonSystemIndexTests.java new file mode 100644 index 0000000000..613f7b08f3 --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/AbstractResourcePluginNonSystemIndexTests.java @@ -0,0 +1,87 @@ +/* + * 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. + */ + +package org.opensearch.sample.nonsystemindex; + +import org.apache.http.HttpStatus; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.opensearch.sample.AbstractSampleResourcePluginTests; +import org.opensearch.security.common.resources.ResourcePluginInfo; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.sample.nonsystemindex.ResourceNonSystemIndexPlugin.SAMPLE_NON_SYSTEM_INDEX_NAME; +import static org.opensearch.security.common.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +/** + * This abstract class defines common tests between different feature flag scenarios where resource plugin does not register its resource index as system index + */ +public abstract class AbstractResourcePluginNonSystemIndexTests extends AbstractSampleResourcePluginTests { + + protected abstract LocalCluster getLocalCluster(); + + private LocalCluster cluster; + + @Before + public void setup() { + cluster = getLocalCluster(); + } + + @After + public void clearIndices() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + client.delete(SAMPLE_NON_SYSTEM_INDEX_NAME); + client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX); + ResourcePluginInfo.getInstance().getResourceIndicesMutable().remove(SAMPLE_NON_SYSTEM_INDEX_NAME); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().remove(SAMPLE_NON_SYSTEM_INDEX_NAME); + } + } + + @Test + public void testPluginInstalledCorrectly() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + TestRestClient.HttpResponse pluginsResponse = client.get("_cat/plugins"); + assertThat(pluginsResponse.getBody(), containsString("org.opensearch.security.OpenSearchSecurityPlugin")); + assertThat(pluginsResponse.getBody(), containsString("org.opensearch.sample.nonsystemindex.ResourceNonSystemIndexPlugin")); + } + } + + @Test + public void testSecurityResourceAPIs() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + SAMPLE_NON_SYSTEM_INDEX_NAME); + assertBadResponse(response); + + String samplePayload = "{ \"resource_index\": \"" + SAMPLE_NON_SYSTEM_INDEX_NAME + "\"}"; + response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, samplePayload); + assertBadResponse(response); + + response = client.postJson(SECURITY_RESOURCE_SHARE_ENDPOINT, samplePayload); + assertBadResponse(response); + + response = client.postJson(SECURITY_RESOURCE_REVOKE_ENDPOINT, samplePayload); + assertBadResponse(response); + + } + } + + private void assertBadResponse(TestRestClient.HttpResponse response) { + response.assertStatusCode(HttpStatus.SC_BAD_REQUEST); + assertThat( + response.getTextFromJsonBody("/message"), + equalTo("Resource index '" + SAMPLE_NON_SYSTEM_INDEX_NAME + "' is not a system index.") + ); + } +} diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexPlugin.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexPlugin.java new file mode 100644 index 0000000000..3a9497b281 --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexPlugin.java @@ -0,0 +1,36 @@ +package org.opensearch.sample.nonsystemindex; + +import java.nio.file.Path; + +import org.opensearch.common.settings.Settings; +import org.opensearch.plugins.Plugin; +import org.opensearch.sample.SampleResource; +import org.opensearch.sample.SampleResourceParser; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.ResourceParser; +import org.opensearch.security.spi.resources.ResourceSharingExtension; + +/** + * Sample resource sharing plugin that doesn't declare its resource index as system index. + * TESTING ONLY + */ +public class ResourceNonSystemIndexPlugin extends Plugin implements ResourceSharingExtension { + public static final String SAMPLE_NON_SYSTEM_INDEX_NAME = "sample_non_system_index"; + + public ResourceNonSystemIndexPlugin(final Settings settings, final Path path) {} + + @Override + public String getResourceType() { + return SampleResource.class.getName(); + } + + @Override + public String getResourceIndex() { + return SAMPLE_NON_SYSTEM_INDEX_NAME; + } + + @Override + public ResourceParser getResourceParser() { + return new SampleResourceParser(); + } +} diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexSIDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexSIDisabledTests.java new file mode 100644 index 0000000000..e2a81f7e64 --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexSIDisabledTests.java @@ -0,0 +1,37 @@ +/* + * 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. + */ + +package org.opensearch.sample.nonsystemindex; + +import org.junit.ClassRule; + +import org.opensearch.painless.PainlessModulePlugin; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +/** + * These tests run with resource sharing enabled but the plugin does not declare a system index and system index protection is disabled + */ +public class ResourceNonSystemIndexSIDisabledTests extends AbstractResourcePluginNonSystemIndexTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .plugin(ResourceNonSystemIndexPlugin.class, PainlessModulePlugin.class) + .anonymousAuth(true) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_ADMIN, SHARED_WITH_USER) + .build(); + + @Override + protected LocalCluster getLocalCluster() { + return cluster; + } +} diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexTests.java new file mode 100644 index 0000000000..787396ba19 --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexTests.java @@ -0,0 +1,41 @@ +/* + * 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. + */ + +package org.opensearch.sample.nonsystemindex; + +import java.util.Map; + +import org.junit.ClassRule; + +import org.opensearch.painless.PainlessModulePlugin; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +/** + * These tests run with resource sharing enabled but the plugin does not declare a system index and system index protection is enabled + */ +public class ResourceNonSystemIndexTests extends AbstractResourcePluginNonSystemIndexTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .plugin(ResourceNonSystemIndexPlugin.class, PainlessModulePlugin.class) + .anonymousAuth(true) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_ADMIN, SHARED_WITH_USER) + .nodeSettings(Map.of(SECURITY_SYSTEM_INDICES_ENABLED_KEY, true)) + .build(); + + @Override + protected LocalCluster getLocalCluster() { + return cluster; + } +} From 4fdfa66120e347ca976bd4480401436ed202ada1 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 4 Mar 2025 20:56:11 -0500 Subject: [PATCH 176/201] Fix CI workflow Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 41 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0433c76562..390e1e52f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,9 +32,27 @@ jobs: run: | echo "separateTestsNames=$(./gradlew listTasksAsJSON -q --console=plain | tail -n 1)" >> $GITHUB_OUTPUT + publish-components-to-maven-local: + runs-on: ubuntu-latest + steps: + - name: Set up JDK for build and test + uses: actions/setup-java@v4 + with: + distribution: temurin # Temurin is a distribution of adoptium + java-version: 21 + + - name: Checkout security + uses: actions/checkout@v4 + + - name: Publish components to Maven Local + run: | + ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false + ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false + ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false + test: name: test - needs: generate-test-list + needs: [generate-test-list, publish-components-to-maven-local] strategy: fail-fast: false matrix: @@ -91,9 +109,9 @@ jobs: fail_ci_if_error: true verbose: true - integration-tests: name: integration-tests + needs: publish-components-to-maven-local strategy: fail-fast: false matrix: @@ -111,24 +129,6 @@ jobs: - name: Checkout security uses: actions/checkout@v4 - - name: Publish SPI to Local Maven - uses: gradle/gradle-build-action@v3 - with: - cache-disabled: true - arguments: :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false - - - name: Publish Common to Local Maven - uses: gradle/gradle-build-action@v3 - with: - cache-disabled: true - arguments: :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false - - - name: Publish Client to Local Maven - uses: gradle/gradle-build-action@v3 - with: - cache-disabled: true - arguments: :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false - - name: Run Integration Tests uses: gradle/gradle-build-action@v3 with: @@ -153,6 +153,7 @@ jobs: resource-tests: env: CI_ENVIRONMENT: resource-test + needs: publish-components-to-maven-local strategy: fail-fast: false matrix: From 27fd23c04e76e5bf6a7329e0a160ae5636654b87 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 4 Mar 2025 23:23:34 -0500 Subject: [PATCH 177/201] Moves spi tests to correct folder Signed-off-by: Darshit Chanpura --- .../spi}/resources/CreatedByTests.java | 2 +- .../resources/RecipientTypeRegistryTests.java | 2 +- .../spi}/resources/ShareWithTests.java | 3 +- .../security/util/ResourceValidation.java | 34 ------------------- 4 files changed, 3 insertions(+), 38 deletions(-) rename {src/test/java/org/opensearch/security => spi/src/test/java/org/opensearch/security/spi}/resources/CreatedByTests.java (99%) rename {src/test/java/org/opensearch/security => spi/src/test/java/org/opensearch/security/spi}/resources/RecipientTypeRegistryTests.java (96%) rename {src/test/java/org/opensearch/security => spi/src/test/java/org/opensearch/security/spi}/resources/ShareWithTests.java (99%) delete mode 100644 src/main/java/org/opensearch/security/util/ResourceValidation.java diff --git a/src/test/java/org/opensearch/security/resources/CreatedByTests.java b/spi/src/test/java/org/opensearch/security/spi/resources/CreatedByTests.java similarity index 99% rename from src/test/java/org/opensearch/security/resources/CreatedByTests.java rename to spi/src/test/java/org/opensearch/security/spi/resources/CreatedByTests.java index c15c4e4ace..cf85166682 100644 --- a/src/test/java/org/opensearch/security/resources/CreatedByTests.java +++ b/spi/src/test/java/org/opensearch/security/spi/resources/CreatedByTests.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.resources; +package org.opensearch.security.spi.resources; import java.io.IOException; diff --git a/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java b/spi/src/test/java/org/opensearch/security/spi/resources/RecipientTypeRegistryTests.java similarity index 96% rename from src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java rename to spi/src/test/java/org/opensearch/security/spi/resources/RecipientTypeRegistryTests.java index 92334c078c..0281d287de 100644 --- a/src/test/java/org/opensearch/security/resources/RecipientTypeRegistryTests.java +++ b/spi/src/test/java/org/opensearch/security/spi/resources/RecipientTypeRegistryTests.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.resources; +package org.opensearch.security.spi.resources; import org.hamcrest.MatcherAssert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/resources/ShareWithTests.java b/spi/src/test/java/org/opensearch/security/spi/resources/ShareWithTests.java similarity index 99% rename from src/test/java/org/opensearch/security/resources/ShareWithTests.java rename to spi/src/test/java/org/opensearch/security/spi/resources/ShareWithTests.java index 71e47efae6..38dfe7290b 100644 --- a/src/test/java/org/opensearch/security/resources/ShareWithTests.java +++ b/spi/src/test/java/org/opensearch/security/spi/resources/ShareWithTests.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.resources; +package org.opensearch.security.spi.resources; import java.io.IOException; import java.util.Collections; @@ -26,7 +26,6 @@ import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.security.spi.resources.ResourceAccessScope; import org.opensearch.security.spi.resources.sharing.RecipientType; import org.opensearch.security.spi.resources.sharing.RecipientTypeRegistry; import org.opensearch.security.spi.resources.sharing.ShareWith; diff --git a/src/main/java/org/opensearch/security/util/ResourceValidation.java b/src/main/java/org/opensearch/security/util/ResourceValidation.java deleted file mode 100644 index 428aae2cf2..0000000000 --- a/src/main/java/org/opensearch/security/util/ResourceValidation.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.util; - -import java.util.HashSet; -import java.util.Set; - -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.security.spi.resources.ResourceAccessScope; - -public class ResourceValidation { - public static ActionRequestValidationException validateScopes(Set scopes) { - Set validScopes = new HashSet<>(); - validScopes.add(ResourceAccessScope.RESTRICTED); - validScopes.add(ResourceAccessScope.PUBLIC); - - // TODO See if we can add custom scopes as part of this validation routine - - for (String s : scopes) { - if (!validScopes.contains(s)) { - ActionRequestValidationException exception = new ActionRequestValidationException(); - exception.addValidationError("Invalid scope: " + s + ". Scope must be one of: " + validScopes); - return exception; - } - } - return null; - } -} From c45ce27b586380b14d37baefc3728d55c8356607 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 4 Mar 2025 23:23:54 -0500 Subject: [PATCH 178/201] Adds missing license header Signed-off-by: Darshit Chanpura --- .../nonsystemindex/ResourceNonSystemIndexPlugin.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexPlugin.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexPlugin.java index 3a9497b281..3f6f2acafd 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexPlugin.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/nonsystemindex/ResourceNonSystemIndexPlugin.java @@ -1,3 +1,11 @@ +/* + * 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. + */ + package org.opensearch.sample.nonsystemindex; import java.nio.file.Path; From 3c8960292d09612d314788331499ab9550d38cbe Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 4 Mar 2025 23:24:37 -0500 Subject: [PATCH 179/201] Converts maven publication to shadow publication for all subprojects Signed-off-by: Darshit Chanpura --- .github/workflows/maven-publish.yml | 5 +---- build.gradle | 23 +++++++++----------- client/build.gradle | 13 ++++++------ common/build.gradle | 33 +++++------------------------ spi/build.gradle | 6 ++++-- 5 files changed, 26 insertions(+), 54 deletions(-) diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index 0ee195f952..42d07fbb0a 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -32,7 +32,4 @@ jobs: export SONATYPE_PASSWORD=$(aws secretsmanager get-secret-value --secret-id maven-snapshots-password --query SecretString --output text) echo "::add-mask::$SONATYPE_USERNAME" echo "::add-mask::$SONATYPE_PASSWORD" - ./gradlew --no-daemon :opensearch-resource-sharing-spi:publishMavenJavaPublicationToSnapshotsRepository - ./gradlew --no-daemon :opensearch-security-common:publishMavenJavaPublicationToSnapshotsRepository - ./gradlew --no-daemon :opensearch-security-client:publishMavenJavaPublicationToSnapshotsRepository - ./gradlew --no-daemon publishPluginZipPublicationToSnapshotsRepository + ./gradlew --no-daemon publishPluginZipPublicationToSnapshotsRepository publishShadowPublicationToSnapshotsRepository diff --git a/build.gradle b/build.gradle index 7fd109030c..4fc8567beb 100644 --- a/build.gradle +++ b/build.gradle @@ -235,17 +235,6 @@ def splitTestConfig = [ ] ] ], - resourceSharingTests: [ - description: "Runs most of the SSL tests.", - filters: [ - includeTestsMatching: [ - "org.opensearch.security.resources.*" - ], - excludeTestsMatching: [ - "org.opensearch.security.ssl.OpenSSL*" - ] - ] - ], ] as ConfigObject List taskNames = splitTestConfig.keySet() as List @@ -527,8 +516,17 @@ configurations { allprojects { configurations { integrationTestImplementation.extendsFrom implementation + compile.extendsFrom compileOnly + compile.extendsFrom testImplementation } dependencies { + compileOnly "com.google.guava:guava:${guava_version}" + // unit test framework + testImplementation 'org.hamcrest:hamcrest:2.2' + testImplementation 'junit:junit:4.13.2' + testImplementation "org.opensearch:opensearch:${opensearch_version}" + testImplementation "org.mockito:mockito-core:5.15.2" + //integration test framework: integrationTestImplementation('com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.2') { exclude(group: 'junit', module: 'junit') @@ -648,8 +646,7 @@ tasks.integrationTest.finalizedBy(jacocoTestReport) // report is always generate check.dependsOn integrationTest dependencies { - implementation project(path: ":opensearch-resource-sharing-spi") - implementation project(path: ":opensearch-security-common") + implementation project(path: ":${rootProject.name}-common", configuration: 'shadow') implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" implementation "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}" implementation "org.apache.httpcomponents.client5:httpclient5-cache:${versions.httpclient5}" diff --git a/client/build.gradle b/client/build.gradle index 6ec8cd0c8c..54579f6a12 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -6,6 +6,7 @@ plugins { id 'java' id 'maven-publish' + id 'io.github.goooler.shadow' version "8.1.7" } ext { @@ -17,8 +18,6 @@ ext { version_tokens = opensearch_version.tokenize('-') opensearch_build = version_tokens[0] + '.0' - common_utils_version = System.getProperty("common_utils.version", '3.0.0.0-alpha1-SNAPSHOT') - if (buildVersionQualifier) { opensearch_build += "-${buildVersionQualifier}" } @@ -34,10 +33,9 @@ repositories { } dependencies { - // Main implementation dependencies compileOnly "org.opensearch:opensearch:${opensearch_version}" - implementation "org.opensearch:opensearch-security-common:${opensearch_build}" - implementation "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" + // spi dependency comes through common + implementation project(path: ":${rootProject.name}-common", configuration: 'shadow') } java { @@ -57,12 +55,13 @@ task javadocJar(type: Jar) { publishing { publications { - mavenJava(MavenPublication) { - from components.java + shadow(MavenPublication) { publication -> + project.shadow.component(publication) artifact sourcesJar artifact javadocJar pom { name.set("OpenSearch Security Client") + packaging = "jar" description.set("OpenSearch Security Client") url.set("https://github.com/opensearch-project/security") licenses { diff --git a/common/build.gradle b/common/build.gradle index 430dbab4e3..de5ab23780 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -6,34 +6,11 @@ plugins { id 'java' id 'maven-publish' + id 'io.github.goooler.shadow' version "8.1.7" } ext { opensearch_version = System.getProperty("opensearch.version", "3.0.0-alpha1-SNAPSHOT") - isSnapshot = "true" == System.getProperty("build.snapshot", "true") - buildVersionQualifier = System.getProperty("build.version_qualifier", "alpha1") - - // 2.0.0-rc1-SNAPSHOT -> 2.0.0.0-rc1-SNAPSHOT - version_tokens = opensearch_version.tokenize('-') - opensearch_build = version_tokens[0] + '.0' - - common_utils_version = System.getProperty("common_utils.version", '3.0.0.0-alpha1-SNAPSHOT') - - kafka_version = '3.7.1' - open_saml_version = '5.1.3' - open_saml_shib_version = "9.1.3" - one_login_java_saml = '2.9.0' - jjwt_version = '0.12.6' - guava_version = '33.4.0-jre' - jaxb_version = '2.3.9' - spring_version = '5.3.39' - - if (buildVersionQualifier) { - opensearch_build += "-${buildVersionQualifier}" - } - if (isSnapshot) { - opensearch_build += "-SNAPSHOT" - } } repositories { @@ -45,8 +22,7 @@ repositories { dependencies { compileOnly "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" compileOnly "org.opensearch.plugin:lang-painless:${opensearch_version}" - implementation "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" - compileOnly "com.google.guava:guava:${guava_version}" + implementation project(path: ":opensearch-resource-sharing-spi", configuration: 'shadow') compileOnly "org.apache.commons:commons-lang3:${versions.commonslang}" compileOnly 'com.password4j:password4j:1.8.2' } @@ -68,12 +44,13 @@ task javadocJar(type: Jar) { publishing { publications { - mavenJava(MavenPublication) { - from components.java + shadow(MavenPublication) { publication -> + project.shadow.component(publication) artifact sourcesJar artifact javadocJar pom { name.set("OpenSearch Security Common") + packaging = "jar" description.set("OpenSearch Security Common") url.set("https://github.com/opensearch-project/security") licenses { diff --git a/spi/build.gradle b/spi/build.gradle index ee79bc0785..4fa95e46ed 100644 --- a/spi/build.gradle +++ b/spi/build.gradle @@ -6,6 +6,7 @@ plugins { id 'java' id 'maven-publish' + id 'io.github.goooler.shadow' version "8.1.7" } ext { @@ -39,12 +40,13 @@ task javadocJar(type: Jar) { publishing { publications { - mavenJava(MavenPublication) { - from components.java + shadow(MavenPublication) { publication -> + project.shadow.component(publication) artifact sourcesJar artifact javadocJar pom { name.set("OpenSearch Resource Sharing SPI") + packaging = "jar" description.set("OpenSearch Security Resource Sharing") url.set("https://github.com/opensearch-project/security") licenses { From 98ff9fab44321f4fee75b39b129dcc3eece6db1c Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 5 Mar 2025 00:42:29 -0500 Subject: [PATCH 180/201] Fixes version reader in demo config installer and fixes a type CI workflow Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 88 +++++++++++++++++-- .../security/tools/democonfig/Installer.java | 10 ++- .../tools/democonfig/InstallerTests.java | 11 ++- 3 files changed, 94 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 390e1e52f0..5c1142ceae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,14 @@ jobs: ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false + - name: Cache artifacts for dependent jobs + uses: actions/cache@v4.2.2 + with: + path: ~/.m2/repository + key: maven-local-${{ github.run_id }} + restore-keys: | + maven-local- + test: name: test needs: [generate-test-list, publish-components-to-maven-local] @@ -71,6 +79,14 @@ jobs: - name: Checkout security uses: actions/checkout@v4 + - name: Restore Maven Local Cache + uses: actions/cache@v4.2.2 + with: + path: ~/.m2/repository + key: maven-local-${{ github.run_id }} + restore-keys: | + maven-local- + - name: Build and Test uses: gradle/gradle-build-action@v3 with: @@ -129,6 +145,14 @@ jobs: - name: Checkout security uses: actions/checkout@v4 + - name: Restore Maven Local Cache + uses: actions/cache@v4.2.2 + with: + path: ~/.m2/repository + key: maven-local-${{ github.run_id }} + restore-keys: | + maven-local- + - name: Run Integration Tests uses: gradle/gradle-build-action@v3 with: @@ -150,6 +174,48 @@ jobs: path: | ./build/reports/ + spi-tests: + name: spi-tests + needs: publish-components-to-maven-local + strategy: + fail-fast: false + matrix: + jdk: [21] + platform: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.platform }} + + steps: + - name: Set up JDK for build and test + uses: actions/setup-java@v4 + with: + distribution: temurin # Temurin is a distribution of adoptium + java-version: ${{ matrix.jdk }} + + - name: Checkout security + uses: actions/checkout@v4 + + - name: Restore Maven Local Cache + uses: actions/cache@v4.2.2 + with: + path: ~/.m2/repository + key: maven-local-${{ github.run_id }} + restore-keys: | + maven-local- + + - name: Run SPI Tests + uses: gradle/gradle-build-action@v3 + with: + cache-disabled: true + arguments: | + :opensearch-resource-sharing-spi:test -Dbuild.snapshot=false + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: spi-${{ matrix.platform }}-JDK${{ matrix.jdk }}-reports + path: | + ./build/reports/ + resource-tests: env: CI_ENVIRONMENT: resource-test @@ -171,6 +237,14 @@ jobs: - name: Checkout security uses: actions/checkout@v4 + - name: Restore Maven Local Cache + uses: actions/cache@v4.2.2 + with: + path: ~/.m2/repository + key: maven-local-${{ github.run_id }} + restore-keys: | + maven-local- + - name: Run Resource Tests uses: gradle/gradle-build-action@v3 with: @@ -277,31 +351,31 @@ jobs: # Publish Client ./gradlew :opensearch-security-client:publishToMavenLocal && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version.jar - ./gradlew :opensearch-security-client-spi:publishToMavenLocal -Dbuild.snapshot=false && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_no_snapshot.jar + ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_no_snapshot.jar ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier.jar ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar # Build artifacts - ./gradlew clean assemble && \ + ./gradlew assemble && \ test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version.zip - ./gradlew clean assemble -Dbuild.snapshot=false && \ + ./gradlew assemble -Dbuild.snapshot=false && \ test -s ./build/distributions/opensearch-security-$security_plugin_version_no_snapshot.zip && \ test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_no_snapshot.zip - ./gradlew clean assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && \ + ./gradlew assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && \ test -s ./build/distributions/opensearch-security-$security_plugin_version_only_number-$test_qualifier.zip && \ test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_only_number-$test_qualifier.zip - ./gradlew clean assemble -Dbuild.version_qualifier=$test_qualifier && \ + ./gradlew assemble -Dbuild.version_qualifier=$test_qualifier && \ test -s ./build/distributions/opensearch-security-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.zip && \ test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.zip - ./gradlew clean publishPluginZipPublicationToZipStagingRepository && \ + ./gradlew publishPluginZipPublicationToZipStagingRepository && \ test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ test -s ./build/distributions/opensearch-security-$security_plugin_version.pom - name: List files in build directory on failure if: failure() - run: ls -al ./build/distributions/ ./*/build/libs + run: ls -al ./*/build/libs/ ./build/distributions/ diff --git a/src/main/java/org/opensearch/security/tools/democonfig/Installer.java b/src/main/java/org/opensearch/security/tools/democonfig/Installer.java index f1ee81f84e..6c3e9b3eed 100644 --- a/src/main/java/org/opensearch/security/tools/democonfig/Installer.java +++ b/src/main/java/org/opensearch/security/tools/democonfig/Installer.java @@ -330,19 +330,21 @@ void setSecurityVariables() { // Extract OpenSearch version and Security version File[] opensearchLibFiles = new File(OPENSEARCH_LIB_PATH).listFiles( - pathname -> pathname.getName().matches("opensearch-core-(.*).jar") + pathname -> pathname.getName().matches("opensearch-core-(\\d+(\\.\\d+)*(-[a-zA-Z0-9]+)?(-SNAPSHOT)?).jar") ); if (opensearchLibFiles != null && opensearchLibFiles.length > 0) { - OPENSEARCH_VERSION = opensearchLibFiles[0].getName().replaceAll("opensearch-core-(.*).jar", "$1"); + OPENSEARCH_VERSION = opensearchLibFiles[0].getName() + .replaceAll("opensearch-core-(\\d+(\\.\\d+)*(-[a-zA-Z0-9]+)?(-SNAPSHOT)?).jar", "$1"); } File[] securityFiles = new File(OPENSEARCH_PLUGINS_DIR + "opensearch-security").listFiles( - pathname -> pathname.getName().startsWith("opensearch-security-") && pathname.getName().endsWith(".jar") + pathname -> pathname.getName().matches("opensearch-security-\\d+(\\.\\d+)*(-[a-zA-Z0-9]+)?(-SNAPSHOT)?.jar") ); if (securityFiles != null && securityFiles.length > 0) { - SECURITY_VERSION = securityFiles[0].getName().replaceAll("opensearch-security-(.*).jar", "$1"); + SECURITY_VERSION = securityFiles[0].getName() + .replaceAll("opensearch-security-(\\d+(\\.\\d+)*(-[a-zA-Z0-9]+)?(-SNAPSHOT)?).jar", "$1"); } } diff --git a/src/test/java/org/opensearch/security/tools/democonfig/InstallerTests.java b/src/test/java/org/opensearch/security/tools/democonfig/InstallerTests.java index cef2d79725..ff065d70e3 100644 --- a/src/test/java/org/opensearch/security/tools/democonfig/InstallerTests.java +++ b/src/test/java/org/opensearch/security/tools/democonfig/InstallerTests.java @@ -307,8 +307,8 @@ public void testSetSecurityVariables() { setUpSecurityDirectories(); installer.setSecurityVariables(); - assertThat(installer.OPENSEARCH_VERSION, is(equalTo("osVersion"))); - assertThat(installer.SECURITY_VERSION, is(equalTo("version"))); + assertThat(installer.OPENSEARCH_VERSION, is(equalTo("3.0.0-Version"))); + assertThat(installer.SECURITY_VERSION, is(equalTo("3.0.0.0-version"))); tearDownSecurityDirectories(); } @@ -481,8 +481,11 @@ public void setUpSecurityDirectories() { createDirectory(installer.OPENSEARCH_LIB_PATH); createDirectory(installer.OPENSEARCH_CONF_DIR); createDirectory(installer.OPENSEARCH_PLUGINS_DIR + "opensearch-security"); - createFile(installer.OPENSEARCH_LIB_PATH + "opensearch-core-osVersion.jar"); - createFile(installer.OPENSEARCH_PLUGINS_DIR + "opensearch-security" + File.separator + "opensearch-security-version.jar"); + createFile(installer.OPENSEARCH_LIB_PATH + "opensearch-core-3.0.0-Version.jar"); + createFile( + installer.OPENSEARCH_PLUGINS_DIR + "opensearch-security" + File.separator + "opensearch-security-common-3.0.0.0-version.jar" + ); + createFile(installer.OPENSEARCH_PLUGINS_DIR + "opensearch-security" + File.separator + "opensearch-security-3.0.0.0-version.jar"); createFile(installer.OPENSEARCH_CONF_DIR + File.separator + "securityadmin_demo.sh"); } From ff06bf3ef755f38a18719ca5e88f24fbd1fd3bb0 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 5 Mar 2025 14:06:37 -0500 Subject: [PATCH 181/201] Cleans up build.gradle files and separates sample plugin integration test workflow Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 52 ++++++++++++++++++++++----- build.gradle | 6 ++-- publish-test.sh | 54 +++++++++++++++++++++++++++++ sample-resource-plugin/build.gradle | 8 ++--- 4 files changed, 102 insertions(+), 18 deletions(-) create mode 100755 publish-test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c1142ceae..36399f0cb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,7 +102,7 @@ jobs: ./build/reports/ report-coverage: - needs: ["test", "integration-tests"] + needs: ["test", "integration-tests", "spi-tests", "sample-plugin-integration-tests"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -127,7 +127,6 @@ jobs: integration-tests: name: integration-tests - needs: publish-components-to-maven-local strategy: fail-fast: false matrix: @@ -160,13 +159,6 @@ jobs: arguments: | :integrationTest -Dbuild.snapshot=false - - name: Run SampleResourcePlugin Integration Tests - uses: gradle/gradle-build-action@v3 - with: - cache-disabled: true - arguments: | - :opensearch-sample-resource-plugin:integrationTest -Dbuild.snapshot=false - - uses: actions/upload-artifact@v4 if: always() with: @@ -174,6 +166,48 @@ jobs: path: | ./build/reports/ + sample-plugin-integration-tests: + name: sample-plugin-integration-tests + needs: publish-components-to-maven-local + strategy: + fail-fast: false + matrix: + jdk: [21] + platform: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.platform }} + + steps: + - name: Set up JDK for build and test + uses: actions/setup-java@v4 + with: + distribution: temurin # Temurin is a distribution of adoptium + java-version: ${{ matrix.jdk }} + + - name: Checkout security + uses: actions/checkout@v4 + + - name: Restore Maven Local Cache + uses: actions/cache@v4.2.2 + with: + path: ~/.m2/repository + key: maven-local-${{ github.run_id }} + restore-keys: | + maven-local- + + - name: Run SampleResourcePlugin Integration Tests + uses: gradle/gradle-build-action@v3 + with: + cache-disabled: true + arguments: | + :opensearch-sample-resource-plugin:integrationTest -Dbuild.snapshot=false + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: sample-plugin-integration-${{ matrix.platform }}-JDK${{ matrix.jdk }}-reports + path: | + ./build/reports/ + spi-tests: name: spi-tests needs: publish-components-to-maven-local diff --git a/build.gradle b/build.gradle index 4fc8567beb..3aed75061b 100644 --- a/build.gradle +++ b/build.gradle @@ -569,9 +569,9 @@ allprojects { integrationTestImplementation 'org.slf4j:slf4j-api:2.0.12' integrationTestImplementation 'com.selectivem.collections:special-collections-complete:1.4.0' integrationTestImplementation "org.opensearch.plugin:lang-painless:${opensearch_version}" - integrationTestImplementation project(path:":opensearch-security-common") - integrationTestImplementation project(path:":opensearch-security-client") - integrationTestImplementation project(path:":opensearch-resource-sharing-spi") + integrationTestImplementation project(path:":opensearch-resource-sharing-spi", configuration: 'shadow') + integrationTestImplementation project(path: ":${rootProject.name}-common", configuration: 'shadow') + integrationTestImplementation project(path: ":${rootProject.name}-client", configuration: 'shadow') } } diff --git a/publish-test.sh b/publish-test.sh new file mode 100755 index 0000000000..bd110a6c31 --- /dev/null +++ b/publish-test.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# Set version variables +security_plugin_version=$(./gradlew properties -q | grep -E '^version:' | awk '{print $2}') +security_plugin_version_no_snapshot=$(echo $security_plugin_version | sed 's/-SNAPSHOT//g') +security_plugin_version_only_number=$(echo $security_plugin_version_no_snapshot | cut -d- -f1) +test_qualifier=alpha2 + +# Debug print versions +echo "Versions:" +echo "security_plugin_version: $security_plugin_version" +echo "security_plugin_version_no_snapshot: $security_plugin_version_no_snapshot" +echo "security_plugin_version_only_number: $security_plugin_version_only_number" +echo "test_qualifier: $test_qualifier" + +echo "Publish SPI" +./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version.jar +./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_no_snapshot.jar +./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier.jar +./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar + +echo "Publish Common" +./gradlew :opensearch-security-common:publishToMavenLocal && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version.jar +./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_no_snapshot.jar +./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier.jar +./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar + +echo "Publish Client" +./gradlew :opensearch-security-client:publishToMavenLocal && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version.jar +./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_no_snapshot.jar +./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier.jar +./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar + +echo "Build artifacts" +./gradlew assemble && \ +test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ +test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version.zip + +./gradlew assemble -Dbuild.snapshot=false && \ +test -s ./build/distributions/opensearch-security-$security_plugin_version_no_snapshot.zip && \ +test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_no_snapshot.zip + +./gradlew assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && \ +test -s ./build/distributions/opensearch-security-$security_plugin_version_only_number-$test_qualifier.zip && \ +test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_only_number-$test_qualifier.zip + +./gradlew assemble -Dbuild.version_qualifier=$test_qualifier && \ +test -s ./build/distributions/opensearch-security-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.zip && \ +test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.zip + +echo "Publish Plugin zip" +./gradlew publishPluginZipPublicationToZipStagingRepository && \ +test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ +test -s ./build/distributions/opensearch-security-$security_plugin_version.pom diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle index 2962a10f1c..b5e3834d83 100644 --- a/sample-resource-plugin/build.gradle +++ b/sample-resource-plugin/build.gradle @@ -66,23 +66,19 @@ configurations.all { 'org.mockito:mockito-core:5.15.2', 'net.bytebuddy:byte-buddy:1.15.11', 'commons-codec:commons-codec:1.16.1', - 'com.fasterxml.jackson.core:jackson-databind:2.18.2', 'com.fasterxml.jackson.core:jackson-databind:2.18.2' } } dependencies { // Main implementation dependencies - compileOnly "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" - compileOnly "org.opensearch:opensearch-security-client:${opensearch_build}" + compileOnly project(path: ":opensearch-resource-sharing-spi", configuration: 'shadow') + compileOnly project(path: ":${rootProject.name}-client", configuration: 'shadow') compileOnly "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" // Integration test dependencies integrationTestImplementation rootProject.sourceSets.integrationTest.output integrationTestImplementation rootProject.sourceSets.main.output - integrationTestImplementation "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" - integrationTestImplementation "org.opensearch:opensearch-security-common:${opensearch_build}" - integrationTestImplementation "org.opensearch:opensearch-security-client:${opensearch_build}" } sourceSets { From a9c8e3c1f3bbfbb62416db812c76b926d6d24b4d Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 5 Mar 2025 14:38:44 -0500 Subject: [PATCH 182/201] Remove temp script Signed-off-by: Darshit Chanpura --- publish-test.sh | 54 ------------------------------------------------- 1 file changed, 54 deletions(-) delete mode 100755 publish-test.sh diff --git a/publish-test.sh b/publish-test.sh deleted file mode 100755 index bd110a6c31..0000000000 --- a/publish-test.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash - -# Set version variables -security_plugin_version=$(./gradlew properties -q | grep -E '^version:' | awk '{print $2}') -security_plugin_version_no_snapshot=$(echo $security_plugin_version | sed 's/-SNAPSHOT//g') -security_plugin_version_only_number=$(echo $security_plugin_version_no_snapshot | cut -d- -f1) -test_qualifier=alpha2 - -# Debug print versions -echo "Versions:" -echo "security_plugin_version: $security_plugin_version" -echo "security_plugin_version_no_snapshot: $security_plugin_version_no_snapshot" -echo "security_plugin_version_only_number: $security_plugin_version_only_number" -echo "test_qualifier: $test_qualifier" - -echo "Publish SPI" -./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version.jar -./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_no_snapshot.jar -./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier.jar -./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar - -echo "Publish Common" -./gradlew :opensearch-security-common:publishToMavenLocal && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version.jar -./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_no_snapshot.jar -./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier.jar -./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar - -echo "Publish Client" -./gradlew :opensearch-security-client:publishToMavenLocal && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version.jar -./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_no_snapshot.jar -./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier.jar -./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar - -echo "Build artifacts" -./gradlew assemble && \ -test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ -test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version.zip - -./gradlew assemble -Dbuild.snapshot=false && \ -test -s ./build/distributions/opensearch-security-$security_plugin_version_no_snapshot.zip && \ -test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_no_snapshot.zip - -./gradlew assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && \ -test -s ./build/distributions/opensearch-security-$security_plugin_version_only_number-$test_qualifier.zip && \ -test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_only_number-$test_qualifier.zip - -./gradlew assemble -Dbuild.version_qualifier=$test_qualifier && \ -test -s ./build/distributions/opensearch-security-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.zip && \ -test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.zip - -echo "Publish Plugin zip" -./gradlew publishPluginZipPublicationToZipStagingRepository && \ -test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ -test -s ./build/distributions/opensearch-security-$security_plugin_version.pom From 6e9516d3bb9dc972c80bf554b7ad506590d79c29 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 5 Mar 2025 22:30:53 -0500 Subject: [PATCH 183/201] Fixes guava dependencies Signed-off-by: Darshit Chanpura --- build.gradle | 3 +-- common/build.gradle | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 3aed75061b..6f9ac75758 100644 --- a/build.gradle +++ b/build.gradle @@ -234,7 +234,7 @@ def splitTestConfig = [ "org.opensearch.security.ssl.OpenSSL*" ] ] - ], + ] ] as ConfigObject List taskNames = splitTestConfig.keySet() as List @@ -520,7 +520,6 @@ allprojects { compile.extendsFrom testImplementation } dependencies { - compileOnly "com.google.guava:guava:${guava_version}" // unit test framework testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'junit:junit:4.13.2' diff --git a/common/build.gradle b/common/build.gradle index de5ab23780..b509c39ec8 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -25,6 +25,7 @@ dependencies { implementation project(path: ":opensearch-resource-sharing-spi", configuration: 'shadow') compileOnly "org.apache.commons:commons-lang3:${versions.commonslang}" compileOnly 'com.password4j:password4j:1.8.2' + compileOnly "com.google.guava:guava:${guava_version}" } java { From c3c549453b49d16161469081b97e7066bc3cdd55 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 6 Mar 2025 01:34:28 -0500 Subject: [PATCH 184/201] Fixes build artifacts workflow Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 64 ++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36399f0cb8..88091ae2b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -372,43 +372,63 @@ jobs: echo $test_qualifier # Publish SPI - ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version.jar - ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_no_snapshot.jar - ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier.jar - ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar + ./gradlew clean :opensearch-resource-sharing-spi:publishToMavenLocal && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version-all.jar + ./gradlew clean :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_no_snapshot-all.jar + ./gradlew clean :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-all.jar + ./gradlew clean :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT-all.jar # Publish Common - ./gradlew :opensearch-security-common:publishToMavenLocal && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version.jar - ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_no_snapshot.jar - ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier.jar - ./gradlew :opensearch-security-common:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar + ./gradlew clean :opensearch-security-common:publishToMavenLocal && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version-all.jar + ./gradlew clean :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_no_snapshot-all.jar + ./gradlew clean :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier-all.jar + ./gradlew clean :opensearch-security-common:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT-all.jar # Publish Client - ./gradlew :opensearch-security-client:publishToMavenLocal && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version.jar - ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_no_snapshot.jar - ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier.jar - ./gradlew :opensearch-security-client:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar + ./gradlew clean :opensearch-security-client:publishToMavenLocal && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version-all.jar + ./gradlew clean :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_no_snapshot-all.jar + ./gradlew clean :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier-all.jar + ./gradlew clean :opensearch-security-client:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT-all.jar # Build artifacts - ./gradlew assemble && \ + ./gradlew clean :assemble && \ test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ - test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version.zip + test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version.zip && \ + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version.jar && \ + test -s ./common/build/libs/opensearch-security-common-$security_plugin_version.jar && \ + test -s ./client/build/libs/opensearch-security-client-$security_plugin_version.jar - ./gradlew assemble -Dbuild.snapshot=false && \ + + ./gradlew clean assemble -Dbuild.snapshot=false && \ test -s ./build/distributions/opensearch-security-$security_plugin_version_no_snapshot.zip && \ - test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_no_snapshot.zip + test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_no_snapshot.zip && \ + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_no_snapshot.jar && \ + test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_no_snapshot.jar && \ + test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_no_snapshot.jar - ./gradlew assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && \ + ./gradlew clean assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && \ test -s ./build/distributions/opensearch-security-$security_plugin_version_only_number-$test_qualifier.zip && \ - test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_only_number-$test_qualifier.zip + test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_only_number-$test_qualifier.zip && \ + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier.jar && \ + test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier.jar && \ + test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier.jar - ./gradlew assemble -Dbuild.version_qualifier=$test_qualifier && \ + ./gradlew clean assemble -Dbuild.version_qualifier=$test_qualifier && \ test -s ./build/distributions/opensearch-security-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.zip && \ - test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.zip + test -s ./sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.zip && \ + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar && \ + test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar && \ + test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar - ./gradlew publishPluginZipPublicationToZipStagingRepository && \ + ./gradlew clean publishPluginZipPublicationToZipStagingRepository && \ test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ - test -s ./build/distributions/opensearch-security-$security_plugin_version.pom + test -s ./build/distributions/opensearch-security-$security_plugin_version.pom && \ + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version-all.jar && \ + test -s ./common/build/libs/opensearch-security-common-$security_plugin_version-all.jar + + ./gradlew clean publishShadowPublicationToMavenLocal && \ + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version-all.jar && \ + test -s ./common/build/libs/opensearch-security-common-$security_plugin_version-all.jar && \ + test -s ./client/build/libs/opensearch-security-client-$security_plugin_version-all.jar - name: List files in build directory on failure if: failure() From d1e6469521cb026fe17c16b85796a626cd6cd954 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 6 Mar 2025 17:22:42 -0500 Subject: [PATCH 185/201] Adds doc and completes README for client package Signed-off-by: Darshit Chanpura --- client/README.md | 69 ++++++++++++++++++- client/build.gradle | 3 +- .../resources/ResourceSharingClient.java | 22 ++++++ .../resources/ResourceSharingNodeClient.java | 35 ++++++++++ .../client/resources/package-info.java | 2 +- 5 files changed, 127 insertions(+), 4 deletions(-) diff --git a/client/README.md b/client/README.md index 5325c6e747..8f5b20b093 100644 --- a/client/README.md +++ b/client/README.md @@ -1,6 +1,71 @@ -# Resource Sharing and Access Control SPI +# Resource Sharing Client -This Client package provides ResourceSharing client to be utilized by resource plugins to implement access control by communicating with security plugins. +This Client package provides a ResourceSharing client to be utilized by resource plugins to implement access control by communicating with security plugin. + +## Usage + +1. Create a client accessor with singleton pattern: +```java +public class ResourceSharingClientAccessor { + private static ResourceSharingNodeClient INSTANCE; + + private ResourceSharingClientAccessor() {} + + /** + * Get resource sharing client + * + * @param nodeClient node client + * @return resource sharing client + */ + public static ResourceSharingNodeClient getResourceSharingClient(NodeClient nodeClient, Settings settings) { + if (INSTANCE == null) { + INSTANCE = new ResourceSharingNodeClient(nodeClient, settings); + } + return INSTANCE; + } +} +``` + +2. In your transport action doExecute function call the client. +Here is an example implementation of client being utilized to verify delete permissions before deleting a resource. +```java +@Override +protected void doExecute(Task task, DeleteResourceRequest request, ActionListener listener) { + + String resourceId = request.getResourceId(); + ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient, settings); + resourceSharingClient.verifyResourceAccess( + resourceId, + RESOURCE_INDEX_NAME, + SampleResourceScope.PUBLIC.value(), + ActionListener.wrap(isAuthorized -> { + if (!isAuthorized) { + listener.onFailure(new ResourceSharingException("Current user is not authorized to delete resource: " + resourceId)); + return; + } + + // Authorization successful, proceed with deletion + ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); + try (ThreadContext.StoredContext ignored = threadContext.stashContext()) { + deleteResource(resourceId, ActionListener.wrap(deleteResponse -> { + if (deleteResponse.getResult() == DocWriteResponse.Result.NOT_FOUND) { + listener.onFailure(new ResourceNotFoundException("Resource " + resourceId + " not found.")); + } else { + listener.onResponse(new DeleteResourceResponse("Resource " + resourceId + " deleted successfully.")); + } + }, exception -> { + log.error("Failed to delete resource: " + resourceId, exception); + listener.onFailure(exception); + })); + } + }, exception -> { + log.error("Failed to verify resource access: " + resourceId, exception); + listener.onFailure(exception); + }) + ); +} +``` +You can checkout other java APIs offered by the client by visiting ResourceSharingClient.java ## License diff --git a/client/build.gradle b/client/build.gradle index 54579f6a12..d958619ec5 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -34,7 +34,8 @@ repositories { dependencies { compileOnly "org.opensearch:opensearch:${opensearch_version}" - // spi dependency comes through common + compileOnly project(path: ":opensearch-resource-sharing-spi", configuration: 'shadow') + // spi runtime dependency comes through opensearch-security-common implementation project(path: ":${rootProject.name}-common", configuration: 'shadow') } diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java index 282dc741f3..dfece1f4d9 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java @@ -19,10 +19,32 @@ */ public interface ResourceSharingClient { + /** + * Verifies if the current user has access to the specified resource. + * @param resourceId The ID of the resource to verify access for. + * @param resourceIndex The index containing the resource. + * @param scope The scope of the resource. + * @param listener The listener to be notified with the access verification result. + */ void verifyResourceAccess(String resourceId, String resourceIndex, String scope, ActionListener listener); + /** + * Shares a resource with the specified users, roles, and backend roles. + * @param resourceId The ID of the resource to share. + * @param resourceIndex The index containing the resource. + * @param shareWith The users, roles, and backend roles to share the resource with. + * @param listener The listener to be notified with the updated ResourceSharing document. + */ void shareResource(String resourceId, String resourceIndex, Map shareWith, ActionListener listener); + /** + * Revokes access to a resource for the specified entities and scopes. + * @param resourceId The ID of the resource to revoke access for. + * @param resourceIndex The index containing the resource. + * @param entitiesToRevoke The entities to revoke access for. + * @param scopes The scopes to revoke access for. + * @param listener The listener to be notified with the updated ResourceSharing document. + */ void revokeResourceAccess( String resourceId, String resourceIndex, diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java index 759914d80e..fd99b942b7 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java @@ -43,6 +43,14 @@ public ResourceSharingNodeClient(Client client, Settings settings) { ); } + /** + * Verifies if the current user has access to the specified resource. + * @param resourceId The ID of the resource to verify access for. + * @param resourceIndex The index containing the resource. + * @param scope The scope of the resource. + * @param listener The listener to be notified with the access verification result. + */ + @Override public void verifyResourceAccess(String resourceId, String resourceIndex, String scope, ActionListener listener) { if (!resourceSharingEnabled) { log.warn("Resource Access Control feature is disabled. Access to resource is automatically granted."); @@ -57,6 +65,14 @@ public void verifyResourceAccess(String resourceId, String resourceIndex, String client.execute(ResourceAccessAction.INSTANCE, request, verifyAccessResponseListener(listener)); } + /** + * Shares the specified resource with the given users, roles, and backend roles. + * @param resourceId The ID of the resource to share. + * @param resourceIndex The index containing the resource. + * @param shareWith The users, roles, and backend roles to share the resource with. + * @param listener The listener to be notified with the updated ResourceSharing document. + */ + @Override public void shareResource( String resourceId, String resourceIndex, @@ -81,6 +97,15 @@ public void shareResource( client.execute(ResourceAccessAction.INSTANCE, request, sharingInfoResponseListener(listener)); } + /** + * Revokes access to the specified resource for the given entities and scopes. + * @param resourceId The ID of the resource to revoke access for. + * @param resourceIndex The index containing the resource. + * @param entitiesToRevoke The entities to revoke access for. + * @param scopes The scopes to revoke access for. + * @param listener The listener to be notified with the updated ResourceSharing document. + */ + @Override public void revokeResourceAccess( String resourceId, String resourceIndex, @@ -107,10 +132,20 @@ public void revokeResourceAccess( client.execute(ResourceAccessAction.INSTANCE, request, sharingInfoResponseListener(listener)); } + /** + * Notifies the listener with the access request result. + * @param listener The listener to be notified with the access request result. + * @return An ActionListener that handles the ResourceAccessResponse and notifies the listener. + */ private ActionListener verifyAccessResponseListener(ActionListener listener) { return ActionListener.wrap(response -> listener.onResponse(response.getHasPermission()), listener::onFailure); } + /** + * Notifies the listener with the updated ResourceSharing document. + * @param listener The listener to be notified with the updated ResourceSharing document. + * @return An ActionListener that handles the ResourceAccessResponse and notifies the listener. + */ private ActionListener sharingInfoResponseListener(ActionListener listener) { return ActionListener.wrap(response -> listener.onResponse(response.getResourceSharing()), listener::onFailure); } diff --git a/client/src/main/java/org/opensearch/security/client/resources/package-info.java b/client/src/main/java/org/opensearch/security/client/resources/package-info.java index 606d8affae..1e15c4c46d 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/package-info.java +++ b/client/src/main/java/org/opensearch/security/client/resources/package-info.java @@ -7,7 +7,7 @@ */ /** - * This package defines a resource sharing client that will be utilized by resource plugins to call security plugin's APIs + * This package defines a resource sharing client that will be utilized by resource plugins to call security plugin's transport actions, which handle resource access * * @opensearch.experimental */ From dfeaca8b7b54a117635b3878522428ceebaefa45 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 6 Mar 2025 17:33:22 -0500 Subject: [PATCH 186/201] Updates doc for common package Signed-off-by: Darshit Chanpura --- .../security/common/package-info.java | 1 + .../resources/ResourceAccessHandler.java | 13 ++- .../resources/ResourceIndexListener.java | 99 ++----------------- .../common/resources/ResourcePluginInfo.java | 4 + .../resources/ResourceSharingConstants.java | 3 + .../ResourceSharingIndexHandler.java | 4 +- ...ourceSharingIndexManagementRepository.java | 4 + 7 files changed, 33 insertions(+), 95 deletions(-) diff --git a/common/src/main/java/org/opensearch/security/common/package-info.java b/common/src/main/java/org/opensearch/security/common/package-info.java index d6651ffd42..01e2ead134 100644 --- a/common/src/main/java/org/opensearch/security/common/package-info.java +++ b/common/src/main/java/org/opensearch/security/common/package-info.java @@ -8,6 +8,7 @@ /** * This package defines common classes required to implement resource access control in OpenSearch. + * TODO: At present it contains multiple duplicates, which will be address in a fast follow PR. * * @opensearch.experimental */ diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java index 1bb48bb77d..abb2c73277 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java @@ -366,7 +366,15 @@ public void revokeAccess( ); } - public void checkRawAccessPermission(String resourceId, String resourceIndex, ActionListener listener) { + /** + * Checks if the current user has permission to modify a resource. + * NOTE: Only admins and owners of the resource can modify the resource. + * TODO: update this method to allow for other users to modify the resource. + * @param resourceId The resource ID to check. + * @param resourceIndex The resource index containing the resource. + * @param listener The listener to be notified with the permission check result. + */ + public void canModifyResource(String resourceId, String resourceIndex, ActionListener listener) { try { validateArguments(resourceId, resourceIndex); @@ -386,7 +394,8 @@ public void checkRawAccessPermission(String resourceId, String resourceIndex, Ac fetchDocListener.whenComplete(document -> { if (document == null) { LOGGER.info("Document {} does not exist in index {}", resourceId, resourceIndex); - listener.onResponse(false); + // Either the document was deleted or has not been created yet. No permission check is needed for this. + listener.onResponse(true); return; } diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java index 3222a1e607..728fe4691c 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java @@ -10,16 +10,12 @@ import java.io.IOException; import java.util.Objects; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.OpenSearchSecurityException; import org.opensearch.core.action.ActionListener; import org.opensearch.core.index.shard.ShardId; -import org.opensearch.core.rest.RestStatus; import org.opensearch.index.engine.Engine; import org.opensearch.index.shard.IndexingOperationListener; import org.opensearch.security.common.auth.UserSubjectImpl; @@ -34,7 +30,6 @@ /** * This class implements an index operation listener for operations performed on resources stored in plugin's indices. - * It verifies permissions before allowing update/delete operations. */ public class ResourceIndexListener implements IndexingOperationListener { @@ -71,44 +66,8 @@ public boolean isInitialized() { } /** - * Ensures that the user has permission to update before proceeding. + * Creates a resource sharing entry for the newly created resource. */ - @Override - public Engine.Index preIndex(ShardId shardId, Engine.Index index) { - String resourceIndex = shardId.getIndexName(); - log.debug("preIndex called on {}", resourceIndex); - String resourceId = index.id(); - - // Validate permissions - if (checkPermission(resourceId, resourceIndex, index.operationType().name())) { - return index; - } - - throw new OpenSearchSecurityException( - "Index operation not permitted for resource " + resourceId + " in index " + resourceIndex + "for current user", - RestStatus.FORBIDDEN - ); - } - - /** - * Ensures that the user has permission to delete before proceeding. - */ - @Override - public Engine.Delete preDelete(ShardId shardId, Engine.Delete delete) { - String resourceIndex = shardId.getIndexName(); - log.debug("preDelete called on {}", resourceIndex); - String resourceId = delete.id(); - - if (checkPermission(resourceId, resourceIndex, delete.operationType().name())) { - return delete; - } - - throw new OpenSearchSecurityException( - "Delete operation not permitted for resource " + resourceId + " in index " + resourceIndex + "for current user", - RestStatus.FORBIDDEN - ); - } - @Override public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { String resourceIndex = shardId.getIndexName(); @@ -134,12 +93,15 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re new CreatedBy(Creator.USER, user.getName()), null ); - log.info("Successfully created a resource sharing entry {}", sharing); + log.debug("Successfully created a resource sharing entry {}", sharing); } catch (IOException e) { - log.error("Failed to create a resource sharing entry for resource: {}", resourceId, e); + log.debug("Failed to create a resource sharing entry for resource: {}", resourceId, e); } } + /** + * Deletes the resource sharing entry for the deleted resource. + */ @Override public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { String resourceIndex = shardId.getIndexName(); @@ -148,55 +110,10 @@ public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResul String resourceId = delete.id(); this.resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex, ActionListener.wrap(deleted -> { if (deleted) { - log.info("Successfully deleted resource sharing entry for resource {}", resourceId); + log.debug("Successfully deleted resource sharing entry for resource {}", resourceId); } else { - log.info("No resource sharing entry found for resource {}", resourceId); + log.debug("No resource sharing entry found for resource {}", resourceId); } }, exception -> log.error("Failed to delete resource sharing entry for resource {}", resourceId, exception))); } - - /** - * Helper method to check permissions synchronously using CountDownLatch. - */ - private boolean checkPermission(String resourceId, String resourceIndex, String operation) { - CountDownLatch latch = new CountDownLatch(1); - AtomicReference permissionGranted = new AtomicReference<>(false); - AtomicReference exceptionRef = new AtomicReference<>(null); - - this.resourceAccessHandler.checkRawAccessPermission(resourceId, resourceIndex, new ActionListener() { - @Override - public void onResponse(Boolean hasPermission) { - permissionGranted.set(hasPermission); - latch.countDown(); - } - - @Override - public void onFailure(Exception e) { - exceptionRef.set(e); - latch.countDown(); - } - }); - - try { - latch.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new OpenSearchSecurityException( - "Interrupted while checking " + operation + " permission for resource " + resourceId, - e, - RestStatus.INTERNAL_SERVER_ERROR - ); - } - - if (exceptionRef.get() != null) { - log.error("Failed to check {} permission for resource {}", operation, resourceId, exceptionRef.get()); - throw new OpenSearchSecurityException( - "Failed to check " + operation + " permission for resource " + resourceId, - exceptionRef.get(), - RestStatus.INTERNAL_SERVER_ERROR - ); - } - - return permissionGranted.get(); - } } diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java b/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java index 7b7f6e23b1..cbeedbac82 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java @@ -10,6 +10,10 @@ import org.opensearch.security.spi.resources.ResourceProvider; +/** + * This class provides information about resource plugins and their associated resource providers and indices. + * It follows the Singleton pattern to ensure that only one instance of the class exists. + */ public class ResourcePluginInfo { private static ResourcePluginInfo INSTANCE; diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java index 387254cbf7..0bc5f5b99d 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java @@ -10,6 +10,9 @@ */ package org.opensearch.security.common.resources; +/** + * This class contains constants related to resource sharing in OpenSearch. + */ public class ResourceSharingConstants { // Resource sharing index public static final String OPENSEARCH_RESOURCE_SHARING_INDEX = ".opensearch_resource_sharing"; diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java index cdec3b7ffe..d119996b57 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java @@ -115,7 +115,7 @@ public ResourceSharingIndexHandler(final String indexName, final Client client, * - created_by (object): Information about the user who created the sharing * - user (keyword): Username of the creator * - share_with (object): Access control configuration for shared resources - * - [group_name] (object): Name of the access group + * - [scope] (object): Name of the scope * - users (array): List of users with access * - roles (array): List of roles with access * - backend_roles (array): List of backend roles with access @@ -160,7 +160,7 @@ public void createResourceSharingIndexIfAbsent(Callable callable) { * @param shareWith Object containing the sharing permissions' configuration. Can be null for initial creation. * When provided, it should contain the access control settings for different groups: * { - * "group_name": { + * "scope": { * "users": ["user1", "user2"], * "roles": ["role1", "role2"], * "backend_roles": ["backend_role1"] diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java index eb3d5b3fa2..b76aeb9471 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java @@ -14,6 +14,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +/** + * This class is responsible for managing the resource sharing index. + * It provides methods to create the index if it doesn't exist. + */ public class ResourceSharingIndexManagementRepository { private static final Logger log = LogManager.getLogger(ResourceSharingIndexManagementRepository.class); From 3064cd2816d70eeb7d007fadb43f2dc3683f09e7 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 6 Mar 2025 17:50:46 -0500 Subject: [PATCH 187/201] Updates doc for sample resource plugin and fixes some code duplication Signed-off-by: Darshit Chanpura --- sample-resource-plugin/README.md | 10 +++-- ...esourcePluginSystemIndexDisabledTests.java | 44 +++++-------------- .../sample/SampleResourcePluginTests.java | 44 +++++-------------- .../org/opensearch/sample/SampleResource.java | 3 ++ .../sample/SampleResourceParser.java | 3 ++ .../sample/SampleResourcePlugin.java | 2 +- .../sample/SampleResourceScope.java | 5 ++- .../rest/create/CreateResourceRestAction.java | 9 ++-- .../rest/create/UpdateResourceAction.java | 4 +- .../rest/delete/DeleteResourceAction.java | 6 +-- .../rest/delete/DeleteResourceRequest.java | 2 +- .../rest/delete/DeleteResourceResponse.java | 3 ++ .../rest/delete/DeleteResourceRestAction.java | 3 ++ .../rest/get/GetResourceRestAction.java | 3 ++ .../revoke/RevokeResourceAccessAction.java | 4 +- .../revoke/RevokeResourceAccessRequest.java | 2 +- .../revoke/RevokeResourceAccessResponse.java | 3 ++ .../RevokeResourceAccessRestAction.java | 3 ++ .../rest/share/ShareResourceResponse.java | 3 ++ .../rest/share/ShareResourceRestAction.java | 3 ++ .../CreateResourceTransportAction.java | 3 ++ .../DeleteResourceTransportAction.java | 3 ++ .../transport/GetResourceTransportAction.java | 3 ++ .../RevokeResourceAccessTransportAction.java | 3 ++ .../ShareResourceTransportAction.java | 3 ++ .../UpdateResourceTransportAction.java | 3 ++ .../client/ResourceSharingClientAccessor.java | 7 ++- .../opensearch/sample/utils/Constants.java | 3 ++ .../utils/SampleResourcePluginException.java | 17 ------- 29 files changed, 103 insertions(+), 101 deletions(-) delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/utils/SampleResourcePluginException.java diff --git a/sample-resource-plugin/README.md b/sample-resource-plugin/README.md index 7d1724242d..d5e5fdbc8b 100644 --- a/sample-resource-plugin/README.md +++ b/sample-resource-plugin/README.md @@ -1,12 +1,17 @@ # Resource Sharing and Access Control Plugin This plugin demonstrates resource sharing and access control functionality, providing sample resource APIs and marking it as a resource sharing plugin via resource-sharing-spi. The access control is implemented on Security plugin and will be performed under the hood. +At present only admin and resource owners can modify/delete the resource ## PreRequisites -Publish SPI to local maven before proceeding: +Publish SPI, Common and Client to local maven before proceeding: ```shell ./gradlew clean :opensearch-resource-sharing-spi:publishToMavenLocal + +./gradlew clean :opensearch-security-common:publishToMavenLocal + +./gradlew clean :opensearch-security-client:publishToMavenLocal ``` System index feature must be enabled to prevent direct access to resource. Add the following setting in case it has not already been enabled. @@ -16,7 +21,7 @@ plugins.security.system_indices.enabled: true ## Features -- Create, update and delete resources. +- Create, update, get, delete resources, as well as share and revoke access to a resource. ## API Endpoints @@ -64,7 +69,6 @@ The plugin exposes the following six API endpoints: } ``` - ### 4. Get Resource - **Endpoint:** `GET /_plugins/sample_resource_sharing/get/{resource_id}` - **Description:** Get a specified resource owned by the requesting user, if the user has access to the resource, else fails. diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java index ddf500fffc..f7aec65b6f 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java @@ -29,7 +29,7 @@ import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** - * These tests run with resource sharing enabled + * These tests run with resource sharing enabled but system index protection disabled */ public class SampleResourcePluginSystemIndexDisabledTests extends AbstractSampleResourcePluginFeatureEnabledTests { @@ -47,7 +47,7 @@ protected LocalCluster getLocalCluster() { } @Test - public void testRawAccess() throws Exception { + public void testDirectAccess() throws Exception { String resourceId; // create sample resource try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { @@ -101,36 +101,6 @@ public void testRawAccess() throws Exception { response.assertStatusCode(HttpStatus.SC_OK); } - // Create an entry in resource-sharing index - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually - String json = String.format( - "{" - + " \"source_idx\": \".sample_resource_sharing_plugin\"," - + " \"resource_id\": \"%s\"," - + " \"created_by\": {" - + " \"user\": \"admin\"" - + " }" - + "}", - resourceId - ); - HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); - assertThat(response.getStatusReason(), containsString("Created")); - // Also update the in-memory map and get - ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); - ResourceProvider provider = new ResourceProvider( - SampleResource.class.getCanonicalName(), - RESOURCE_INDEX_NAME, - new SampleResourceParser() - ); - ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); - - Thread.sleep(1000); - response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), containsString("sample")); - } - // shared_with_user will be able to access resource directly since system index protection is disabled even-though resource is not // shared with this user, but cannot access via sample plugin APIs try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { @@ -141,6 +111,16 @@ public void testRawAccess() throws Exception { response.assertStatusCode(HttpStatus.SC_FORBIDDEN); } + // Update sample resource shared_with_user will be able to update admin's resource because system index protection is disabled + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + TestRestClient.HttpResponse updateResponse = client.postJson( + RESOURCE_INDEX_NAME + "/_doc/" + resourceId, + sampleResourceUpdated + ); + updateResponse.assertStatusCode(HttpStatus.SC_OK); + } + // share resource with shared_with user try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { Thread.sleep(1000); diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index 07314b5e43..c18c068bda 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -32,7 +32,7 @@ import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** - * These tests run with resource sharing enabled and system index enabled + * These tests run with resource sharing enabled and system index protection enabled */ public class SampleResourcePluginTests extends AbstractSampleResourcePluginFeatureEnabledTests { @@ -51,7 +51,7 @@ protected LocalCluster getLocalCluster() { } @Test - public void testRawAccess() throws Exception { + public void testDirectAccess() throws Exception { String resourceId; // create sample resource try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { @@ -105,36 +105,6 @@ public void testRawAccess() throws Exception { response.assertStatusCode(HttpStatus.SC_OK); } - // Create an entry in resource-sharing index - try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually - String json = String.format( - "{" - + " \"source_idx\": \".sample_resource_sharing_plugin\"," - + " \"resource_id\": \"%s\"," - + " \"created_by\": {" - + " \"user\": \"admin\"" - + " }" - + "}", - resourceId - ); - HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); - assertThat(response.getStatusReason(), containsString("Created")); - // Also update the in-memory map and get - ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); - ResourceProvider provider = new ResourceProvider( - SampleResource.class.getCanonicalName(), - RESOURCE_INDEX_NAME, - new SampleResourceParser() - ); - ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); - - Thread.sleep(1000); - response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); - response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), containsString("sample")); - } - // shared_with_user should not be able to delete the resource since system index protection is enabled try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { HttpResponse response = client.delete(RESOURCE_INDEX_NAME + "/_doc/" + resourceId); @@ -151,6 +121,16 @@ public void testRawAccess() throws Exception { response.assertStatusCode(HttpStatus.SC_FORBIDDEN); } + // Update sample resource (shared_with_user cannot update admin's resource) because system index protection is enabled + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + TestRestClient.HttpResponse updateResponse = client.postJson( + RESOURCE_INDEX_NAME + "/_doc/" + resourceId, + sampleResourceUpdated + ); + updateResponse.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + // share resource with shared_with user try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { Thread.sleep(1000); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java index 4a84191200..66d519bcd6 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java @@ -24,6 +24,9 @@ import static org.opensearch.core.xcontent.ConstructingObjectParser.constructorArg; import static org.opensearch.core.xcontent.ConstructingObjectParser.optionalConstructorArg; +/** + * Sample resource declared by this plugin. + */ public class SampleResource implements Resource { private String name; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceParser.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceParser.java index 42fb2582e2..0b4601f5a3 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceParser.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceParser.java @@ -16,6 +16,9 @@ import org.opensearch.core.xcontent.XContentParser; import org.opensearch.security.spi.resources.ResourceParser; +/** + * Responsible for parsing the XContent into a SampleResource object. + */ public class SampleResourceParser implements ResourceParser { @Override public SampleResource parseXContent(XContentParser parser) throws IOException { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index 9d92bb43ad..a453579681 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -64,7 +64,7 @@ /** * Sample Resource plugin. - * It uses ".sample_resources" index to manage its resources, and exposes a REST API + * It uses ".sample_resource_sharing_plugin" index to manage its resources, and exposes few REST APIs that manage CRUD operations on sample resources. * */ public class SampleResourcePlugin extends Plugin implements ActionPlugin, SystemIndexPlugin, ResourceSharingExtension { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java index cfec368aa7..908fc2323f 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java @@ -14,8 +14,9 @@ import org.opensearch.security.spi.resources.ResourceAccessScope; /** - * This class demonstrates a sample implementation of Basic Access Scopes to fit each plugin's use-case. - * The plugin then uses this scope when seeking access evaluation for a user on a particular resource. + * This class implements two scopes for the sample plugin. + * The first scope is SAMPLE_FULL_ACCESS, which allows full access to the sample plugin. + * The second scope is PUBLIC, which allows public access to the sample plugin. */ public enum SampleResourceScope implements ResourceAccessScope { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java index 370d39e50f..21c392565e 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java @@ -23,6 +23,9 @@ import static org.opensearch.rest.RestRequest.Method.PUT; import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; +/** + * Rest Action to create a Sample Resource. Registers Create and Update REST APIs. + */ public class CreateResourceRestAction extends BaseRestHandler { public CreateResourceRestAction() {} @@ -31,13 +34,13 @@ public CreateResourceRestAction() {} public List routes() { return List.of( new Route(PUT, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/create"), - new Route(POST, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/update/{resourceId}") + new Route(POST, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/update/{resource_id}") ); } @Override public String getName() { - return "create_sample_resource"; + return "create_update_sample_resource"; } @Override @@ -51,7 +54,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli case PUT: return createResource(source, client); case POST: - return updateResource(source, request.param("resourceId"), client); + return updateResource(source, request.param("resource_id"), client); default: throw new IllegalArgumentException("Illegal method: " + request.method()); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/UpdateResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/UpdateResourceAction.java index 129c2d1546..ec5f84adfb 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/UpdateResourceAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/UpdateResourceAction.java @@ -15,11 +15,11 @@ */ public class UpdateResourceAction extends ActionType { /** - * Create sample resource action instance + * Update sample resource action instance */ public static final UpdateResourceAction INSTANCE = new UpdateResourceAction(); /** - * Create sample resource action name + * Update sample resource action name */ public static final String NAME = "cluster:admin/sample-resource-plugin/update"; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceAction.java index bfb672dfec..d7410e6388 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceAction.java @@ -11,15 +11,15 @@ import org.opensearch.action.ActionType; /** - * Action to create a sample resource + * Action to delete a sample resource */ public class DeleteResourceAction extends ActionType { /** - * Create sample resource action instance + * Delete sample resource action instance */ public static final DeleteResourceAction INSTANCE = new DeleteResourceAction(); /** - * Create sample resource action name + * Delete sample resource action name */ public static final String NAME = "cluster:admin/sample-resource-plugin/delete"; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRequest.java index d7c4637f31..9aa4332fe8 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRequest.java @@ -16,7 +16,7 @@ import org.opensearch.core.common.io.stream.StreamOutput; /** - * Request object for CreateSampleResource transport action + * Request object for DeleteSampleResource transport action */ public class DeleteResourceRequest extends ActionRequest { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceResponse.java index 31bf86ca79..7940b664db 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceResponse.java @@ -16,6 +16,9 @@ import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; +/** + * Response to a DeleteSampleResourceRequest + */ public class DeleteResourceResponse extends ActionResponse implements ToXContentObject { private final String message; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java index df53f54bd1..32dec08084 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/delete/DeleteResourceRestAction.java @@ -20,6 +20,9 @@ import static org.opensearch.rest.RestRequest.Method.DELETE; import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; +/** + * Rest Action to delete a Sample Resource. + */ public class DeleteResourceRestAction extends BaseRestHandler { public DeleteResourceRestAction() {} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java index 13ea45c9f0..a78b6b95f7 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java @@ -20,6 +20,9 @@ import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; +/** + * Rest action to get a sample resource + */ public class GetResourceRestAction extends BaseRestHandler { public GetResourceRestAction() {} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessAction.java index 6f6a308797..9231683499 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessAction.java @@ -15,11 +15,11 @@ */ public class RevokeResourceAccessAction extends ActionType { /** - * Share sample resource action instance + * Revoke sample resource action instance */ public static final RevokeResourceAccessAction INSTANCE = new RevokeResourceAccessAction(); /** - * Share sample resource action name + * Revoke sample resource action name */ public static final String NAME = "cluster:admin/sample-resource-plugin/revoke"; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRequest.java index 6038b4c996..f32e54c203 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRequest.java @@ -20,7 +20,7 @@ import org.opensearch.core.common.io.stream.StreamOutput; /** - * Request object for revoking access to a sample resource transport action + * Request object for revoking access to a sample resource */ public class RevokeResourceAccessRequest extends ActionRequest { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessResponse.java index 18b8d78a3e..2a1bf47e6f 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessResponse.java @@ -17,6 +17,9 @@ import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.security.spi.resources.sharing.ShareWith; +/** + * Response for the RevokeResourceAccessAction + */ public class RevokeResourceAccessResponse extends ActionResponse implements ToXContentObject { private final ShareWith shareWith; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java index 06aefe0f46..03d1cf8053 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java @@ -23,6 +23,9 @@ import static org.opensearch.rest.RestRequest.Method.POST; import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; +/** + * Rest Action to revoke sample resource access + */ public class RevokeResourceAccessRestAction extends BaseRestHandler { public RevokeResourceAccessRestAction() {} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceResponse.java index abadf88b49..e8df82b841 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceResponse.java @@ -17,6 +17,9 @@ import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.security.spi.resources.sharing.ShareWith; +/** + * Response object for ShareResourceAction + */ public class ShareResourceResponse extends ActionResponse implements ToXContentObject { private final ShareWith shareWith; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java index 4ce5ee2f69..00665f66fb 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java @@ -23,6 +23,9 @@ import static org.opensearch.rest.RestRequest.Method.POST; import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; +/** + * Rest Action to share a resource + */ public class ShareResourceRestAction extends BaseRestHandler { public ShareResourceRestAction() {} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java index 786588eff1..820b9ef591 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java @@ -33,6 +33,9 @@ import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +/** + * Transport action for creating a new resource. + */ public class CreateResourceTransportAction extends HandledTransportAction { private static final Logger log = LogManager.getLogger(CreateResourceTransportAction.class); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java index fbdb9229ba..ef92a3b4c2 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java @@ -35,6 +35,9 @@ import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +/** + * Transport action for deleting a resource + */ public class DeleteResourceTransportAction extends HandledTransportAction { private static final Logger log = LogManager.getLogger(DeleteResourceTransportAction.class); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java index 83e1171a86..8798b38d47 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java @@ -38,6 +38,9 @@ import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +/** + * Transport action for getting a resource + */ public class GetResourceTransportAction extends HandledTransportAction { private static final Logger log = LogManager.getLogger(GetResourceTransportAction.class); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java index e6c2210718..10aa6e837a 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java @@ -27,6 +27,9 @@ import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +/** + * Transport action for revoking resource access. + */ public class RevokeResourceAccessTransportAction extends HandledTransportAction { private static final Logger log = LogManager.getLogger(RevokeResourceAccessTransportAction.class); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java index 21d7571cf4..e30bde7cb8 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java @@ -27,6 +27,9 @@ import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +/** + * Transport action implementation for sharing a resource. + */ public class ShareResourceTransportAction extends HandledTransportAction { private static final Logger log = LogManager.getLogger(ShareResourceTransportAction.class); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java index b2b64fd1be..57ddc2a4af 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java @@ -38,6 +38,9 @@ import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +/** + * Transport action for updating a resource. + */ public class UpdateResourceTransportAction extends HandledTransportAction { private static final Logger log = LogManager.getLogger(UpdateResourceTransportAction.class); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java index 83e78f803d..c8cacc49fd 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/client/ResourceSharingClientAccessor.java @@ -12,16 +12,19 @@ import org.opensearch.security.client.resources.ResourceSharingNodeClient; import org.opensearch.transport.client.node.NodeClient; +/** + * Accessor for resource sharing node client. + */ public class ResourceSharingClientAccessor { private static ResourceSharingNodeClient INSTANCE; private ResourceSharingClientAccessor() {} /** - * get machine learning client. + * Get resource sharing client * * @param nodeClient node client - * @return machine learning client + * @return resource sharing client */ public static ResourceSharingNodeClient getResourceSharingClient(NodeClient nodeClient, Settings settings) { if (INSTANCE == null) { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Constants.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Constants.java index 3be49d033e..8cccb7e178 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Constants.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/Constants.java @@ -8,6 +8,9 @@ package org.opensearch.sample.utils; +/** + * Constants for Sample Resource Sharing Plugin + */ public class Constants { public static final String RESOURCE_INDEX_NAME = ".sample_resource_sharing_plugin"; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/SampleResourcePluginException.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/SampleResourcePluginException.java deleted file mode 100644 index 1ac2baaaae..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/utils/SampleResourcePluginException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.utils; - -import org.opensearch.OpenSearchException; - -public class SampleResourcePluginException extends OpenSearchException { - public SampleResourcePluginException(String msg, Object... args) { - super(msg, args); - } -} From 681a77aee9ecae59bf001b3cdbc3eb34c0213010 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 7 Mar 2025 15:01:59 -0500 Subject: [PATCH 188/201] Updates doc for spi and re-organizes a class Signed-off-by: Darshit Chanpura --- build.gradle | 1 + .../resources/ResourceAccessHandler.java | 51 +------------------ .../common/resources/ResourcePluginInfo.java | 2 - .../common/resources/ResourceProvider.java | 19 +++++++ ...mpleResourcePluginFeatureEnabledTests.java | 2 +- ...esourcePluginSystemIndexDisabledTests.java | 2 +- .../sample/SampleResourcePluginTests.java | 2 +- spi/README.md | 6 +++ .../security/spi/resources/Resource.java | 2 +- .../spi/resources/ResourceAccessScope.java | 2 +- .../spi/resources/ResourceProvider.java | 33 ------------ .../resources/ResourceSharingExtension.java | 12 +++-- .../security/spi/resources/package-info.java | 5 +- .../spi/resources/sharing/CreatedBy.java | 1 - .../spi/resources/sharing/Creator.java | 5 ++ .../spi/resources/sharing/Recipient.java | 4 ++ .../sharing/RecipientTypeRegistry.java | 3 +- .../resources/sharing/ResourceSharing.java | 4 -- .../resources/sharing/SharedWithScope.java | 2 +- .../security/OpenSearchSecurityPlugin.java | 2 +- 20 files changed, 56 insertions(+), 104 deletions(-) create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceProvider.java diff --git a/build.gradle b/build.gradle index 43d06aef1e..d1fd693bf8 100644 --- a/build.gradle +++ b/build.gradle @@ -645,6 +645,7 @@ tasks.integrationTest.finalizedBy(jacocoTestReport) // report is always generate check.dependsOn integrationTest dependencies { + compileOnly project(path: ":opensearch-resource-sharing-spi", configuration: 'shadow') implementation project(path: ":${rootProject.name}-common", configuration: 'shadow') implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" implementation "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}" diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java index abb2c73277..fdfd1c091e 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java @@ -173,7 +173,7 @@ public void getAccessibleResourcesForCurrentUser(String res try { validateArguments(resourceIndex); - ResourceParser parser = ResourcePluginInfo.getInstance().getResourceProviders().get(resourceIndex).getResourceParser(); + ResourceParser parser = ResourcePluginInfo.getInstance().getResourceProviders().get(resourceIndex).resourceParser(); StepListener> resourceIdsListener = new StepListener<>(); StepListener> resourcesListener = new StepListener<>(); @@ -366,55 +366,6 @@ public void revokeAccess( ); } - /** - * Checks if the current user has permission to modify a resource. - * NOTE: Only admins and owners of the resource can modify the resource. - * TODO: update this method to allow for other users to modify the resource. - * @param resourceId The resource ID to check. - * @param resourceIndex The resource index containing the resource. - * @param listener The listener to be notified with the permission check result. - */ - public void canModifyResource(String resourceId, String resourceIndex, ActionListener listener) { - try { - validateArguments(resourceId, resourceIndex); - - final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( - ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER - ); - final User user = (userSubject == null) ? null : userSubject.getUser(); - - if (user == null) { - listener.onFailure(new ResourceSharingException("No authenticated user available.")); - return; - } - - StepListener fetchDocListener = new StepListener<>(); - resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId, fetchDocListener); - - fetchDocListener.whenComplete(document -> { - if (document == null) { - LOGGER.info("Document {} does not exist in index {}", resourceId, resourceIndex); - // Either the document was deleted or has not been created yet. No permission check is needed for this. - listener.onResponse(true); - return; - } - - boolean isAdmin = adminDNs.isAdmin(user); - boolean isOwner = isOwnerOfResource(document, user.getName()); - - if (!isAdmin && !isOwner) { - LOGGER.info("User {} does not have access to delete the record {}", user.getName(), resourceId); - listener.onResponse(false); - } else { - listener.onResponse(true); - } - }, listener::onFailure); - } catch (Exception e) { - LOGGER.error("Failed to check delete permission for resource {}", resourceId, e); - listener.onFailure(e); - } - } - /** * Deletes a resource sharing record by its ID and the resource index it belongs to. * diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java b/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java index cbeedbac82..5624fc9985 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java @@ -8,8 +8,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import org.opensearch.security.spi.resources.ResourceProvider; - /** * This class provides information about resource plugins and their associated resource providers and indices. * It follows the Singleton pattern to ensure that only one instance of the class exists. diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java new file mode 100644 index 0000000000..826004cd36 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java @@ -0,0 +1,19 @@ +/* + * 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. + */ + +package org.opensearch.security.common.resources; + +import org.opensearch.security.spi.resources.ResourceParser; + +/** + * This record class represents a resource provider. + * It holds information about the resource type, resource index name, and a resource parser. + */ +public record ResourceProvider(String resourceType, String resourceIndexName, ResourceParser resourceParser) { + +} diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java index 2c32112d08..d6c8e3c9d3 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java @@ -14,8 +14,8 @@ import org.junit.Test; import org.opensearch.security.common.resources.ResourcePluginInfo; +import org.opensearch.security.common.resources.ResourceProvider; import org.opensearch.security.spi.resources.ResourceAccessScope; -import org.opensearch.security.spi.resources.ResourceProvider; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java index f7aec65b6f..15ac908e05 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginSystemIndexDisabledTests.java @@ -14,7 +14,7 @@ import org.opensearch.painless.PainlessModulePlugin; import org.opensearch.security.common.resources.ResourcePluginInfo; -import org.opensearch.security.spi.resources.ResourceProvider; +import org.opensearch.security.common.resources.ResourceProvider; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java index c18c068bda..2ec7cbbc20 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginTests.java @@ -16,7 +16,7 @@ import org.opensearch.painless.PainlessModulePlugin; import org.opensearch.security.common.resources.ResourcePluginInfo; -import org.opensearch.security.spi.resources.ResourceProvider; +import org.opensearch.security.common.resources.ResourceProvider; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; diff --git a/spi/README.md b/spi/README.md index 38efb1cf85..9e2c43f6c7 100644 --- a/spi/README.md +++ b/spi/README.md @@ -2,6 +2,12 @@ This SPI provides interfaces to implement Resource Sharing and Access Control. + +## Usage + +A plugin defining a resource and aiming to implement access control over that resource must extend ResourceSharingExtension class to register itself + + ## License This code is licensed under the Apache 2.0 License. diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java b/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java index 5af2ab7b26..f254f39937 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java @@ -17,7 +17,7 @@ public interface Resource extends NamedWriteable, ToXContentFragment { /** * Abstract method to get the resource name. - * Must be implemented by subclasses. + * Must be implemented by plugins defining resources. * * @return resource name */ diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java index e6fd2a76f6..c3b54a8c23 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java @@ -11,7 +11,7 @@ import java.util.Arrays; /** - * This interface defines the two basic access scopes for resource-access. + * This interface defines the two basic access scopes for resource-access. Plugins can decide whether to use these. * Each plugin must implement their own scopes and manage them. * These access scopes will then be used to verify the type of access being requested. * diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceProvider.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceProvider.java deleted file mode 100644 index d6bde36a75..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceProvider.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.spi.resources; - -public class ResourceProvider { - private final String resourceType; - private final String resourceIndexName; - private final ResourceParser resourceParser; - - public ResourceParser getResourceParser() { - return resourceParser; - } - - public String getResourceIndexName() { - return resourceIndexName; - } - - public String getResourceType() { - return resourceType; - } - - public ResourceProvider(String resourceType, String resourceIndexName, ResourceParser resourceParser) { - this.resourceType = resourceType; - this.resourceIndexName = resourceIndexName; - this.resourceParser = resourceParser; - } -} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java index 5b46c0bfaf..bbfc802d82 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java @@ -9,7 +9,7 @@ package org.opensearch.security.spi.resources; /** - * This interface should be implemented by all the plugins that define one or more resources. + * This interface should be implemented by all the plugins that define one or more resources and need access control over those resources. * * @opensearch.experimental */ @@ -17,15 +17,19 @@ public interface ResourceSharingExtension { /** * Type of the resource - * @return a string containing the type of the resource + * @return a string containing the type of the resource. A qualified class name can be supplied here. */ String getResourceType(); /** - * The index where resource meta-data is stored - * @return the name of the parent index where resource meta-data is stored + * The index where resource is stored + * @return the name of the parent index where resource is stored */ String getResourceIndex(); + /** + * The parser for the resource, which will be used by security plugin to parse the resource + * @return the parser for the resource + */ ResourceParser getResourceParser(); } diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/package-info.java b/spi/src/main/java/org/opensearch/security/spi/resources/package-info.java index 8990889429..f2e210a5e5 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/package-info.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/package-info.java @@ -7,8 +7,9 @@ */ /** - * This package defines class required to implement resource access control in OpenSearch. + * This package defines classes required to implement resource access control in OpenSearch. + * This package will be added as a dependency by all OpenSearch plugins that require resource access control. * * @opensearch.experimental */ -package main.java.org.opensearch.security.spi.resources; +package org.opensearch.security.spi.resources; diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java index fbe8d1208b..50bdd1aea7 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java @@ -19,7 +19,6 @@ /** * This class is used to store information about the creator of a resource. - * Concrete implementation will be provided by security plugin * * @opensearch.experimental */ diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java index 6ca338488e..75e2415b93 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java @@ -8,6 +8,11 @@ package org.opensearch.security.spi.resources.sharing; +/** + * This enum is used to store information about the creator of a resource. + * + * @opensearch.experimental + */ public enum Creator { USER("user"); diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java index 7fdd4bf30c..0806fcd4bb 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java @@ -8,6 +8,10 @@ package org.opensearch.security.spi.resources.sharing; +/** + * Enum representing the recipients of a shared resource. + * It includes USERS, ROLES, and BACKEND_ROLES. + */ public enum Recipient { USERS("users"), ROLES("roles"), diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java index bb10b677f6..a14a75487c 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java @@ -13,8 +13,9 @@ /** * This class determines a collection of recipient types a resource can be shared with. + * Allows addition of other recipient types in the future. * - * @opensearch.experimental + * @opensearch.experimental */ public final class RecipientTypeRegistry { // TODO: Check what size should this be. A cap should be added to avoid infinite addition of objects diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java index 731e589fbb..1690213872 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java @@ -20,10 +20,6 @@ /** * Represents a resource sharing configuration that manages access control for OpenSearch resources. * This class holds information about shared resources including their source, creator, and sharing permissions. - * - *

          This class implements {@link ToXContentFragment} for JSON serialization and {@link NamedWriteable} - * for stream-based serialization.

          - *

          * The class maintains information about: *

            *
          • The source index where the resource is defined
          • diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java index 81386da422..1dfca103a3 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java @@ -22,7 +22,7 @@ import org.opensearch.core.xcontent.XContentParser; /** - * This class represents the scope at which a resource is shared with. + * This class represents the scope at which a resource is shared with for a particular scope. * Example: * "read_only": { * "users": [], diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 3f5697f8a6..aa3cc6f0f5 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -147,6 +147,7 @@ import org.opensearch.security.common.resources.ResourceAccessHandler; import org.opensearch.security.common.resources.ResourceIndexListener; import org.opensearch.security.common.resources.ResourcePluginInfo; +import org.opensearch.security.common.resources.ResourceProvider; import org.opensearch.security.common.resources.ResourceSharingConstants; import org.opensearch.security.common.resources.ResourceSharingIndexHandler; import org.opensearch.security.common.resources.ResourceSharingIndexManagementRepository; @@ -195,7 +196,6 @@ import org.opensearch.security.setting.TransportPassiveAuthSetting; import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.ResourceParser; -import org.opensearch.security.spi.resources.ResourceProvider; import org.opensearch.security.spi.resources.ResourceSharingExtension; import org.opensearch.security.ssl.ExternalSecurityKeyStore; import org.opensearch.security.ssl.OpenSearchSecureSettingsFactory; From 61d83549c0061c5d471d2c3e8702a41748eb93f8 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 7 Mar 2025 15:57:23 -0500 Subject: [PATCH 189/201] Fixes jar hell and addresses some PR comments Signed-off-by: Darshit Chanpura --- build.gradle | 3 +- client/build.gradle | 3 +- .../resources/ResourceSharingNodeClient.java | 36 ++++++------ .../resources/ResourceAccessHandler.java | 19 ++----- .../resources/ResourceIndexListener.java | 5 +- .../security/common/auth/UserSubjectImpl.java | 55 ------------------- sample-resource-plugin/build.gradle | 3 +- .../exceptions/ResourceSharingException.java | 2 +- .../security/OpenSearchSecurityPlugin.java | 5 +- 9 files changed, 34 insertions(+), 97 deletions(-) delete mode 100644 common/test/java/org/opensearch/security/common/auth/UserSubjectImpl.java diff --git a/build.gradle b/build.gradle index d1fd693bf8..99ccec93ef 100644 --- a/build.gradle +++ b/build.gradle @@ -645,7 +645,6 @@ tasks.integrationTest.finalizedBy(jacocoTestReport) // report is always generate check.dependsOn integrationTest dependencies { - compileOnly project(path: ":opensearch-resource-sharing-spi", configuration: 'shadow') implementation project(path: ":${rootProject.name}-common", configuration: 'shadow') implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" implementation "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}" @@ -805,6 +804,8 @@ dependencies { implementation('com.google.googlejavaformat:google-java-format:1.25.2') { exclude group: 'com.google.guava' } + + testImplementation project(path: ":${rootProject.name}-common", configuration: 'shadow') } jar { diff --git a/client/build.gradle b/client/build.gradle index d958619ec5..1006726066 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -34,8 +34,7 @@ repositories { dependencies { compileOnly "org.opensearch:opensearch:${opensearch_version}" - compileOnly project(path: ":opensearch-resource-sharing-spi", configuration: 'shadow') - // spi runtime dependency comes through opensearch-security-common + // SPI dependency comes through common implementation project(path: ":${rootProject.name}-common", configuration: 'shadow') } diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java index fd99b942b7..eafe7a1800 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java @@ -79,14 +79,7 @@ public void shareResource( Map shareWith, ActionListener listener ) { - if (!resourceSharingEnabled) { - log.warn("Resource Access Control feature is disabled. Resource is not shareable."); - listener.onFailure( - new OpenSearchException( - "Resource Access Control feature is disabled. Resource is not shareable.", - RestStatus.NOT_IMPLEMENTED - ) - ); + if (isResourceAccessControlDisabled("Resource is not shareable.", listener)) { return; } ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.SHARE) @@ -113,14 +106,7 @@ public void revokeResourceAccess( Set scopes, ActionListener listener ) { - if (!resourceSharingEnabled) { - log.warn("Resource Access Control feature is disabled. Resource access is not revoked."); - listener.onFailure( - new OpenSearchException( - "Resource Access Control feature is disabled. Resource access is not revoked.", - RestStatus.NOT_IMPLEMENTED - ) - ); + if (isResourceAccessControlDisabled("Resource access is not revoked.", listener)) { return; } ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.REVOKE) @@ -132,6 +118,24 @@ public void revokeResourceAccess( client.execute(ResourceAccessAction.INSTANCE, request, sharingInfoResponseListener(listener)); } + /** + * Helper method for share/revoke to check and return early is resource sharing is disabled + * @param disabledMessage The message to be logged if resource sharing is disabled. + * @param listener The listener to be notified with the error. + * @return true if resource sharing is enabled, false otherwise. + */ + private boolean isResourceAccessControlDisabled(String disabledMessage, ActionListener listener) { + if (!resourceSharingEnabled) { + log.warn("Resource Access Control feature is disabled. {}", disabledMessage); + + listener.onFailure( + new OpenSearchException("Resource Access Control feature is disabled. " + disabledMessage, RestStatus.NOT_IMPLEMENTED) + ); + return true; + } + return false; + } + /** * Notifies the listener with the access request result. * @param listener The listener to be notified with the access request result. diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java index fdfd1c091e..eb671c7f2c 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java @@ -38,7 +38,7 @@ import org.opensearch.threadpool.ThreadPool; /** - * This class handles resource access permissions for users and roles. + * This class handles resource access permissions for users, roles and backend-roles. * It provides methods to check if a user has permission to access a resource * based on the resource sharing configuration. */ @@ -219,7 +219,7 @@ public void hasPermission(String resourceId, String resourceIndex, String scope, final User user = (userSubject == null) ? null : userSubject.getUser(); if (user == null) { - LOGGER.warn("No authenticated user found in ThreadContext"); + LOGGER.warn("No authenticated user found. Access to resource {} is not authorized", resourceId); listener.onResponse(false); return; } @@ -282,8 +282,8 @@ public void shareWith(String resourceId, String resourceIndex, ShareWith shareWi final User user = (userSubject == null) ? null : userSubject.getUser(); if (user == null) { - LOGGER.warn("No authenticated user found in the ThreadContext."); - listener.onFailure(new ResourceSharingException("No authenticated user found.")); + LOGGER.warn("No authenticated user found. Failed to share resource {}", resourceId); + listener.onFailure(new ResourceSharingException("No authenticated user found. Failed to share resource " + resourceId)); return; } @@ -338,16 +338,9 @@ public void revokeAccess( final User user = (userSubject == null) ? null : userSubject.getUser(); if (user != null) { - LOGGER.info("User {} revoking access to resource {} for {} for scopes {} ", user.getName(), resourceId, revokeAccess, scopes); + LOGGER.info("User {} revoking access to resource {} for {} for scopes {}.", user.getName(), resourceId, revokeAccess, scopes); } else { - listener.onFailure( - new ResourceSharingException( - "Failed to revoke access to resource {} for {} for scopes {} with no authenticated user", - resourceId, - revokeAccess, - scopes - ) - ); + listener.onFailure(new ResourceSharingException("No authenticated user found. Failed to share resource " + resourceId)); } boolean isAdmin = (user != null) && adminDNs.isAdmin(user); diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java index 728fe4691c..feb92f6026 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java @@ -19,7 +19,6 @@ import org.opensearch.index.engine.Engine; import org.opensearch.index.shard.IndexingOperationListener; import org.opensearch.security.common.auth.UserSubjectImpl; -import org.opensearch.security.common.configuration.AdminDNs; import org.opensearch.security.common.support.ConfigConstants; import org.opensearch.security.common.user.User; import org.opensearch.security.spi.resources.sharing.CreatedBy; @@ -36,7 +35,6 @@ public class ResourceIndexListener implements IndexingOperationListener { private static final Logger log = LogManager.getLogger(ResourceIndexListener.class); private static final ResourceIndexListener INSTANCE = new ResourceIndexListener(); private ResourceSharingIndexHandler resourceSharingIndexHandler; - private ResourceAccessHandler resourceAccessHandler; private boolean initialized; private ThreadPool threadPool; @@ -47,7 +45,7 @@ public static ResourceIndexListener getInstance() { return ResourceIndexListener.INSTANCE; } - public void initialize(ThreadPool threadPool, Client client, AdminDNs adminDns) { + public void initialize(ThreadPool threadPool, Client client) { if (initialized) { return; } @@ -58,7 +56,6 @@ public void initialize(ThreadPool threadPool, Client client, AdminDNs adminDns) client, threadPool ); - this.resourceAccessHandler = new ResourceAccessHandler(threadPool, this.resourceSharingIndexHandler, adminDns); } public boolean isInitialized() { diff --git a/common/test/java/org/opensearch/security/common/auth/UserSubjectImpl.java b/common/test/java/org/opensearch/security/common/auth/UserSubjectImpl.java deleted file mode 100644 index a28ed8dd63..0000000000 --- a/common/test/java/org/opensearch/security/common/auth/UserSubjectImpl.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * 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. - * - */ -package org.opensearch.security.auth; - -import java.security.Principal; -import java.util.concurrent.Callable; - -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.identity.NamedPrincipal; -import org.opensearch.identity.UserSubject; -import org.opensearch.identity.tokens.AuthToken; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; - -public class UserSubjectImpl implements UserSubject { - private final NamedPrincipal userPrincipal; - private final ThreadPool threadPool; - private final User user; - - UserSubjectImpl(ThreadPool threadPool, User user) { - this.threadPool = threadPool; - this.user = user; - this.userPrincipal = new NamedPrincipal(user.getName()); - } - - @Override - public void authenticate(AuthToken authToken) { - // not implemented - } - - @Override - public Principal getPrincipal() { - return userPrincipal; - } - - @Override - public T runAs(Callable callable) throws Exception { - try (ThreadContext.StoredContext ctx = threadPool.getThreadContext().stashContext()) { - threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); - return callable.call(); - } - } - - public User getUser() { - return user; - } -} diff --git a/sample-resource-plugin/build.gradle b/sample-resource-plugin/build.gradle index b5e3834d83..ede9ce48f3 100644 --- a/sample-resource-plugin/build.gradle +++ b/sample-resource-plugin/build.gradle @@ -72,8 +72,7 @@ configurations.all { dependencies { // Main implementation dependencies - compileOnly project(path: ":opensearch-resource-sharing-spi", configuration: 'shadow') - compileOnly project(path: ":${rootProject.name}-client", configuration: 'shadow') + implementation project(path: ":${rootProject.name}-client", configuration: 'shadow') compileOnly "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" // Integration test dependencies diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java index e966d7fc10..4a1775ad1b 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java @@ -43,7 +43,7 @@ public RestStatus status() { String message = getMessage(); if (message.contains("not authorized")) { return RestStatus.FORBIDDEN; - } else if (message.contains("no authenticated")) { + } else if (message.startsWith("No authenticated")) { return RestStatus.UNAUTHORIZED; } else if (message.contains("not found")) { return RestStatus.NOT_FOUND; diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index aa3cc6f0f5..56fda88a42 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -292,7 +292,6 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile PasswordHasher passwordHasher; private volatile DlsFlsBaseContext dlsFlsBaseContext; private ResourceSharingIndexManagementRepository rmr; - private ResourceAccessHandler resourceAccessHandler; public static boolean isActionTraceEnabled() { @@ -754,7 +753,7 @@ public void onIndexModule(IndexModule indexModule) { // Listening on POST and DELETE operations in resource indices ResourceIndexListener resourceIndexListener = ResourceIndexListener.getInstance(); - resourceIndexListener.initialize(threadPool, localClient, adminDNsCommon); + resourceIndexListener.initialize(threadPool, localClient); if (settings.getAsBoolean( ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT @@ -1170,7 +1169,7 @@ public Collection createComponents( final var resourceSharingIndex = ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; ResourceSharingIndexHandler rsIndexHandler = new ResourceSharingIndexHandler(resourceSharingIndex, localClient, threadPool); - resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDNsCommon); + ResourceAccessHandler resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDNsCommon); resourceAccessHandler.initializeRecipientTypes(); // Resource Sharing index is enabled by default boolean isResourceSharingEnabled = settings.getAsBoolean( From f1cd8aef1d445996963cbf2b2510046df989b0e9 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Fri, 7 Mar 2025 16:59:39 -0500 Subject: [PATCH 190/201] Fixes log levels in common Signed-off-by: Darshit Chanpura --- .../resources/ResourceAccessHandler.java | 56 +++++++++---------- .../resources/ResourceIndexListener.java | 7 ++- .../ResourceSharingIndexHandler.java | 26 +++++---- ...ourceSharingIndexManagementRepository.java | 4 +- 4 files changed, 49 insertions(+), 44 deletions(-) diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java index eb671c7f2c..36d862a691 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java @@ -86,12 +86,12 @@ public void getAccessibleResourceIdsForCurrentUser(String resourceIndex, ActionL // If no user is authenticated, return an empty set if (user == null) { - LOGGER.info("Unable to fetch user details."); + LOGGER.warn("Unable to fetch user details. User is null."); listener.onResponse(Collections.emptySet()); return; } - LOGGER.info("Listing accessible resources within the resource index {} for user: {}", resourceIndex, user.getName()); + LOGGER.debug("Listing accessible resources within the resource index {} for user: {}", resourceIndex, user.getName()); // 2. If the user is admin, simply fetch all resources if (adminDNs.isAdmin(user)) { @@ -198,6 +198,7 @@ public void getAccessibleResourcesForCurrentUser(String res ex -> listener.onFailure(new ResourceSharingException("Failed to get accessible resources: " + ex.getMessage(), ex)) ); } catch (Exception e) { + LOGGER.warn("Failed to process accessible resources request: {}", e.getMessage()); listener.onFailure(new ResourceSharingException("Failed to process accessible resources request: " + e.getMessage(), e)); } } @@ -224,10 +225,10 @@ public void hasPermission(String resourceId, String resourceIndex, String scope, return; } - LOGGER.info("Checking if user '{}' has '{}' permission to resource '{}'", user.getName(), scope, resourceId); + LOGGER.debug("Checking if user '{}' has '{}' permission to resource '{}'", user.getName(), scope, resourceId); if (adminDNs.isAdmin(user)) { - LOGGER.info("User '{}' is admin, automatically granted '{}' permission on '{}'", user.getName(), scope, resourceId); + LOGGER.debug("User '{}' is admin, automatically granted '{}' permission on '{}'", user.getName(), scope, resourceId); listener.onResponse(true); return; } @@ -248,10 +249,10 @@ public void hasPermission(String resourceId, String resourceIndex, String scope, || isSharedWithEntity(document, Recipient.ROLES, userRoles, scope) || isSharedWithEntity(document, Recipient.BACKEND_ROLES, userBackendRoles, scope)) { - LOGGER.info("User '{}' has '{}' permission to resource '{}'", user.getName(), scope, resourceId); + LOGGER.debug("User '{}' has '{}' permission to resource '{}'", user.getName(), scope, resourceId); listener.onResponse(true); } else { - LOGGER.info("User '{}' does not have '{}' permission to resource '{}'", user.getName(), scope, resourceId); + LOGGER.debug("User '{}' does not have '{}' permission to resource '{}'", user.getName(), scope, resourceId); listener.onResponse(false); } }, exception -> { @@ -287,7 +288,7 @@ public void shareWith(String resourceId, String resourceIndex, ShareWith shareWi return; } - LOGGER.info("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith.toString()); + LOGGER.debug("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith.toString()); boolean isAdmin = adminDNs.isAdmin(user); @@ -297,18 +298,13 @@ public void shareWith(String resourceId, String resourceIndex, ShareWith shareWi user.getName(), shareWith, isAdmin, - ActionListener.wrap( - // On success, return the updated ResourceSharing - updatedResourceSharing -> { - LOGGER.info("Successfully shared resource {} with {}", resourceId, shareWith.toString()); - listener.onResponse(updatedResourceSharing); - }, - // On failure, log and pass the exception along - e -> { - LOGGER.error("Failed to share resource {} with {}: {}", resourceId, shareWith.toString(), e.getMessage()); - listener.onFailure(e); - } - ) + ActionListener.wrap(updatedResourceSharing -> { + LOGGER.debug("Successfully shared resource {} with {}", resourceId, shareWith.toString()); + listener.onResponse(updatedResourceSharing); + }, e -> { + LOGGER.error("Failed to share resource {} with {}: {}", resourceId, shareWith.toString(), e.getMessage()); + listener.onFailure(e); + }) ); } @@ -328,29 +324,31 @@ public void revokeAccess( Set scopes, ActionListener listener ) { - // Validate input validateArguments(resourceId, resourceIndex, revokeAccess, scopes); - // Retrieve user final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER ); final User user = (userSubject == null) ? null : userSubject.getUser(); - if (user != null) { - LOGGER.info("User {} revoking access to resource {} for {} for scopes {}.", user.getName(), resourceId, revokeAccess, scopes); - } else { - listener.onFailure(new ResourceSharingException("No authenticated user found. Failed to share resource " + resourceId)); + if (user == null) { + LOGGER.warn("No authenticated user found. Failed to revoker access to resource {}", resourceId); + listener.onFailure( + new ResourceSharingException("No authenticated user found. Failed to revoke access to resource {}" + resourceId) + ); + return; } - boolean isAdmin = (user != null) && adminDNs.isAdmin(user); + LOGGER.debug("User {} revoking access to resource {} for {} for scopes {}.", user.getName(), resourceId, revokeAccess, scopes); + + boolean isAdmin = adminDNs.isAdmin(user); this.resourceSharingIndexHandler.revokeAccess( resourceId, resourceIndex, revokeAccess, scopes, - (user != null ? user.getName() : null), + user.getName(), isAdmin, ActionListener.wrap(listener::onResponse, exception -> { LOGGER.error("Failed to revoke access to resource {} in index {}: {}", resourceId, resourceIndex, exception.getMessage()); @@ -370,7 +368,7 @@ public void deleteResourceSharingRecord(String resourceId, String resourceIndex, try { validateArguments(resourceId, resourceIndex); - LOGGER.info("Deleting resource sharing record for resource {} in {}", resourceId, resourceIndex); + LOGGER.debug("Deleting resource sharing record for resource {} in {}", resourceId, resourceIndex); StepListener deleteDocListener = new StepListener<>(); resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex, deleteDocListener); @@ -398,7 +396,7 @@ public void deleteAllResourceSharingRecordsForCurrentUser(ActionListener { LOGGER.error( diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java index feb92f6026..0ace3667ca 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java @@ -90,7 +90,12 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re new CreatedBy(Creator.USER, user.getName()), null ); - log.debug("Successfully created a resource sharing entry {}", sharing); + log.debug( + "Successfully created a resource sharing entry {} for resource {} within index {}", + sharing, + resourceId, + resourceIndex + ); } catch (IOException e) { log.debug("Failed to create a resource sharing entry for resource: {}", resourceId, e); } diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java index d119996b57..27a9006962 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java @@ -182,16 +182,20 @@ public ResourceSharing indexResourceSharing(String resourceId, String resourceIn .request(); ActionListener irListener = ActionListener.wrap( - idxResponse -> LOGGER.info("Successfully created {} entry.", resourceSharingIndex), + idxResponse -> LOGGER.info( + "Successfully created {} entry for resource {} in index {}.", + resourceSharingIndex, + resourceId, + resourceIndex + ), (failResponse) -> { LOGGER.error(failResponse.getMessage()); - LOGGER.info("Failed to create {} entry.", resourceSharingIndex); } ); client.index(ir, irListener); return entry; } catch (Exception e) { - LOGGER.info("Failed to create {} entry.", resourceSharingIndex, e); + LOGGER.error("Failed to create {} entry.", resourceSharingIndex, e); throw new ResourceSharingException("Failed to create " + resourceSharingIndex + " entry.", e); } } @@ -563,7 +567,7 @@ public void fetchDocumentsByField(String pluginIndex, String field, String value .must(QueryBuilders.termQuery(field + ".keyword", value)); executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery, ActionListener.wrap(success -> { - LOGGER.info("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); + LOGGER.debug("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); listener.onResponse(resourceIds); }, exception -> { LOGGER.error("Failed to fetch documents from {} where {} = {}", resourceSharingIndex, field, value, exception); @@ -985,10 +989,10 @@ private void updateByQueryResourceSharing(String sourceIdx, String resourceId, S public void onResponse(BulkByScrollResponse response) { long updated = response.getUpdated(); if (updated > 0) { - LOGGER.info("Successfully updated {} documents in {}.", updated, resourceSharingIndex); + LOGGER.debug("Successfully updated {} documents in {}.", updated, resourceSharingIndex); listener.onResponse(true); } else { - LOGGER.info( + LOGGER.debug( "No documents found to update in {} for source_idx: {} and resource_id: {}", resourceSharingIndex, sourceIdx, @@ -996,12 +1000,10 @@ public void onResponse(BulkByScrollResponse response) { ); listener.onResponse(false); } - } @Override public void onFailure(Exception e) { - LOGGER.error("Failed to update documents in {}.", resourceSharingIndex, e); listener.onFailure(e); @@ -1226,10 +1228,10 @@ public void onResponse(BulkByScrollResponse response) { long deleted = response.getDeleted(); if (deleted > 0) { - LOGGER.info("Successfully deleted {} documents from {}", deleted, resourceSharingIndex); + LOGGER.debug("Successfully deleted {} documents from {}", deleted, resourceSharingIndex); listener.onResponse(true); } else { - LOGGER.info( + LOGGER.debug( "No documents found to delete in {} for source_idx: {} and resource_id: {}", resourceSharingIndex, sourceIdx, @@ -1316,10 +1318,10 @@ public void deleteAllRecordsForUser(String name, ActionListener listene public void onResponse(BulkByScrollResponse response) { long deletedDocs = response.getDeleted(); if (deletedDocs > 0) { - LOGGER.info("Successfully deleted {} documents created by user {}", deletedDocs, name); + LOGGER.debug("Successfully deleted {} documents created by user {}", deletedDocs, name); listener.onResponse(true); } else { - LOGGER.info("No documents found for user {}", name); + LOGGER.warn("No documents found for user {}", name); // No documents matched => success = false listener.onResponse(false); } diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java index b76aeb9471..a12dd29591 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java @@ -20,7 +20,7 @@ */ public class ResourceSharingIndexManagementRepository { - private static final Logger log = LogManager.getLogger(ResourceSharingIndexManagementRepository.class); + private static final Logger LOGGER = LogManager.getLogger(ResourceSharingIndexManagementRepository.class); private final ResourceSharingIndexHandler resourceSharingIndexHandler; private final boolean resourceSharingEnabled; @@ -49,7 +49,7 @@ public static ResourceSharingIndexManagementRepository create( public void createResourceSharingIndexIfAbsent() { // TODO check if this should be wrapped in an atomic completable future if (resourceSharingEnabled) { - log.info("Attempting to create Resource Sharing index"); + LOGGER.debug("Attempting to create Resource Sharing index"); this.resourceSharingIndexHandler.createResourceSharingIndexIfAbsent(() -> null); } From c4c577522f552c773ba59be9374b86c4a300f513 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 8 Mar 2025 18:29:30 -0500 Subject: [PATCH 191/201] Remove duplicate assert in IndexIntegrationTests Signed-off-by: Darshit Chanpura --- .../java/org/opensearch/security/IndexIntegrationTests.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/java/org/opensearch/security/IndexIntegrationTests.java b/src/test/java/org/opensearch/security/IndexIntegrationTests.java index 9aa9819be9..91a92ab97d 100644 --- a/src/test/java/org/opensearch/security/IndexIntegrationTests.java +++ b/src/test/java/org/opensearch/security/IndexIntegrationTests.java @@ -849,9 +849,6 @@ public void testIndexResolveMinus() throws Exception { resc = rh.executeGetRequest("/*,-*security,-*resource*/_search", encodeBasicHeader("foo_all", "nagilum")); assertThat(resc.getStatusCode(), is(HttpStatus.SC_OK)); - resc = rh.executeGetRequest("/*,-*security,-*resource*/_search", encodeBasicHeader("foo_all", "nagilum")); - assertThat(resc.getStatusCode(), is(HttpStatus.SC_OK)); - resc = rh.executeGetRequest("/*,-*security,-foo*,-*resource*/_search", encodeBasicHeader("foo_all", "nagilum")); assertThat(resc.getStatusCode(), is(HttpStatus.SC_OK)); From 748b7db4c790d6468562c3df2d54a7c20882527d Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 8 Mar 2025 19:14:27 -0500 Subject: [PATCH 192/201] Completes SPI readme Signed-off-by: Darshit Chanpura --- spi/README.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/spi/README.md b/spi/README.md index 9e2c43f6c7..359c8b706d 100644 --- a/spi/README.md +++ b/spi/README.md @@ -5,7 +5,44 @@ This SPI provides interfaces to implement Resource Sharing and Access Control. ## Usage -A plugin defining a resource and aiming to implement access control over that resource must extend ResourceSharingExtension class to register itself +A plugin defining a resource and aiming to implement access control over that resource must extend ResourceSharingExtension class to register itself as a Resource Plugin. Here is an example: + +```java + +public class SampleResourcePlugin extends Plugin implements SystemIndexPlugin, ResourceSharingExtension { + + // override any required methods + + @Override + public Collection getSystemIndexDescriptors(Settings settings) { + final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(RESOURCE_INDEX_NAME, "Sample index with resources"); + return Collections.singletonList(systemIndexDescriptor); + } + + @Override + public String getResourceType() { + return SampleResource.class.getCanonicalName(); + } + + @Override + public String getResourceIndex() { + return RESOURCE_INDEX_NAME; + } + + @Override + public ResourceParser getResourceParser() { + return new SampleResourceParser(); + } +} +``` + +Checklist for resource plugin: +1. Add a dependency on `opensearch-security-client` and `opensearch-resource-sharing-spi` in build.gradle. +2. Declare a resource class and implement `Resource` class from SPI. +3. Implement a `ResourceParser`. +4. Implement `ResourceSharingExtension` interface in the plugin declaration class, and implement required methods (as shown above). Ensure that resource index is marked as a system index. +5. Create a client accessor that will instantiate `ResourceSharingNodeClient`. +6. Use the methods provided by `ResourceSharingNodeClient` to implement resource access-control. ## License From e58541f6126de80ec8636fca2479eef0a394ca08 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 8 Mar 2025 19:23:56 -0500 Subject: [PATCH 193/201] Updates sample plugin logger statements Signed-off-by: Darshit Chanpura --- .../java/org/opensearch/sample/SampleResourcePlugin.java | 1 - .../actions/transport/CreateResourceTransportAction.java | 6 +++--- .../actions/transport/UpdateResourceTransportAction.java | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index a453579681..1ea1096e74 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -84,7 +84,6 @@ public Collection createComponents( IndexNameExpressionResolver indexNameExpressionResolver, Supplier repositoriesServiceSupplier ) { - log.info("Loaded SampleResourcePlugin components."); return Collections.emptyList(); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java index 820b9ef591..d6d12022d5 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/CreateResourceTransportAction.java @@ -55,7 +55,7 @@ protected void doExecute(Task task, CreateResourceRequest request, ActionListene try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { createResource(request, listener); } catch (Exception e) { - log.info("Failed to create resource", e); + log.error("Failed to create resource", e); listener.onFailure(e); } } @@ -69,10 +69,10 @@ private void createResource(CreateResourceRequest request, ActionListener { - log.info("Created resource: {}", idxResponse.getId()); + log.debug("Created resource: {}", idxResponse.getId()); listener.onResponse(new CreateResourceResponse("Created resource: " + idxResponse.getId())); }, listener::onFailure)); } catch (IOException e) { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java index 57ddc2a4af..f2a65e35bc 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java @@ -95,12 +95,12 @@ private void updateResource(UpdateResourceRequest request, ActionListener { log.info("Updated resource: {}", updateResponse.toString()); }, + updateResponse -> { log.debug("Updated resource: {}", updateResponse.toString()); }, listener::onFailure ) ); @@ -111,7 +111,7 @@ private void updateResource(UpdateResourceRequest request, ActionListener Date: Sat, 8 Mar 2025 19:39:52 -0500 Subject: [PATCH 194/201] Adds info about utilizing SPI to the readme Signed-off-by: Darshit Chanpura --- spi/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spi/README.md b/spi/README.md index 359c8b706d..d90d84980a 100644 --- a/spi/README.md +++ b/spi/README.md @@ -38,11 +38,12 @@ public class SampleResourcePlugin extends Plugin implements SystemIndexPlugin, R Checklist for resource plugin: 1. Add a dependency on `opensearch-security-client` and `opensearch-resource-sharing-spi` in build.gradle. -2. Declare a resource class and implement `Resource` class from SPI. -3. Implement a `ResourceParser`. -4. Implement `ResourceSharingExtension` interface in the plugin declaration class, and implement required methods (as shown above). Ensure that resource index is marked as a system index. -5. Create a client accessor that will instantiate `ResourceSharingNodeClient`. -6. Use the methods provided by `ResourceSharingNodeClient` to implement resource access-control. +2. Under `src/main/resources` folder of the plugin, and declare a file named `org.opensearch.security.spi.resources.ResourceSharingExtension`. Edit that file to add single line containing classpath of your plugin, e.g `org.opensearch.sample.SampleResourcePlugin`. This is required to utilize Java's Service Provider Interface mechanism. +3. Declare a resource class and implement `Resource` class from SPI. +4. Implement a `ResourceParser`. +5. Implement `ResourceSharingExtension` interface in the plugin declaration class, and implement required methods (as shown above). Ensure that resource index is marked as a system index. +6. Create a client accessor that will instantiate `ResourceSharingNodeClient`. +7. Use the methods provided by `ResourceSharingNodeClient` to implement resource access-control. ## License From 30f39a671ab8dd6c85ab22d06252447c1adf2fd6 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 8 Mar 2025 19:51:49 -0500 Subject: [PATCH 195/201] Adds missing @opensearch.experimental and adds any remaining javadoc Signed-off-by: Darshit Chanpura --- .../security/client/resources/ResourceSharingClient.java | 2 ++ .../client/resources/ResourceSharingNodeClient.java | 2 ++ .../security/common/resources/ResourceAccessHandler.java | 2 ++ .../security/common/resources/ResourceIndexListener.java | 2 ++ .../security/common/resources/ResourcePluginInfo.java | 2 ++ .../security/common/resources/ResourceProvider.java | 2 ++ .../security/common/resources/ResourceSharingConstants.java | 2 ++ .../common/resources/ResourceSharingIndexHandler.java | 2 ++ .../resources/ResourceSharingIndexManagementRepository.java | 2 ++ .../common/resources/rest/ResourceAccessAction.java | 6 ++++++ .../common/resources/rest/ResourceAccessRequest.java | 6 ++++++ .../common/resources/rest/ResourceAccessRequestParams.java | 6 ++++++ .../common/resources/rest/ResourceAccessResponse.java | 6 ++++++ .../common/resources/rest/ResourceAccessRestAction.java | 3 +++ .../resources/rest/ResourceAccessTransportAction.java | 5 +++++ .../org/opensearch/security/spi/resources/Resource.java | 2 ++ .../opensearch/security/spi/resources/ResourceParser.java | 6 ++++++ .../spi/resources/exceptions/ResourceSharingException.java | 2 ++ .../security/spi/resources/sharing/Recipient.java | 2 ++ .../spi/resources/sharing/RecipientTypeRegistry.java | 2 +- .../opensearch/security/spi/resources/CreatedByTests.java | 5 +++++ .../security/spi/resources/RecipientTypeRegistryTests.java | 5 +++++ .../opensearch/security/spi/resources/ShareWithTests.java | 5 +++++ 23 files changed, 78 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java index dfece1f4d9..7451deb1e1 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java @@ -16,6 +16,8 @@ /** * Interface for resource sharing client operations. + * + * @opensearch.experimental */ public interface ResourceSharingClient { diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java index eafe7a1800..0d5ada63c8 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java @@ -27,6 +27,8 @@ /** * Client for resource sharing operations. + * + * @opensearch.experimental */ public final class ResourceSharingNodeClient implements ResourceSharingClient { diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java index 36d862a691..0458b58aaa 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java @@ -41,6 +41,8 @@ * This class handles resource access permissions for users, roles and backend-roles. * It provides methods to check if a user has permission to access a resource * based on the resource sharing configuration. + * + * @opensearch.experimental */ public class ResourceAccessHandler { private static final Logger LOGGER = LogManager.getLogger(ResourceAccessHandler.class); diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java index 0ace3667ca..4b502096b4 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java @@ -29,6 +29,8 @@ /** * This class implements an index operation listener for operations performed on resources stored in plugin's indices. + * + * @opensearch.experimental */ public class ResourceIndexListener implements IndexingOperationListener { diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java b/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java index 5624fc9985..fde006c198 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java @@ -11,6 +11,8 @@ /** * This class provides information about resource plugins and their associated resource providers and indices. * It follows the Singleton pattern to ensure that only one instance of the class exists. + * + * @opensearch.experimental */ public class ResourcePluginInfo { private static ResourcePluginInfo INSTANCE; diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java index 826004cd36..b2537fc849 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java @@ -13,6 +13,8 @@ /** * This record class represents a resource provider. * It holds information about the resource type, resource index name, and a resource parser. + * + * @opensearch.experimental */ public record ResourceProvider(String resourceType, String resourceIndexName, ResourceParser resourceParser) { diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java index 0bc5f5b99d..a1004566e5 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java @@ -12,6 +12,8 @@ /** * This class contains constants related to resource sharing in OpenSearch. + * + * @opensearch.experimental */ public class ResourceSharingConstants { // Resource sharing index diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java index 27a9006962..266c2639fa 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java @@ -79,6 +79,8 @@ /** * This class handles the creation and management of the resource sharing index. * It provides methods to create the index, index resource sharing entries along with updates and deletion, retrieve shared resources. + * + * @opensearch.experimental */ public class ResourceSharingIndexHandler { diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java index a12dd29591..166d410f86 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java @@ -17,6 +17,8 @@ /** * This class is responsible for managing the resource sharing index. * It provides methods to create the index if it doesn't exist. + * + * @opensearch.experimental */ public class ResourceSharingIndexManagementRepository { diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java index 31c7038113..5820d21a8c 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java @@ -10,6 +10,12 @@ import org.opensearch.action.ActionType; +/** + * This class represents the action type for resource access. + * It is used to execute the resource access request and retrieve the response. + * + * @opensearch.experimental + */ public class ResourceAccessAction extends ActionType { public static final ResourceAccessAction INSTANCE = new ResourceAccessAction(); diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java index bf2b79cb42..86f6c26f1c 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java @@ -28,6 +28,12 @@ import org.opensearch.core.xcontent.XContentParser; import org.opensearch.security.spi.resources.sharing.ShareWith; +/** + * This class represents a request to access a resource. + * It encapsulates the operation, resource ID, resource index, scope, share with information, revoked entities, and scopes. + * + * @opensearch.experimental + */ public class ResourceAccessRequest extends ActionRequest { public enum Operation { diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java index ed45d34cf5..880cfe00ec 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java @@ -13,6 +13,12 @@ import org.opensearch.core.common.io.stream.NamedWriteable; import org.opensearch.core.common.io.stream.StreamOutput; +/** + * This class is used to represent the request parameters for resource access. + * It implements the NamedWriteable interface to allow serialization and deserialization of the request parameters. + * + * @opensearch.experimental + */ public class ResourceAccessRequestParams implements NamedWriteable { @Override public String getWriteableName() { diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java index b9ba76ff4d..ac3ebf602f 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java @@ -20,6 +20,12 @@ import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.sharing.ResourceSharing; +/** + * This class is used to represent the response of a resource access request. + * It contains the response type and the response data. + * + * @opensearch.experimental + */ public class ResourceAccessResponse extends ActionResponse implements ToXContentObject { public enum ResponseType { RESOURCES, diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java index c6cd5dc111..700a064ed5 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java @@ -41,6 +41,9 @@ /** * This class handles the REST API for resource access management. + * It provides endpoints for listing, revoking, sharing, and verifying resource access. + * + * @opensearch.experimental */ public class ResourceAccessRestAction extends BaseRestHandler { private static final Logger LOGGER = LogManager.getLogger(ResourceAccessRestAction.class); diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java index b795d7258e..37c3a696af 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java @@ -24,6 +24,11 @@ import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; +/** + * Transport action for handling resource access requests. + * + * @opensearch.experimental + */ public class ResourceAccessTransportAction extends HandledTransportAction { private final ResourceAccessHandler resourceAccessHandler; diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java b/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java index f254f39937..72e0b7b5d1 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java @@ -13,6 +13,8 @@ /** * Marker interface for all resources + * + * @opensearch.experimental */ public interface Resource extends NamedWriteable, ToXContentFragment { /** diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java index be57200da4..b02269322e 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java @@ -12,6 +12,12 @@ import org.opensearch.core.xcontent.XContentParser; +/** + * Interface for parsing resources from XContentParser + * @param the type of resource to be parsed + * + * @opensearch.experimental + */ public interface ResourceParser { /** * Parse source bytes supplied by the parser to a desired Resource type diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java index 4a1775ad1b..60d91aa243 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java @@ -20,6 +20,8 @@ /** * This class represents an exception that occurs during resource sharing operations. * It extends the OpenSearchException class. + * + * @opensearch.experimental */ public class ResourceSharingException extends OpenSearchException { public ResourceSharingException(Throwable cause) { diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java index 0806fcd4bb..77215071de 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java @@ -11,6 +11,8 @@ /** * Enum representing the recipients of a shared resource. * It includes USERS, ROLES, and BACKEND_ROLES. + * + * @opensearch.experimental */ public enum Recipient { USERS("users"), diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java index a14a75487c..a1bdb89089 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java @@ -15,7 +15,7 @@ * This class determines a collection of recipient types a resource can be shared with. * Allows addition of other recipient types in the future. * - * @opensearch.experimental + * @opensearch.experimental */ public final class RecipientTypeRegistry { // TODO: Check what size should this be. A cap should be added to avoid infinite addition of objects diff --git a/spi/src/test/java/org/opensearch/security/spi/resources/CreatedByTests.java b/spi/src/test/java/org/opensearch/security/spi/resources/CreatedByTests.java index cf85166682..7d6eb5c61a 100644 --- a/spi/src/test/java/org/opensearch/security/spi/resources/CreatedByTests.java +++ b/spi/src/test/java/org/opensearch/security/spi/resources/CreatedByTests.java @@ -32,6 +32,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +/** + * Test class for CreatedBy class + * + * @opensearch.experimental + */ public class CreatedByTests { private static final Creator CREATOR_TYPE = Creator.USER; diff --git a/spi/src/test/java/org/opensearch/security/spi/resources/RecipientTypeRegistryTests.java b/spi/src/test/java/org/opensearch/security/spi/resources/RecipientTypeRegistryTests.java index 0281d287de..8b0bfa3297 100644 --- a/spi/src/test/java/org/opensearch/security/spi/resources/RecipientTypeRegistryTests.java +++ b/spi/src/test/java/org/opensearch/security/spi/resources/RecipientTypeRegistryTests.java @@ -19,6 +19,11 @@ import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThrows; +/** + * Tests for {@link RecipientTypeRegistry}. + * + * @opensearch.experimental + */ public class RecipientTypeRegistryTests { @Test diff --git a/spi/src/test/java/org/opensearch/security/spi/resources/ShareWithTests.java b/spi/src/test/java/org/opensearch/security/spi/resources/ShareWithTests.java index 38dfe7290b..d7ffa0ce5e 100644 --- a/spi/src/test/java/org/opensearch/security/spi/resources/ShareWithTests.java +++ b/spi/src/test/java/org/opensearch/security/spi/resources/ShareWithTests.java @@ -46,6 +46,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +/** + * Test class for ShareWith class + * + * @opensearch.experimental + */ public class ShareWithTests { @Before From ef282ffa2d3bc2818ff41f6538d267708daa54c6 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Sat, 8 Mar 2025 20:19:23 -0500 Subject: [PATCH 196/201] Updates SPI readme Signed-off-by: Darshit Chanpura --- spi/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spi/README.md b/spi/README.md index d90d84980a..de41ab7095 100644 --- a/spi/README.md +++ b/spi/README.md @@ -38,7 +38,7 @@ public class SampleResourcePlugin extends Plugin implements SystemIndexPlugin, R Checklist for resource plugin: 1. Add a dependency on `opensearch-security-client` and `opensearch-resource-sharing-spi` in build.gradle. -2. Under `src/main/resources` folder of the plugin, and declare a file named `org.opensearch.security.spi.resources.ResourceSharingExtension`. Edit that file to add single line containing classpath of your plugin, e.g `org.opensearch.sample.SampleResourcePlugin`. This is required to utilize Java's Service Provider Interface mechanism. +2. Under `src/main/resources` folder of the plugin, locate or create a folder `META-INF/services`and in the services folder, declare a file named `org.opensearch.security.spi.resources.ResourceSharingExtension`. Edit that file to add single line containing classpath of your plugin, e.g `org.opensearch.sample.SampleResourcePlugin`. This is required to utilize Java's Service Provider Interface mechanism. 3. Declare a resource class and implement `Resource` class from SPI. 4. Implement a `ResourceParser`. 5. Implement `ResourceSharingExtension` interface in the plugin declaration class, and implement required methods (as shown above). Ensure that resource index is marked as a system index. From 004e0b678fdcf9877d5796c1d364f994f1f6d31f Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 11 Mar 2025 14:40:51 -0400 Subject: [PATCH 197/201] Makes haspermission accept multiple scopes Signed-off-by: Darshit Chanpura --- .../resources/ResourceSharingClient.java | 4 +- .../resources/ResourceSharingNodeClient.java | 6 +-- .../resources/ResourceAccessHandler.java | 50 +++++++++++-------- .../resources/rest/ResourceAccessRequest.java | 16 ------ .../rest/ResourceAccessTransportAction.java | 2 +- ...mpleResourcePluginFeatureEnabledTests.java | 20 ++------ .../AbstractSampleResourcePluginTests.java | 14 ++++++ .../sample/SampleResourceScope.java | 3 ++ .../DeleteResourceTransportAction.java | 8 ++- .../transport/GetResourceTransportAction.java | 8 ++- .../UpdateResourceTransportAction.java | 7 ++- 11 files changed, 75 insertions(+), 63 deletions(-) diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java index 7451deb1e1..a5b7403e33 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java @@ -25,10 +25,10 @@ public interface ResourceSharingClient { * Verifies if the current user has access to the specified resource. * @param resourceId The ID of the resource to verify access for. * @param resourceIndex The index containing the resource. - * @param scope The scope of the resource. + * @param scopes The scopes to be checked against. * @param listener The listener to be notified with the access verification result. */ - void verifyResourceAccess(String resourceId, String resourceIndex, String scope, ActionListener listener); + void verifyResourceAccess(String resourceId, String resourceIndex, Set scopes, ActionListener listener); /** * Shares a resource with the specified users, roles, and backend roles. diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java index 0d5ada63c8..1a1ef05a24 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java @@ -49,11 +49,11 @@ public ResourceSharingNodeClient(Client client, Settings settings) { * Verifies if the current user has access to the specified resource. * @param resourceId The ID of the resource to verify access for. * @param resourceIndex The index containing the resource. - * @param scope The scope of the resource. + * @param scopes The scopes to be checked against. * @param listener The listener to be notified with the access verification result. */ @Override - public void verifyResourceAccess(String resourceId, String resourceIndex, String scope, ActionListener listener) { + public void verifyResourceAccess(String resourceId, String resourceIndex, Set scopes, ActionListener listener) { if (!resourceSharingEnabled) { log.warn("Resource Access Control feature is disabled. Access to resource is automatically granted."); listener.onResponse(true); @@ -62,7 +62,7 @@ public void verifyResourceAccess(String resourceId, String resourceIndex, String ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.VERIFY) .resourceId(resourceId) .resourceIndex(resourceIndex) - .scope(scope) + .scopes(scopes) .build(); client.execute(ResourceAccessAction.INSTANCE, request, verifyAccessResponseListener(listener)); } diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java index 0458b58aaa..5a21c3472b 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java @@ -210,11 +210,11 @@ public void getAccessibleResourcesForCurrentUser(String res * * @param resourceId The resource ID to check access for. * @param resourceIndex The resource index containing the resource. - * @param scope The permission scope to check. + * @param scopes The permission scope(s) to check. * @param listener The listener to be notified with the permission check result. */ - public void hasPermission(String resourceId, String resourceIndex, String scope, ActionListener listener) { - validateArguments(resourceId, resourceIndex, scope); + public void hasPermission(String resourceId, String resourceIndex, Set scopes, ActionListener listener) { + validateArguments(resourceId, resourceIndex, scopes); final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER @@ -227,16 +227,21 @@ public void hasPermission(String resourceId, String resourceIndex, String scope, return; } - LOGGER.debug("Checking if user '{}' has '{}' permission to resource '{}'", user.getName(), scope, resourceId); + LOGGER.debug("Checking if user '{}' has '{}' permission to resource '{}'", user.getName(), scopes.toString(), resourceId); if (adminDNs.isAdmin(user)) { - LOGGER.debug("User '{}' is admin, automatically granted '{}' permission on '{}'", user.getName(), scope, resourceId); + LOGGER.debug( + "User '{}' is admin, automatically granted '{}' permission on '{}'", + user.getName(), + scopes.toString(), + resourceId + ); listener.onResponse(true); return; } - Set userRoles = user.getSecurityRoles(); - Set userBackendRoles = user.getRoles(); + Set userRoles = new HashSet<>(user.getSecurityRoles()); + Set userBackendRoles = new HashSet<>(user.getRoles()); this.resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId, ActionListener.wrap(document -> { if (document == null) { @@ -245,16 +250,19 @@ public void hasPermission(String resourceId, String resourceIndex, String scope, return; } + // All public entities are designated with "*" + userRoles.add("*"); + userBackendRoles.add("*"); if (isSharedWithEveryone(document) || isOwnerOfResource(document, user.getName()) - || isSharedWithEntity(document, Recipient.USERS, Set.of(user.getName()), scope) - || isSharedWithEntity(document, Recipient.ROLES, userRoles, scope) - || isSharedWithEntity(document, Recipient.BACKEND_ROLES, userBackendRoles, scope)) { + || isSharedWithEntity(document, Recipient.USERS, Set.of(user.getName(), "*"), scopes) + || isSharedWithEntity(document, Recipient.ROLES, userRoles, scopes) + || isSharedWithEntity(document, Recipient.BACKEND_ROLES, userBackendRoles, scopes)) { - LOGGER.debug("User '{}' has '{}' permission to resource '{}'", user.getName(), scope, resourceId); + LOGGER.debug("User '{}' has '{}' permission to resource '{}'", user.getName(), scopes.toString(), resourceId); listener.onResponse(true); } else { - LOGGER.debug("User '{}' does not have '{}' permission to resource '{}'", user.getName(), scope, resourceId); + LOGGER.debug("User '{}' does not have '{}' permission to resource '{}'", user.getName(), scopes.toString(), resourceId); listener.onResponse(false); } }, exception -> { @@ -469,12 +477,12 @@ private boolean isOwnerOfResource(ResourceSharing document, String userName) { * @param document The ResourceSharing document to check. * @param recipient The recipient entity * @param entities The set of entities to check for sharing. - * @param scope The permission scope to check. + * @param scopes The permission scope(s) to check. * @return True if the resource is shared with the entities and scope, false otherwise. */ - private boolean isSharedWithEntity(ResourceSharing document, Recipient recipient, Set entities, String scope) { + private boolean isSharedWithEntity(ResourceSharing document, Recipient recipient, Set entities, Set scopes) { for (String entity : entities) { - if (checkSharing(document, recipient, entity, scope)) { + if (checkSharing(document, recipient, entity, scopes)) { return true; } } @@ -482,7 +490,7 @@ private boolean isSharedWithEntity(ResourceSharing document, Recipient recipient } /** - * Checks if the given resource is shared with everyone. + * Checks if the given resource is shared with everyone, i.e. the scope is named "*" * * @param document The ResourceSharing document to check. * @return True if the resource is shared with everyone, false otherwise. @@ -497,11 +505,11 @@ private boolean isSharedWithEveryone(ResourceSharing document) { * * @param document The ResourceSharing document to check. * @param recipient The recipient entity - * @param identifier The identifier of the entity to check for sharing. - * @param scope The permission scope to check. + * @param entity The entity to check for sharing. + * @param scopes The permission scope(s) to check. * @return True if the resource is shared with the entity and scope, false otherwise. */ - private boolean checkSharing(ResourceSharing document, Recipient recipient, String identifier, String scope) { + private boolean checkSharing(ResourceSharing document, Recipient recipient, String entity, Set scopes) { if (document.getShareWith() == null) { return false; } @@ -509,7 +517,7 @@ private boolean checkSharing(ResourceSharing document, Recipient recipient, Stri return document.getShareWith() .getSharedWithScopes() .stream() - .filter(sharedWithScope -> sharedWithScope.getScope().equals(scope)) + .filter(sharedWithScope -> scopes.contains(sharedWithScope.getScope())) .findFirst() .map(sharedWithScope -> { SharedWithScope.ScopeRecipients scopePermissions = sharedWithScope.getSharedWithPerScope(); @@ -518,7 +526,7 @@ private boolean checkSharing(ResourceSharing document, Recipient recipient, Stri return switch (recipient) { case Recipient.USERS, Recipient.ROLES, Recipient.BACKEND_ROLES -> recipients.get( RecipientTypeRegistry.fromValue(recipient.getName()) - ).contains(identifier); + ).contains(entity); }; }) .orElse(false); // Return false if no matching scope is found diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java index 86f6c26f1c..1df9c244bb 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java @@ -46,7 +46,6 @@ public enum Operation { private final Operation operation; private final String resourceId; private final String resourceIndex; - private final String scope; private final ShareWith shareWith; private final Map> revokedEntities; private final Set scopes; @@ -58,7 +57,6 @@ private ResourceAccessRequest(Builder builder) { this.operation = builder.operation; this.resourceId = builder.resourceId; this.resourceIndex = builder.resourceIndex; - this.scope = builder.scope; this.shareWith = builder.shareWith; this.revokedEntities = builder.revokedEntities; this.scopes = builder.scopes; @@ -84,8 +82,6 @@ public static ResourceAccessRequest from(Map source, Map) source.get("share_with")); } @@ -106,7 +102,6 @@ public ResourceAccessRequest(StreamInput in) throws IOException { this.operation = in.readEnum(Operation.class); this.resourceId = in.readOptionalString(); this.resourceIndex = in.readOptionalString(); - this.scope = in.readOptionalString(); this.shareWith = in.readOptionalWriteable(ShareWith::new); this.revokedEntities = in.readMap(StreamInput::readString, valIn -> valIn.readSet(StreamInput::readString)); this.scopes = in.readSet(StreamInput::readString); @@ -117,7 +112,6 @@ public void writeTo(StreamOutput out) throws IOException { out.writeEnum(operation); out.writeOptionalString(resourceId); out.writeOptionalString(resourceIndex); - out.writeOptionalString(scope); out.writeOptionalWriteable(shareWith); out.writeMap(revokedEntities, StreamOutput::writeString, StreamOutput::writeStringCollection); out.writeStringCollection(scopes); @@ -140,10 +134,6 @@ public String getResourceIndex() { return resourceIndex; } - public String getScope() { - return scope; - } - public ShareWith getShareWith() { return shareWith; } @@ -163,7 +153,6 @@ public static class Builder { private Operation operation; private String resourceId; private String resourceIndex; - private String scope; private ShareWith shareWith; private Map> revokedEntities; private Set scopes; @@ -183,11 +172,6 @@ public Builder resourceIndex(String resourceIndex) { return this; } - public Builder scope(String scope) { - this.scope = scope; - return this; - } - public Builder shareWith(Map source) { try { this.shareWith = parseShareWith(source); diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java index 37c3a696af..6bd58246c8 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java @@ -104,7 +104,7 @@ private void handleVerifyAccess(ResourceAccessRequest request, ActionListener listener.onResponse(new ResourceAccessResponse(hasPermission)), listener::onFailure) ); } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java index d6c8e3c9d3..28cc634576 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java @@ -15,7 +15,6 @@ import org.opensearch.security.common.resources.ResourcePluginInfo; import org.opensearch.security.common.resources.ResourceProvider; -import org.opensearch.security.spi.resources.ResourceAccessScope; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -198,14 +197,7 @@ public void testCreateUpdateDeleteSampleResourceWithSecurityAPIs() throws Except // verify access try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - String verifyAccessPayload = "{\"resource_id\":\"" - + resourceId - + "\",\"resource_index\":\"" - + RESOURCE_INDEX_NAME - + "\",\"scope\":\"" - + ResourceAccessScope.PUBLIC - + "\"}"; - TestRestClient.HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); + TestRestClient.HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload(resourceId)); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.bodyAsJsonNode().get("has_permission").asBoolean(), equalTo(true)); } @@ -249,14 +241,8 @@ public void testCreateUpdateDeleteSampleResourceWithSecurityAPIs() throws Except // verify access - share_with_user should no longer have access to admin's resource try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { - String verifyAccessPayload = "{\"resource_id\":\"" - + resourceId - + "\",\"resource_index\":\"" - + RESOURCE_INDEX_NAME - + "\",\"scope\":\"" - + ResourceAccessScope.PUBLIC - + "\"}"; - TestRestClient.HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload); + + TestRestClient.HttpResponse response = client.postJson(SECURITY_RESOURCE_VERIFY_ENDPOINT, verifyAccessPayload(resourceId)); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.bodyAsJsonNode().get("has_permission").asBoolean(), equalTo(false)); } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java index 20ae984444..b4430725fb 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java @@ -111,4 +111,18 @@ protected static String revokeAccessPayload() { + "\"]" + "}"; } + + protected static String verifyAccessPayload(String resourceId) { + return "{" + + "\"resource_id\":\"" + + resourceId + + "\"," + + "\"resource_index\":\"" + + RESOURCE_INDEX_NAME + + "\"," + + "\"scopes\":[\"" + + ResourceAccessScope.PUBLIC + + "\"]" + + "}"; + } } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java index 908fc2323f..a473e46b61 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceScope.java @@ -21,6 +21,9 @@ public enum SampleResourceScope implements ResourceAccessScope { SAMPLE_FULL_ACCESS("sample_full_access"), + SAMPLE_READ_ACCESS("sample_read_access"), + SAMPLE_WRITE_ACCESS("sample_write_access"), + SAMPLE_DELETE_ACCESS("sample_delete_access"), PUBLIC("public"); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java index ef92a3b4c2..77455a3e73 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/DeleteResourceTransportAction.java @@ -8,6 +8,8 @@ package org.opensearch.sample.resource.actions.transport; +import java.util.Set; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -72,7 +74,11 @@ protected void doExecute(Task task, DeleteResourceRequest request, ActionListene resourceSharingClient.verifyResourceAccess( resourceId, RESOURCE_INDEX_NAME, - SampleResourceScope.PUBLIC.value(), + Set.of( + SampleResourceScope.SAMPLE_DELETE_ACCESS.value(), + SampleResourceScope.SAMPLE_FULL_ACCESS.value(), + SampleResourceScope.PUBLIC.value() + ), ActionListener.wrap(isAuthorized -> { if (!isAuthorized) { listener.onFailure(new ResourceSharingException("Current user is not authorized to delete resource: " + resourceId)); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java index 8798b38d47..02d0908388 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java @@ -8,6 +8,8 @@ package org.opensearch.sample.resource.actions.transport; +import java.util.Set; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -73,7 +75,11 @@ protected void doExecute(Task task, GetResourceRequest request, ActionListener { if (!isAuthorized) { listener.onFailure( diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java index f2a65e35bc..6128c9134b 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/UpdateResourceTransportAction.java @@ -9,6 +9,7 @@ package org.opensearch.sample.resource.actions.transport; import java.io.IOException; +import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -72,7 +73,11 @@ protected void doExecute(Task task, UpdateResourceRequest request, ActionListene resourceSharingClient.verifyResourceAccess( request.getResourceId(), RESOURCE_INDEX_NAME, - SampleResourceScope.PUBLIC.value(), + Set.of( + SampleResourceScope.SAMPLE_WRITE_ACCESS.value(), + SampleResourceScope.SAMPLE_FULL_ACCESS.value(), + SampleResourceScope.PUBLIC.value() + ), ActionListener.wrap(isAuthorized -> { if (!isAuthorized) { listener.onFailure( From e6d43b606608617667e5823aaf7ba3a022f56cec Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 11 Mar 2025 16:19:39 -0400 Subject: [PATCH 198/201] Adds a new get all accessible resources endpoint for current user Signed-off-by: Darshit Chanpura --- .../resources/ResourceSharingClient.java | 8 + .../resources/ResourceSharingNodeClient.java | 21 + .../ResourceSharingIndexHandler.java | 382 +++++++++--------- ...mpleResourcePluginFeatureEnabledTests.java | 12 + ...pleResourcePluginFeatureDisabledTests.java | 6 + .../org/opensearch/sample/SampleResource.java | 10 +- .../actions/rest/get/GetResourceResponse.java | 15 +- .../rest/get/GetResourceRestAction.java | 10 +- .../transport/GetResourceTransportAction.java | 63 ++- 9 files changed, 319 insertions(+), 208 deletions(-) diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java index a5b7403e33..615f27ed68 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java @@ -12,6 +12,7 @@ import java.util.Set; import org.opensearch.core.action.ActionListener; +import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.sharing.ResourceSharing; /** @@ -54,4 +55,11 @@ void revokeResourceAccess( Set scopes, ActionListener listener ); + + /** + * Lists all resources accessible by the current user. + * @param resourceIndex The index containing the resources. + * @param listener The listener to be notified with the set of accessible resources. + */ + void listAllAccessibleResources(String resourceIndex, ActionListener> listener); } diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java index 1a1ef05a24..b04708b797 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java @@ -22,6 +22,7 @@ import org.opensearch.security.common.resources.rest.ResourceAccessRequest; import org.opensearch.security.common.resources.rest.ResourceAccessResponse; import org.opensearch.security.common.support.ConfigConstants; +import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.sharing.ResourceSharing; import org.opensearch.transport.client.Client; @@ -120,6 +121,26 @@ public void revokeResourceAccess( client.execute(ResourceAccessAction.INSTANCE, request, sharingInfoResponseListener(listener)); } + /** + * Lists all resources accessible by the current user. + * + * @param listener The listener to be notified with the set of accessible resources. + */ + @Override + public void listAllAccessibleResources(String resourceIndex, ActionListener> listener) { + if (isResourceAccessControlDisabled("Unable to list all accessible resources.", listener)) { + return; + } + ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.LIST) + .resourceIndex(resourceIndex) + .build(); + client.execute( + ResourceAccessAction.INSTANCE, + request, + ActionListener.wrap(response -> { listener.onResponse(response.getResources()); }, listener::onFailure) + ); + } + /** * Helper method for share/revoke to check and return early is resource sharing is disabled * @param disabledMessage The message to be logged if resource sharing is disabled. diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java index 266c2639fa..8ff771d74e 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java @@ -706,106 +706,6 @@ public void onFailure(Exception e) { } } - /** - * Helper method to execute a search request and collect resource IDs from the results. - * - * @param resourceIds List to collect resource IDs - * @param scroll Search Scroll - * @param searchRequest Request to execute - * @param boolQuery Query to execute with the request - * @param listener Listener to be notified when the operation completes - */ - private void executeSearchRequest( - Set resourceIds, - Scroll scroll, - SearchRequest searchRequest, - BoolQueryBuilder boolQuery, - ActionListener listener - ) { - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery) - .size(1000) - .fetchSource(new String[] { "resource_id" }, null); - - searchRequest.source(searchSourceBuilder); - - StepListener searchStep = new StepListener<>(); - - client.search(searchRequest, searchStep); - - searchStep.whenComplete(initialResponse -> { - String scrollId = initialResponse.getScrollId(); - processScrollResults(resourceIds, scroll, scrollId, initialResponse.getHits().getHits(), listener); - }, listener::onFailure); - } - - /** - * Helper method to process scroll results recursively. - * - * @param resourceIds List to collect resource IDs - * @param scroll Search Scroll - * @param scrollId Scroll ID - * @param hits Search hits - * @param listener Listener to be notified when the operation completes - */ - private void processScrollResults( - Set resourceIds, - Scroll scroll, - String scrollId, - SearchHit[] hits, - ActionListener listener - ) { - // If no hits, clean up and complete - if (hits == null || hits.length == 0) { - clearScroll(scrollId, listener); - return; - } - - // Process current batch of hits - for (SearchHit hit : hits) { - Map sourceAsMap = hit.getSourceAsMap(); - if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { - resourceIds.add(sourceAsMap.get("resource_id").toString()); - } - } - - // Prepare next scroll request - SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); - scrollRequest.scroll(scroll); - - // Execute next scroll - client.searchScroll(scrollRequest, ActionListener.wrap(scrollResponse -> { - // Process next batch recursively - processScrollResults(resourceIds, scroll, scrollResponse.getScrollId(), scrollResponse.getHits().getHits(), listener); - }, e -> { - // Clean up scroll context on failure - clearScroll(scrollId, ActionListener.wrap(r -> listener.onFailure(e), ex -> { - e.addSuppressed(ex); - listener.onFailure(e); - })); - })); - } - - /** - * Helper method to clear scroll context. - * - * @param scrollId Scroll ID - * @param listener Listener to be notified when the operation completes - */ - private void clearScroll(String scrollId, ActionListener listener) { - if (scrollId == null) { - listener.onResponse(null); - return; - } - - ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); - clearScrollRequest.addScrollId(scrollId); - - client.clearScroll(clearScrollRequest, ActionListener.wrap(r -> listener.onResponse(null), e -> { - LOGGER.warn("Failed to clear scroll context", e); - listener.onResponse(null); - })); - } - /** * Updates the sharing configuration for an existing resource in the resource sharing index. * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map, Set, String, boolean, ActionListener)} @@ -926,97 +826,6 @@ public void updateResourceSharingInfo( updatedSharingListener.whenComplete(listener::onResponse, listener::onFailure); } - /** - * Updates resource sharing entries that match the specified source index and resource ID - * using the provided update script. This method performs an update-by-query operation - * in the resource sharing index. - * - *

            The method executes the following steps: - *

              - *
            1. Creates a bool query to match exact source index and resource ID
            2. - *
            3. Constructs an update-by-query request with the query and update script
            4. - *
            5. Executes the update operation
            6. - *
            7. Returns success/failure status based on update results
            8. - *
            - * - *

            Example document matching structure: - *

            -     * {
            -     *   "source_idx": "source_index_name",
            -     *   "resource_id": "resource_id_value",
            -     *   "share_with": {
            -     *     // sharing configuration to be updated
            -     *   }
            -     * }
            -     * 
            - * - * @param sourceIdx The source index to match in the query (exact match) - * @param resourceId The resource ID to match in the query (exact match) - * @param updateScript The script containing the update operations to be performed. - * This script defines how the matching documents should be modified - * @param listener Listener to be notified when the operation completes - * @apiNote This method: - *
              - *
            • Uses term queries for exact matching of source_idx and resource_id
            • - *
            • Returns false for both "no matching documents" and "operation failure" cases
            • - *
            • Logs the complete update request for debugging purposes
            • - *
            • Provides detailed logging for success and failure scenarios
            • - *
            - * @implNote The update operation uses a bool query with two must clauses: - *
            -     * {
            -     *   "query": {
            -     *     "bool": {
            -     *       "must": [
            -     *         { "term": { "source_idx.keyword": sourceIdx } },
            -     *         { "term": { "resource_id.keyword": resourceId } }
            -     *       ]
            -     *     }
            -     *   }
            -     * }
            -     * 
            - */ - private void updateByQueryResourceSharing(String sourceIdx, String resourceId, Script updateScript, ActionListener listener) { - try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { - BoolQueryBuilder query = QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) - .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); - - UpdateByQueryRequest ubq = new UpdateByQueryRequest(resourceSharingIndex).setQuery(query) - .setScript(updateScript) - .setRefresh(true); - - client.execute(UpdateByQueryAction.INSTANCE, ubq, new ActionListener<>() { - @Override - public void onResponse(BulkByScrollResponse response) { - long updated = response.getUpdated(); - if (updated > 0) { - LOGGER.debug("Successfully updated {} documents in {}.", updated, resourceSharingIndex); - listener.onResponse(true); - } else { - LOGGER.debug( - "No documents found to update in {} for source_idx: {} and resource_id: {}", - resourceSharingIndex, - sourceIdx, - resourceId - ); - listener.onResponse(false); - } - } - - @Override - public void onFailure(Exception e) { - LOGGER.error("Failed to update documents in {}.", resourceSharingIndex, e); - listener.onFailure(e); - - } - }); - } catch (Exception e) { - LOGGER.error("Failed to update documents in {} before request submission.", resourceSharingIndex, e); - listener.onFailure(e); - } - } - /** * Revokes access for specified entities from a resource sharing document. This method removes the specified * entities (users, roles, or backend roles) from the existing sharing configuration while preserving other @@ -1399,4 +1208,195 @@ public void getResourceDocumentsFromIds( } } + /** + * Updates resource sharing entries that match the specified source index and resource ID + * using the provided update script. This method performs an update-by-query operation + * in the resource sharing index. + * + *

            The method executes the following steps: + *

              + *
            1. Creates a bool query to match exact source index and resource ID
            2. + *
            3. Constructs an update-by-query request with the query and update script
            4. + *
            5. Executes the update operation
            6. + *
            7. Returns success/failure status based on update results
            8. + *
            + * + *

            Example document matching structure: + *

            +     * {
            +     *   "source_idx": "source_index_name",
            +     *   "resource_id": "resource_id_value",
            +     *   "share_with": {
            +     *     // sharing configuration to be updated
            +     *   }
            +     * }
            +     * 
            + * + * @param sourceIdx The source index to match in the query (exact match) + * @param resourceId The resource ID to match in the query (exact match) + * @param updateScript The script containing the update operations to be performed. + * This script defines how the matching documents should be modified + * @param listener Listener to be notified when the operation completes + * @apiNote This method: + *
              + *
            • Uses term queries for exact matching of source_idx and resource_id
            • + *
            • Returns false for both "no matching documents" and "operation failure" cases
            • + *
            • Logs the complete update request for debugging purposes
            • + *
            • Provides detailed logging for success and failure scenarios
            • + *
            + * @implNote The update operation uses a bool query with two must clauses: + *
            +     * {
            +     *   "query": {
            +     *     "bool": {
            +     *       "must": [
            +     *         { "term": { "source_idx.keyword": sourceIdx } },
            +     *         { "term": { "resource_id.keyword": resourceId } }
            +     *       ]
            +     *     }
            +     *   }
            +     * }
            +     * 
            + */ + private void updateByQueryResourceSharing(String sourceIdx, String resourceId, Script updateScript, ActionListener listener) { + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + BoolQueryBuilder query = QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) + .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); + + UpdateByQueryRequest ubq = new UpdateByQueryRequest(resourceSharingIndex).setQuery(query) + .setScript(updateScript) + .setRefresh(true); + + client.execute(UpdateByQueryAction.INSTANCE, ubq, new ActionListener<>() { + @Override + public void onResponse(BulkByScrollResponse response) { + long updated = response.getUpdated(); + if (updated > 0) { + LOGGER.debug("Successfully updated {} documents in {}.", updated, resourceSharingIndex); + listener.onResponse(true); + } else { + LOGGER.debug( + "No documents found to update in {} for source_idx: {} and resource_id: {}", + resourceSharingIndex, + sourceIdx, + resourceId + ); + listener.onResponse(false); + } + } + + @Override + public void onFailure(Exception e) { + LOGGER.error("Failed to update documents in {}.", resourceSharingIndex, e); + listener.onFailure(e); + + } + }); + } catch (Exception e) { + LOGGER.error("Failed to update documents in {} before request submission.", resourceSharingIndex, e); + listener.onFailure(e); + } + } + + /** + * Helper method to execute a search request and collect resource IDs from the results. + * + * @param resourceIds List to collect resource IDs + * @param scroll Search Scroll + * @param searchRequest Request to execute + * @param boolQuery Query to execute with the request + * @param listener Listener to be notified when the operation completes + */ + private void executeSearchRequest( + Set resourceIds, + Scroll scroll, + SearchRequest searchRequest, + BoolQueryBuilder boolQuery, + ActionListener listener + ) { + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery) + .size(1000) + .fetchSource(new String[] { "resource_id" }, null); + + searchRequest.source(searchSourceBuilder); + + StepListener searchStep = new StepListener<>(); + + client.search(searchRequest, searchStep); + + searchStep.whenComplete(initialResponse -> { + String scrollId = initialResponse.getScrollId(); + processScrollResults(resourceIds, scroll, scrollId, initialResponse.getHits().getHits(), listener); + }, listener::onFailure); + } + + /** + * Helper method to process scroll results recursively. + * + * @param resourceIds List to collect resource IDs + * @param scroll Search Scroll + * @param scrollId Scroll ID + * @param hits Search hits + * @param listener Listener to be notified when the operation completes + */ + private void processScrollResults( + Set resourceIds, + Scroll scroll, + String scrollId, + SearchHit[] hits, + ActionListener listener + ) { + // If no hits, clean up and complete + if (hits == null || hits.length == 0) { + clearScroll(scrollId, listener); + return; + } + + // Process current batch of hits + for (SearchHit hit : hits) { + Map sourceAsMap = hit.getSourceAsMap(); + if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { + resourceIds.add(sourceAsMap.get("resource_id").toString()); + } + } + + // Prepare next scroll request + SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); + scrollRequest.scroll(scroll); + + // Execute next scroll + client.searchScroll(scrollRequest, ActionListener.wrap(scrollResponse -> { + // Process next batch recursively + processScrollResults(resourceIds, scroll, scrollResponse.getScrollId(), scrollResponse.getHits().getHits(), listener); + }, e -> { + // Clean up scroll context on failure + clearScroll(scrollId, ActionListener.wrap(r -> listener.onFailure(e), ex -> { + e.addSuppressed(ex); + listener.onFailure(e); + })); + })); + } + + /** + * Helper method to clear scroll context. + * + * @param scrollId Scroll ID + * @param listener Listener to be notified when the operation completes + */ + private void clearScroll(String scrollId, ActionListener listener) { + if (scrollId == null) { + listener.onResponse(null); + return; + } + + ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); + clearScrollRequest.addScrollId(scrollId); + + client.clearScroll(clearScrollRequest, ActionListener.wrap(r -> listener.onResponse(null), e -> { + LOGGER.warn("Failed to clear scroll context", e); + listener.onResponse(null); + })); + } + } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java index 28cc634576..9972249ea8 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginFeatureEnabledTests.java @@ -357,6 +357,10 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(0)); } // shared_with_user should not be able to share admin's resource with itself @@ -387,6 +391,10 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString("sampleUpdated")); + + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); } // resource is still visible to super-admin @@ -411,6 +419,10 @@ public void testCreateUpdateDeleteSampleResource() throws Exception { try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) { TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(0)); } // delete sample resource with shared_with_user diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java index 26c2ca1c31..f1508be1ad 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginFeatureDisabledTests.java @@ -23,6 +23,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; import static org.opensearch.security.common.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; import static org.opensearch.security.support.ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED; @@ -105,6 +106,11 @@ public void testNoResourceRestrictions() throws Exception { HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString("sample")); + + response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sample")); } // shared_with_user is able to update admin's resource diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java index 66d519bcd6..a7fe2bccf3 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResource.java @@ -15,8 +15,10 @@ import java.util.Map; import org.opensearch.core.ParseField; +import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.ConstructingObjectParser; +import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.security.spi.resources.Resource; @@ -37,6 +39,12 @@ public SampleResource() throws IOException { super(); } + public SampleResource(StreamInput in) throws IOException { + this.name = in.readString(); + this.description = in.readString(); + this.attributes = in.readMap(StreamInput::readString, StreamInput::readString); + } + @SuppressWarnings("unchecked") private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "sample_resource", @@ -66,7 +74,7 @@ public static SampleResource fromXContent(XContentParser parser) throws IOExcept } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { return builder.startObject().field("name", name).field("description", description).field("attributes", attributes).endObject(); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceResponse.java index b6d986e257..78cc06fe24 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceResponse.java @@ -9,6 +9,7 @@ package org.opensearch.sample.resource.actions.rest.get; import java.io.IOException; +import java.util.Set; import org.opensearch.core.action.ActionResponse; import org.opensearch.core.common.io.stream.StreamInput; @@ -18,20 +19,20 @@ import org.opensearch.sample.SampleResource; public class GetResourceResponse extends ActionResponse implements ToXContentObject { - private final SampleResource resource; + private final Set resources; /** * Default constructor * - * @param resource The resource + * @param resources The resources */ - public GetResourceResponse(SampleResource resource) { - this.resource = resource; + public GetResourceResponse(Set resources) { + this.resources = resources; } @Override public void writeTo(StreamOutput out) throws IOException { - out.writeNamedWriteable(resource); + out.writeCollection(resources, (o, r) -> r.writeTo(o)); } /** @@ -40,13 +41,13 @@ public void writeTo(StreamOutput out) throws IOException { * @param in the stream input */ public GetResourceResponse(final StreamInput in) throws IOException { - resource = in.readNamedWriteable(SampleResource.class); + resources = in.readSet(SampleResource::new); } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field("resource", resource); + builder.field("resources", resources); builder.endObject(); return builder; } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java index a78b6b95f7..f534543fde 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/get/GetResourceRestAction.java @@ -10,13 +10,11 @@ import java.util.List; -import org.opensearch.core.common.Strings; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.RestRequest; import org.opensearch.rest.action.RestToXContentListener; import org.opensearch.transport.client.node.NodeClient; -import static java.util.Collections.singletonList; import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; @@ -29,7 +27,10 @@ public GetResourceRestAction() {} @Override public List routes() { - return singletonList(new Route(GET, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/get/{resource_id}")); + return List.of( + new Route(GET, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/get/{resource_id}"), + new Route(GET, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/get") + ); } @Override @@ -40,9 +41,6 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { String resourceId = request.param("resource_id"); - if (Strings.isNullOrEmpty(resourceId)) { - throw new IllegalArgumentException("resource_id parameter is required"); - } final GetResourceRequest getResourceRequest = new GetResourceRequest(resourceId); return channel -> client.executeLocally(GetResourceAction.INSTANCE, getResourceRequest, new RestToXContentListener<>(channel)); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java index 02d0908388..ed4ba2d414 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java @@ -8,6 +8,7 @@ package org.opensearch.sample.resource.actions.transport; +import java.util.HashSet; import java.util.Set; import org.apache.logging.log4j.LogManager; @@ -16,6 +17,8 @@ import org.opensearch.ResourceNotFoundException; import org.opensearch.action.get.GetRequest; import org.opensearch.action.get.GetResponse; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.common.inject.Inject; @@ -26,13 +29,17 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.index.query.QueryBuilders; import org.opensearch.sample.SampleResource; import org.opensearch.sample.SampleResourceScope; import org.opensearch.sample.resource.actions.rest.get.GetResourceAction; import org.opensearch.sample.resource.actions.rest.get.GetResourceRequest; import org.opensearch.sample.resource.actions.rest.get.GetResourceResponse; import org.opensearch.sample.resource.client.ResourceSharingClientAccessor; +import org.opensearch.search.SearchHit; +import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.security.client.resources.ResourceSharingClient; +import org.opensearch.security.common.support.ConfigConstants; import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; @@ -63,15 +70,27 @@ public GetResourceTransportAction( this.settings = settings; } + @SuppressWarnings("unchecked") @Override protected void doExecute(Task task, GetResourceRequest request, ActionListener listener) { + ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient, settings); if (request.getResourceId() == null || request.getResourceId().isEmpty()) { - listener.onFailure(new IllegalArgumentException("Resource ID cannot be null or empty")); + // get all request + if (this.settings.getAsBoolean( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT + )) { + resourceSharingClient.listAllAccessibleResources(RESOURCE_INDEX_NAME, ActionListener.wrap(resources -> { + listener.onResponse(new GetResourceResponse((Set) resources)); + }, listener::onFailure)); + } else { + // if feature is disabled, return all resources + getAllResourcesAction(listener); + } return; } // Check permission to resource - ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient, settings); resourceSharingClient.verifyResourceAccess( request.getResourceId(), RESOURCE_INDEX_NAME, @@ -104,7 +123,7 @@ private void getResourceAction(GetResourceRequest request, ActionListener nodeClient.get(getRequest, listener); } + private void getAllResourcesAction(ActionListener listener) { + ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); + try (ThreadContext.StoredContext ignored = threadContext.stashContext()) { + getAllResources(ActionListener.wrap(searchResponse -> { + SearchHit[] hits = searchResponse.getHits().getHits(); + if (hits.length == 0) { + listener.onFailure(new ResourceNotFoundException("No resources found in index: " + RESOURCE_INDEX_NAME)); + return; + } + + Set resources = new HashSet<>(); + try { + for (SearchHit hit : hits) { + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, hit.getSourceAsString()) + ) { + resources.add(SampleResource.fromXContent(parser)); + } + } + listener.onResponse(new GetResourceResponse(resources)); + } catch (Exception e) { + listener.onFailure(new ResourceSharingException("Failed to parse resources: " + e.getMessage(), e)); + } + }, listener::onFailure)); + } + } + + private void getAllResources(ActionListener listener) { + SearchRequest searchRequest = new SearchRequest(RESOURCE_INDEX_NAME); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.query(QueryBuilders.matchAllQuery()); + searchSourceBuilder.size(1000); + + searchRequest.source(searchSourceBuilder); + nodeClient.search(searchRequest, listener); + } + } From a4faa6ff8fa0dc40c7ea8f8ca389819ddd420103 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 11 Mar 2025 18:04:53 -0400 Subject: [PATCH 199/201] Adds plugin dev readme for the feature setup and improves other readmes Signed-off-by: Darshit Chanpura --- RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md | 406 +++++++++++++++++++++++++ client/README.md | 187 +++++++++++- sample-resource-plugin/README.md | 18 +- spi/README.md | 150 +++++++-- 4 files changed, 724 insertions(+), 37 deletions(-) create mode 100644 RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md diff --git a/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md b/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md new file mode 100644 index 0000000000..71f908f7c0 --- /dev/null +++ b/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md @@ -0,0 +1,406 @@ +# **Resource Sharing and Access Control in OpenSearch** + +This guide provides an **in-depth overview** for **plugin developers**, covering the **features, setup, and utilization** of the **Resource Sharing and Access Control** functionality in OpenSearch. + +## **1. What is the Feature?** +The **Resource Sharing and Access Control** feature in OpenSearch Security Plugin enables fine-grained access management for resources declared by plugins. It allows: +- Users to **share and revoke access** to their own resources. +- **Super admins** to access all resources. +- Plugins to **define and manage resource access** via a standardized interface. + +This feature ensures **secure** and **controlled** access to resources while leveraging existing **index-level authorization** in OpenSearch. + +--- + +## **2. What are the Components?** +This feature introduces **two primary components** for plugin developers: + +### **1. `opensearch-security-client`** +- Provides a client with methods for **resource access control**. +- Plugins must declare a **dependency** on this client to integrate with security features. + +### **2. `opensearch-resource-sharing-spi`** +- A **Service Provider Interface (SPI)** that plugins must implement to declare themselves as **Resource Plugins**. +- The security plugin keeps track of these plugins (similar to how JobScheduler tracks `JobSchedulerExtension`). + +### **Plugin Implementation Requirements:** + +- This feature is marked as **`@opensearch.experimental`** and can be toggled using the feature flag: **`plugins.security.resource_sharing.enabled`**, which is **enabled by default**. +- **Resource indices must be system indices**, and **system index protection must be enabled** (`plugins.security.system_indices.enabled: true`) to prevent unauthorized direct access. +- Plugins must declare dependencies on **`opensearch-security-client`** and **`opensearch-resource-sharing-spi`** in their `build.gradle`. + +### **Plugin Implementation Requirements** +Each plugin must: +- **Implement** the `ResourceSharingExtension` class. +- **Ensure** that its declared resources implement the `Resource` interface. +- **Provide a resource parser**, which the security plugin uses to extract resource details from the resource index. +- **Register itself** in `META-INF/services` by creating the following file: + ``` + src/main/resources/META-INF/services/org.opensearch.security.spi.ResourceSharingExtension + ``` + - This file must contain a **single line** specifying the **fully qualified class name** of the plugin’s `ResourceSharingExtension` implementation, e.g.: + ``` + org.opensearch.sample.SampleResourcePlugin + ``` +--- + +## **3. Feature Flag** +This feature is controlled by the following flag: + +- **Feature flag:** `plugins.security.resource_sharing.enabled` +- **Default value:** `true` +- **How to disable?** Set the flag to `false` in the opensearch configuration: + ```yaml + plugins.security.resource_sharing.enabled: false + ``` + +--- + +## **4. Declaring a Resource Plugin and Using the Client for Access Control** +### **Declaring a Plugin as a Resource Plugin** +To integrate with the security plugin, your plugin must: +1. Extend `ResourceSharingExtension` and implement required methods. +2. Implement the `Resource` interface for resource declaration. +3. Implement a resource parser to extract resource details. + +[`opensearch-resource-sharing-spi` README.md](./spi/README.md) is a great resource to learn more about the components of SPI and how to set up. + +Tip: Refer to the `org.opensearch.sample.SampleResourcePlugin` class to understand the setup in further detail. + +Example usage: +```java + +public class SampleResourcePlugin extends Plugin implements SystemIndexPlugin, ResourceSharingExtension { + + // override any required methods + + @Override + public Collection getSystemIndexDescriptors(Settings settings) { + final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(RESOURCE_INDEX_NAME, "Sample index with resources"); + return Collections.singletonList(systemIndexDescriptor); + } + + @Override + public String getResourceType() { + return SampleResource.class.getCanonicalName(); + } + + @Override + public String getResourceIndex() { + return RESOURCE_INDEX_NAME; + } + + @Override + public ResourceParser getResourceParser() { + return new SampleResourceParser(); + } +} +``` + + +### **Calling Access Control Methods from the ResourceSharingClient Client** +Plugins must **declare a dependency** on `opensearch-security-client` and use it to call access control methods. +The client provides **four access control methods** for plugins. For detailed usage and implementation, refer to the [`opensearch-security-client` README.md](./client/README.md) + + +Tip: Refer to the `org.opensearch.sample.resource.client.ResourceSharingClientAccessor` class to understand the client setup in further detail. + +Example usage: +```java + @Override +void doExecute(Task task, ShareResourceRequest request, ActionListener listener) { + if (request.getResourceId() == null || request.getResourceId().isEmpty()) { + listener.onFailure(new IllegalArgumentException("Resource ID cannot be null or empty")); + return; + } + + ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient, settings); + resourceSharingClient.shareResource( + request.getResourceId(), + RESOURCE_INDEX_NAME, + request.getShareWith(), + ActionListener.wrap(sharing -> { + ShareResourceResponse response = new ShareResourceResponse(sharing.getShareWith()); + listener.onResponse(response); + }, listener::onFailure) + ); +} +``` + + +--- + +## **5. What are Scopes?** + +This feature introduces a new **sharing mechanism** called **scopes**. Scopes define **the level of access** granted to users for a resource. They are **defined and maintained by plugins**, and the security plugin does **not** interpret or enforce their specific meanings. This approach gives plugins the **flexibility** to define scope names and behaviors based on their use case. + +Each plugin must **document its scope definitions** so that users understand the **sharing semantics** and how different scopes affect access control. + +Scopes enable **granular access control**, allowing resources to be shared with **customized permission levels**, making the system more flexible and adaptable to different use cases. + +### **Common Scopes for Plugins to declare** +| Scope | Description | +|-------------|-----------------------------------------------------| +| `PUBLIC` | The resource is accessible to all users. | +| `READ_ONLY` | Users can view but not modify the resource. | +| `READ_WRITE` | Users can view and modify the resource. | + +By default, all resources are private and only visible to the owner and super-admins. Resources become accessible to others only when explicitly shared. + +SPI provides you an interface, with two default scopes `PUBLIC` and `RESTRICTED`, which can be extended to introduce more plugin-specific values. + +### **Using Scopes in API Design** +- APIs should be logically paired with correct scopes. + - Example, **GET APIs** should be logically paired with **`READ_ONLY`**, **`READ_WRITE`**, or **`PUBLIC`** scopes. When verifying access, these scopes must be **passed to the security plugin** via the `ResourceSharingNodeClient` to determine whether a user has the required permissions. + + +--- + +## **6. Restrictions** +1. At present, **only resource owners can share/revoke access** to their own resources. + - **Admins** can manage access for any resource. +2. **Resources must be stored in a system index**, and system index protection **must be enabled**. + - **Disabling system index protection** allows users to access resources **directly** if they have relevant index permissions. +3. **This feature works on top of existing index-level authorization** and does not replace it. +4. **A user must already have index access** in order to access the resource. + +--- + +## **7. REST APIs Introduced by the Security Plugin** + +In addition to client methods, the **Security Plugin** introduces new **REST APIs** for managing resource access when the feature is enabled. These APIs allow users to **verify, grant, revoke, and list access** to resources. + +--- + +### **1. Verify Access** +- **Endpoint:** + ``` + POST /_plugins/_security/resources/verify_access + ``` +- **Description:** + Verifies whether the current user has access to a specified resource within the given index and scopes. + +#### **Request Body:** +```json +{ + "resource_id": "my-resource", + "resource_index": "resource-index", + "scopes": ["READ_ONLY"] +} +``` + +#### **Request Fields:** +| Field | Type | Description | +|-----------------|----------|-------------| +| `resource_id` | String | Unique identifier of the resource being accessed. | +| `resource_index`| String | The OpenSearch index where the resource is stored. | +| `scopes` | Array | The list of scopes to check access against (e.g., `"READ_ONLY"`, `"READ_WRITE"`). | + +#### **Response:** +Returns whether the user has permission to access the resource. +```json +{ + "has_permission": true +} +``` + +#### **Response Fields:** +| Field | Type | Description | +|-----------------|---------|-------------| +| `has_permission` | Boolean | `true` if the user has access, `false` otherwise. | + +--- + +### **2. Grant Access** +- **Endpoint:** + ``` + POST /_plugins/_security/resources/share + ``` +- **Description:** + Grants access to a resource for specified **users, roles, and backend roles** under defined **scopes**. + +#### **Request Body:** +```json +{ + "resource_id": "my-resource", + "resource_index": "resource-index", + "share_with": { + "your-scope-name": { + "users": ["shared-user-name"], + "backend_roles": ["shared-backend-roles"] + }, + "your-scope-name-2": { + "roles": ["shared-roles"] + } + } +} +``` + +#### **Request Fields:** +| Field | Type | Description | +|-----------------|---------|-------------| +| `resource_id` | String | The unique identifier of the resource to be shared. | +| `resource_index`| String | The OpenSearch index where the resource is stored. | +| `share_with` | Object | Defines which **users, roles, or backend roles** will gain access. | +| `your-scope-name` | Object | The scope under which the resource is shared (e.g., `"READ_ONLY"`, `"PUBLIC"`). | +| `users` | Array | List of usernames allowed to access the resource. | +| `roles` | Array | List of role names granted access. | +| `backend_roles`| Array | List of backend roles assigned to the resource. | + +#### **Response:** +Returns the updated **resource sharing state**. +```json +{ + "sharing_info": { + "source_idx": "resource-index", + "resource_id": "my-resource", + "created_by": { + "user": "you" + }, + "share_with": { + "your-scope-name": { + "users": ["shared-user-name"], + "backend_roles": ["shared-backend-roles"] + }, + "your-scope-name-2": { + "roles": ["shared-roles"] + } + } + } +} +``` + +#### **Response Fields:** +| Field | Type | Description | +|---------------|---------|-------------| +| `sharing_info` | Object | Contains information about how the resource is shared. | +| `source_idx` | String | The OpenSearch index containing the resource. | +| `resource_id` | String | The unique identifier of the resource being shared. | +| `created_by` | Object | Information about the user who created the sharing entry. | +| `share_with` | Object | Defines users, roles, and backend roles with access to the resource. | + +--- + +### **3. Revoke Access** +- **Endpoint:** + ``` + POST /_plugins/_security/resources/revoke + ``` +- **Description:** + Revokes access to a resource for specific users, roles, or backend roles under certain scopes. + +#### **Request Body:** +```json +{ + "resource_id": "my-resource", + "resource_index": "resource-index", + "entities_to_revoke": { + "roles": ["shared-roles"] + }, + "scopes": ["your-scope-name-2"] +} +``` + +#### **Request Fields:** +| Field | Type | Description | +|-----------------|---------|-------------| +| `resource_id` | String | The unique identifier of the resource whose access is being revoked. | +| `resource_index`| String | The OpenSearch index where the resource is stored. | +| `entities_to_revoke` | Object | Specifies which **users, roles, or backend roles** should have their access removed. | +| `roles` | Array | List of roles to revoke access from. | +| `scopes` | Array | List of scopes from which access should be revoked. | + +#### **Response:** +Returns the updated **resource sharing state** after revocation. +```json +{ + "sharing_info": { + "source_idx": "resource-index", + "resource_id": "my-resource", + "created_by": { + "user": "admin" + }, + "share_with": { + "your-scope-name": { + "users": ["shared-user-name"], + "backend_roles": ["shared-backend-roles"] + } + } + } +} +``` + +#### **Response Fields:** +| Field | Type | Description | +|---------------|---------|-------------| +| `sharing_info` | Object | Contains information about the updated resource sharing state. | +| `source_idx` | String | The OpenSearch index containing the resource. | +| `resource_id` | String | The unique identifier of the resource. | +| `created_by` | Object | Information about the user who created the sharing entry. | +| `share_with` | Object | Defines users, roles, and backend roles that still have access to the resource. | + +--- + +### **4. List Accessible Resources** +- **Endpoint:** + ``` + GET /_plugins/_security/resources/list/{resource_index} + ``` +- **Description:** + Retrieves a list of **resources that the current user has access to** within the specified `{resource_index}`. + +#### **Response:** +Returns an array of accessible resources. +```json +{ + "resources": [ + { + "name": "my-resource-name", + "description": "My resource description.", + "attributes": { + "type": "model" + } + } + ] +} +``` +*This is an example resource. Actual structure will vary based on your configuration.* + +--- + +## **Additional Notes** +- **Feature Flag:** These APIs are available only when `plugins.security.resource_sharing.enabled` is set to `true` in the configuration. +- **Index Restrictions:** Resources must be stored in **system indices**, and **system index protection** must be enabled to prevent unauthorized access. +- **Scopes Flexibility:** The `share_with` field allows defining **custom access scopes** as per plugin requirements. + +--- + +## **8. Best Practices** +### **For Plugin Developers** +- **Declare resources properly** in the `ResourceSharingExtension`. +- **Use the security client** instead of direct index queries to check access. +- **Implement a resource parser** to ensure correct resource extraction. + +### **For Users & Admins** +- **Keep system index protection enabled** for better security. +- **Grant access only when necessary** to limit exposure. +- **Regularly audit resource access**. + +--- + +## **Conclusion** +The **Resource Sharing and Access Control** feature enhances OpenSearch security by introducing an **additional layer of fine-grained access management** for plugin-defined resources. While **Fine-Grained Access Control (FGAC)** is already enabled, this feature provides **even more granular control** specifically for **resource-level access** within plugins. + +By implementing the **Service Provider Interface (SPI)**, utilizing the **security client**, and following **best practices**, developers can seamlessly integrate this feature into their plugins to enforce controlled resource sharing and access management. + +For detailed implementation and examples, refer to the **[sample plugin](./sample-resource-plugin)** included in the security plugin repository. + +--- + +## **License** +This project is licensed under the **Apache 2.0 License**. + +--- + +## **Copyright** +Β© OpenSearch Contributors. diff --git a/client/README.md b/client/README.md index 8f5b20b093..e3285a201c 100644 --- a/client/README.md +++ b/client/README.md @@ -1,10 +1,18 @@ -# Resource Sharing Client +Here's a **refined and corrected** version of your `README.md` file with improved clarity, grammar, and formatting: -This Client package provides a ResourceSharing client to be utilized by resource plugins to implement access control by communicating with security plugin. +--- -## Usage +# **Resource Sharing Client** + +This package provides a **ResourceSharing client** that resource plugins can use to **implement access control** by communicating with the **OpenSearch Security Plugin**. + +--- + +## **Usage** + +### **1. Creating a Client Accessor with Singleton Pattern** +To ensure a single instance of the `ResourceSharingNodeClient`, use the **Singleton pattern**: -1. Create a client accessor with singleton pattern: ```java public class ResourceSharingClientAccessor { private static ResourceSharingNodeClient INSTANCE; @@ -12,10 +20,11 @@ public class ResourceSharingClientAccessor { private ResourceSharingClientAccessor() {} /** - * Get resource sharing client + * Get the resource sharing client instance. * - * @param nodeClient node client - * @return resource sharing client + * @param nodeClient The OpenSearch NodeClient instance. + * @param settings The OpenSearch settings. + * @return A singleton instance of ResourceSharingNodeClient. */ public static ResourceSharingNodeClient getResourceSharingClient(NodeClient nodeClient, Settings settings) { if (INSTANCE == null) { @@ -26,14 +35,18 @@ public class ResourceSharingClientAccessor { } ``` -2. In your transport action doExecute function call the client. -Here is an example implementation of client being utilized to verify delete permissions before deleting a resource. +--- + +### **2. Using the Client in a Transport Action** +The following example demonstrates how to use the **Resource Sharing Client** inside a `TransportAction` to verify **delete permissions** before deleting a resource. + ```java @Override protected void doExecute(Task task, DeleteResourceRequest request, ActionListener listener) { - String resourceId = request.getResourceId(); + ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient, settings); + resourceSharingClient.verifyResourceAccess( resourceId, RESOURCE_INDEX_NAME, @@ -65,12 +78,156 @@ protected void doExecute(Task task, DeleteResourceRequest request, ActionListene ); } ``` -You can checkout other java APIs offered by the client by visiting ResourceSharingClient.java -## License +--- + +## **Available Java APIs** + +The **`ResourceSharingClient`** provides **four Java APIs** for **resource access control**, enabling plugins to **verify, share, revoke, and list** resources. + +πŸ“Œ **Package Location:** +πŸ”— [`org.opensearch.security.client.resources.ResourceSharingClient`](../client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java) + +--- + +### **API Usage Examples** +Below are examples demonstrating how to use each API effectively. + +--- + +### **1. `verifyResourceAccess`** +πŸ” **Checks if the current user has access to a resource** based on predefined **scopes**. + +#### **Method Signature:** +```java +void verifyResourceAccess(String resourceId, String resourceIndex, Set scopes, ActionListener listener); +``` + +#### **Example Usage:** +```java +Set scopes = Set.of("READ_ONLY"); +resourceSharingClient.verifyResourceAccess( + "resource-123", + "resource_index", + scopes, + ActionListener.wrap(isAuthorized -> { + if (isAuthorized) { + System.out.println("User has access to the resource."); + } else { + System.out.println("Access denied."); + } + }, e -> { + System.err.println("Failed to verify access: " + e.getMessage()); + }) +); +``` +> βœ… **Use Case:** Before performing operations like **deletion or modifications**, ensure the user has the right permissions. + +--- + +### **2. `shareResource`** +πŸ”„ **Grants access to a resource** for specific users, roles, or backend roles. + +#### **Method Signature:** +```java +void shareResource(String resourceId, String resourceIndex, Map shareWith, ActionListener listener); +``` + +#### **Example Usage:** +```java +Map shareWith = Map.of( + "users", List.of("user_1", "user_2"), + "roles", List.of("admin_role"), + "backend_roles", List.of("backend_group") +); + +resourceSharingClient.shareResource( + "resource-123", + "resource_index", + shareWith, + ActionListener.wrap(response -> { + System.out.println("Resource successfully shared with: " + shareWith); + }, e -> { + System.err.println("Failed to share resource: " + e.getMessage()); + }) +); +``` +> βœ… **Use Case:** Used when an **owner/admin wants to share a resource** with specific users or groups. + +--- + +### **3. `revokeResourceAccess`** +🚫 **Removes access permissions** for specified users, roles, or backend roles. + +#### **Method Signature:** +```java +void revokeResourceAccess(String resourceId, String resourceIndex, Map entitiesToRevoke, Set scopes, ActionListener listener); +``` + +#### **Example Usage:** +```java +Map entitiesToRevoke = Map.of( + "users", List.of("user_2"), + "roles", List.of("viewer_role") +); +Set scopesToRevoke = Set.of("READ_ONLY"); + +resourceSharingClient.revokeResourceAccess( + "resource-123", + "resource_index", + entitiesToRevoke, + scopesToRevoke, + ActionListener.wrap(response -> { + System.out.println("Resource access successfully revoked for: " + entitiesToRevoke); + }, e -> { + System.err.println("Failed to revoke access: " + e.getMessage()); + }) +); +``` +> βœ… **Use Case:** When a user no longer needs access to a **resource**, their permissions can be revoked. + +--- + +### **4. `listAllAccessibleResources`** +πŸ“œ **Retrieves all resources the current user has access to.** + +#### **Method Signature:** +```java +void listAllAccessibleResources(String resourceIndex, ActionListener> listener); +``` + +#### **Example Usage:** +```java +resourceSharingClient.listAllAccessibleResources( + "resource_index", + ActionListener.wrap(resources -> { + for (Resource resource : resources) { + System.out.println("Accessible Resource: " + resource.getId()); + } + }, e -> { + System.err.println("Failed to list accessible resources: " + e.getMessage()); + }) +); +``` +> βœ… **Use Case:** Helps a user identify **which resources they can interact with**. + +--- + +## **Conclusion** +These APIs provide essential methods for **fine-grained resource access control**, enabling: + +βœ” **Verification** of resource access. +βœ” **Granting and revoking** access dynamically. +βœ” **Retrieval** of all accessible resources. + +For further details, refer to the [`ResourceSharingClient` Java class](../client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java). πŸš€ + +--- -This code is licensed under the Apache 2.0 License. +## **License** +This project is licensed under the **Apache 2.0 License**. -## Copyright +--- -Copyright OpenSearch Contributors. +## **Copyright** +Β© OpenSearch Contributors. diff --git a/sample-resource-plugin/README.md b/sample-resource-plugin/README.md index d5e5fdbc8b..efb64d4630 100644 --- a/sample-resource-plugin/README.md +++ b/sample-resource-plugin/README.md @@ -71,15 +71,27 @@ The plugin exposes the following six API endpoints: ### 4. Get Resource - **Endpoint:** `GET /_plugins/sample_resource_sharing/get/{resource_id}` -- **Description:** Get a specified resource owned by the requesting user, if the user has access to the resource, else fails. +- **Description:** Get a specified resource owned by or shared_with the requesting user, if the user has access to the resource, else fails. - **Response:** ```json { - "resource" : { + "resources" : [{ "name" : "", "description" : null, "attributes" : null - } + }] + } + ``` +- **Endpoint:** `GET /_plugins/sample_resource_sharing/get` +- **Description:** Get all resources owned by or shared with the requesting user. +- **Response:** + ```json + { + "resources" : [{ + "name" : "", + "description" : null, + "attributes" : null + }] } ``` diff --git a/spi/README.md b/spi/README.md index de41ab7095..4e0f04f53f 100644 --- a/spi/README.md +++ b/spi/README.md @@ -1,21 +1,23 @@ -# Resource Sharing and Access Control SPI +# **Resource Sharing and Access Control SPI** -This SPI provides interfaces to implement Resource Sharing and Access Control. +This **Service Provider Interface (SPI)** provides the necessary **interfaces and mechanisms** to implement **Resource Sharing and Access Control** in OpenSearch. +--- -## Usage +## **Usage** -A plugin defining a resource and aiming to implement access control over that resource must extend ResourceSharingExtension class to register itself as a Resource Plugin. Here is an example: +A plugin that **defines a resource** and aims to implement **access control** over that resource must **extend** the `ResourceSharingExtension` class to register itself as a **Resource Plugin**. +### **Example: Implementing a Resource Plugin** ```java - public class SampleResourcePlugin extends Plugin implements SystemIndexPlugin, ResourceSharingExtension { - // override any required methods + // Override required methods @Override public Collection getSystemIndexDescriptors(Settings settings) { - final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(RESOURCE_INDEX_NAME, "Sample index with resources"); + final SystemIndexDescriptor systemIndexDescriptor = + new SystemIndexDescriptor(RESOURCE_INDEX_NAME, "Sample index with resources"); return Collections.singletonList(systemIndexDescriptor); } @@ -36,20 +38,130 @@ public class SampleResourcePlugin extends Plugin implements SystemIndexPlugin, R } ``` -Checklist for resource plugin: -1. Add a dependency on `opensearch-security-client` and `opensearch-resource-sharing-spi` in build.gradle. -2. Under `src/main/resources` folder of the plugin, locate or create a folder `META-INF/services`and in the services folder, declare a file named `org.opensearch.security.spi.resources.ResourceSharingExtension`. Edit that file to add single line containing classpath of your plugin, e.g `org.opensearch.sample.SampleResourcePlugin`. This is required to utilize Java's Service Provider Interface mechanism. -3. Declare a resource class and implement `Resource` class from SPI. -4. Implement a `ResourceParser`. -5. Implement `ResourceSharingExtension` interface in the plugin declaration class, and implement required methods (as shown above). Ensure that resource index is marked as a system index. -6. Create a client accessor that will instantiate `ResourceSharingNodeClient`. -7. Use the methods provided by `ResourceSharingNodeClient` to implement resource access-control. +--- + +## **Checklist for Implementing a Resource Plugin** + +To properly integrate with the **Resource Sharing and Access Control SPI**, follow these steps: + +### **1. Add Required Dependencies** +Include **`opensearch-security-client`** and **`opensearch-resource-sharing-spi`** in your **`build.gradle`** file. +Example: +```gradle +dependencies { + implementation 'org.opensearch:opensearch-security-client:VERSION' + implementation 'org.opensearch:opensearch-resource-sharing-spi:VERSION' +} +``` + +--- + +### **2. Register the Plugin Using the Java SPI Mechanism** +- Navigate to your plugin's `src/main/resources` folder. +- Locate or create the `META-INF/services` directory. +- Inside `META-INF/services`, create a file named: + ``` + org.opensearch.security.spi.resources.ResourceSharingExtension + ``` +- Edit the file and add a **single line** containing the **fully qualified class name** of your plugin implementation. + Example: + ``` + org.opensearch.sample.SampleResourcePlugin + ``` + > βœ… This step ensures that OpenSearch **dynamically loads your plugin** as a resource-sharing extension. + +--- + +### **3. Declare a Resource Class** +Each plugin must define a **resource class** that implements the `Resource` interface. +Example: +```java +public class SampleResource implements Resource { + private String id; + private String owner; + + // Constructor, getters, setters, etc. + + @Override + public String getResourceId() { + return id; + } +} +``` + +--- + +### **4. Implement a Resource Parser** +A **`ResourceParser`** is required to convert **resource data** from OpenSearch indices. +Example: +```java +public class SampleResourceParser implements ResourceParser { + @Override + public SampleResource parseXContent(XContentParser parser) throws IOException { + return SampleResource.fromXContent(parser); + } +} +``` + +--- + +### **5. Implement the `ResourceSharingExtension` Interface** +Ensure that your **plugin declaration class** implements `ResourceSharingExtension` and provides **all required methods**. + +βœ… **Important:** Mark the resource **index as a system index** to enforce security protections. + +--- + +### **6. Create a Client Accessor** +A **singleton accessor** should be created to manage the `ResourceSharingNodeClient`. +Example: +```java +public class ResourceSharingClientAccessor { + private static ResourceSharingNodeClient INSTANCE; + + private ResourceSharingClientAccessor() {} + + public static ResourceSharingNodeClient getResourceSharingClient(NodeClient nodeClient, Settings settings) { + if (INSTANCE == null) { + INSTANCE = new ResourceSharingNodeClient(nodeClient, settings); + } + return INSTANCE; + } +} +``` + +--- + +### **7. Utilize `ResourceSharingNodeClient` for Access Control** +Use the **client API methods** to manage resource sharing. + +#### **Example: Verifying Resource Access** +```java +Set scopes = Set.of("READ_ONLY"); +resourceSharingClient.verifyResourceAccess( + "resource-123", + "resource_index", + scopes, + ActionListener.wrap(isAuthorized -> { + if (isAuthorized) { + System.out.println("User has access to the resource."); + } else { + System.out.println("Access denied."); + } + }, e -> { + System.err.println("Failed to verify access: " + e.getMessage()); + }) +); +``` +--- -## License +## **License** +This project is licensed under the **Apache 2.0 License**. -This code is licensed under the Apache 2.0 License. +--- -## Copyright +## **Copyright** +Β© OpenSearch Contributors. -Copyright OpenSearch Contributors. +--- From 162b50f709092d78fcd3f0c0cbbbfc8af403439e Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 12 Mar 2025 16:52:03 -0400 Subject: [PATCH 200/201] Adds integ tests for limited permissions user Signed-off-by: Darshit Chanpura --- .../resources/ResourceAccessHandler.java | 4 +- .../AbstractSampleResourcePluginTests.java | 23 +- ...ResourcePluginLimitedPermissionsTests.java | 200 ++++++++++++++++++ .../rest/create/CreateResourceRestAction.java | 1 + 4 files changed, 223 insertions(+), 5 deletions(-) create mode 100644 sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginLimitedPermissionsTests.java diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java index 5a21c3472b..3912001aa1 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java @@ -253,8 +253,8 @@ public void hasPermission(String resourceId, String resourceIndex, Set s // All public entities are designated with "*" userRoles.add("*"); userBackendRoles.add("*"); - if (isSharedWithEveryone(document) - || isOwnerOfResource(document, user.getName()) + if (isOwnerOfResource(document, user.getName()) + || isSharedWithEveryone(document) || isSharedWithEntity(document, Recipient.USERS, Set.of(user.getName(), "*"), scopes) || isSharedWithEntity(document, Recipient.ROLES, userRoles, scopes) || isSharedWithEntity(document, Recipient.BACKEND_ROLES, userBackendRoles, scopes)) { diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java index b4430725fb..391ba70e69 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/AbstractSampleResourcePluginTests.java @@ -30,9 +30,18 @@ public abstract class AbstractSampleResourcePluginTests { new TestSecurityConfig.Role("shared_role").indexPermissions("*").on("*").clusterPermissions("*") ); + // No update permission protected final static TestSecurityConfig.User SHARED_WITH_USER_LIMITED_PERMISSIONS = new TestSecurityConfig.User( "resource_sharing_test_user" - ).roles(new TestSecurityConfig.Role("shared_role").indexPermissions("*").on(RESOURCE_INDEX_NAME)); + ).roles( + new TestSecurityConfig.Role("shared_role").clusterPermissions( + "cluster:admin/security/resource_access", + "cluster:admin/sample-resource-plugin/get", + "cluster:admin/sample-resource-plugin/create", + "cluster:admin/sample-resource-plugin/share", + "cluster:admin/sample-resource-plugin/revoke" + ) + ); protected static final String SAMPLE_RESOURCE_CREATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/create"; protected static final String SAMPLE_RESOURCE_GET_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/get"; @@ -67,13 +76,17 @@ protected static String shareWithPayloadSecurityApi(String resourceId) { } protected static String shareWithPayload() { + return shareWithPayload(SHARED_WITH_USER.getName()); + } + + protected static String shareWithPayload(String user) { return "{" + "\"share_with\":{" + "\"" + SampleResourceScope.PUBLIC.value() + "\":{" + "\"users\": [\"" - + SHARED_WITH_USER.getName() + + user + "\"]" + "}" + "}" @@ -100,10 +113,14 @@ protected static String revokeAccessPayloadSecurityApi(String resourceId) { } protected static String revokeAccessPayload() { + return revokeAccessPayload(SHARED_WITH_USER.getName()); + } + + protected static String revokeAccessPayload(String user) { return "{" + "\"entities_to_revoke\": {" + "\"users\": [\"" - + SHARED_WITH_USER.getName() + + user + "\"]" + "}," + "\"scopes\": [\"" diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginLimitedPermissionsTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginLimitedPermissionsTests.java new file mode 100644 index 0000000000..ab618aab11 --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/SampleResourcePluginLimitedPermissionsTests.java @@ -0,0 +1,200 @@ +/* + * 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. + */ + +package org.opensearch.sample; + +import java.util.Map; + +import org.apache.http.HttpStatus; +import org.junit.ClassRule; +import org.junit.Test; + +import org.opensearch.painless.PainlessModulePlugin; +import org.opensearch.security.common.resources.ResourcePluginInfo; +import org.opensearch.security.common.resources.ResourceProvider; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.security.common.resources.ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +/** + * These tests run with resource sharing enabled and system index protection enabled + */ +public class SampleResourcePluginLimitedPermissionsTests extends AbstractSampleResourcePluginFeatureEnabledTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .plugin(SampleResourcePlugin.class, PainlessModulePlugin.class) + .anonymousAuth(true) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_ADMIN, SHARED_WITH_USER_LIMITED_PERMISSIONS) + .nodeSettings(Map.of(SECURITY_SYSTEM_INDICES_ENABLED_KEY, true)) + .build(); + + @Override + protected LocalCluster getLocalCluster() { + return cluster; + } + + @Test + public void testAccessWithLimitedIP() throws Exception { + String resourceId; + // create sample resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER_LIMITED_PERMISSIONS)) { + String sampleResource = "{\"name\":\"sample\"}"; + HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + response.assertStatusCode(HttpStatus.SC_OK); + + resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + Thread.sleep(1000); + } + + // Create an entry in resource-sharing index + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually + + String json = String.format( + "{" + + " \"source_idx\": \"" + + RESOURCE_INDEX_NAME + + "\"," + + " \"resource_id\": \"%s\"," + + " \"created_by\": {" + + " \"user\": \"%s\"" + + " }" + + "}", + resourceId, + SHARED_WITH_USER_LIMITED_PERMISSIONS.getName() + ); + HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json); + assertThat(response.getStatusReason(), containsString("Created")); + + // Also update the in-memory map and get + ResourcePluginInfo.getInstance().getResourceIndicesMutable().add(RESOURCE_INDEX_NAME); + ResourceProvider provider = new ResourceProvider( + SampleResource.class.getCanonicalName(), + RESOURCE_INDEX_NAME, + new SampleResourceParser() + ); + ResourcePluginInfo.getInstance().getResourceProvidersMutable().put(RESOURCE_INDEX_NAME, provider); + + Thread.sleep(1000); + response = client.get(SECURITY_RESOURCE_LIST_ENDPOINT + "/" + RESOURCE_INDEX_NAME); + response.assertStatusCode(HttpStatus.SC_OK); + assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1)); + assertThat(response.getBody(), containsString("sample")); + } + + // user should be able to get its own resource as it has get API access + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER_LIMITED_PERMISSIONS)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // Update user's sample resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER_LIMITED_PERMISSIONS)) { + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); + // cannot update because this user doesnt have access to update API + updateResponse.assertStatusCode(HttpStatus.SC_FORBIDDEN); + assertThat( + updateResponse.bodyAsJsonNode().get("error").get("root_cause").get(0).get("reason").asText(), + containsString( + "no permissions for [cluster:admin/sample-resource-plugin/update] and User [name=resource_sharing_test_user, backend_roles=[], requestedTenant=null]" + ) + ); + } + + // User admin should not be able to update, since resource is not shared with it + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); + // cannot update because this user doesnt have access to the resource + updateResponse.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // Super admin can update the resource + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + HttpResponse updateResponse = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); + // cannot update because this user doesnt have access to update API + updateResponse.assertStatusCode(HttpStatus.SC_OK); + assertThat(updateResponse.getBody(), containsString("sample")); + } + + // share resource with admin + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER_LIMITED_PERMISSIONS)) { + HttpResponse response = client.postJson( + SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, + shareWithPayload(USER_ADMIN.getName()) + ); + + response.assertStatusCode(HttpStatus.SC_OK); + } + + // admin is able to access resource now + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_OK); + } + + // revoke admin's access + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER_LIMITED_PERMISSIONS)) { + HttpResponse response = client.postJson( + SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, + revokeAccessPayload(USER_ADMIN.getName()) + ); + + response.assertStatusCode(HttpStatus.SC_OK); + } + + // admin can no longer access the resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // delete sample resource + try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER_LIMITED_PERMISSIONS)) { + HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + + // cannot delete because this user doesnt have access to delete API + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + assertThat( + response.bodyAsJsonNode().get("error").get("root_cause").get(0).get("reason").asText(), + containsString( + "no permissions for [cluster:admin/sample-resource-plugin/delete] and User [name=resource_sharing_test_user, backend_roles=[], requestedTenant=null]" + ) + ); + } + + // User admin should not be able to delete share_with_user's resource + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + + // cannot delete because user admin doesn't have access to resource + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + + // Super admin can delete the resource + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); + + response.assertStatusCode(HttpStatus.SC_OK); + } + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java index 21c392565e..8cfc00d013 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRestAction.java @@ -77,6 +77,7 @@ private RestChannelConsumer updateResource(Map source, String re ); } + @SuppressWarnings("unchecked") private RestChannelConsumer createResource(Map source, NodeClient client) throws IOException { String name = (String) source.get("name"); String description = source.containsKey("description") ? (String) source.get("description") : null; From b317ed19f4854316e59e308e7ba8be5acbab2c49 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 12 Mar 2025 16:52:28 -0400 Subject: [PATCH 201/201] Updates READMEs to include section on user setup Signed-off-by: Darshit Chanpura --- RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md | 61 +++++++++++++++++++--- sample-resource-plugin/README.md | 71 +++++++++++++++++++------- 2 files changed, 106 insertions(+), 26 deletions(-) diff --git a/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md b/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md index 71f908f7c0..0a3893fb02 100644 --- a/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md +++ b/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md @@ -156,17 +156,63 @@ SPI provides you an interface, with two default scopes `PUBLIC` and `RESTRICTED` --- -## **6. Restrictions** +## **6. User Setup** + +To enable users to interact with the **Resource Sharing and Access Control** feature, they must be assigned the appropriate cluster permissions along with resource-specific access. + +### **Required Cluster Permissions** +Users must be assigned the following **cluster permissions** in `roles.yml`: + +- **`cluster:admin/security/resource_access`** β†’ Required to evaluate resource permissions. +- **Plugin-specific cluster permissions** β†’ Required to interact with the plugin’s APIs. + +#### **Example Role Configurations** +```yaml +sample_full_access: + cluster_permissions: + - 'cluster:admin/security/resource_access' + - 'cluster:admin/sample-resource-plugin/*' + +sample_read_access: + cluster_permissions: + - 'cluster:admin/security/resource_access' + - 'cluster:admin/sample-resource-plugin/get' +``` + + +### **User Access Rules** +1. **Users must have the required cluster permissions** + - Even if a resource is shared with a user, they **cannot access it** unless they have the **plugin’s cluster permissions**. + +2. **Granting plugin API permissions does not automatically grant resource access** + - A resource must be **explicitly shared** with the user. + - **Or, the user must be the resource owner.** + +3. **No index permissions are required** + - Access control is **handled at the cluster level**. + - The `.opensearch_resource_sharing` index and the resource indices are protected under system index security. + + +### **Summary** +| **Requirement** | **Description** | +|---------------|---------------------------------------------------------------------------------------| +| **Cluster Permission** | `cluster:admin/security/resource_access` required for resource evaluation. | +| **Plugin API Permissions** | Users must also have relevant plugin API cluster permissions. | +| **Resource Sharing** | Access is granted only if the resource is shared with the user or they are the owner. | +| **No Index Permissions Needed** | The `.opensearch_resource_sharing` index and resource indices are system-protected. | + + +--- + +## **7. Restrictions** 1. At present, **only resource owners can share/revoke access** to their own resources. - - **Admins** can manage access for any resource. + - **Super admins** can manage access for any resource. 2. **Resources must be stored in a system index**, and system index protection **must be enabled**. - **Disabling system index protection** allows users to access resources **directly** if they have relevant index permissions. -3. **This feature works on top of existing index-level authorization** and does not replace it. -4. **A user must already have index access** in order to access the resource. --- -## **7. REST APIs Introduced by the Security Plugin** +## **8. REST APIs Introduced by the Security Plugin** In addition to client methods, the **Security Plugin** introduces new **REST APIs** for managing resource access when the feature is enabled. These APIs allow users to **verify, grant, revoke, and list access** to resources. @@ -375,7 +421,7 @@ Returns an array of accessible resources. --- -## **8. Best Practices** +## **9. Best Practices** ### **For Plugin Developers** - **Declare resources properly** in the `ResourceSharingExtension`. - **Use the security client** instead of direct index queries to check access. @@ -384,7 +430,6 @@ Returns an array of accessible resources. ### **For Users & Admins** - **Keep system index protection enabled** for better security. - **Grant access only when necessary** to limit exposure. -- **Regularly audit resource access**. --- @@ -393,7 +438,7 @@ The **Resource Sharing and Access Control** feature enhances OpenSearch security By implementing the **Service Provider Interface (SPI)**, utilizing the **security client**, and following **best practices**, developers can seamlessly integrate this feature into their plugins to enforce controlled resource sharing and access management. -For detailed implementation and examples, refer to the **[sample plugin](./sample-resource-plugin)** included in the security plugin repository. +For detailed implementation and examples, refer to the **[sample plugin](./sample-resource-plugin/README.md)** included in the security plugin repository. --- diff --git a/sample-resource-plugin/README.md b/sample-resource-plugin/README.md index efb64d4630..b6101f463f 100644 --- a/sample-resource-plugin/README.md +++ b/sample-resource-plugin/README.md @@ -23,6 +23,59 @@ plugins.security.system_indices.enabled: true - Create, update, get, delete resources, as well as share and revoke access to a resource. +## Installation + +1. Clone the repository: + ```bash + git clone git@github.com:opensearch-project/security.git + ``` + +2. Navigate to the project directory: + ```bash + cd sample-resource-plugin + ``` + +3. Build and deploy the plugin: + ```bash + $ ./gradlew clean build -x test -x integrationTest -x spotbugsIntegrationTest + $ ./bin/opensearch-plugin install file: /sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-.zip + ``` + + +## User setup: +1. **New Security Permission Requirement** + - Users need **`cluster:admin/security/resource_access`** in their role to **interact with shared resources**. + - This applies **in addition to** any plugin-specific cluster permissions. + +2. **No Index-Level Permissions Required** + - **Resource access is controlled at the cluster level**. + - Users **do not** need explicit index-level permissions to access shared resources. + +3. **Sample Role Configurations** + - Below are **two sample roles** demonstrating how to configure permissions in `roles.yml`: + + ```yaml + sample_full_access: + cluster_permissions: + - 'cluster:admin/security/resource_access' + - 'cluster:admin/sample-resource-plugin/*' + + sample_read_access: + cluster_permissions: + - 'cluster:admin/security/resource_access' + - 'cluster:admin/sample-resource-plugin/get' + ``` + +4. **Interaction Rules** + - If a **user is not the resource owner**, they must: + - Be assigned **a role with `sample_read_access`** permissions. + - **Have the resource shared with them** via the resource-sharing API. + - A user **without** the necessary `sample-resource-plugin` cluster permissions: + - **Cannot access the resource**, even if it is shared with them. + - A user **with `sample-resource-plugin` permissions** but **without a shared resource**: + - **Cannot access the resource**, since resource-level access control applies. + + ## API Endpoints The plugin exposes the following six API endpoints: @@ -138,24 +191,6 @@ The plugin exposes the following six API endpoints: } ``` -## Installation - -1. Clone the repository: - ```bash - git clone git@github.com:opensearch-project/security.git - ``` - -2. Navigate to the project directory: - ```bash - cd sample-resource-plugin - ``` - -3. Build and deploy the plugin: - ```bash - $ ./gradlew clean build -x test -x integrationTest -x spotbugsIntegrationTest - $ ./bin/opensearch-plugin install file: /sample-resource-plugin/build/distributions/opensearch-sample-resource-plugin-.zip - ``` - ## License This code is licensed under the Apache 2.0 License.