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); + } + } }