From 857557623cba94da00680ea426436bc842168a6d Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 25 Sep 2025 22:16:04 -0400 Subject: [PATCH 01/40] WIP on multiple sharable resource types per index Signed-off-by: Craig Perkins --- .../RevokeResourceAccessTransportAction.java | 4 +- .../ShareResourceTransportAction.java | 4 +- .../client/ResourceSharingClient.java | 16 ++-- .../privileges/ResourceAccessEvaluator.java | 2 +- .../ResourceAccessControlClient.java | 24 +++--- .../resources/ResourceAccessHandler.java | 73 ++++++++++--------- .../resources/ResourcePluginInfo.java | 7 +- .../ResourceSharingIndexHandler.java | 20 +++-- .../resources/api/share/ShareRequest.java | 27 +++++-- .../api/share/ShareTransportAction.java | 6 +- .../ResourceAccessEvaluatorTest.java | 3 +- .../resources/ResourceAccessHandlerTest.java | 15 ++-- 12 files changed, 112 insertions(+), 89 deletions(-) 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 af6d0fc9b5..fd6e09fdd4 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 @@ -26,7 +26,7 @@ import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; -import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; /** * Transport action for revoking resource access. @@ -54,7 +54,7 @@ protected void doExecute(Task task, RevokeResourceAccessRequest request, ActionL return; } ShareWith target = request.getEntitiesToRevoke(); - resourceSharingClient.revoke(request.getResourceId(), RESOURCE_INDEX_NAME, target, ActionListener.wrap(success -> { + resourceSharingClient.revoke(request.getResourceId(), RESOURCE_TYPE, target, ActionListener.wrap(success -> { RevokeResourceAccessResponse response = new RevokeResourceAccessResponse(success.getShareWith()); log.debug("Revoked resource access: {}", response.toString()); listener.onResponse(response); 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 35482821bd..50de064c2b 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 @@ -26,7 +26,7 @@ import org.opensearch.tasks.Task; import org.opensearch.transport.TransportService; -import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; /** * Transport action implementation for sharing a resource. @@ -58,7 +58,7 @@ protected void doExecute(Task task, ShareResourceRequest request, ActionListener return; } ShareWith shareWith = request.getShareWith(); - resourceSharingClient.share(request.getResourceId(), RESOURCE_INDEX_NAME, shareWith, ActionListener.wrap(sharing -> { + resourceSharingClient.share(request.getResourceId(), RESOURCE_TYPE, shareWith, ActionListener.wrap(sharing -> { ShareWith finalShareWith = sharing == null ? null : sharing.getShareWith(); ShareResourceResponse response = new ShareResourceResponse(finalShareWith); log.debug("Shared resource: {}", response.toString()); diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/client/ResourceSharingClient.java b/spi/src/main/java/org/opensearch/security/spi/resources/client/ResourceSharingClient.java index 9a92020254..b6561811c9 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/client/ResourceSharingClient.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/client/ResourceSharingClient.java @@ -24,34 +24,34 @@ 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 resourceType The resource type. * @param action The action to be verified * @param listener The listener to be notified with the access verification result. */ - void verifyAccess(String resourceId, String resourceIndex, String action, ActionListener listener); + void verifyAccess(String resourceId, String resourceType, String action, 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 resourceType The resource type. * @param target The users, roles, and backend roles to share the resource with and respective access levels. * @param listener The listener to be notified with the updated ResourceSharing document. */ - void share(String resourceId, String resourceIndex, ShareWith target, ActionListener listener); + void share(String resourceId, String resourceType, ShareWith target, ActionListener listener); /** * Revokes access to a resource for the specified entities. * @param resourceId The ID of the resource to revoke access for. - * @param resourceIndex The index containing the resource. + * @param resourceType The index containing the resource. * @param target The entities to revoke access for. * @param listener The listener to be notified with the updated ResourceSharing document. */ - void revoke(String resourceId, String resourceIndex, ShareWith target, ActionListener listener); + void revoke(String resourceId, String resourceType, ShareWith target, ActionListener listener); /** * Lists resourceIds of all shareable resources accessible by the current user. - * @param resourceIndex The index containing the resources. + * @param resourceType The resource type * @param listener The listener to be notified with the set of accessible resources. */ - void getAccessibleResourceIds(String resourceIndex, ActionListener> listener); + void getAccessibleResourceIds(String resourceType, ActionListener> listener); } diff --git a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java index 1984028b77..84fe24c5ae 100644 --- a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java @@ -78,7 +78,7 @@ public void evaluateAsync( // if it reached this evaluator, it is safe to assume that the request if of DocRequest type DocRequest req = (DocRequest) request; - resourceAccessHandler.hasPermission(req.id(), req.index(), action, context, ActionListener.wrap(hasAccess -> { + resourceAccessHandler.hasPermission(req.id(), req.type(), action, context, ActionListener.wrap(hasAccess -> { if (hasAccess) { pResponse.allowed = true; pResponseListener.onResponse(pResponse.markComplete()); diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java index c8d08e42fa..ccc7c6d8af 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java @@ -37,49 +37,49 @@ public ResourceAccessControlClient(ResourceAccessHandler resourceAccessHandler) * Verifies whether the current user has access to the specified resource. * * @param resourceId The ID of the resource to verify. - * @param resourceIndex The index in which the resource resides. + * @param resourceType The resource tupe. * @param action The action to be evaluated against * @param listener Callback that receives {@code true} if access is granted, {@code false} otherwise. */ @Override - public void verifyAccess(String resourceId, String resourceIndex, String action, ActionListener listener) { - resourceAccessHandler.hasPermission(resourceId, resourceIndex, action, null, listener); + public void verifyAccess(String resourceId, String resourceType, String action, ActionListener listener) { + resourceAccessHandler.hasPermission(resourceId, resourceType, action, null, listener); } /** * Shares a resource with specified users, roles, or backend roles. * * @param resourceId The ID of the resource to share. - * @param resourceIndex The index containing the resource. + * @param resourceType The resource type. * @param target The recipients of the resource, including users, roles, and backend roles and respective access levels. * @param listener Callback receiving the updated {@link ResourceSharing} document. */ @Override - public void share(String resourceId, String resourceIndex, ShareWith target, ActionListener listener) { - resourceAccessHandler.share(resourceId, resourceIndex, target, listener); + public void share(String resourceId, String resourceType, ShareWith target, ActionListener listener) { + resourceAccessHandler.share(resourceId, resourceType, target, listener); } /** * Revokes previously granted access to a resource for specific users or roles. * * @param resourceId The ID of the resource. - * @param resourceIndex The index containing the resource. + * @param resourceType The resource type. * @param target A map of entities whose access is to be revoked. * @param listener Callback receiving the updated {@link ResourceSharing} document. */ @Override - public void revoke(String resourceId, String resourceIndex, ShareWith target, ActionListener listener) { - resourceAccessHandler.revoke(resourceId, resourceIndex, target, listener); + public void revoke(String resourceId, String resourceType, ShareWith target, ActionListener listener) { + resourceAccessHandler.revoke(resourceId, resourceType, target, listener); } /** * Lists all resources the current user has access to within the given index. * - * @param resourceIndex The index to search for accessible resources. + * @param resourceType The resource type. * @param listener Callback receiving a set of resource ids. */ @Override - public void getAccessibleResourceIds(String resourceIndex, ActionListener> listener) { - resourceAccessHandler.getOwnAndSharedResourceIdsForCurrentUser(resourceIndex, listener); + public void getAccessibleResourceIds(String resourceType, ActionListener> listener) { + resourceAccessHandler.getOwnAndSharedResourceIdsForCurrentUser(resourceType, listener); } } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 0e05651081..a0426fae2c 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -76,10 +76,10 @@ public ResourceAccessHandler( /** * 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 resourceType The resource type. * @param listener The listener to be notified with the set of accessible resource IDs. */ - public void getOwnAndSharedResourceIdsForCurrentUser(@NonNull String resourceIndex, ActionListener> listener) { + public void getOwnAndSharedResourceIdsForCurrentUser(@NonNull String resourceType, ActionListener> listener) { UserSubjectImpl userSub = (UserSubjectImpl) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); User user = userSub == null ? null : userSub.getUser(); @@ -90,22 +90,22 @@ public void getOwnAndSharedResourceIdsForCurrentUser(@NonNull String resourceInd } if (adminDNs.isAdmin(user)) { - loadAllResourceIds(resourceIndex, ActionListener.wrap(listener::onResponse, listener::onFailure)); + loadAllResourceIds(resourceType, ActionListener.wrap(listener::onResponse, listener::onFailure)); return; } Set flatPrincipals = getFlatPrincipals(user); // 3) Fetch all accessible resource IDs - resourceSharingIndexHandler.fetchAccessibleResourceIds(resourceIndex, flatPrincipals, listener); + resourceSharingIndexHandler.fetchAccessibleResourceIds(resourceType, flatPrincipals, listener); } /** * Returns a set of resource sharing records for the current user within the specified resource index. * - * @param resourceIndex The resource index to check for accessible resources. + * @param resourceType The resource type. * @param listener The listener to be notified with the set of resource sharing records. */ - public void getResourceSharingInfoForCurrentUser(@NonNull String resourceIndex, ActionListener> listener) { + public void getResourceSharingInfoForCurrentUser(@NonNull String resourceType, ActionListener> listener) { UserSubjectImpl userSub = (UserSubjectImpl) threadContext.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); User user = userSub == null ? null : userSub.getUser(); @@ -116,28 +116,30 @@ public void getResourceSharingInfoForCurrentUser(@NonNull String resourceIndex, } if (adminDNs.isAdmin(user)) { - loadAllResourceSharingRecords(resourceIndex, ActionListener.wrap(listener::onResponse, listener::onFailure)); + loadAllResourceSharingRecords(resourceType, ActionListener.wrap(listener::onResponse, listener::onFailure)); return; } Set flatPrincipals = getFlatPrincipals(user); + String resourceIndex = resourcePluginInfo.indexByType(resourceType); + // 3) Fetch all accessible resource sharing records - resourceSharingIndexHandler.fetchAccessibleResourceSharingRecords(resourceIndex, user, flatPrincipals, listener); + resourceSharingIndexHandler.fetchAccessibleResourceSharingRecords(resourceIndex, resourceType, user, flatPrincipals, listener); } /** * Checks whether current user has permission to access given resource. * * @param resourceId The resource ID to check access for. - * @param resourceIndex The resource index containing the resource. + * @param resourceType The resource type. * @param action The action to check permission for * @param context The evaluation context to be used. Will be null when used by {@link ResourceAccessControlClient}. * @param listener The listener to be notified with the permission check result. */ public void hasPermission( @NonNull String resourceId, - @NonNull String resourceIndex, + @NonNull String resourceType, @NonNull String action, PrivilegesEvaluationContext context, ActionListener listener @@ -177,6 +179,13 @@ public void hasPermission( return; } + String resourceIndex = resourcePluginInfo.indexByType(resourceType); + if (resourceIndex == null) { + LOGGER.debug("No resourceIndex mapping found for type '{}'; denying action {}", resourceType, action); + listener.onResponse(false); + return; + } + resourceSharingIndexHandler.fetchSharingInfo(resourceIndex, resourceId, ActionListener.wrap(document -> { // Document may be null when cluster has enabled resource-sharing protection for that index, but have not migrated any records. // This also means that for non-existing documents, the evaluator will return 403 instead @@ -205,13 +214,6 @@ public void hasPermission( } // Fetch the static action-groups registered by plugins on bootstrap and check whether any match - final String resourceType = resourcePluginInfo.typeByIndex(resourceIndex); - if (resourceType == null) { - LOGGER.debug("No resourceType mapping found for index '{}'; denying action {}", resourceIndex, action); - listener.onResponse(false); - return; - } - final FlattenedActionGroups agForType = resourcePluginInfo.flattenedForType(resourceType); final Set allowedActions = agForType.resolve(accessLevels); final WildcardMatcher matcher = WildcardMatcher.from(allowedActions); @@ -286,10 +288,10 @@ public void patchSharingInfo( /** * Get sharing info for this record * @param resourceId id of the resource whose sharing info is to be fetched - * @param resourceIndex name of the resource index + * @param resourceType the resource type * @param listener listener to be notified of final resource sharing record */ - public void getSharingInfo(@NonNull String resourceId, @NonNull String resourceIndex, ActionListener listener) { + public void getSharingInfo(@NonNull String resourceId, @NonNull String resourceType, ActionListener listener) { final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER ); @@ -306,13 +308,13 @@ public void getSharingInfo(@NonNull String resourceId, @NonNull String resourceI return; } - LOGGER.debug("User {} is fetching sharing info for resource {} in index {}", user.getName(), resourceId, resourceIndex); + LOGGER.debug("User {} is fetching sharing info for resource {} in index {}", user.getName(), resourceId, resourceType); - this.resourceSharingIndexHandler.fetchSharingInfo(resourceIndex, resourceId, ActionListener.wrap(sharingInfo -> { - LOGGER.debug("Successfully fetched sharing info for resource {} in index {}", resourceId, resourceIndex); + this.resourceSharingIndexHandler.fetchSharingInfo(resourceType, resourceId, ActionListener.wrap(sharingInfo -> { + LOGGER.debug("Successfully fetched sharing info for resource {} in index {}", resourceId, resourceType); listener.onResponse(sharingInfo); }, e -> { - LOGGER.error("Failed to fetched sharing info for resource {} in index {}: {}", resourceId, resourceIndex, e.getMessage()); + LOGGER.error("Failed to fetched sharing info for resource {} in index {}: {}", resourceId, resourceType, e.getMessage()); listener.onFailure(e); })); @@ -322,13 +324,13 @@ public void getSharingInfo(@NonNull String resourceId, @NonNull String resourceI * 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 resourceType The resource type * @param target The users, roles, and backend roles as well as the action group to share the resource with. * @param listener The listener to be notified with the updated ResourceSharing document. */ public void share( @NonNull String resourceId, - @NonNull String resourceIndex, + @NonNull String resourceType, @NonNull ShareWith target, ActionListener listener ) { @@ -350,6 +352,8 @@ public void share( LOGGER.debug("Sharing resource {} created by {} with {}", resourceId, user.getName(), target.toString()); + String resourceIndex = resourcePluginInfo.indexByType(resourceType); + this.resourceSharingIndexHandler.share(resourceId, resourceIndex, target, ActionListener.wrap(sharingInfo -> { LOGGER.debug("Successfully shared resource {} with {}", resourceId, target.toString()); listener.onResponse(sharingInfo); @@ -363,13 +367,13 @@ public void share( * 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 resourceType The resource type * @param target The access levels, users, roles, and backend roles to revoke access for. * @param listener The listener to be notified with the updated ResourceSharing document. */ public void revoke( @NonNull String resourceId, - @NonNull String resourceIndex, + @NonNull String resourceType, @NonNull ShareWith target, ActionListener listener ) { @@ -391,6 +395,8 @@ public void revoke( LOGGER.debug("User {} revoking access to resource {} for {}.", user.getName(), resourceId, target); + String resourceIndex = resourcePluginInfo.indexByType(resourceType); + this.resourceSharingIndexHandler.revoke(resourceId, resourceIndex, target, ActionListener.wrap(listener::onResponse, exception -> { LOGGER.error("Failed to revoke access to resource {} in index {}: {}", resourceId, resourceIndex, exception.getMessage()); listener.onFailure(exception); @@ -400,21 +406,22 @@ public void revoke( /** * Loads all resource-ids within the specified resource index. * - * @param resourceIndex The resource index to load resources from. + * @param resourceType The resource type. * @param listener The listener to be notified with the set of resource IDs. */ - private void loadAllResourceIds(String resourceIndex, ActionListener> listener) { - this.resourceSharingIndexHandler.fetchAllResourceIds(resourceIndex, listener); + private void loadAllResourceIds(String resourceType, ActionListener> listener) { + this.resourceSharingIndexHandler.fetchAllResourceIds(resourceType, listener); } /** * Loads all resource-sharing records for the specified resource index. * - * @param resourceIndex The resource index to load records from. + * @param resourceType The resource type. * @param listener The listener to be notified with the set of resource-sharing records. */ - private void loadAllResourceSharingRecords(String resourceIndex, ActionListener> listener) { - this.resourceSharingIndexHandler.fetchAllResourceSharingRecords(resourceIndex, listener); + private void loadAllResourceSharingRecords(String resourceType, ActionListener> listener) { + String resourceIndex = resourcePluginInfo.indexByType(resourceType); + this.resourceSharingIndexHandler.fetchAllResourceSharingRecords(resourceIndex, resourceType, listener); } /** diff --git a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index 8080f9e561..a84b262ba0 100644 --- a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -40,7 +40,6 @@ public class ResourcePluginInfo { // type <-> index private final Map typeToIndex = new HashMap<>(); - private final Map indexToType = new HashMap<>(); // UI: action-group *names* per type private final Map> typeToGroupNames = new HashMap<>(); @@ -51,7 +50,6 @@ public class ResourcePluginInfo { public void setResourceSharingExtensions(Set extensions) { resourceSharingExtensions.clear(); typeToIndex.clear(); - indexToType.clear(); // Enforce resource-type unique-ness Set resourceTypes = new HashSet<>(); for (ResourceSharingExtension extension : extensions) { @@ -61,7 +59,6 @@ public void setResourceSharingExtensions(Set extension resourceTypes.add(rp.resourceType()); // also cache type->index and index->type mapping typeToIndex.put(rp.resourceType(), rp.resourceIndexName()); - indexToType.put(rp.resourceIndexName(), rp.resourceType()); } else { throw new OpenSearchSecurityException( String.format( @@ -128,8 +125,8 @@ public Set getResourceTypes() { .collect(Collectors.toCollection(LinkedHashSet::new)); } - public Set getResourceIndices() { - return indexToType.keySet(); + public Collection getResourceIndices() { + return typeToIndex.values(); } } diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 260b35ae94..805d2c99ee 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -323,9 +323,10 @@ public void fetchAllResourceIds(String resourceIndex, ActionListener /** * Fetches all resource-sharing records for a given resource-index * @param resourceIndex the index whose resource-sharing records are to be fetched + * @param resourceType the resource type * @param listener to collect and return the sharing records */ - public void fetchAllResourceSharingRecords(String resourceIndex, ActionListener> listener) { + public void fetchAllResourceSharingRecords(String resourceIndex, String resourceType, ActionListener> listener) { String resourceSharingIndex = getSharingIndex(resourceIndex); LOGGER.debug("Fetching all resource-sharing records asynchronously from {}", resourceSharingIndex); Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); @@ -336,7 +337,7 @@ public void fetchAllResourceSharingRecords(String resourceIndex, ActionListener< MatchAllQueryBuilder query = QueryBuilders.matchAllQuery(); - executeAllSearchRequest(resourceIndex, scroll, searchRequest, query, ActionListener.wrap(recs -> { + executeAllSearchRequest(resourceIndex, resourceType, scroll, searchRequest, query, ActionListener.wrap(recs -> { ctx.restore(); LOGGER.debug("Found {} resource-sharing records in {}", recs.size(), resourceSharingIndex); listener.onResponse(recs); @@ -953,6 +954,7 @@ private void executeSearchRequest( /** * Executes a search request and returns a set of collected resource-sharing documents using scroll. * @param resourceIndex the index whose records are to be searched + * @param resourceType the resource type * @param scroll Search scroll context * @param searchRequest Initial search request * @param query Query builder for the request @@ -960,6 +962,7 @@ private void executeSearchRequest( */ private void executeAllSearchRequest( String resourceIndex, + String resourceType, Scroll scroll, SearchRequest searchRequest, AbstractQueryBuilder> query, @@ -981,6 +984,7 @@ private void executeAllSearchRequest( null, true, resourceIndex, + resourceType, recs, scroll, scrollId, @@ -999,12 +1003,14 @@ private void executeAllSearchRequest( * - Use mget in batches of 1000 to get the resource sharing records. * * @param resourceIndex the index for which records are to be searched + * @param resourceIndex the resource type * @param user the user that is requesting the records * @param flatPrincipals user's name, roles, backend_roles to be used for matching. * @param listener to collect and return accessible sharing records */ public void fetchAccessibleResourceSharingRecords( String resourceIndex, + String resourceType, User user, Set flatPrincipals, ActionListener> listener @@ -1062,7 +1068,7 @@ public void fetchAccessibleResourceSharingRecords( ) { p.nextToken(); ResourceSharing rs = ResourceSharing.fromXContent(p); - boolean canShare = canUserShare(user, /* isAdmin */ false, rs, resourceIndex); + boolean canShare = canUserShare(user, /* isAdmin */ false, rs, resourceType); out.add(new SharingRecord(rs, canShare)); } catch (Exception ex) { LOGGER.warn("Failed to parse resource-sharing doc id={}", gr.getId(), ex); @@ -1148,6 +1154,7 @@ private void processScrollResultsAndCollectSharingRecords( User user, boolean isAdmin, String resourceIndex, + String resourceType, Set resourceSharingRecords, Scroll scroll, String scrollId, @@ -1170,7 +1177,7 @@ private void processScrollResultsAndCollectSharingRecords( ) { parser.nextToken(); ResourceSharing rs = ResourceSharing.fromXContent(parser); - boolean canShare = canUserShare(user, isAdmin, rs, resourceIndex); + boolean canShare = canUserShare(user, isAdmin, rs, resourceType); resourceSharingRecords.add(new SharingRecord(rs, canShare)); } catch (Exception e) { // TODO: Decide how strict should this failure be: @@ -1189,6 +1196,7 @@ private void processScrollResultsAndCollectSharingRecords( user, isAdmin, resourceIndex, + resourceType, resourceSharingRecords, scroll, sr.getScrollId(), @@ -1225,9 +1233,7 @@ private void clearScroll(String scrollId, ActionListener listener) { // **** Check whether user can share this record further /** Resolve access-level for THIS resource type and check required action. */ - public boolean groupAllows(String resourceIndex, String accessLevel, String requiredAction) { - String resourceType = resourcePluginInfo.typeByIndex(resourceIndex); - if (resourceType == null || accessLevel == null || requiredAction == null) return false; + public boolean groupAllows(String resourceType, String accessLevel, String requiredAction) { return resourcePluginInfo.flattenedForType(resourceType).resolve(Set.of(accessLevel)).contains(requiredAction); } diff --git a/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java b/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java index fd9aa8a608..eb10c601b5 100644 --- a/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java +++ b/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java @@ -33,7 +33,7 @@ public class ShareRequest extends ActionRequest implements DocRequest { @JsonProperty("resource_id") private final String resourceId; @JsonProperty("resource_type") - private final String resourceIndex; + private final String resourceType; @JsonProperty("share_with") private final ShareWith shareWith; @JsonProperty("add") @@ -48,7 +48,7 @@ public class ShareRequest extends ActionRequest implements DocRequest { */ private ShareRequest(Builder builder) { this.resourceId = builder.resourceId; - this.resourceIndex = builder.resourceIndex; + this.resourceType = builder.resourceType; this.shareWith = builder.shareWith; this.add = builder.add; this.revoke = builder.revoke; @@ -59,7 +59,7 @@ public ShareRequest(StreamInput in) throws IOException { super(in); this.method = in.readEnum(RestRequest.Method.class); this.resourceId = in.readString(); - this.resourceIndex = in.readString(); + this.resourceType = in.readString(); this.shareWith = in.readOptionalWriteable(ShareWith::new); this.add = in.readOptionalWriteable(ShareWith::new); this.revoke = in.readOptionalWriteable(ShareWith::new); @@ -69,7 +69,7 @@ public ShareRequest(StreamInput in) throws IOException { public void writeTo(StreamOutput out) throws IOException { out.writeEnum(method); out.writeString(resourceId); - out.writeString(resourceIndex); + out.writeString(resourceType); out.writeOptionalWriteable(shareWith); out.writeOptionalWriteable(add); out.writeOptionalWriteable(revoke); @@ -78,7 +78,7 @@ public void writeTo(StreamOutput out) throws IOException { @Override public ActionRequestValidationException validate() { var arv = new ActionRequestValidationException(); - if (Strings.isNullOrEmpty(resourceIndex) || Strings.isNullOrEmpty(resourceId)) { + if (Strings.isNullOrEmpty(resourceType) || Strings.isNullOrEmpty(resourceId)) { arv.addValidationError("resource_id and resource_type must be present"); throw arv; } @@ -115,6 +115,7 @@ public RestRequest.Method getMethod() { return method; } + // TODO what should this return if we don't know the index? /** * Get the index that this request operates on * @@ -122,7 +123,17 @@ public RestRequest.Method getMethod() { */ @Override public String index() { - return resourceIndex; + return resourceType; + } + + /** + * Get the index that this request operates on + * + * @return the index + */ + @Override + public String type() { + return resourceType; } /** @@ -140,7 +151,7 @@ public String id() { */ public static class Builder { private String resourceId; - private String resourceIndex; + private String resourceType; private ShareWith shareWith; private ShareWith add; private ShareWith revoke; @@ -151,7 +162,7 @@ public void resourceId(String resourceId) { } public void resourceIndex(String resourceIndex) { - this.resourceIndex = resourceIndex; + this.resourceType = resourceIndex; } public void shareWith(ShareWith shareWith) { diff --git a/src/main/java/org/opensearch/security/resources/api/share/ShareTransportAction.java b/src/main/java/org/opensearch/security/resources/api/share/ShareTransportAction.java index d5ca79eeff..c8fffbdd97 100644 --- a/src/main/java/org/opensearch/security/resources/api/share/ShareTransportAction.java +++ b/src/main/java/org/opensearch/security/resources/api/share/ShareTransportAction.java @@ -42,19 +42,19 @@ protected void doExecute(Task task, ShareRequest request, ActionListener { ActionListener listener = inv.getArgument(4); listener.onResponse(hasPermission); return null; - }).when(resourceAccessHandler).hasPermission(eq("anyId"), eq(IDX), eq("read"), any(), any()); + }).when(resourceAccessHandler).hasPermission(eq("anyId"), eq("indices"), eq("read"), any(), any()); ActionListener callback = mock(ActionListener.class); diff --git a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java index 3da5213859..8e4ff7fa10 100644 --- a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java +++ b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java @@ -69,6 +69,7 @@ public class ResourceAccessHandlerTest { private ResourceAccessHandler handler; private static final String INDEX = "test-index"; + private static final String TYPE = "test"; private static final String RESOURCE_ID = "res-1"; private static final String ACTION = "read"; @@ -79,7 +80,7 @@ public void setup() { handler = new ResourceAccessHandler(threadPool, sharingIndexHandler, adminDNs, privilegesEvaluator, resourcePluginInfo); // For tests that verify permission with action-group - when(resourcePluginInfo.typeByIndex(any())).thenReturn("org.example.Type"); + when(resourcePluginInfo.typeByIndex(any())).thenReturn(TYPE); when(resourcePluginInfo.flattenedForType(any())).thenReturn(mock(FlattenedActionGroups.class)); } @@ -96,7 +97,7 @@ public void testHasPermission_adminUserAllowed() { when(adminDNs.isAdmin(user)).thenReturn(true); ActionListener listener = mock(ActionListener.class); - handler.hasPermission(RESOURCE_ID, INDEX, ACTION, context, listener); + handler.hasPermission(RESOURCE_ID, TYPE, ACTION, context, listener); verify(listener).onResponse(true); } @@ -119,7 +120,7 @@ public void testHasPermission_ownerAllowed() { }).when(sharingIndexHandler).fetchSharingInfo(eq(INDEX), eq(RESOURCE_ID), any()); ActionListener listener = mock(ActionListener.class); - handler.hasPermission(RESOURCE_ID, INDEX, ACTION, null, listener); + handler.hasPermission(RESOURCE_ID, TYPE, ACTION, null, listener); verify(listener).onResponse(true); } @@ -155,7 +156,7 @@ public void testHasPermission_sharedWithUserAllowed() { }).when(sharingIndexHandler).fetchSharingInfo(eq(INDEX), eq(RESOURCE_ID), any()); ActionListener listener = mock(ActionListener.class); - handler.hasPermission(RESOURCE_ID, INDEX, ACTION, null, listener); + handler.hasPermission(RESOURCE_ID, TYPE, ACTION, null, listener); verify(listener).onResponse(true); } @@ -179,7 +180,7 @@ public void testHasPermission_noAccessLevelsDenied() { }).when(sharingIndexHandler).fetchSharingInfo(eq(INDEX), eq(RESOURCE_ID), any()); ActionListener listener = mock(ActionListener.class); - handler.hasPermission(RESOURCE_ID, INDEX, ACTION, null, listener); + handler.hasPermission(RESOURCE_ID, TYPE, ACTION, null, listener); verify(listener).onResponse(false); } @@ -199,7 +200,7 @@ public void testHasPermission_nullDocumentDenied() { }).when(sharingIndexHandler).fetchSharingInfo(eq(INDEX), eq(RESOURCE_ID), any()); ActionListener listener = mock(ActionListener.class); - handler.hasPermission(RESOURCE_ID, INDEX, ACTION, null, listener); + handler.hasPermission(RESOURCE_ID, TYPE, ACTION, null, listener); verify(listener).onResponse(false); } @@ -213,7 +214,7 @@ public void testHasPermission_pluginUserDenied() { when(privilegesEvaluator.createContext(user, ACTION)).thenReturn(subjectContext); ActionListener listener = mock(ActionListener.class); - handler.hasPermission(RESOURCE_ID, INDEX, ACTION, null, listener); + handler.hasPermission(RESOURCE_ID, TYPE, ACTION, null, listener); verify(listener).onResponse(false); } From 166396910fa3348b072e916457296e07d2cdad91 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 26 Sep 2025 09:32:40 -0400 Subject: [PATCH 02/40] Fix compilation issues Signed-off-by: Craig Perkins --- .../opensearch/security/resources/ResourcePluginInfo.java | 4 ---- .../security/resources/ResourceAccessHandlerTest.java | 6 ++---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index a84b262ba0..0a86e4235b 100644 --- a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -105,10 +105,6 @@ public FlattenedActionGroups flattenedForType(String resourceType) { return typeToFlattened.getOrDefault(resourceType, FlattenedActionGroups.EMPTY); } - public String typeByIndex(String index) { - return indexToType.get(index); - } - public String indexByType(String type) { return typeToIndex.get(type); } diff --git a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java index 8e4ff7fa10..8c843167e5 100644 --- a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java +++ b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java @@ -80,7 +80,6 @@ public void setup() { handler = new ResourceAccessHandler(threadPool, sharingIndexHandler, adminDNs, privilegesEvaluator, resourcePluginInfo); // For tests that verify permission with action-group - when(resourcePluginInfo.typeByIndex(any())).thenReturn(TYPE); when(resourcePluginInfo.flattenedForType(any())).thenReturn(mock(FlattenedActionGroups.class)); } @@ -140,11 +139,10 @@ public void testHasPermission_sharedWithUserAllowed() { when(doc.fetchAccessLevels(eq(Recipient.ROLES), any())).thenReturn(Set.of()); when(doc.fetchAccessLevels(eq(Recipient.BACKEND_ROLES), any())).thenReturn(Set.of()); - final String TYPE_FQN = "org.example.Type"; - when(resourcePluginInfo.typeByIndex(INDEX)).thenReturn(TYPE_FQN); + final String RESOURCE_TYPE = "type"; FlattenedActionGroups ag = mock(FlattenedActionGroups.class); - when(resourcePluginInfo.flattenedForType(TYPE_FQN)).thenReturn(ag); + when(resourcePluginInfo.flattenedForType(RESOURCE_TYPE)).thenReturn(ag); // Resolve the access level "read" to the concrete allowed action "read" (could also be a wildcard) when(ag.resolve(any())).thenReturn(ImmutableSet.of("read")); From c46cd2dd6b2457514f1602352b2922516d82a5e2 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 26 Sep 2025 10:53:15 -0400 Subject: [PATCH 03/40] Make more generic collection Signed-off-by: Craig Perkins --- .../org/opensearch/security/OpenSearchSecurityPlugin.java | 4 ++-- .../opensearch/security/configuration/DlsFlsValveImpl.java | 4 ++-- .../security/privileges/ResourceAccessEvaluator.java | 6 +++--- .../security/resources/ResourceSharingIndexHandler.java | 3 ++- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index cc1be7274c..9645f247ca 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -782,7 +782,7 @@ public void onIndexModule(IndexModule indexModule) { // Listening on POST and DELETE operations in resource indices ResourceIndexListener resourceIndexListener = new ResourceIndexListener(threadPool, localClient, resourcePluginInfo); // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions - Set resourceIndices = resourcePluginInfo.getResourceIndices(); + Collection resourceIndices = resourcePluginInfo.getResourceIndices(); // CS-ENFORCE-SINGLE if (resourceIndices.contains(indexModule.getIndex().getName())) { indexModule.addIndexOperationListener(resourceIndexListener); @@ -2309,7 +2309,7 @@ public void onNodeStarted(DiscoveryNode localNode) { // create resource sharing index if absent // TODO check if this should be wrapped in an atomic completable future log.debug("Attempting to create Resource Sharing index"); - Set resourceIndices = new HashSet<>(); + Collection resourceIndices = new HashSet<>(); if (resourcePluginInfo != null) { resourceIndices = resourcePluginInfo.getResourceIndices(); } diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 4264ba4bfe..edb2f6d730 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -14,10 +14,10 @@ import java.lang.reflect.Field; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.Collection; 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; @@ -119,7 +119,7 @@ public DlsFlsValveImpl( ThreadPool threadPool, DlsFlsBaseContext dlsFlsBaseContext, AdminDNs adminDNs, - Set resourceIndices + Collection resourceIndices ) { super(); this.nodeClient = nodeClient; diff --git a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java index 84fe24c5ae..293b7adc24 100644 --- a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java @@ -10,7 +10,7 @@ package org.opensearch.security.privileges; -import java.util.Set; +import java.util.Collection; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -40,11 +40,11 @@ public class ResourceAccessEvaluator { private static final Logger log = LogManager.getLogger(ResourceAccessEvaluator.class); - private final Set resourceIndices; + private final Collection resourceIndices; private final Settings settings; private final ResourceAccessHandler resourceAccessHandler; - public ResourceAccessEvaluator(Set resourceIndices, Settings settings, ResourceAccessHandler resourceAccessHandler) { + public ResourceAccessEvaluator(Collection resourceIndices, Settings settings, ResourceAccessHandler resourceAccessHandler) { this.resourceIndices = resourceIndices; this.settings = settings; this.resourceAccessHandler = resourceAccessHandler; diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 805d2c99ee..bb9a8ef665 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -124,7 +125,7 @@ public ResourceSharingIndexHandler(final Client client, final ThreadPool threadP * or communicating with the cluster */ - public void createResourceSharingIndicesIfAbsent(Set resourceIndices) { + public void createResourceSharingIndicesIfAbsent(Collection resourceIndices) { // TODO: Once stashContext is replaced with switchContext this call will have to be modified try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { for (String resourceIndex : resourceIndices) { From 6e4b6797e2b493aa6025f9335de68a08f6d1b7f9 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 26 Sep 2025 11:53:12 -0400 Subject: [PATCH 04/40] Fix unit tests Signed-off-by: Craig Perkins --- .../security/resources/ResourceAccessHandlerTest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java index 8c843167e5..ab299daea9 100644 --- a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java +++ b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java @@ -81,6 +81,7 @@ public void setup() { // For tests that verify permission with action-group when(resourcePluginInfo.flattenedForType(any())).thenReturn(mock(FlattenedActionGroups.class)); + when(resourcePluginInfo.indexByType(TYPE)).thenReturn(INDEX); } private void injectUser(User user) { @@ -139,10 +140,8 @@ public void testHasPermission_sharedWithUserAllowed() { when(doc.fetchAccessLevels(eq(Recipient.ROLES), any())).thenReturn(Set.of()); when(doc.fetchAccessLevels(eq(Recipient.BACKEND_ROLES), any())).thenReturn(Set.of()); - final String RESOURCE_TYPE = "type"; - FlattenedActionGroups ag = mock(FlattenedActionGroups.class); - when(resourcePluginInfo.flattenedForType(RESOURCE_TYPE)).thenReturn(ag); + when(resourcePluginInfo.flattenedForType(TYPE)).thenReturn(ag); // Resolve the access level "read" to the concrete allowed action "read" (could also be a wildcard) when(ag.resolve(any())).thenReturn(ImmutableSet.of("read")); @@ -268,7 +267,7 @@ public void testShareSuccess() { }).when(sharingIndexHandler).share(eq(RESOURCE_ID), eq(INDEX), eq(shareWith), any()); ActionListener listener = mock(ActionListener.class); - handler.share(RESOURCE_ID, INDEX, shareWith, listener); + handler.share(RESOURCE_ID, TYPE, shareWith, listener); verify(listener).onResponse(doc); } @@ -298,7 +297,7 @@ public void testRevokeSuccess() { }).when(sharingIndexHandler).revoke(eq(RESOURCE_ID), eq(INDEX), eq(revokeTarget), any()); ActionListener listener = mock(ActionListener.class); - handler.revoke(RESOURCE_ID, INDEX, revokeTarget, listener); + handler.revoke(RESOURCE_ID, TYPE, revokeTarget, listener); verify(listener).onResponse(doc); } From f37c54c6dfc86ce9b8fc904b63a393beffaad812 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 26 Sep 2025 12:23:54 -0400 Subject: [PATCH 05/40] Make DocRequests specify type Signed-off-by: Craig Perkins --- .../resource/actions/rest/create/CreateResourceRequest.java | 6 ++++++ .../resource/actions/rest/create/UpdateResourceRequest.java | 6 ++++++ .../resource/actions/rest/delete/DeleteResourceRequest.java | 6 ++++++ .../resource/actions/rest/get/GetResourceRequest.java | 6 ++++++ .../actions/rest/revoke/RevokeResourceAccessRequest.java | 6 ++++++ .../resource/actions/rest/share/ShareResourceRequest.java | 6 ++++++ 6 files changed, 36 insertions(+) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRequest.java index 3079f4c28f..92a7c9cc8d 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRequest.java @@ -18,6 +18,7 @@ import org.opensearch.sample.SampleResource; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; /** * Request object for CreateSampleResource transport action @@ -68,4 +69,9 @@ public String index() { public String id() { return null; } + + @Override + public String type() { + return RESOURCE_TYPE; + } } 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 index 52f6b76d24..a4ebcb78a1 100644 --- 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 @@ -18,6 +18,7 @@ import org.opensearch.sample.SampleResource; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; /** * Request object for UpdateResource transport action @@ -68,4 +69,9 @@ public String index() { public String id() { return resourceId; } + + @Override + public String type() { + return RESOURCE_TYPE; + } } 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 e0009cface..7c25d8d59d 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 @@ -17,6 +17,7 @@ import org.opensearch.core.common.io.stream.StreamOutput; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; /** * Request object for DeleteSampleResource transport action @@ -59,4 +60,9 @@ public String index() { public String id() { return resourceId; } + + @Override + public String type() { + return RESOURCE_TYPE; + } } 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 index ebf3122c52..b41175d865 100644 --- 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 @@ -17,6 +17,7 @@ import org.opensearch.core.common.io.stream.StreamOutput; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; /** * Request object for GetSampleResource transport action @@ -59,4 +60,9 @@ public String index() { public String id() { return resourceId; } + + @Override + public String type() { + return RESOURCE_TYPE; + } } 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 ee5e3bf597..50cd0f655f 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 @@ -18,6 +18,7 @@ import org.opensearch.security.spi.resources.sharing.ShareWith; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; /** * Request object for revoking access to a sample resource @@ -65,4 +66,9 @@ public String index() { public String id() { return resourceId; } + + @Override + public String type() { + return RESOURCE_TYPE; + } } 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 index 0688012ae7..7371c370bb 100644 --- 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 @@ -18,6 +18,7 @@ import org.opensearch.security.spi.resources.sharing.ShareWith; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; /** * Request object for sharing sample resource transport action @@ -66,4 +67,9 @@ public String index() { public String id() { return resourceId; } + + @Override + public String type() { + return RESOURCE_TYPE; + } } From 14869cf788b8a3fb761fa7c14497da397d34e955 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 26 Sep 2025 14:04:20 -0400 Subject: [PATCH 06/40] Few test fixes Signed-off-by: Craig Perkins --- .../resources/ResourceAccessHandler.java | 17 ++++++++++++++--- .../resources/api/share/ShareRequest.java | 16 ++++++++++++---- .../resources/api/share/ShareRestAction.java | 2 ++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index a0426fae2c..637256c84a 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -232,14 +232,14 @@ public void hasPermission( * 3. Share with new entity - add op * A final resource-sharing object will be returned upon successful application of the patch to the index record * @param resourceId id of the resource whose sharing info is to be updated - * @param resourceIndex name of the resource index + * @param resourceType the resource type * @param add the recipients to be shared with * @param revoke the recipients to be revoked with * @param listener listener to be notified of final resource sharing record */ public void patchSharingInfo( @NonNull String resourceId, - @NonNull String resourceIndex, + @NonNull String resourceType, @Nullable ShareWith add, @Nullable ShareWith revoke, ActionListener listener @@ -260,6 +260,12 @@ public void patchSharingInfo( return; } + String resourceIndex = resourcePluginInfo.indexByType(resourceType); + if (resourceIndex == null) { + LOGGER.debug("No resourceIndex mapping found for type '{}';", resourceType); + return; + } + LOGGER.debug( "User {} is updating sharing info for resource {} in index {} with add: {}, revoke: {} ", user.getName(), @@ -310,7 +316,12 @@ public void getSharingInfo(@NonNull String resourceId, @NonNull String resourceT LOGGER.debug("User {} is fetching sharing info for resource {} in index {}", user.getName(), resourceId, resourceType); - this.resourceSharingIndexHandler.fetchSharingInfo(resourceType, resourceId, ActionListener.wrap(sharingInfo -> { + String resourceIndex = resourcePluginInfo.indexByType(resourceType); + if (resourceIndex == null) { + LOGGER.debug("No resourceIndex mapping found for type '{}';", resourceType); + return; + } + this.resourceSharingIndexHandler.fetchSharingInfo(resourceIndex, resourceId, ActionListener.wrap(sharingInfo -> { LOGGER.debug("Successfully fetched sharing info for resource {} in index {}", resourceId, resourceType); listener.onResponse(sharingInfo); }, e -> { diff --git a/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java b/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java index eb10c601b5..eeef1529ad 100644 --- a/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java +++ b/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java @@ -127,9 +127,9 @@ public String index() { } /** - * Get the index that this request operates on + * Get the type * - * @return the index + * @return the resource type */ @Override public String type() { @@ -151,6 +151,7 @@ public String id() { */ public static class Builder { private String resourceId; + private String resourceIndex; private String resourceType; private ShareWith shareWith; private ShareWith add; @@ -162,7 +163,11 @@ public void resourceId(String resourceId) { } public void resourceIndex(String resourceIndex) { - this.resourceType = resourceIndex; + this.resourceIndex = resourceIndex; + } + + public void resourceType(String resourceType) { + this.resourceType = resourceType; } public void shareWith(ShareWith shareWith) { @@ -196,9 +201,12 @@ public void parseContent(XContentParser xContentParser, ResourcePluginInfo resou case "resource_id": this.resourceId(parser.text()); break; - case "resource_type": + case "resource_index": this.resourceIndex(resourcePluginInfo.indexByType(parser.text())); break; + case "resource_type": + this.resourceType(parser.text()); + break; case "share_with": this.shareWith(ShareWith.fromXContent(parser)); break; diff --git a/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java b/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java index cce1a0aae4..d9fdb9c800 100644 --- a/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java +++ b/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java @@ -78,6 +78,8 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli builder.resourceId(resourceId); } + builder.resourceType(resourceType); + if (request.hasContent()) { builder.parseContent(request.contentParser(), resourcePluginInfo); } From 9a6686497113c80559d7283db2ecc4d13ea85be6 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 26 Sep 2025 15:30:28 -0400 Subject: [PATCH 07/40] Fix Share API tests Signed-off-by: Craig Perkins --- .../resources/api/share/ShareRequest.java | 26 ++++++++++--------- .../resources/api/share/ShareRestAction.java | 21 +++++++++++---- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java b/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java index eeef1529ad..bbc83dcc83 100644 --- a/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java +++ b/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java @@ -34,6 +34,8 @@ public class ShareRequest extends ActionRequest implements DocRequest { private final String resourceId; @JsonProperty("resource_type") private final String resourceType; + @JsonProperty("resource_index") + private final String resourceIndex; @JsonProperty("share_with") private final ShareWith shareWith; @JsonProperty("add") @@ -49,6 +51,7 @@ public class ShareRequest extends ActionRequest implements DocRequest { private ShareRequest(Builder builder) { this.resourceId = builder.resourceId; this.resourceType = builder.resourceType; + this.resourceIndex = builder.resourceIndex; this.shareWith = builder.shareWith; this.add = builder.add; this.revoke = builder.revoke; @@ -60,6 +63,7 @@ public ShareRequest(StreamInput in) throws IOException { this.method = in.readEnum(RestRequest.Method.class); this.resourceId = in.readString(); this.resourceType = in.readString(); + this.resourceIndex = in.readString(); this.shareWith = in.readOptionalWriteable(ShareWith::new); this.add = in.readOptionalWriteable(ShareWith::new); this.revoke = in.readOptionalWriteable(ShareWith::new); @@ -70,6 +74,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeEnum(method); out.writeString(resourceId); out.writeString(resourceType); + out.writeString(resourceIndex); out.writeOptionalWriteable(shareWith); out.writeOptionalWriteable(add); out.writeOptionalWriteable(revoke); @@ -115,7 +120,6 @@ public RestRequest.Method getMethod() { return method; } - // TODO what should this return if we don't know the index? /** * Get the index that this request operates on * @@ -123,7 +127,7 @@ public RestRequest.Method getMethod() { */ @Override public String index() { - return resourceType; + return resourceIndex; } /** @@ -150,13 +154,13 @@ public String id() { * Builder for ShareRequest */ public static class Builder { - private String resourceId; - private String resourceIndex; - private String resourceType; - private ShareWith shareWith; - private ShareWith add; - private ShareWith revoke; - private RestRequest.Method method; + String resourceId; + String resourceIndex; + String resourceType; + ShareWith shareWith; + ShareWith add; + ShareWith revoke; + RestRequest.Method method; public void resourceId(String resourceId) { this.resourceId = resourceId; @@ -201,11 +205,9 @@ public void parseContent(XContentParser xContentParser, ResourcePluginInfo resou case "resource_id": this.resourceId(parser.text()); break; - case "resource_index": - this.resourceIndex(resourcePluginInfo.indexByType(parser.text())); - break; case "resource_type": this.resourceType(parser.text()); + this.resourceIndex(resourcePluginInfo.indexByType(parser.text())); break; case "share_with": this.shareWith(ShareWith.fromXContent(parser)); diff --git a/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java b/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java index d9fdb9c800..6a8fe1ddb2 100644 --- a/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java +++ b/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java @@ -66,14 +66,9 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli String resourceId = request.param("resource_id"); String resourceType = request.param("resource_type"); - String resourceIndex = resourcePluginInfo.indexByType(resourceType); - ShareRequest.Builder builder = new ShareRequest.Builder(); builder.method(request.method()); - if (resourceIndex != null) { - builder.resourceIndex(resourceIndex); - } if (resourceId != null) { builder.resourceId(resourceId); } @@ -84,6 +79,22 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli builder.parseContent(request.contentParser(), resourcePluginInfo); } + if (builder.resourceId == null || builder.resourceType == null) { + return channel -> { + channel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "Resource type and id are both required.")); + }; + } + + String resourceIndex = resourcePluginInfo.indexByType(builder.resourceType); + + if (resourceIndex == null) { + return channel -> { + channel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "Invalid resource type: " + resourceType)); + }; + } + + builder.resourceIndex(resourceIndex); + ShareRequest shareRequest = builder.build(); return channel -> { From 4c83aa5cb516262a9143ff9c7c8a0ce1c9be31b3 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 26 Sep 2025 16:31:41 -0400 Subject: [PATCH 08/40] Fix tests Signed-off-by: Craig Perkins --- .../resources/ResourceAccessHandler.java | 7 +++++-- .../api/list/AccessibleResourcesRestAction.java | 2 +- .../resources/ResourceAccessHandlerTest.java | 16 ++++++++-------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 637256c84a..91a4042a0b 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -89,6 +89,8 @@ public void getOwnAndSharedResourceIdsForCurrentUser(@NonNull String resourceTyp return; } + String resourceIndex = resourcePluginInfo.indexByType(resourceType); + if (adminDNs.isAdmin(user)) { loadAllResourceIds(resourceType, ActionListener.wrap(listener::onResponse, listener::onFailure)); return; @@ -96,7 +98,7 @@ public void getOwnAndSharedResourceIdsForCurrentUser(@NonNull String resourceTyp Set flatPrincipals = getFlatPrincipals(user); // 3) Fetch all accessible resource IDs - resourceSharingIndexHandler.fetchAccessibleResourceIds(resourceType, flatPrincipals, listener); + resourceSharingIndexHandler.fetchAccessibleResourceIds(resourceIndex, flatPrincipals, listener); } /** @@ -421,7 +423,8 @@ public void revoke( * @param listener The listener to be notified with the set of resource IDs. */ private void loadAllResourceIds(String resourceType, ActionListener> listener) { - this.resourceSharingIndexHandler.fetchAllResourceIds(resourceType, listener); + String resourceIndex = resourcePluginInfo.indexByType(resourceType); + this.resourceSharingIndexHandler.fetchAllResourceIds(resourceIndex, listener); } /** diff --git a/src/main/java/org/opensearch/security/resources/api/list/AccessibleResourcesRestAction.java b/src/main/java/org/opensearch/security/resources/api/list/AccessibleResourcesRestAction.java index cef53f82b9..05c304c084 100644 --- a/src/main/java/org/opensearch/security/resources/api/list/AccessibleResourcesRestAction.java +++ b/src/main/java/org/opensearch/security/resources/api/list/AccessibleResourcesRestAction.java @@ -70,7 +70,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli if (resourceIndex == null) { return channel -> { handleResponse(channel, Set.of()); }; } - return channel -> resourceAccessHandler.getResourceSharingInfoForCurrentUser(resourceIndex, ActionListener.wrap(rows -> { + return channel -> resourceAccessHandler.getResourceSharingInfoForCurrentUser(resourceType, ActionListener.wrap(rows -> { handleResponse(channel, rows); }, e -> handleError(channel, e))); } diff --git a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java index ab299daea9..c375f87885 100644 --- a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java +++ b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java @@ -230,7 +230,7 @@ public void testGetOwnAndSharedResources_asAdmin() { return null; }).when(sharingIndexHandler).fetchAllResourceIds(eq(INDEX), any()); - handler.getOwnAndSharedResourceIdsForCurrentUser(INDEX, listener); + handler.getOwnAndSharedResourceIdsForCurrentUser(TYPE, listener); verify(listener).onResponse(Set.of("res1", "res2")); } @@ -248,7 +248,7 @@ public void testGetOwnAndSharedResources_asNormalUser() { return null; }).when(sharingIndexHandler).fetchAccessibleResourceIds(any(), any(), any()); - handler.getOwnAndSharedResourceIdsForCurrentUser(INDEX, listener); + handler.getOwnAndSharedResourceIdsForCurrentUser(TYPE, listener); verify(listener).onResponse(Set.of("res1")); } @@ -278,7 +278,7 @@ public void testShareFailsIfNoUser() { ActionListener listener = mock(ActionListener.class); - handler.share(RESOURCE_ID, INDEX, shareWith, listener); + handler.share(RESOURCE_ID, TYPE, shareWith, listener); verify(listener).onFailure(any(OpenSearchStatusException.class)); } @@ -308,7 +308,7 @@ public void testRevokeFailsIfNoUser() { ActionListener listener = mock(ActionListener.class); - handler.revoke(RESOURCE_ID, INDEX, revokeTarget, listener); + handler.revoke(RESOURCE_ID, TYPE, revokeTarget, listener); verify(listener).onFailure(any(OpenSearchStatusException.class)); } @@ -325,7 +325,7 @@ public void testGetSharingInfoSuccess() { }).when(sharingIndexHandler).fetchSharingInfo(eq(INDEX), eq(RESOURCE_ID), any()); ActionListener listener = mock(ActionListener.class); - handler.getSharingInfo(RESOURCE_ID, INDEX, listener); + handler.getSharingInfo(RESOURCE_ID, TYPE, listener); verify(listener).onResponse(doc); } @@ -333,7 +333,7 @@ public void testGetSharingInfoSuccess() { @Test public void testGetSharingInfoFailsIfNoUser() { ActionListener listener = mock(ActionListener.class); - handler.getSharingInfo(RESOURCE_ID, INDEX, listener); + handler.getSharingInfo(RESOURCE_ID, TYPE, listener); verify(listener).onFailure(any(OpenSearchStatusException.class)); } @@ -353,7 +353,7 @@ public void testPatchSharingInfoSuccess() { }).when(sharingIndexHandler).patchSharingInfo(eq(RESOURCE_ID), eq(INDEX), eq(add), eq(revoke), any()); ActionListener listener = mock(ActionListener.class); - handler.patchSharingInfo(RESOURCE_ID, INDEX, add, revoke, listener); + handler.patchSharingInfo(RESOURCE_ID, TYPE, add, revoke, listener); verify(listener).onResponse(doc); } @@ -362,7 +362,7 @@ public void testPatchSharingInfoSuccess() { public void testPatchSharingInfoFailsIfNoUser() { ShareWith x = new ShareWith(ImmutableMap.of()); ActionListener listener = mock(ActionListener.class); - handler.patchSharingInfo(RESOURCE_ID, INDEX, x, x, listener); + handler.patchSharingInfo(RESOURCE_ID, TYPE, x, x, listener); verify(listener).onFailure(any(OpenSearchStatusException.class)); } From ebfd5bd496f4bca045a7b731bc7a8a7f593c4e96 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 1 Oct 2025 22:04:46 -0400 Subject: [PATCH 09/40] Makes resources settings dynamically updateable Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 53 +++++++------ .../configuration/DlsFlsValveImpl.java | 13 ++-- .../privileges/ResourceAccessEvaluator.java | 28 +++---- .../ResourceAccessControlClient.java | 16 +++- .../resources/ResourceIndexListener.java | 41 +++++++++- .../resources/ResourcePluginInfo.java | 33 ++++++++ .../ResourceSharingFeatureFlagSetting.java | 75 +++++++++++++++++++ ...ourceSharingProtectedResourcesSetting.java | 66 ++++++++++++++++ .../resources/settings/package-info.java | 4 + .../setting/OpensearchDynamicSetting.java | 2 +- .../ResourceAccessEvaluatorTest.java | 8 +- 11 files changed, 290 insertions(+), 49 deletions(-) create mode 100644 src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java create mode 100644 src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java create mode 100644 src/main/java/org/opensearch/security/resources/settings/package-info.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index d380f0baa8..4fd23c0e02 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -189,6 +189,8 @@ import org.opensearch.security.resources.api.share.ShareAction; import org.opensearch.security.resources.api.share.ShareRestAction; import org.opensearch.security.resources.api.share.ShareTransportAction; +import org.opensearch.security.resources.settings.ResourceSharingFeatureFlagSetting; +import org.opensearch.security.resources.settings.ResourceSharingProtectedResourcesSetting; import org.opensearch.security.rest.DashboardsInfoAction; import org.opensearch.security.rest.SecurityConfigUpdateAction; import org.opensearch.security.rest.SecurityHealthAction; @@ -296,6 +298,8 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private final AtomicReference namedXContentRegistry = new AtomicReference<>(NamedXContentRegistry.EMPTY);; private volatile DlsFlsRequestValve dlsFlsValve = null; private volatile OpensearchDynamicSetting transportPassiveAuthSetting; + private volatile OpensearchDynamicSetting resourceSharingEnabledSetting; + private volatile OpensearchDynamicSetting> resourceSharingProtectedResourceTypesSetting; private volatile PasswordHasher passwordHasher; private volatile DlsFlsBaseContext dlsFlsBaseContext; private ResourceSharingIndexHandler rsIndexHandler; @@ -364,7 +368,11 @@ public OpenSearchSecurityPlugin(final Settings settings, final Path configPath) disabled = isDisabled(settings); sslCertReloadEnabled = isSslCertReloadEnabled(settings); + // dynamic settings transportPassiveAuthSetting = new TransportPassiveAuthSetting(settings); + resourceSharingEnabledSetting = new ResourceSharingFeatureFlagSetting(settings, resourcePluginInfo); // not filtered + resourceSharingProtectedResourceTypesSetting = new ResourceSharingProtectedResourcesSetting(settings, resourcePluginInfo); // not + // filtered if (disabled) { this.sslCertReloadEnabled = false; @@ -781,7 +789,13 @@ public void onIndexModule(IndexModule indexModule) { ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT )) { // Listening on POST and DELETE operations in resource indices - ResourceIndexListener resourceIndexListener = new ResourceIndexListener(threadPool, localClient, resourcePluginInfo); + ResourceIndexListener resourceIndexListener = new ResourceIndexListener( + threadPool, + localClient, + resourcePluginInfo, + resourceSharingEnabledSetting, + resourceSharingProtectedResourceTypesSetting + ); // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions Set resourceIndices = resourcePluginInfo.getResourceIndices(); // CS-ENFORCE-SINGLE @@ -1135,6 +1149,8 @@ public Collection createComponents( // Register opensearch dynamic settings transportPassiveAuthSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); + resourceSharingEnabledSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); + resourceSharingProtectedResourceTypesSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); final ClusterInfoHolder cih = new ClusterInfoHolder(this.cs.getClusterName().value()); this.cs.addListener(cih); @@ -1213,7 +1229,8 @@ public Collection createComponents( threadPool, dlsFlsBaseContext, adminDns, - resourcePluginInfo.getResourceIndices() + resourcePluginInfo.getResourceIndices(), + resourceSharingEnabledSetting ); cr.subscribeOnChange(configMap -> { ((DlsFlsValveImpl) dlsFlsValve).updateConfiguration(cr.getConfiguration(CType.ROLES)); }); } @@ -1226,10 +1243,8 @@ public Collection createComponents( // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions // Assign resource sharing client to each extension // Using the non-gated client (i.e. no additional permissions required) - ResourceSharingClient resourceAccessControlClient = new ResourceAccessControlClient( - resourceAccessHandler, - resourcePluginInfo.getResourceIndices() - ); + ResourceSharingClient resourceAccessControlClient = new ResourceAccessControlClient(resourceAccessHandler, resourcePluginInfo); + resourcePluginInfo.setResourceSharingClient(resourceAccessControlClient); resourcePluginInfo.getResourceSharingExtensions().forEach(extension -> { extension.assignResourceSharingClient(resourceAccessControlClient); }); @@ -1238,7 +1253,12 @@ public Collection createComponents( // CS-ENFORCE-SINGLE } - resourceAccessEvaluator = new ResourceAccessEvaluator(resourcePluginInfo.getResourceIndices(), settings, resourceAccessHandler); + resourceAccessEvaluator = new ResourceAccessEvaluator( + resourcePluginInfo.getResourceIndices(), + resourceAccessHandler, + resourceSharingEnabledSetting, + resourceSharingProtectedResourceTypesSetting + ); sf = new SecurityFilter( settings, @@ -2231,26 +2251,11 @@ public List> getSettings() { settings.add(RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_ENABLED); // Resource Sharing - settings.add( - Setting.boolSetting( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT, - Property.NodeScope, - Property.Filtered - ) - ); + settings.add(resourceSharingEnabledSetting.getDynamicSetting()); // resource marked here will be protected, other resources will not be protected with resource sharing model // Defaults to no resources as protected - settings.add( - Setting.listSetting( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES_DEFAULT, - Function.identity(), - Property.NodeScope, - Property.Filtered - ) - ); + settings.add(resourceSharingProtectedResourceTypesSetting.getDynamicSetting()); settings.add(UserFactory.Caching.MAX_SIZE); settings.add(UserFactory.Caching.EXPIRE_AFTER_ACCESS); diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 4264ba4bfe..7cad2b2393 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -83,6 +83,7 @@ import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.support.WildcardMatcher; @@ -107,7 +108,7 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve { private final FieldMasking.Config fieldMaskingConfig; private final Settings settings; private final AdminDNs adminDNs; - private boolean isResourceSharingFeatureEnabled = false; + private final OpensearchDynamicSetting resourceSharingEnabledSetting; private final WildcardMatcher resourceIndicesMatcher; public DlsFlsValveImpl( @@ -119,7 +120,8 @@ public DlsFlsValveImpl( ThreadPool threadPool, DlsFlsBaseContext dlsFlsBaseContext, AdminDNs adminDNs, - Set resourceIndices + Set resourceIndices, + OpensearchDynamicSetting resourceSharingEnabledSetting ) { super(); this.nodeClient = nodeClient; @@ -141,10 +143,7 @@ public DlsFlsValveImpl( config.updateClusterStateMetadataAsync(clusterService, threadPool); } }); - this.isResourceSharingFeatureEnabled = settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - ); + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; } /** @@ -164,7 +163,7 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< ActionRequest request = context.getRequest(); if (HeaderHelper.isInternalOrPluginRequest(threadContext)) { IndexResolverReplacer.Resolved resolved = context.getResolvedRequest(); - if (isResourceSharingFeatureEnabled + if (resourceSharingEnabledSetting.getDynamicSettingValue() && request instanceof SearchRequest && resourceIndicesMatcher.matchAll(resolved.getAllIndices())) { diff --git a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java index 3758b3a5cf..4bba1d6666 100644 --- a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java @@ -19,11 +19,10 @@ import org.opensearch.action.ActionRequest; import org.opensearch.action.DocRequest; import org.opensearch.action.get.GetRequest; -import org.opensearch.common.settings.Settings; import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.Strings; import org.opensearch.security.resources.ResourceAccessHandler; -import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.setting.OpensearchDynamicSetting; /** * Evaluates access to resources. The resource plugins must register the indices which hold resource information. @@ -42,13 +41,21 @@ public class ResourceAccessEvaluator { private static final Logger log = LogManager.getLogger(ResourceAccessEvaluator.class); private final Set resourceIndices; - private final Settings settings; private final ResourceAccessHandler resourceAccessHandler; - public ResourceAccessEvaluator(Set resourceIndices, Settings settings, ResourceAccessHandler resourceAccessHandler) { + private final OpensearchDynamicSetting resourceSharingEnabledSetting; + private final OpensearchDynamicSetting> protectedResourceTypesSetting; + + public ResourceAccessEvaluator( + Set resourceIndices, + ResourceAccessHandler resourceAccessHandler, + final OpensearchDynamicSetting resourceSharingEnabledSetting, + final OpensearchDynamicSetting> protectedResourceTypesSetting + ) { this.resourceIndices = resourceIndices; - this.settings = settings; this.resourceAccessHandler = resourceAccessHandler; + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; + this.protectedResourceTypesSetting = protectedResourceTypesSetting; } /** @@ -95,14 +102,9 @@ public void evaluateAsync( * @return true if request should be evaluated, false otherwise */ public boolean shouldEvaluate(ActionRequest request) { - boolean isResourceSharingFeatureEnabled = settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - ); - List protectedTypes = settings.getAsList( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES_DEFAULT - ); + boolean isResourceSharingFeatureEnabled = resourceSharingEnabledSetting.getDynamicSettingValue(); + List protectedTypes = protectedResourceTypesSetting.getDynamicSettingValue(); + if (!isResourceSharingFeatureEnabled) return false; if (!(request instanceof DocRequest docRequest)) return false; if (request instanceof GetRequest) return false; diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java index ad2be8da0d..7bda2257ad 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java @@ -29,14 +29,16 @@ public final class ResourceAccessControlClient implements ResourceSharingClient private final ResourceAccessHandler resourceAccessHandler; private final Set resourceIndices; + private final ResourcePluginInfo resourcePluginInfo; /** * Constructs a new ResourceAccessControlClient. * */ - public ResourceAccessControlClient(ResourceAccessHandler resourceAccessHandler, Set resourceIndices) { + public ResourceAccessControlClient(ResourceAccessHandler resourceAccessHandler, ResourcePluginInfo resourcePluginInfo) { this.resourceAccessHandler = resourceAccessHandler; - this.resourceIndices = resourceIndices; + this.resourceIndices = resourcePluginInfo.getResourceIndices(); + this.resourcePluginInfo = resourcePluginInfo; } /** @@ -98,4 +100,14 @@ public void revoke(String resourceId, String resourceIndex, ShareWith target, Ac public void getAccessibleResourceIds(String resourceIndex, ActionListener> listener) { resourceAccessHandler.getOwnAndSharedResourceIdsForCurrentUser(resourceIndex, listener); } + + /** + * Returns a flag to indicate whether resource-sharing is enabled for resource-type + * @param resourceType the type for which resource-sharing status is to be checked + * @return true if enabled, false otherwise + */ + @Override + public boolean isFeatureEnabledForType(String resourceType) { + return resourcePluginInfo.indexByType(resourceType) != null; + } } diff --git a/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java index 5dab5316c3..0d166d47f0 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java @@ -9,6 +9,7 @@ package org.opensearch.security.resources; import java.io.IOException; +import java.util.List; import java.util.Objects; import org.apache.logging.log4j.LogManager; @@ -19,6 +20,7 @@ import org.opensearch.index.engine.Engine; import org.opensearch.index.shard.IndexingOperationListener; import org.opensearch.security.auth.UserSubjectImpl; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.spi.resources.sharing.CreatedBy; import org.opensearch.security.spi.resources.sharing.ResourceSharing; import org.opensearch.security.support.ConfigConstants; @@ -37,10 +39,24 @@ public class ResourceIndexListener implements IndexingOperationListener { private final ResourceSharingIndexHandler resourceSharingIndexHandler; private final ThreadPool threadPool; + private final ResourcePluginInfo resourcePluginInfo; - public ResourceIndexListener(ThreadPool threadPool, Client client, ResourcePluginInfo resourcePluginInfo) { + private final OpensearchDynamicSetting resourceSharingEnabledSetting; + + private final OpensearchDynamicSetting> protectedResourceTypesSetting; + + public ResourceIndexListener( + ThreadPool threadPool, + Client client, + ResourcePluginInfo resourcePluginInfo, + OpensearchDynamicSetting resourceSharingEnabledSetting, + OpensearchDynamicSetting> resourceSharingProtectedResourceTypesSetting + ) { this.threadPool = threadPool; this.resourceSharingIndexHandler = new ResourceSharingIndexHandler(client, threadPool, resourcePluginInfo); + this.resourcePluginInfo = resourcePluginInfo; + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; + this.protectedResourceTypesSetting = resourceSharingProtectedResourceTypesSetting; } /** @@ -48,7 +64,19 @@ public ResourceIndexListener(ThreadPool threadPool, Client client, ResourcePlugi */ @Override public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { + + if (!resourceSharingEnabledSetting.getDynamicSettingValue()) { + // feature is disabled + return; + } String resourceIndex = shardId.getIndexName(); + + List protectedResourceTypes = protectedResourceTypesSetting.getDynamicSettingValue(); + if (!protectedResourceTypes.contains(resourcePluginInfo.typeByIndex(resourceIndex))) { + // type is marked as not protected + return; + } + log.debug("postIndex called on {}", resourceIndex); String resourceId = index.id(); @@ -104,7 +132,18 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re */ @Override public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { + if (!resourceSharingEnabledSetting.getDynamicSettingValue()) { + // feature is disabled + return; + } String resourceIndex = shardId.getIndexName(); + + List protectedResourceTypes = protectedResourceTypesSetting.getDynamicSettingValue(); + if (!protectedResourceTypes.contains(resourcePluginInfo.typeByIndex(resourceIndex))) { + // type is marked as not protected + return; + } + log.debug("postDelete called on {}", resourceIndex); String resourceId = delete.id(); diff --git a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index 23c22f0032..9546c529a6 100644 --- a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -28,6 +28,7 @@ import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.spi.resources.ResourceSharingExtension; +import org.opensearch.security.spi.resources.client.ResourceSharingClient; /** * This class provides information about resource plugins and their associated resource providers and indices. @@ -37,6 +38,8 @@ */ public class ResourcePluginInfo { + private ResourceSharingClient resourceAccessControlClient; + private final Set resourceSharingExtensions = new HashSet<>(); // type <-> index @@ -84,10 +87,40 @@ public void setResourceSharingExtensions(Set extension resourceSharingExtensions.addAll(extensions); } + public void updateProtectedTypes(List protectedTypes) { + // Rebuild mappings based on the current allowlist + typeToIndex.clear(); + indexToType.clear(); + + if (protectedTypes == null || protectedTypes.isEmpty()) { + // No protected types -> leave maps empty + return; + } + + for (ResourceSharingExtension extension : resourceSharingExtensions) { + for (var rp : extension.getResourceProviders()) { + final String type = rp.resourceType(); + if (!protectedTypes.contains(type)) continue; + + final String index = rp.resourceIndexName(); + typeToIndex.put(type, index); + indexToType.put(index, type); + } + } + } + public Set getResourceSharingExtensions() { return ImmutableSet.copyOf(resourceSharingExtensions); } + public void setResourceSharingClient(ResourceSharingClient resourceAccessControlClient) { + this.resourceAccessControlClient = resourceAccessControlClient; + } + + public ResourceSharingClient getResourceAccessControlClient() { + return resourceAccessControlClient; + } + /** Register/merge action-group names for a given resource type. */ public record ResourceDashboardInfo(String resourceType, Set actionGroups // names only (for UI) diff --git a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java new file mode 100644 index 0000000000..1461ef690e --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java @@ -0,0 +1,75 @@ +/* + * 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.settings; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.ClusterSettings; +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.resources.ResourcePluginInfo; +import org.opensearch.security.setting.OpensearchDynamicSetting; +import org.opensearch.security.spi.resources.client.ResourceSharingClient; +import org.opensearch.security.support.ConfigConstants; + +public class ResourceSharingFeatureFlagSetting extends OpensearchDynamicSetting { + + private final Logger logger = LogManager.getLogger(getClass()); + + private static final String SETTING = ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED; + + private final ResourcePluginInfo resourcePluginInfo; + + public ResourceSharingFeatureFlagSetting(final Settings settings, final ResourcePluginInfo resourcePluginInfo) { + super(getSetting(), getSettingInitialValue(settings)); + this.resourcePluginInfo = resourcePluginInfo; + } + + private static Setting getSetting() { + return Setting.boolSetting( + SETTING, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT, + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); + } + + private static Boolean getSettingInitialValue(final Settings settings) { + return settings.getAsBoolean(SETTING, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT); + } + + @Override + public void registerClusterSettingsChangeListener(final ClusterSettings clusterSettings) { + clusterSettings.addSettingsUpdateConsumer(getSetting(), isEnabled -> { + logger.info(getClusterChangeMessage(isEnabled)); + setDynamicSettingValue(isEnabled); + if (isEnabled) { + ResourceSharingClient resourceSharingClient = resourcePluginInfo.getResourceAccessControlClient(); + resourcePluginInfo.getResourceSharingExtensions().forEach(resourceSharingExtension -> { + resourceSharingExtension.assignResourceSharingClient(resourceSharingClient); // associate the client + }); + } else { + resourcePluginInfo.getResourceSharingExtensions().forEach(resourceSharingExtension -> { + resourceSharingExtension.assignResourceSharingClient(null); // dissociate the client + }); + } + }); + } + + @Override + protected String getClusterChangeMessage(final Boolean isEnabled) { + return String.format( + "Detected change in settings, cluster setting for resource-sharing feature flag is %s", + isEnabled ? "enabled" : "disabled" + ); + } +} diff --git a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java new file mode 100644 index 0000000000..56d97e9a92 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java @@ -0,0 +1,66 @@ +/* + * 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.settings; + +import java.util.List; +import java.util.function.Function; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.ClusterSettings; +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.resources.ResourcePluginInfo; +import org.opensearch.security.setting.OpensearchDynamicSetting; +import org.opensearch.security.support.ConfigConstants; + +public class ResourceSharingProtectedResourcesSetting extends OpensearchDynamicSetting> { + + private final Logger logger = LogManager.getLogger(getClass()); + + private static final String SETTING = ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES; + + private final ResourcePluginInfo resourcePluginInfo; + + public ResourceSharingProtectedResourcesSetting(final Settings settings, ResourcePluginInfo resourcePluginInfo) { + super(getSetting(), getSettingInitialValue(settings)); + this.resourcePluginInfo = resourcePluginInfo; + } + + private static Setting> getSetting() { + return Setting.listSetting( + SETTING, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES_DEFAULT, + Function.identity(), + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); + } + + private static List getSettingInitialValue(final Settings settings) { + return settings.getAsList(SETTING, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES_DEFAULT); + } + + @Override + public void registerClusterSettingsChangeListener(final ClusterSettings clusterSettings) { + clusterSettings.addSettingsUpdateConsumer(getSetting(), dynamicSettingNewValue -> { + logger.info(getClusterChangeMessage(dynamicSettingNewValue)); + setDynamicSettingValue(dynamicSettingNewValue); + this.resourcePluginInfo.updateProtectedTypes(dynamicSettingNewValue); + }); + } + + @Override + protected String getClusterChangeMessage(final List dynamicSettingNewValue) { + return String.format("Detected change in settings, new resource-sharing protected resource-types are %s", dynamicSettingNewValue); + } +} diff --git a/src/main/java/org/opensearch/security/resources/settings/package-info.java b/src/main/java/org/opensearch/security/resources/settings/package-info.java new file mode 100644 index 0000000000..bfd57ad4c7 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/settings/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains settings related to resource-sharing feature + */ +package org.opensearch.security.resources.settings; diff --git a/src/main/java/org/opensearch/security/setting/OpensearchDynamicSetting.java b/src/main/java/org/opensearch/security/setting/OpensearchDynamicSetting.java index 7c84cf779d..81e0657fb1 100644 --- a/src/main/java/org/opensearch/security/setting/OpensearchDynamicSetting.java +++ b/src/main/java/org/opensearch/security/setting/OpensearchDynamicSetting.java @@ -45,7 +45,7 @@ protected String getClusterChangeMessage(final T dynamicSettingNewValue) { return String.format("Detected change in settings, updated cluster setting value is %s", dynamicSettingNewValue); } - private void setDynamicSettingValue(final T dynamicSettingValue) { + protected void setDynamicSettingValue(final T dynamicSettingValue) { this.dynamicSettingValue = dynamicSettingValue; } diff --git a/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java index c82f2f9823..46ad719b72 100644 --- a/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java @@ -20,6 +20,7 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.resources.ResourceAccessHandler; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.support.ConfigConstants; import org.mockito.ArgumentCaptor; @@ -53,7 +54,12 @@ public class ResourceAccessEvaluatorTest { public void setup() { Settings settings = Settings.builder().put(ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, true).build(); threadContext = new ThreadContext(Settings.EMPTY); - evaluator = new ResourceAccessEvaluator(Collections.singleton(IDX), settings, resourceAccessHandler); + evaluator = new ResourceAccessEvaluator( + Collections.singleton(IDX), + resourceAccessHandler, + mock(OpensearchDynamicSetting.class), + mock(OpensearchDynamicSetting.class) + ); } private void stubAuthenticatedUser() { From 32f903b4370478cfbb4e261155c68a365a8eca36 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 1 Oct 2025 22:05:25 -0400 Subject: [PATCH 10/40] Allows plugin to be control codepath based on protected resource types Signed-off-by: Darshit Chanpura --- .../spi/resources/client/ResourceSharingClient.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/client/ResourceSharingClient.java b/spi/src/main/java/org/opensearch/security/spi/resources/client/ResourceSharingClient.java index 9a92020254..01ed81c906 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/client/ResourceSharingClient.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/client/ResourceSharingClient.java @@ -54,4 +54,11 @@ public interface ResourceSharingClient { * @param listener The listener to be notified with the set of accessible resources. */ void getAccessibleResourceIds(String resourceIndex, ActionListener> listener); + + /** + * Returns a flag to indicate whether resource-sharing is enabled for resource-type + * @param resourceType the type for which resource-sharing status is to be checked + * @return true if enabled, false otherwise + */ + boolean isFeatureEnabledForType(String resourceType); } From 5f7beb414fe52937e7626f1d2a4fb38fa9603dc6 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 2 Oct 2025 00:02:13 -0400 Subject: [PATCH 11/40] Fix setting registration Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 6 +-- .../ResourceSharingFeatureFlagSetting.java | 40 ++++++----------- ...ourceSharingProtectedResourcesSetting.java | 43 +++++++------------ 3 files changed, 32 insertions(+), 57 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 4fd23c0e02..cf6212c3f7 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -297,9 +297,9 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile IndexResolverReplacer irr; private final AtomicReference namedXContentRegistry = new AtomicReference<>(NamedXContentRegistry.EMPTY);; private volatile DlsFlsRequestValve dlsFlsValve = null; - private volatile OpensearchDynamicSetting transportPassiveAuthSetting; - private volatile OpensearchDynamicSetting resourceSharingEnabledSetting; - private volatile OpensearchDynamicSetting> resourceSharingProtectedResourceTypesSetting; + private final OpensearchDynamicSetting transportPassiveAuthSetting; + private final OpensearchDynamicSetting resourceSharingEnabledSetting; + private final OpensearchDynamicSetting> resourceSharingProtectedResourceTypesSetting; private volatile PasswordHasher passwordHasher; private volatile DlsFlsBaseContext dlsFlsBaseContext; private ResourceSharingIndexHandler rsIndexHandler; diff --git a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java index 1461ef690e..33e068c8e7 100644 --- a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java +++ b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java @@ -1,11 +1,9 @@ /* - * 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.settings; @@ -22,45 +20,33 @@ import org.opensearch.security.support.ConfigConstants; public class ResourceSharingFeatureFlagSetting extends OpensearchDynamicSetting { + private static final Logger logger = LogManager.getLogger(ResourceSharingFeatureFlagSetting.class); - private final Logger logger = LogManager.getLogger(getClass()); - - private static final String SETTING = ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED; + private static final Setting RESOURCE_SHARING_ENABLED = Setting.boolSetting( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT, + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); private final ResourcePluginInfo resourcePluginInfo; public ResourceSharingFeatureFlagSetting(final Settings settings, final ResourcePluginInfo resourcePluginInfo) { - super(getSetting(), getSettingInitialValue(settings)); + super(RESOURCE_SHARING_ENABLED, RESOURCE_SHARING_ENABLED.get(settings)); this.resourcePluginInfo = resourcePluginInfo; } - private static Setting getSetting() { - return Setting.boolSetting( - SETTING, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT, - Setting.Property.NodeScope, - Setting.Property.Dynamic - ); - } - - private static Boolean getSettingInitialValue(final Settings settings) { - return settings.getAsBoolean(SETTING, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT); - } - @Override public void registerClusterSettingsChangeListener(final ClusterSettings clusterSettings) { - clusterSettings.addSettingsUpdateConsumer(getSetting(), isEnabled -> { + clusterSettings.addSettingsUpdateConsumer(RESOURCE_SHARING_ENABLED, isEnabled -> { logger.info(getClusterChangeMessage(isEnabled)); setDynamicSettingValue(isEnabled); + if (isEnabled) { - ResourceSharingClient resourceSharingClient = resourcePluginInfo.getResourceAccessControlClient(); - resourcePluginInfo.getResourceSharingExtensions().forEach(resourceSharingExtension -> { - resourceSharingExtension.assignResourceSharingClient(resourceSharingClient); // associate the client - }); + ResourceSharingClient client = resourcePluginInfo.getResourceAccessControlClient(); + resourcePluginInfo.getResourceSharingExtensions().forEach(ext -> ext.assignResourceSharingClient(client)); } else { - resourcePluginInfo.getResourceSharingExtensions().forEach(resourceSharingExtension -> { - resourceSharingExtension.assignResourceSharingClient(null); // dissociate the client - }); + resourcePluginInfo.getResourceSharingExtensions().forEach(ext -> ext.assignResourceSharingClient(null)); } }); } diff --git a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java index 56d97e9a92..b68bc6a588 100644 --- a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java +++ b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java @@ -1,11 +1,9 @@ /* - * 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.settings; @@ -24,43 +22,34 @@ import org.opensearch.security.support.ConfigConstants; public class ResourceSharingProtectedResourcesSetting extends OpensearchDynamicSetting> { + private static final Logger logger = LogManager.getLogger(ResourceSharingProtectedResourcesSetting.class); - private final Logger logger = LogManager.getLogger(getClass()); - - private static final String SETTING = ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES; + private static final Setting> PROTECTED_TYPES = Setting.listSetting( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES_DEFAULT, + Function.identity(), + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); private final ResourcePluginInfo resourcePluginInfo; - public ResourceSharingProtectedResourcesSetting(final Settings settings, ResourcePluginInfo resourcePluginInfo) { - super(getSetting(), getSettingInitialValue(settings)); + public ResourceSharingProtectedResourcesSetting(final Settings settings, final ResourcePluginInfo resourcePluginInfo) { + super(PROTECTED_TYPES, PROTECTED_TYPES.get(settings)); this.resourcePluginInfo = resourcePluginInfo; } - private static Setting> getSetting() { - return Setting.listSetting( - SETTING, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES_DEFAULT, - Function.identity(), - Setting.Property.NodeScope, - Setting.Property.Dynamic - ); - } - - private static List getSettingInitialValue(final Settings settings) { - return settings.getAsList(SETTING, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES_DEFAULT); - } - @Override public void registerClusterSettingsChangeListener(final ClusterSettings clusterSettings) { - clusterSettings.addSettingsUpdateConsumer(getSetting(), dynamicSettingNewValue -> { - logger.info(getClusterChangeMessage(dynamicSettingNewValue)); - setDynamicSettingValue(dynamicSettingNewValue); - this.resourcePluginInfo.updateProtectedTypes(dynamicSettingNewValue); + clusterSettings.addSettingsUpdateConsumer(PROTECTED_TYPES, newValue -> { + logger.info(getClusterChangeMessage(newValue)); + setDynamicSettingValue(newValue); + this.resourcePluginInfo.updateProtectedTypes(newValue); }); } @Override - protected String getClusterChangeMessage(final List dynamicSettingNewValue) { - return String.format("Detected change in settings, new resource-sharing protected resource-types are %s", dynamicSettingNewValue); + protected String getClusterChangeMessage(final List newValue) { + return String.format("Detected change in settings, new resource-sharing protected resource-types are %s", newValue); } } From c2988eae9520b3b0c5aaceb4fa465d8e24f1bca3 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 2 Oct 2025 15:22:01 -0400 Subject: [PATCH 12/40] Adds changelog entry Signed-off-by: Darshit Chanpura --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17058f91f5..328fac6640 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Direct JWKS (JSON Web Key Set) support in the JWT authentication backend ([#5578](https://github.com/opensearch-project/security/pull/5578)) - Adds a list setting to explicitly specify resources to be protected ([#5671](https://github.com/opensearch-project/security/pull/5671)) - Make configuration setting for user custom attribute serialization dynamic ([#5657](https://github.com/opensearch-project/security/pull/5657)) +- Makes resource settings dynamic ([#5677](https://github.com/opensearch-project/security/pull/5677)) ### Bug Fixes From fbdae9fbe29fbe29dc369669815e4659e9627f85 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 2 Oct 2025 15:22:32 -0400 Subject: [PATCH 13/40] Corrects usage of resource-sharing flag Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 131 ++++++------------ .../list/AccessibleResourcesRestAction.java | 12 +- .../api/list/ResourceTypesRestAction.java | 19 ++- .../resources/api/share/ShareRestAction.java | 8 +- .../ResourceSharingFeatureFlagSetting.java | 2 +- ...ourceSharingProtectedResourcesSetting.java | 2 +- .../resources/settings/package-info.java | 8 ++ .../security/rest/DashboardsInfoAction.java | 17 +-- 8 files changed, 93 insertions(+), 106 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index cf6212c3f7..0ea49949ac 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -247,7 +247,6 @@ import static org.opensearch.security.resources.ResourceSharingIndexHandler.getSharingIndex; import static org.opensearch.security.setting.DeprecatedSettings.checkForDeprecatedSetting; import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER; -import static org.opensearch.security.support.ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT; import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE; import static org.opensearch.security.support.ConfigConstants.SECURITY_SSL_CERTIFICATES_HOT_RELOAD_ENABLED; @@ -654,10 +653,9 @@ public List getRestHandlers( ); handlers.add( new DashboardsInfoAction( - settings, - restController, Objects.requireNonNull(evaluator), - Objects.requireNonNull(threadPool) + Objects.requireNonNull(threadPool), + resourceSharingEnabledSetting ) ); handlers.add( @@ -714,14 +712,9 @@ public List getRestHandlers( ); // Resource sharing API to update sharing info - if (settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - )) { - handlers.add(new ShareRestAction(resourcePluginInfo)); - handlers.add(new ResourceTypesRestAction(resourcePluginInfo)); - handlers.add(new AccessibleResourcesRestAction(resourceAccessHandler, resourcePluginInfo)); - } + handlers.add(new ShareRestAction(resourcePluginInfo, resourceSharingEnabledSetting)); + handlers.add(new ResourceTypesRestAction(resourcePluginInfo, resourceSharingEnabledSetting)); + handlers.add(new AccessibleResourcesRestAction(resourceAccessHandler, resourcePluginInfo, resourceSharingEnabledSetting)); } log.debug("Added {} rest handler(s)", handlers.size()); @@ -752,10 +745,7 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class)); // transport action to handle sharing info update - if (settings.getAsBoolean(ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT)) { - actions.add(new ActionHandler<>(ShareAction.INSTANCE, ShareTransportAction.class)); - } - + actions.add(new ActionHandler<>(ShareAction.INSTANCE, ShareTransportAction.class)); } return actions; } @@ -784,25 +774,21 @@ public void onIndexModule(IndexModule indexModule) { ) ); - if (settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - )) { - // Listening on POST and DELETE operations in resource indices - ResourceIndexListener resourceIndexListener = new ResourceIndexListener( - threadPool, - localClient, - resourcePluginInfo, - resourceSharingEnabledSetting, - resourceSharingProtectedResourceTypesSetting - ); - // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions - Set resourceIndices = resourcePluginInfo.getResourceIndices(); - // CS-ENFORCE-SINGLE - if (resourceIndices.contains(indexModule.getIndex().getName())) { - indexModule.addIndexOperationListener(resourceIndexListener); - log.info("Security plugin started listening to operations on resource-index {}", indexModule.getIndex().getName()); - } + + // Listening on POST and DELETE operations in resource indices + ResourceIndexListener resourceIndexListener = new ResourceIndexListener( + threadPool, + localClient, + resourcePluginInfo, + resourceSharingEnabledSetting, + resourceSharingProtectedResourceTypesSetting + ); + // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions + Set resourceIndices = resourcePluginInfo.getResourceIndices(); + // CS-ENFORCE-SINGLE + if (resourceIndices.contains(indexModule.getIndex().getName())) { + indexModule.addIndexOperationListener(resourceIndexListener); + log.info("Security plugin started listening to operations on resource-index {}", indexModule.getIndex().getName()); } indexModule.forceQueryCacheProvider((indexSettings, nodeCache) -> new QueryCache() { @@ -1236,22 +1222,18 @@ public Collection createComponents( } resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns, evaluator, resourcePluginInfo); - if (settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - )) { - // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions - // Assign resource sharing client to each extension - // Using the non-gated client (i.e. no additional permissions required) - ResourceSharingClient resourceAccessControlClient = new ResourceAccessControlClient(resourceAccessHandler, resourcePluginInfo); - resourcePluginInfo.setResourceSharingClient(resourceAccessControlClient); - resourcePluginInfo.getResourceSharingExtensions().forEach(extension -> { - extension.assignResourceSharingClient(resourceAccessControlClient); - }); - components.add(resourcePluginInfo); - components.add(resourceAccessHandler); - // CS-ENFORCE-SINGLE - } + + // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions + // Assign resource sharing client to each extension + // Using the non-gated client (i.e. no additional permissions required) + ResourceSharingClient resourceAccessControlClient = new ResourceAccessControlClient(resourceAccessHandler, resourcePluginInfo); + resourcePluginInfo.setResourceSharingClient(resourceAccessControlClient); + resourcePluginInfo.getResourceSharingExtensions().forEach(extension -> { + extension.assignResourceSharingClient(resourceAccessControlClient); + }); + components.add(resourcePluginInfo); + components.add(resourceAccessHandler); + // CS-ENFORCE-SINGLE resourceAccessEvaluator = new ResourceAccessEvaluator( resourcePluginInfo.getResourceIndices(), @@ -2326,21 +2308,11 @@ public void onNodeStarted(DiscoveryNode localNode) { // resourceSharingIndexManagementRepository will be null when sec plugin is disabled or is in SSLOnly mode, hence it will not be // instantiated - if (settings != null - && settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - )) { - // create resource sharing index if absent - // TODO check if this should be wrapped in an atomic completable future - log.debug("Attempting to create Resource Sharing index"); - Set resourceIndices = new HashSet<>(); - if (resourcePluginInfo != null) { - resourceIndices = resourcePluginInfo.getResourceIndices(); - } - rsIndexHandler.createResourceSharingIndicesIfAbsent(resourceIndices); - - } + // create resource sharing index if absent + // TODO check if this should be wrapped in an atomic completable future + log.debug("Attempting to create Resource Sharing index"); + Set resourceIndices = resourcePluginInfo.getResourceIndices(); + rsIndexHandler.createResourceSharingIndicesIfAbsent(resourceIndices); final Set securityModules = ReflectionHelper.getModulesLoaded(); log.info("{} OpenSearch Security modules loaded so far: {}", securityModules.size(), securityModules); @@ -2390,18 +2362,13 @@ public Collection getSystemIndexDescriptors(Settings sett ); final SystemIndexDescriptor securityIndexDescriptor = new SystemIndexDescriptor(indexPattern, "Security index"); systemIndexDescriptors.add(securityIndexDescriptor); - if (settings != null - && settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - )) { - for (String resourceIndex : resourcePluginInfo.getResourceIndices()) { - final SystemIndexDescriptor resourceSharingIndexDescriptor = new SystemIndexDescriptor( - getSharingIndex(resourceIndex), - "Resource Sharing index for index: " + resourceIndex - ); - systemIndexDescriptors.add(resourceSharingIndexDescriptor); - } + + for (String resourceIndex : resourcePluginInfo.getResourceIndices()) { + final SystemIndexDescriptor resourceSharingIndexDescriptor = new SystemIndexDescriptor( + getSharingIndex(resourceIndex), + "Resource Sharing index for index: " + resourceIndex + ); + systemIndexDescriptors.add(resourceSharingIndexDescriptor); } if (SecurityConfigVersionHandler.isVersionIndexEnabled(settings)) { @@ -2479,14 +2446,6 @@ private void tryAddSecurityProvider() { // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions @Override public void loadExtensions(ExtensionLoader loader) { - if (settings == null - || !settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - )) { - return; - } - // discover & register extensions and their types Set exts = new HashSet<>(loader.loadExtensions(ResourceSharingExtension.class)); resourcePluginInfo.setResourceSharingExtensions( diff --git a/src/main/java/org/opensearch/security/resources/api/list/AccessibleResourcesRestAction.java b/src/main/java/org/opensearch/security/resources/api/list/AccessibleResourcesRestAction.java index cef53f82b9..d9c6975dfc 100644 --- a/src/main/java/org/opensearch/security/resources/api/list/AccessibleResourcesRestAction.java +++ b/src/main/java/org/opensearch/security/resources/api/list/AccessibleResourcesRestAction.java @@ -29,6 +29,7 @@ import org.opensearch.security.resources.ResourceAccessHandler; import org.opensearch.security.resources.ResourcePluginInfo; import org.opensearch.security.resources.SharingRecord; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.transport.client.node.NodeClient; import static org.opensearch.rest.RestRequest.Method.GET; @@ -44,11 +45,17 @@ public class AccessibleResourcesRestAction extends BaseRestHandler { private final ResourceAccessHandler resourceAccessHandler; private final ResourcePluginInfo resourcePluginInfo; + private final OpensearchDynamicSetting resourceSharingEnabledSetting; - public AccessibleResourcesRestAction(final ResourceAccessHandler resourceAccessHandler, ResourcePluginInfo resourcePluginInfo) { + public AccessibleResourcesRestAction( + final ResourceAccessHandler resourceAccessHandler, + ResourcePluginInfo resourcePluginInfo, + OpensearchDynamicSetting resourceSharingEnabledSetting + ) { super(); this.resourceAccessHandler = resourceAccessHandler; this.resourcePluginInfo = resourcePluginInfo; + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; } @Override @@ -63,6 +70,9 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + if (!resourceSharingEnabledSetting.getDynamicSettingValue()) { + return channel -> { channel.sendResponse(new BytesRestResponse(RestStatus.NOT_IMPLEMENTED, "Feature disabled.")); }; + } final String resourceType = Objects.requireNonNull(request.param("resource_type"), "resource_type is required"); final String resourceIndex = resourcePluginInfo.indexByType(resourceType); diff --git a/src/main/java/org/opensearch/security/resources/api/list/ResourceTypesRestAction.java b/src/main/java/org/opensearch/security/resources/api/list/ResourceTypesRestAction.java index 733c36eb4d..6a5c0f4d1f 100644 --- a/src/main/java/org/opensearch/security/resources/api/list/ResourceTypesRestAction.java +++ b/src/main/java/org/opensearch/security/resources/api/list/ResourceTypesRestAction.java @@ -10,7 +10,6 @@ import java.io.IOException; import java.util.List; -import java.util.Set; import com.google.common.collect.ImmutableList; import org.apache.logging.log4j.LogManager; @@ -25,6 +24,7 @@ import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.security.resources.ResourcePluginInfo; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.transport.client.node.NodeClient; import static org.opensearch.rest.RestRequest.Method.GET; @@ -38,11 +38,17 @@ public class ResourceTypesRestAction extends BaseRestHandler { private static final Logger LOGGER = LogManager.getLogger(ResourceTypesRestAction.class); - private final Set resourceTypes; + private final ResourcePluginInfo resourcePluginInfo; - public ResourceTypesRestAction(final ResourcePluginInfo resourcePluginInfo) { + private final OpensearchDynamicSetting resourceSharingEnabledSetting; + + public ResourceTypesRestAction( + final ResourcePluginInfo resourcePluginInfo, + OpensearchDynamicSetting resourceSharingEnabledSetting + ) { super(); - this.resourceTypes = resourcePluginInfo.getResourceTypes(); + this.resourcePluginInfo = resourcePluginInfo; + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; } @Override @@ -57,11 +63,14 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + if (!resourceSharingEnabledSetting.getDynamicSettingValue()) { + return channel -> { channel.sendResponse(new BytesRestResponse(RestStatus.NOT_IMPLEMENTED, "Feature disabled.")); }; + } return channel -> { try (XContentBuilder builder = channel.newBuilder()) { // NOSONAR builder.startObject(); builder.startArray("types"); - for (var p : resourceTypes) { + for (var p : resourcePluginInfo.getResourceTypes()) { p.toXContent(builder, ToXContent.EMPTY_PARAMS); } builder.endArray(); diff --git a/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java b/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java index 8cc9d87032..8d5c685d3a 100644 --- a/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java +++ b/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java @@ -23,6 +23,7 @@ import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.security.resources.ResourcePluginInfo; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.transport.client.node.NodeClient; import static org.opensearch.rest.RestRequest.Method.GET; @@ -42,9 +43,11 @@ public class ShareRestAction extends BaseRestHandler { private static final Logger LOGGER = LogManager.getLogger(ShareRestAction.class); private final ResourcePluginInfo resourcePluginInfo; + private final OpensearchDynamicSetting resourceSharingEnabledSetting; - public ShareRestAction(ResourcePluginInfo resourcePluginInfo) { + public ShareRestAction(ResourcePluginInfo resourcePluginInfo, OpensearchDynamicSetting resourceSharingEnabledSetting) { this.resourcePluginInfo = resourcePluginInfo; + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; } @Override @@ -62,6 +65,9 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + if (!resourceSharingEnabledSetting.getDynamicSettingValue()) { + return channel -> { channel.sendResponse(new BytesRestResponse(RestStatus.NOT_IMPLEMENTED, "Feature disabled.")); }; + } // These two params will only be present with GET request String resourceId = request.param("resource_id"); String resourceType = request.param("resource_type"); diff --git a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java index 33e068c8e7..cbb64e1b4d 100644 --- a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java +++ b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java @@ -22,7 +22,7 @@ public class ResourceSharingFeatureFlagSetting extends OpensearchDynamicSetting { private static final Logger logger = LogManager.getLogger(ResourceSharingFeatureFlagSetting.class); - private static final Setting RESOURCE_SHARING_ENABLED = Setting.boolSetting( + public static final Setting RESOURCE_SHARING_ENABLED = Setting.boolSetting( ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT, Setting.Property.NodeScope, diff --git a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java index b68bc6a588..3afe137432 100644 --- a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java +++ b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java @@ -24,7 +24,7 @@ public class ResourceSharingProtectedResourcesSetting extends OpensearchDynamicSetting> { private static final Logger logger = LogManager.getLogger(ResourceSharingProtectedResourcesSetting.class); - private static final Setting> PROTECTED_TYPES = Setting.listSetting( + public static final Setting> PROTECTED_TYPES = Setting.listSetting( ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES_DEFAULT, Function.identity(), diff --git a/src/main/java/org/opensearch/security/resources/settings/package-info.java b/src/main/java/org/opensearch/security/resources/settings/package-info.java index bfd57ad4c7..896f465ca2 100644 --- a/src/main/java/org/opensearch/security/resources/settings/package-info.java +++ b/src/main/java/org/opensearch/security/resources/settings/package-info.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. + */ + /** * Contains settings related to resource-sharing feature */ diff --git a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java index 9203f4e92c..7353633071 100644 --- a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java @@ -33,16 +33,15 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; @@ -82,7 +81,7 @@ public class DashboardsInfoAction extends BaseRestHandler { private final PrivilegesEvaluator evaluator; private final ThreadContext threadContext; - private final boolean isResourceSharingFeatureEnabled; + private final OpensearchDynamicSetting resourceSharingEnabledSetting; public static final String DEFAULT_PASSWORD_MESSAGE = "Password should be at least 8 characters long and contain at least one " + "uppercase letter, one lowercase letter, one digit, and one special character."; @@ -90,16 +89,12 @@ public class DashboardsInfoAction extends BaseRestHandler { public static final String DEFAULT_PASSWORD_REGEX = "(?=.*[A-Z])(?=.*[^a-zA-Z\\d])(?=.*[0-9])(?=.*[a-z]).{8,}"; public DashboardsInfoAction( - final Settings settings, - final RestController controller, final PrivilegesEvaluator evaluator, - final ThreadPool threadPool + final ThreadPool threadPool, + OpensearchDynamicSetting resourceSharingEnabledSetting ) { super(); - isResourceSharingFeatureEnabled = settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - ); + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; this.threadContext = threadPool.getThreadContext(); this.evaluator = evaluator; } @@ -145,7 +140,7 @@ public void accept(RestChannel channel) throws Exception { "password_validation_regex", client.settings().get(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, DEFAULT_PASSWORD_REGEX) ); - builder.field("resource_sharing_enabled", isResourceSharingFeatureEnabled); + builder.field("resource_sharing_enabled", resourceSharingEnabledSetting.getDynamicSettingValue()); builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); From a3493a604cb4eb13165be64adc64de3f4bcbf150 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 7 Oct 2025 14:04:59 -0400 Subject: [PATCH 14/40] Fix share action rest channel consumer Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 27 +++++++------------ .../resources/api/share/ShareRestAction.java | 24 ++--------------- 2 files changed, 12 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 0ea49949ac..88bd4b4fce 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -774,7 +774,6 @@ public void onIndexModule(IndexModule indexModule) { ) ); - // Listening on POST and DELETE operations in resource indices ResourceIndexListener resourceIndexListener = new ResourceIndexListener( threadPool, @@ -2306,14 +2305,6 @@ public void onNodeStarted(DiscoveryNode localNode) { cr.initOnNodeStart(); } - // resourceSharingIndexManagementRepository will be null when sec plugin is disabled or is in SSLOnly mode, hence it will not be - // instantiated - // create resource sharing index if absent - // TODO check if this should be wrapped in an atomic completable future - log.debug("Attempting to create Resource Sharing index"); - Set resourceIndices = resourcePluginInfo.getResourceIndices(); - rsIndexHandler.createResourceSharingIndicesIfAbsent(resourceIndices); - final Set securityModules = ReflectionHelper.getModulesLoaded(); log.info("{} OpenSearch Security modules loaded so far: {}", securityModules.size(), securityModules); } @@ -2446,15 +2437,17 @@ private void tryAddSecurityProvider() { // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions @Override public void loadExtensions(ExtensionLoader loader) { - // discover & register extensions and their types - Set exts = new HashSet<>(loader.loadExtensions(ResourceSharingExtension.class)); - resourcePluginInfo.setResourceSharingExtensions( - exts, - settings.getAsList(ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES) - ); + if (settings != null) { + // discover & register extensions and their types + Set exts = new HashSet<>(loader.loadExtensions(ResourceSharingExtension.class)); + resourcePluginInfo.setResourceSharingExtensions( + exts, + settings.getAsList(ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES) + ); - // load action-groups in memory - ResourceActionGroupsHelper.loadActionGroupsConfig(resourcePluginInfo); + // load action-groups in memory + ResourceActionGroupsHelper.loadActionGroupsConfig(resourcePluginInfo); + } } // CS-ENFORCE-SINGLE diff --git a/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java b/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java index 8d5c685d3a..0c1f98a1b4 100644 --- a/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java +++ b/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java @@ -15,13 +15,11 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.OpenSearchStatusException; -import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; 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.ResourcePluginInfo; import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.transport.client.node.NodeClient; @@ -29,8 +27,6 @@ import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.PATCH; import static org.opensearch.rest.RestRequest.Method.PUT; -import static org.opensearch.security.dlic.rest.api.Responses.ok; -import static org.opensearch.security.dlic.rest.api.Responses.response; import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_API_RESOURCE_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; @@ -91,22 +87,6 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli ShareRequest shareRequest = builder.build(); - return channel -> { - client.executeLocally( - ShareAction.INSTANCE, - shareRequest, - ActionListener.wrap(resp -> ok(channel, resp::toXContent), e -> handleError(channel, e)) - ); - }; - } - - private void handleError(RestChannel channel, Exception e) { - LOGGER.error("Error while processing request", e); - String message = e.getMessage(); - if (e instanceof OpenSearchStatusException ex) { - response(channel, ex.status(), message); - } else { - channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, message)); - } + return channel -> { client.executeLocally(ShareAction.INSTANCE, shareRequest, new RestToXContentListener<>(channel)); }; } } From 9dd02a1985ab3643d83684032bc61fa8428e492e Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 7 Oct 2025 14:43:02 -0400 Subject: [PATCH 15/40] Fix sample plugin tests Signed-off-by: Darshit Chanpura --- .../resource/SecurityDisabledTests.java | 33 +++--- .../opensearch/sample/resource/TestUtils.java | 111 ++++++------------ .../feature/disabled/ApiAccessTests.java | 60 ++++++---- .../feature/enabled/ApiAccessTests.java | 56 ++++++--- .../enabled/DirectIndexAccessTests.java | 45 ++++--- .../feature/enabled/DryRunAccessTests.java | 48 +++++--- .../AdminCertificateAccessTests.java | 28 +++-- .../enabled/multi_share/MixedAccessTests.java | 9 +- .../AccessibleResourcesApiTests.java | 2 +- .../resource/securityapis/ShareApiTests.java | 36 +++++- .../sample/SampleResourcePlugin.java | 11 -- .../revoke/RevokeResourceAccessAction.java | 29 ----- .../revoke/RevokeResourceAccessRequest.java | 74 ------------ .../revoke/RevokeResourceAccessResponse.java | 46 -------- .../RevokeResourceAccessRestAction.java | 91 -------------- .../rest/share/ShareResourceAction.java | 29 ----- .../rest/share/ShareResourceRequest.java | 75 ------------ .../rest/share/ShareResourceResponse.java | 46 -------- .../rest/share/ShareResourceRestAction.java | 86 -------------- .../RevokeResourceAccessTransportAction.java | 64 ---------- .../ShareResourceTransportAction.java | 69 ----------- 21 files changed, 243 insertions(+), 805 deletions(-) delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRequest.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessResponse.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRequest.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceResponse.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java delete mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/SecurityDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/SecurityDisabledTests.java index fbd460efcc..fe651253ca 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/SecurityDisabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/SecurityDisabledTests.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; +import java.util.Set; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; @@ -22,6 +23,8 @@ import org.opensearch.plugins.PluginInfo; import org.opensearch.sample.SampleResourcePlugin; import org.opensearch.security.OpenSearchSecurityPlugin; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -33,12 +36,11 @@ import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_CREATE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_REVOKE_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.revokeAccessPayload; -import static org.opensearch.sample.resource.TestUtils.shareWithPayload; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** @@ -109,17 +111,18 @@ public void testSamplePluginAPIs() { response = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); response.assertStatusCode(HttpStatus.SC_OK); - response = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, - shareWithPayload(USER_ADMIN.getName(), SAMPLE_READ_ONLY_RESOURCE_AG) + response = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(resourceId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, Recipient.USERS, USER_ADMIN.getName()) ); - assertNotImplementedResponse(response, "Cannot share resource"); + assertBadRequest(response, "no handler found for uri [/_plugins/_security/api/resource/share] and method [PUT]"); - response = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, - revokeAccessPayload(USER_ADMIN.getName(), SAMPLE_READ_ONLY_RESOURCE_AG) - ); - assertNotImplementedResponse(response, "Cannot revoke access to resource"); + TestUtils.PatchSharingInfoPayloadBuilder patchBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + patchBuilder.resourceType(RESOURCE_TYPE); + patchBuilder.resourceId(resourceId); + patchBuilder.revoke(new Recipients(Map.of(Recipient.USERS, Set.of(USER_ADMIN.getName()))), SAMPLE_READ_ONLY_RESOURCE_AG); + response = client.patch(SECURITY_SHARE_ENDPOINT, patchBuilder.build()); + assertBadRequest(response, "no handler found for uri [/_plugins/_security/api/resource/share] and method [PATCH]"); response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); response.assertStatusCode(HttpStatus.SC_OK); @@ -127,7 +130,7 @@ public void testSamplePluginAPIs() { } } - private void assertNotImplementedResponse(TestRestClient.HttpResponse response, String msg) { - assertThat(response, RestMatchers.isMethodNotImplemented("/error/reason", msg)); + private void assertBadRequest(TestRestClient.HttpResponse response, String msg) { + assertThat(response, RestMatchers.isBadRequest("/error", msg)); } } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java index 6bd3b40bbc..8c2fbfa25a 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.apache.hc.core5.http.Header; import org.apache.http.HttpStatus; @@ -26,6 +27,7 @@ import org.opensearch.plugins.PluginInfo; import org.opensearch.sample.SampleResourcePlugin; import org.opensearch.security.OpenSearchSecurityPlugin; +import org.opensearch.security.spi.resources.sharing.Recipient; import org.opensearch.security.spi.resources.sharing.Recipients; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.certificate.CertificateData; @@ -66,8 +68,8 @@ public final class TestUtils { "cluster:admin/sample-resource-plugin/get", "cluster:admin/sample-resource-plugin/search", "cluster:admin/sample-resource-plugin/create", - "cluster:admin/sample-resource-plugin/share", - "cluster:admin/sample-resource-plugin/revoke" + "cluster:admin/security/resource/share", + "cluster:admin/security/resource/share" ).indexPermissions("indices:data/read*").on(RESOURCE_INDEX_NAME) ); @@ -83,8 +85,6 @@ public final class TestUtils { public static final String SAMPLE_RESOURCE_UPDATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/update"; public static final String SAMPLE_RESOURCE_DELETE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/delete"; public static final String SAMPLE_RESOURCE_SEARCH_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/search"; - public static final String SAMPLE_RESOURCE_SHARE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/share"; - public static final String SAMPLE_RESOURCE_REVOKE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/revoke"; public static final String RESOURCE_SHARING_MIGRATION_ENDPOINT = "_plugins/_security/api/resources/migrate"; public static final String SECURITY_SHARE_ENDPOINT = "_plugins/_security/api/resource/share"; @@ -126,18 +126,6 @@ public static LocalCluster newCluster(boolean featureEnabled, boolean systemInde .build(); } - public static String shareWithPayload(String user, String accessLevel) { - return """ - { - "share_with": { - "%s" : { - "users": ["%s"] - } - } - } - """.formatted(accessLevel, user); - } - public static String directSharePayload(String resourceId, String creator, String target, String accessLevel) { return """ { @@ -154,31 +142,6 @@ public static String directSharePayload(String resourceId, String creator, Strin """.formatted(resourceId, creator, accessLevel, target); } - public static String revokeAccessPayload(String user, String accessLevel) { - return """ - { - "entities_to_revoke": { - "%s" : { - "users": ["%s"] - } - } - } - """.formatted(accessLevel, user); - - } - - public static String shareWithRolePayload(String role, String accessLevel) { - return """ - { - "share_with": { - "%s" : { - "roles": ["%s"] - } - } - } - """.formatted(accessLevel, role); - } - public static String migrationPayload_valid() { return """ { @@ -227,18 +190,24 @@ public static String migrationPayload_missingBackendRoles() { """.formatted(RESOURCE_INDEX_NAME, "user/name"); } - public static String putSharingInfoPayload(String resourceId, String resourceType, String accessLevel, String user) { + public static String putSharingInfoPayload( + String resourceId, + String resourceType, + String accessLevel, + Recipient recipient, + String entity + ) { return """ { "resource_id": "%s", "resource_type": "%s", "share_with": { "%s" : { - "users": ["%s"] + "%s": ["%s"] } } } - """.formatted(resourceId, resourceType, accessLevel, user); + """.formatted(resourceId, resourceType, accessLevel, recipient.getName(), entity); } public static class PatchSharingInfoPayloadBuilder { @@ -522,7 +491,7 @@ private void assertUpdate(String endpoint, String newName, TestSecurityConfig.Us } } - public void assertDirectShare( + public void assertDirectUpdateSharingInfo( String resourceId, TestSecurityConfig.User user, TestSecurityConfig.User target, @@ -544,31 +513,30 @@ public void assertApiShare( TestSecurityConfig.User target, String accessLevel, int status - ) { - assertShare(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, user, target, accessLevel, status); - } - - private void assertShare( - String endpoint, - TestSecurityConfig.User user, - TestSecurityConfig.User target, - String accessLevel, - int status ) { try (TestRestClient client = cluster.getRestClient(user)) { - TestRestClient.HttpResponse response = client.postJson(endpoint, shareWithPayload(target.getName(), accessLevel)); + TestRestClient.HttpResponse response = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(resourceId, RESOURCE_TYPE, accessLevel, Recipient.USERS, target.getName()) + ); response.assertStatusCode(status); } } - public void assertDirectRevoke( + public void assertApiShareByRole( String resourceId, TestSecurityConfig.User user, - TestSecurityConfig.User target, + String targetRole, String accessLevel, int status ) { - assertRevoke(RESOURCE_SHARING_INDEX + "/_doc/" + resourceId, user, target, accessLevel, status); + try (TestRestClient client = cluster.getRestClient(user)) { + TestRestClient.HttpResponse response = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(resourceId, RESOURCE_TYPE, accessLevel, Recipient.ROLES, targetRole) + ); + response.assertStatusCode(status); + } } public void assertApiRevoke( @@ -578,34 +546,23 @@ public void assertApiRevoke( String accessLevel, int status ) { - assertRevoke(SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, user, target, accessLevel, status); - } - - public void assertApiShareByRole( - String resourceId, - TestSecurityConfig.User user, - String targetRole, - String accessLevel, - int status - ) { - try (TestRestClient client = cluster.getRestClient(user)) { - TestRestClient.HttpResponse response = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, - shareWithRolePayload(targetRole, accessLevel) - ); - response.assertStatusCode(status); - } + assertRevoke(SECURITY_SHARE_ENDPOINT, resourceId, user, target, accessLevel, status); } private void assertRevoke( String endpoint, + String resourceId, TestSecurityConfig.User user, TestSecurityConfig.User target, String accessLevel, int status ) { + PatchSharingInfoPayloadBuilder patchBuilder = new PatchSharingInfoPayloadBuilder(); + patchBuilder.resourceType(RESOURCE_TYPE); + patchBuilder.resourceId(resourceId); + patchBuilder.revoke(new Recipients(Map.of(Recipient.USERS, Set.of(target.getName()))), accessLevel); try (TestRestClient client = cluster.getRestClient(user)) { - TestRestClient.HttpResponse response = client.postJson(endpoint, revokeAccessPayload(target.getName(), accessLevel)); + TestRestClient.HttpResponse response = client.patch(endpoint, patchBuilder.build()); response.assertStatusCode(status); } } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/disabled/ApiAccessTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/disabled/ApiAccessTests.java index fe5f37c5f5..8aa2f87107 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/disabled/ApiAccessTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/disabled/ApiAccessTests.java @@ -8,6 +8,9 @@ package org.opensearch.sample.resource.feature.disabled; +import java.util.Map; +import java.util.Set; + import com.carrotsearch.randomizedtesting.RandomizedRunner; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; @@ -19,6 +22,8 @@ import org.junit.runners.Suite; import org.opensearch.sample.resource.TestUtils; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; @@ -30,19 +35,19 @@ import static org.opensearch.sample.resource.TestUtils.FULL_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.LIMITED_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.NO_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.PatchSharingInfoPayloadBuilder; import static org.opensearch.sample.resource.TestUtils.RESOURCE_SHARING_INDEX; import static org.opensearch.sample.resource.TestUtils.SAMPLE_FULL_ACCESS_RESOURCE_AG; import static org.opensearch.sample.resource.TestUtils.SAMPLE_READ_ONLY_RESOURCE_AG; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_CREATE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_REVOKE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SEARCH_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.newCluster; -import static org.opensearch.sample.resource.TestUtils.revokeAccessPayload; -import static org.opensearch.sample.resource.TestUtils.shareWithPayload; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** @@ -117,8 +122,8 @@ public void testApiAccess_noAccessUser() { // feature is disabled, and thus request is treated as normal request. // Since user doesn't have permission to the share and revoke endpoints they will receive 403s - api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); - api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); // search returns 403 since user doesn't have access to invoke search api.assertApiGetSearchForbidden(NO_ACCESS_USER); @@ -243,17 +248,27 @@ public void testApiAccess_adminCertificateUsers() { assertThat(resp.getBody(), containsString("sampleUpdated")); // can't share or revoke, as handlers don't exist - resp = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + adminResId, - shareWithPayload(FULL_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + FULL_ACCESS_USER.getName() + ) ); resp.assertStatusCode(HttpStatus.SC_NOT_IMPLEMENTED); - resp = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + adminResId, - revokeAccessPayload(FULL_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + PatchSharingInfoPayloadBuilder patchBuilder = new PatchSharingInfoPayloadBuilder(); + patchBuilder.resourceId(adminResId); + patchBuilder.resourceType(RESOURCE_TYPE); + patchBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(FULL_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, patchBuilder.build()); resp.assertStatusCode(HttpStatus.SC_NOT_IMPLEMENTED); @@ -339,8 +354,8 @@ public void testApiAccess_noAccessUser() { api.assertApiGet(adminResId, USER_ADMIN, HttpStatus.SC_OK, "sample"); // feature is disabled, no handler's exist - api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); - api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); // search returns 403 since user doesn't have access to invoke search api.assertApiGetSearchForbidden(NO_ACCESS_USER); @@ -468,16 +483,21 @@ public void testApiAccess_adminCertificateUser() { assertThat(resp.getBody(), containsString("sampleUpdated")); // can't share or revoke, as handlers don't exist - resp = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + id, - shareWithPayload(FULL_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(id, RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, Recipient.USERS, FULL_ACCESS_USER.getName()) ); resp.assertStatusCode(HttpStatus.SC_NOT_IMPLEMENTED); - resp = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + id, - revokeAccessPayload(FULL_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + PatchSharingInfoPayloadBuilder patchBuilder = new PatchSharingInfoPayloadBuilder(); + patchBuilder.resourceId(id); + patchBuilder.resourceType(RESOURCE_TYPE); + patchBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(FULL_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG ); + + resp = client.patch(SECURITY_SHARE_ENDPOINT, patchBuilder.build()); resp.assertStatusCode(HttpStatus.SC_NOT_IMPLEMENTED); // can search resources diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ApiAccessTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ApiAccessTests.java index 3f30d73dd8..caa413ab1d 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ApiAccessTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ApiAccessTests.java @@ -8,6 +8,9 @@ package org.opensearch.sample.resource.feature.enabled; +import java.util.Map; +import java.util.Set; + import com.carrotsearch.randomizedtesting.RandomizedRunner; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; @@ -19,6 +22,8 @@ import org.junit.runners.Suite; import org.opensearch.sample.resource.TestUtils; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; @@ -36,13 +41,12 @@ import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_CREATE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_REVOKE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SEARCH_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.newCluster; -import static org.opensearch.sample.resource.TestUtils.revokeAccessPayload; -import static org.opensearch.sample.resource.TestUtils.shareWithPayload; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** @@ -243,17 +247,27 @@ public void testApiAccess_superAdmin() { assertThat(resp.getBody(), containsString("sampleUpdated")); // can share and revoke admin's resource - resp = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + adminResId, - shareWithPayload(NO_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + NO_ACCESS_USER.getName() + ) ); resp.assertStatusCode(HttpStatus.SC_OK); - resp = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + adminResId, - revokeAccessPayload(NO_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + TestUtils.PatchSharingInfoPayloadBuilder payloadBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(NO_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, payloadBuilder.build()); resp.assertStatusCode(HttpStatus.SC_OK); @@ -460,17 +474,27 @@ public void testApiAccess_superAdmin() { assertThat(resp.getBody(), containsString("sampleUpdated")); // can share and revoke admin's resource - resp = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + adminResId, - shareWithPayload(NO_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + NO_ACCESS_USER.getName() + ) ); resp.assertStatusCode(HttpStatus.SC_OK); - resp = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + adminResId, - revokeAccessPayload(NO_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + TestUtils.PatchSharingInfoPayloadBuilder payloadBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(NO_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, payloadBuilder.build()); resp.assertStatusCode(HttpStatus.SC_OK); diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DirectIndexAccessTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DirectIndexAccessTests.java index 11169e2cca..d6157376ea 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DirectIndexAccessTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DirectIndexAccessTests.java @@ -80,8 +80,7 @@ private void assertResourceIndexAccess(String id, TestSecurityConfig.User user) private void assertResourceSharingIndexAccess(String id, TestSecurityConfig.User user) { // cannot interact with resource sharing index api.assertDirectViewSharingRecord(id, user, HttpStatus.SC_FORBIDDEN); - api.assertDirectShare(id, user, user, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); - api.assertDirectRevoke(id, user, user, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertDirectUpdateSharingInfo(id, user, user, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); api.assertDirectDeleteResourceSharingRecord(id, user, HttpStatus.SC_FORBIDDEN); } @@ -134,8 +133,13 @@ public void testRawAccess_allAccessUser() { // cannot interact with resource sharing index api.assertDirectViewSharingRecord(id, FULL_ACCESS_USER, HttpStatus.SC_NOT_FOUND); - api.assertDirectShare(id, FULL_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); - api.assertDirectRevoke(id, FULL_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertDirectUpdateSharingInfo( + id, + FULL_ACCESS_USER, + FULL_ACCESS_USER, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + HttpStatus.SC_FORBIDDEN + ); api.assertDirectDeleteResourceSharingRecord(id, FULL_ACCESS_USER, HttpStatus.SC_FORBIDDEN); } @@ -213,8 +217,13 @@ public void testRawAccess_noAccessUser() { // cannot interact with resource sharing index api.assertDirectViewSharingRecord(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN); - api.assertDirectShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); - api.assertDirectRevoke(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertDirectUpdateSharingInfo( + adminResId, + NO_ACCESS_USER, + NO_ACCESS_USER, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + HttpStatus.SC_FORBIDDEN + ); api.assertDirectDeleteResourceSharingRecord(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN); } @@ -230,7 +239,7 @@ public void testRawAccess_limitedAccessUser() { } api.assertDirectGet(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); // once admin share's record, user can then query it directly - api.assertDirectShare(adminResId, USER_ADMIN, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertDirectUpdateSharingInfo(adminResId, USER_ADMIN, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); api.awaitSharingEntry(adminResId, LIMITED_ACCESS_USER.getName()); api.assertDirectGet(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); @@ -245,14 +254,7 @@ public void testRawAccess_limitedAccessUser() { // cannot access resource sharing index since user doesn't have permissions on that index api.assertDirectViewSharingRecord(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); - api.assertDirectShare( - adminResId, - LIMITED_ACCESS_USER, - LIMITED_ACCESS_USER, - SAMPLE_FULL_ACCESS_RESOURCE_AG, - HttpStatus.SC_FORBIDDEN - ); - api.assertDirectRevoke( + api.assertDirectUpdateSharingInfo( adminResId, LIMITED_ACCESS_USER, LIMITED_ACCESS_USER, @@ -276,12 +278,12 @@ public void testRawAccess_allAccessUser() { } api.assertDirectGet(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); // once admin share's record, user can then query it directly - api.assertDirectShare(adminResId, USER_ADMIN, FULL_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertDirectUpdateSharingInfo(adminResId, USER_ADMIN, FULL_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); api.awaitSharingEntry(adminResId, FULL_ACCESS_USER.getName()); api.assertDirectGet(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); api.assertDirectGet(userResId, USER_ADMIN, HttpStatus.SC_OK, "sample"); - api.assertDirectShare(userResId, FULL_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertDirectUpdateSharingInfo(userResId, FULL_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); api.assertDirectGet(userResId, USER_ADMIN, HttpStatus.SC_OK, "sample"); api.assertDirectGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); @@ -295,8 +297,13 @@ public void testRawAccess_allAccessUser() { // can view, share, revoke and delete resource sharing record(s) directly api.assertDirectViewSharingRecord(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK); - api.assertDirectShare(adminResId, FULL_ACCESS_USER, NO_ACCESS_USER, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_OK); - api.assertDirectRevoke(adminResId, FULL_ACCESS_USER, NO_ACCESS_USER, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_OK); + api.assertDirectUpdateSharingInfo( + adminResId, + FULL_ACCESS_USER, + NO_ACCESS_USER, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + HttpStatus.SC_OK + ); api.assertDirectDeleteResourceSharingRecord(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK); // can update or delete admin resource, since system index protection is disabled and user has direct index access. diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DryRunAccessTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DryRunAccessTests.java index 2021811fe3..9785f85213 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DryRunAccessTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DryRunAccessTests.java @@ -9,6 +9,8 @@ package org.opensearch.sample.resource.feature.enabled; import java.util.List; +import java.util.Map; +import java.util.Set; import com.carrotsearch.randomizedtesting.RandomizedRunner; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; @@ -20,6 +22,8 @@ import org.junit.runner.RunWith; import org.opensearch.sample.resource.TestUtils; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; @@ -29,18 +33,18 @@ import static org.hamcrest.Matchers.equalTo; import static org.opensearch.sample.resource.TestUtils.FULL_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.NO_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.PatchSharingInfoPayloadBuilder; import static org.opensearch.sample.resource.TestUtils.RESOURCE_SHARING_INDEX; import static org.opensearch.sample.resource.TestUtils.SAMPLE_FULL_ACCESS_RESOURCE_AG; import static org.opensearch.sample.resource.TestUtils.SAMPLE_READ_ONLY_RESOURCE_AG; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_CREATE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_REVOKE_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.newCluster; -import static org.opensearch.sample.resource.TestUtils.revokeAccessPayload; -import static org.opensearch.sample.resource.TestUtils.shareWithPayload; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** @@ -121,22 +125,26 @@ public void testDryRunAccess() { assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of("cluster:admin/sample-resource-plugin/update"))); // cannot share resource - resp = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + adminResId + "?perform_permission_check=true", - shareWithPayload(FULL_ACCESS_USER.getName(), SAMPLE_READ_ONLY_RESOURCE_AG) + resp = client.putJson( + SECURITY_SHARE_ENDPOINT + "?perform_permission_check=true", + putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, Recipient.USERS, FULL_ACCESS_USER.getName()) ); resp.assertStatusCode(HttpStatus.SC_OK); assertThat(resp.bodyAsMap().get("accessAllowed"), equalTo(false)); - assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of("cluster:admin/sample-resource-plugin/share"))); + assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of("cluster:admin/security/resource/share"))); // cannot revoke resource access - resp = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + adminResId + "?perform_permission_check=true", - revokeAccessPayload(FULL_ACCESS_USER.getName(), SAMPLE_READ_ONLY_RESOURCE_AG) + PatchSharingInfoPayloadBuilder payloadBuilder = new PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(FULL_ACCESS_USER.getName()))), + SAMPLE_READ_ONLY_RESOURCE_AG ); + resp = client.patch(SECURITY_SHARE_ENDPOINT + "?perform_permission_check=true", payloadBuilder.build()); resp.assertStatusCode(HttpStatus.SC_OK); assertThat(resp.bodyAsMap().get("accessAllowed"), equalTo(false)); - assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of("cluster:admin/sample-resource-plugin/revoke"))); + assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of("cluster:admin/security/resource/share"))); // cannot delete resource resp = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + adminResId + "?perform_permission_check=true"); @@ -164,19 +172,23 @@ public void testDryRunAccess() { assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of())); // can share resource - resp = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + adminResId + "?perform_permission_check=true", - shareWithPayload(FULL_ACCESS_USER.getName(), SAMPLE_READ_ONLY_RESOURCE_AG) + resp = client.putJson( + SECURITY_SHARE_ENDPOINT + "?perform_permission_check=true", + putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, Recipient.USERS, FULL_ACCESS_USER.getName()) ); resp.assertStatusCode(HttpStatus.SC_OK); assertThat(resp.bodyAsMap().get("accessAllowed"), equalTo(true)); assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of())); // can revoke resource access - resp = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + adminResId + "?perform_permission_check=true", - revokeAccessPayload(FULL_ACCESS_USER.getName(), SAMPLE_READ_ONLY_RESOURCE_AG) + PatchSharingInfoPayloadBuilder payloadBuilder = new PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(FULL_ACCESS_USER.getName()))), + SAMPLE_READ_ONLY_RESOURCE_AG ); + resp = client.patch(SECURITY_SHARE_ENDPOINT + "?perform_permission_check=true", payloadBuilder.build()); resp.assertStatusCode(HttpStatus.SC_OK); assertThat(resp.bodyAsMap().get("accessAllowed"), equalTo(true)); assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of())); diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/AdminCertificateAccessTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/AdminCertificateAccessTests.java index 66e4ba4758..d5556ff9e0 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/AdminCertificateAccessTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/AdminCertificateAccessTests.java @@ -8,6 +8,9 @@ package org.opensearch.sample.resource.feature.enabled.multi_share; +import java.util.Map; +import java.util.Set; + import com.carrotsearch.randomizedtesting.RandomizedRunner; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; @@ -16,6 +19,8 @@ import org.junit.runner.RunWith; import org.opensearch.sample.resource.TestUtils; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; @@ -23,15 +28,15 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.opensearch.sample.resource.TestUtils.NO_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.PatchSharingInfoPayloadBuilder; import static org.opensearch.sample.resource.TestUtils.SAMPLE_FULL_ACCESS_RESOURCE_AG; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_REVOKE_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.newCluster; -import static org.opensearch.sample.resource.TestUtils.revokeAccessPayload; -import static org.opensearch.sample.resource.TestUtils.shareWithPayload; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** @@ -66,17 +71,18 @@ public void adminCertificate_canCRUD() { // can share and revoke admin's resource try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - HttpResponse response = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, - shareWithPayload(NO_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + HttpResponse response = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(resourceId, RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, Recipient.USERS, NO_ACCESS_USER.getName()) ); response.assertStatusCode(HttpStatus.SC_OK); - response = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, - revokeAccessPayload(NO_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) - ); + PatchSharingInfoPayloadBuilder patchBuilder = new PatchSharingInfoPayloadBuilder(); + patchBuilder.resourceType(RESOURCE_TYPE); + patchBuilder.resourceId(resourceId); + patchBuilder.revoke(new Recipients(Map.of(Recipient.USERS, Set.of(NO_ACCESS_USER.getName()))), SAMPLE_FULL_ACCESS_RESOURCE_AG); + response = client.patch(SECURITY_SHARE_ENDPOINT, patchBuilder.build()); response.assertStatusCode(HttpStatus.SC_OK); } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/MixedAccessTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/MixedAccessTests.java index 6655b7577c..5f10cd61a1 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/MixedAccessTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/MixedAccessTests.java @@ -26,8 +26,9 @@ import static org.opensearch.sample.resource.TestUtils.LIMITED_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.SAMPLE_FULL_ACCESS_RESOURCE_AG; import static org.opensearch.sample.resource.TestUtils.SAMPLE_READ_ONLY_RESOURCE_AG; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SHARE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.newCluster; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** @@ -172,6 +173,8 @@ public void initialShare_multipleLevels() { String shareWithPayload = """ { + "resource_id": "%s", + "resource_type": "%s", "share_with": { "%s" : { "users": ["%s"] @@ -182,6 +185,8 @@ public void initialShare_multipleLevels() { } } """.formatted( + resourceId, + RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, LIMITED_ACCESS_USER.getName(), SAMPLE_READ_ONLY_RESOURCE_AG, @@ -189,7 +194,7 @@ public void initialShare_multipleLevels() { ); try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - TestRestClient.HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload); + TestRestClient.HttpResponse response = client.putJson(SECURITY_SHARE_ENDPOINT, shareWithPayload); response.assertStatusCode(HttpStatus.SC_OK); // wait for one of the users to be populated api.awaitSharingEntry(resourceId, FULL_ACCESS_USER.getName()); diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/AccessibleResourcesApiTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/AccessibleResourcesApiTests.java index 0d9ed56c4b..055f1abe37 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/AccessibleResourcesApiTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/AccessibleResourcesApiTests.java @@ -98,7 +98,7 @@ private void assertListApiWithUser(TestSecurityConfig.User user) { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { TestRestClient.HttpResponse response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, user.getName()) + putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, Recipient.USERS, user.getName()) ); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString(user.getName())); diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ShareApiTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ShareApiTests.java index b88983b30a..37d9b4c0a9 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ShareApiTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ShareApiTests.java @@ -103,14 +103,14 @@ public void testGibberishPayload() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { TestRestClient.HttpResponse response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload("some-id", RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, NO_ACCESS_USER.getName()) + putSharingInfoPayload("some-id", RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, Recipient.USERS, NO_ACCESS_USER.getName()) ); response.assertStatusCode(HttpStatus.SC_FORBIDDEN); // since resource-index exists but resource-id doesn't, but user // shouldn't know that response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload(adminResId, "some_type", SAMPLE_READ_ONLY_RESOURCE_AG, NO_ACCESS_USER.getName()) + putSharingInfoPayload(adminResId, "some_type", SAMPLE_READ_ONLY_RESOURCE_AG, Recipient.USERS, NO_ACCESS_USER.getName()) ); response.assertStatusCode(HttpStatus.SC_BAD_REQUEST); // since type doesn't exist, so does the corresponding index } @@ -146,7 +146,13 @@ public void testPutSharingInfo() { try (TestRestClient client = cluster.getRestClient(LIMITED_ACCESS_USER)) { TestRestClient.HttpResponse response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, NO_ACCESS_USER.getName()) + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_READ_ONLY_RESOURCE_AG, + Recipient.USERS, + NO_ACCESS_USER.getName() + ) ); response.assertStatusCode(HttpStatus.SC_FORBIDDEN); } @@ -155,7 +161,13 @@ public void testPutSharingInfo() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { TestRestClient.HttpResponse response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, LIMITED_ACCESS_USER.getName()) + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + LIMITED_ACCESS_USER.getName() + ) ); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString(LIMITED_ACCESS_USER.getName())); @@ -166,7 +178,13 @@ public void testPutSharingInfo() { try (TestRestClient client = cluster.getRestClient(LIMITED_ACCESS_USER)) { TestRestClient.HttpResponse response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, NO_ACCESS_USER.getName()) + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_READ_ONLY_RESOURCE_AG, + Recipient.USERS, + NO_ACCESS_USER.getName() + ) ); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString(NO_ACCESS_USER.getName())); @@ -187,7 +205,13 @@ public void testGetSharingInfo() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { TestRestClient.HttpResponse response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, FULL_ACCESS_USER.getName()) + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + FULL_ACCESS_USER.getName() + ) ); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString(FULL_ACCESS_USER.getName())); 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 7a457485fc..e1b63cadd1 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,18 +43,12 @@ 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.search.SearchResourceAction; import org.opensearch.sample.resource.actions.rest.search.SearchResourceRestAction; -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.SearchResourceTransportAction; -import org.opensearch.sample.resource.actions.transport.ShareResourceTransportAction; import org.opensearch.sample.resource.actions.transport.UpdateResourceTransportAction; import org.opensearch.sample.secure.actions.rest.create.SecurePluginAction; import org.opensearch.sample.secure.actions.rest.create.SecurePluginRestAction; @@ -112,9 +106,6 @@ public List getRestHandlers( handlers.add(new DeleteResourceRestAction()); handlers.add(new SearchResourceRestAction()); - handlers.add(new ShareResourceRestAction()); - handlers.add(new RevokeResourceAccessRestAction()); - handlers.add(new SecurePluginRestAction()); return handlers; } @@ -127,8 +118,6 @@ public List getRestHandlers( actions.add(new ActionHandler<>(UpdateResourceAction.INSTANCE, UpdateResourceTransportAction.class)); actions.add(new ActionHandler<>(DeleteResourceAction.INSTANCE, DeleteResourceTransportAction.class)); actions.add(new ActionHandler<>(SearchResourceAction.INSTANCE, SearchResourceTransportAction.class)); - actions.add(new ActionHandler<>(ShareResourceAction.INSTANCE, ShareResourceTransportAction.class)); - actions.add(new ActionHandler<>(RevokeResourceAccessAction.INSTANCE, RevokeResourceAccessTransportAction.class)); actions.add(new ActionHandler<>(SecurePluginAction.INSTANCE, SecurePluginTransportAction.class)); return actions; } 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 deleted file mode 100644 index 9231683499..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessAction.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.resource.actions.rest.revoke; - -import org.opensearch.action.ActionType; - -/** - * Action to revoke a sample resource - */ -public class RevokeResourceAccessAction extends ActionType { - /** - * Revoke sample resource action instance - */ - public static final RevokeResourceAccessAction INSTANCE = new RevokeResourceAccessAction(); - /** - * Revoke 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 deleted file mode 100644 index f47f9b1c63..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRequest.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.sample.resource.actions.rest.revoke; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.action.DocRequest; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.security.spi.resources.sharing.ShareWith; - -import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; -import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; - -/** - * Request object for revoking access to a sample resource - */ -public class RevokeResourceAccessRequest extends ActionRequest implements DocRequest { - - String resourceId; - ShareWith revokeAccess; - - public RevokeResourceAccessRequest(String resourceId, ShareWith entitiesToRevoke) { - this.resourceId = resourceId; - this.revokeAccess = entitiesToRevoke; - } - - public RevokeResourceAccessRequest(StreamInput in) throws IOException { - resourceId = in.readString(); - revokeAccess = new ShareWith(in); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeString(resourceId); - revokeAccess.writeTo(out); - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - public String getResourceId() { - return resourceId; - } - - public ShareWith getEntitiesToRevoke() { - return revokeAccess; - } - - @Override - public String type() { - return RESOURCE_TYPE; - } - - @Override - public String index() { - return RESOURCE_INDEX_NAME; - } - - @Override - public String id() { - return resourceId; - } -} 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 deleted file mode 100644 index 2a1bf47e6f..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessResponse.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.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; - -/** - * Response for the RevokeResourceAccessAction - */ -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 deleted file mode 100644 index f8b2000753..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java +++ /dev/null @@ -1,91 +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.resource.actions.rest.revoke; - -import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -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.security.spi.resources.sharing.Recipient; -import org.opensearch.security.spi.resources.sharing.Recipients; -import org.opensearch.security.spi.resources.sharing.ShareWith; -import org.opensearch.transport.client.node.NodeClient; - -import static java.util.Collections.singletonList; -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() {} - - @Override - public List routes() { - return singletonList(new Route(POST, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/revoke/{resource_id}")); - } - - @Override - public String getName() { - return "revoke_sample_resource"; - } - - @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(); - } - - Map revokeEntities = (Map) source.get("entities_to_revoke"); - - Map revokeRecipients = new HashMap<>(); - if (revokeEntities != null) { - Map> recipients; - for (Map.Entry entry : revokeEntities.entrySet()) { - String accessLevel = entry.getKey(); - Map recs = (Map) entry.getValue(); - recipients = new HashMap<>(); - for (Map.Entry rec : recs.entrySet()) { - Recipient recipient = Recipient.valueOf(rec.getKey().toUpperCase(Locale.ROOT)); - Set targets = new HashSet<>((Collection) rec.getValue()); - recipients.put(recipient, targets); - } - revokeRecipients.put(accessLevel, new Recipients(recipients)); - } - } - - final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest( - resourceId, - new ShareWith(revokeRecipients) - ); - return channel -> client.executeLocally( - RevokeResourceAccessAction.INSTANCE, - revokeResourceAccessRequest, - 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 deleted file mode 100644 index 52de757b1b..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceAction.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.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/share"; - - 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 deleted file mode 100644 index 9ad7efd6b8..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRequest.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.resource.actions.rest.share; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.action.DocRequest; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.security.spi.resources.sharing.ShareWith; - -import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; -import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; - -/** - * Request object for sharing sample resource transport action - */ -public class ShareResourceRequest extends ActionRequest implements DocRequest { - - private final String resourceId; - - private final ShareWith shareWithRecipients; - - public ShareResourceRequest(String resourceId, ShareWith shareWithRecipients) { - this.resourceId = resourceId; - this.shareWithRecipients = shareWithRecipients; - } - - public ShareResourceRequest(StreamInput in) throws IOException { - this.resourceId = in.readString(); - this.shareWithRecipients = new ShareWith(in); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeString(this.resourceId); - shareWithRecipients.writeTo(out); - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - public String getResourceId() { - return this.resourceId; - } - - public ShareWith getShareWith() { - return shareWithRecipients; - } - - @Override - public String type() { - return RESOURCE_TYPE; - } - - @Override - public String index() { - return RESOURCE_INDEX_NAME; - } - - @Override - public String id() { - return resourceId; - } -} 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 deleted file mode 100644 index e8df82b841..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceResponse.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.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; - -/** - * Response object for ShareResourceAction - */ -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 deleted file mode 100644 index ceb7efc29e..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java +++ /dev/null @@ -1,86 +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.resource.actions.rest.share; - -import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -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.security.spi.resources.sharing.Recipient; -import org.opensearch.security.spi.resources.sharing.Recipients; -import org.opensearch.security.spi.resources.sharing.ShareWith; -import org.opensearch.transport.client.node.NodeClient; - -import static java.util.Collections.singletonList; -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() {} - - @Override - public List routes() { - return singletonList(new Route(POST, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/share/{resource_id}")); - } - - @Override - public String getName() { - return "share_sample_resource"; - } - - // NOTE: Do NOT use @SuppressWarnings("unchecked") on untrusted data in production code. This is used here only to keep the code simple - @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(); - } - - Map shareWith = (Map) source.get("share_with"); - - Map shareWithRecipients = new HashMap<>(); - if (shareWith != null) { - Map> recipients; - for (Map.Entry entry : shareWith.entrySet()) { - String accessLevel = entry.getKey(); - Map recs = (Map) entry.getValue(); - recipients = new HashMap<>(); - for (Map.Entry rec : recs.entrySet()) { - Recipient recipient = Recipient.valueOf(rec.getKey().toUpperCase(Locale.ROOT)); - Set targets = new HashSet<>((Collection) rec.getValue()); - recipients.put(recipient, targets); - } - shareWithRecipients.put(accessLevel, new Recipients(recipients)); - } - } - - final ShareResourceRequest shareResourceRequest = new ShareResourceRequest(resourceId, new ShareWith(shareWithRecipients)); - 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/RevokeResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java deleted file mode 100644 index af6d0fc9b5..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java +++ /dev/null @@ -1,64 +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.resource.actions.transport; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.OpenSearchStatusException; -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.core.rest.RestStatus; -import org.opensearch.sample.client.ResourceSharingClientAccessor; -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.security.spi.resources.client.ResourceSharingClient; -import org.opensearch.security.spi.resources.sharing.ShareWith; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -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); - - private final ResourceSharingClient resourceSharingClient; - - @Inject - public RevokeResourceAccessTransportAction(TransportService transportService, ActionFilters actionFilters) { - super(RevokeResourceAccessAction.NAME, transportService, actionFilters, RevokeResourceAccessRequest::new); - this.resourceSharingClient = ResourceSharingClientAccessor.getInstance().getResourceSharingClient(); - } - - @Override - protected void doExecute(Task task, RevokeResourceAccessRequest request, ActionListener listener) { - if (resourceSharingClient == null) { - listener.onFailure( - new OpenSearchStatusException( - "Resource sharing is not enabled. Cannot revoke access to resource " + request.getResourceId(), - RestStatus.NOT_IMPLEMENTED - ) - ); - return; - } - ShareWith target = request.getEntitiesToRevoke(); - resourceSharingClient.revoke(request.getResourceId(), RESOURCE_INDEX_NAME, target, ActionListener.wrap(success -> { - RevokeResourceAccessResponse response = new RevokeResourceAccessResponse(success.getShareWith()); - log.debug("Revoked resource access: {}", response.toString()); - 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 deleted file mode 100644 index 35482821bd..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.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.resource.actions.transport; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.OpenSearchStatusException; -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.core.rest.RestStatus; -import org.opensearch.sample.client.ResourceSharingClientAccessor; -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.security.spi.resources.client.ResourceSharingClient; -import org.opensearch.security.spi.resources.sharing.ShareWith; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -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); - private final ResourceSharingClient resourceSharingClient; - - @Inject - public ShareResourceTransportAction(TransportService transportService, ActionFilters actionFilters) { - super(ShareResourceAction.NAME, transportService, actionFilters, ShareResourceRequest::new); - this.resourceSharingClient = ResourceSharingClientAccessor.getInstance().getResourceSharingClient(); - } - - @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; - } - - if (resourceSharingClient == null) { - listener.onFailure( - new OpenSearchStatusException( - "Resource sharing is not enabled. Cannot share resource " + request.getResourceId(), - RestStatus.NOT_IMPLEMENTED - ) - ); - return; - } - ShareWith shareWith = request.getShareWith(); - resourceSharingClient.share(request.getResourceId(), RESOURCE_INDEX_NAME, shareWith, ActionListener.wrap(sharing -> { - ShareWith finalShareWith = sharing == null ? null : sharing.getShareWith(); - ShareResourceResponse response = new ShareResourceResponse(finalShareWith); - log.debug("Shared resource: {}", response.toString()); - listener.onResponse(response); - }, listener::onFailure)); - } - -} From b333d1296b0db45beae688113d7208387de38505 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 8 Oct 2025 14:32:34 -0400 Subject: [PATCH 16/40] Registers extensions regardless of whether their resources are marked as protected and fixes share api action to throw 400 on resources not marked as protected Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 35 +++++++++----- .../configuration/DlsFlsValveImpl.java | 9 ++-- .../resources/ResourcePluginInfo.java | 46 ++++++++----------- .../resources/api/share/ShareRestAction.java | 17 ++++++- .../ResourceSharingFeatureFlagSetting.java | 2 + 5 files changed, 66 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 88bd4b4fce..3885d6d40d 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -712,7 +712,9 @@ public List getRestHandlers( ); // Resource sharing API to update sharing info - handlers.add(new ShareRestAction(resourcePluginInfo, resourceSharingEnabledSetting)); + handlers.add( + new ShareRestAction(resourcePluginInfo, resourceSharingEnabledSetting, resourceSharingProtectedResourceTypesSetting) + ); handlers.add(new ResourceTypesRestAction(resourcePluginInfo, resourceSharingEnabledSetting)); handlers.add(new AccessibleResourcesRestAction(resourceAccessHandler, resourcePluginInfo, resourceSharingEnabledSetting)); @@ -1214,7 +1216,7 @@ public Collection createComponents( threadPool, dlsFlsBaseContext, adminDns, - resourcePluginInfo.getResourceIndices(), + resourcePluginInfo, resourceSharingEnabledSetting ); cr.subscribeOnChange(configMap -> { ((DlsFlsValveImpl) dlsFlsValve).updateConfiguration(cr.getConfiguration(CType.ROLES)); }); @@ -2305,6 +2307,20 @@ public void onNodeStarted(DiscoveryNode localNode) { cr.initOnNodeStart(); } + // resourceSharingIndexManagementRepository will be null when sec plugin is disabled or is in SSLOnly mode, hence it will not be + // instantiated + if (resourceSharingEnabledSetting.getDynamicSettingValue()) { + // create resource sharing index if absent + // TODO check if this should be wrapped in an atomic completable future + log.debug("Attempting to create Resource Sharing index"); + Set resourceIndices = new HashSet<>(); + if (resourcePluginInfo != null) { + resourceIndices = resourcePluginInfo.getResourceIndices(); + } + rsIndexHandler.createResourceSharingIndicesIfAbsent(resourceIndices); + + } + final Set securityModules = ReflectionHelper.getModulesLoaded(); log.info("{} OpenSearch Security modules loaded so far: {}", securityModules.size(), securityModules); } @@ -2437,17 +2453,12 @@ private void tryAddSecurityProvider() { // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions @Override public void loadExtensions(ExtensionLoader loader) { - if (settings != null) { - // discover & register extensions and their types - Set exts = new HashSet<>(loader.loadExtensions(ResourceSharingExtension.class)); - resourcePluginInfo.setResourceSharingExtensions( - exts, - settings.getAsList(ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES) - ); + // discover & register extensions and their types + Set exts = new HashSet<>(loader.loadExtensions(ResourceSharingExtension.class)); + resourcePluginInfo.setResourceSharingExtensions(exts); - // load action-groups in memory - ResourceActionGroupsHelper.loadActionGroupsConfig(resourcePluginInfo); - } + // load action-groups in memory + ResourceActionGroupsHelper.loadActionGroupsConfig(resourcePluginInfo); } // CS-ENFORCE-SINGLE diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 7cad2b2393..8b714e0146 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; @@ -79,6 +78,7 @@ 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.ResourcePluginInfo; import org.opensearch.security.resources.ResourceSharingDlsUtils; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; @@ -109,7 +109,7 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve { private final Settings settings; private final AdminDNs adminDNs; private final OpensearchDynamicSetting resourceSharingEnabledSetting; - private final WildcardMatcher resourceIndicesMatcher; + private final ResourcePluginInfo resourcePluginInfo; public DlsFlsValveImpl( Settings settings, @@ -120,7 +120,7 @@ public DlsFlsValveImpl( ThreadPool threadPool, DlsFlsBaseContext dlsFlsBaseContext, AdminDNs adminDNs, - Set resourceIndices, + ResourcePluginInfo resourcePluginInfo, OpensearchDynamicSetting resourceSharingEnabledSetting ) { super(); @@ -134,7 +134,7 @@ public DlsFlsValveImpl( this.dlsFlsBaseContext = dlsFlsBaseContext; this.settings = settings; this.adminDNs = adminDNs; - this.resourceIndicesMatcher = WildcardMatcher.from(resourceIndices); + this.resourcePluginInfo = resourcePluginInfo; clusterService.addListener(event -> { DlsFlsProcessedConfig config = dlsFlsProcessedConfig.get(); @@ -163,6 +163,7 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< ActionRequest request = context.getRequest(); if (HeaderHelper.isInternalOrPluginRequest(threadContext)) { IndexResolverReplacer.Resolved resolved = context.getResolvedRequest(); + WildcardMatcher resourceIndicesMatcher = WildcardMatcher.from(resourcePluginInfo.getResourceIndices()); if (resourceSharingEnabledSetting.getDynamicSettingValue() && request instanceof SearchRequest && resourceIndicesMatcher.matchAll(resolved.getAllIndices())) { diff --git a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index 9546c529a6..1b5d8797c2 100644 --- a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -52,35 +52,29 @@ public class ResourcePluginInfo { // AuthZ: resolved (flattened) groups per type private final Map typeToFlattened = new HashMap<>(); - public void setResourceSharingExtensions(Set extensions, List protectedTypes) { + public void setResourceSharingExtensions(Set extensions) { resourceSharingExtensions.clear(); typeToIndex.clear(); indexToType.clear(); - // only assign types if the list setting is non-empty - if (!protectedTypes.isEmpty()) { - // Enforce resource-type unique-ness - Set resourceTypes = new HashSet<>(); - for (ResourceSharingExtension extension : extensions) { - for (var rp : extension.getResourceProviders()) { - // exclude resource types not mentioned in the explicit list. defaults to no resource marked as protected resources - if (!protectedTypes.contains(rp.resourceType())) { - continue; - } - if (!resourceTypes.contains(rp.resourceType())) { - // add name seen so far to the resource-types set - resourceTypes.add(rp.resourceType()); - // also cache type->index and index->type mapping - typeToIndex.put(rp.resourceType(), rp.resourceIndexName()); - indexToType.put(rp.resourceIndexName(), rp.resourceType()); - } else { - throw new OpenSearchSecurityException( - String.format( - "Resource type [%s] is already registered. Please provide a different unique-name for the resource declared by %s.", - rp.resourceType(), - extension.getClass().getName() - ) - ); - } + + // Enforce resource-type unique-ness + Set resourceTypes = new HashSet<>(); + for (ResourceSharingExtension extension : extensions) { + for (var rp : extension.getResourceProviders()) { + if (!resourceTypes.contains(rp.resourceType())) { + // add name seen so far to the resource-types set + resourceTypes.add(rp.resourceType()); + // also cache type->index and index->type mapping + typeToIndex.put(rp.resourceType(), rp.resourceIndexName()); + indexToType.put(rp.resourceIndexName(), rp.resourceType()); + } else { + throw new OpenSearchSecurityException( + String.format( + "Resource type [%s] is already registered. Please provide a different unique-name for the resource declared by %s.", + rp.resourceType(), + extension.getClass().getName() + ) + ); } } } diff --git a/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java b/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java index 0c1f98a1b4..16a37afd6f 100644 --- a/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java +++ b/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java @@ -40,10 +40,16 @@ public class ShareRestAction extends BaseRestHandler { private final ResourcePluginInfo resourcePluginInfo; private final OpensearchDynamicSetting resourceSharingEnabledSetting; + private final OpensearchDynamicSetting> resourceSharingProtectedTypesSetting; - public ShareRestAction(ResourcePluginInfo resourcePluginInfo, OpensearchDynamicSetting resourceSharingEnabledSetting) { + public ShareRestAction( + ResourcePluginInfo resourcePluginInfo, + OpensearchDynamicSetting resourceSharingEnabledSetting, + OpensearchDynamicSetting> resourceSharingProtectedTypesSetting + ) { this.resourcePluginInfo = resourcePluginInfo; this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; + this.resourceSharingProtectedTypesSetting = resourceSharingProtectedTypesSetting; } @Override @@ -64,6 +70,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli if (!resourceSharingEnabledSetting.getDynamicSettingValue()) { return channel -> { channel.sendResponse(new BytesRestResponse(RestStatus.NOT_IMPLEMENTED, "Feature disabled.")); }; } + // These two params will only be present with GET request String resourceId = request.param("resource_id"); String resourceType = request.param("resource_type"); @@ -87,6 +94,14 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli ShareRequest shareRequest = builder.build(); + if (shareRequest.type() != null && !resourceSharingProtectedTypesSetting.getDynamicSettingValue().contains(shareRequest.type())) { + return channel -> { + channel.sendResponse( + new BytesRestResponse(RestStatus.BAD_REQUEST, "Resource type " + resourceType + " is not marked as protected.") + ); + }; + } + return channel -> { client.executeLocally(ShareAction.INSTANCE, shareRequest, new RestToXContentListener<>(channel)); }; } } diff --git a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java index cbb64e1b4d..786fed8e40 100644 --- a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java +++ b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java @@ -19,6 +19,7 @@ import org.opensearch.security.spi.resources.client.ResourceSharingClient; import org.opensearch.security.support.ConfigConstants; +// CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions public class ResourceSharingFeatureFlagSetting extends OpensearchDynamicSetting { private static final Logger logger = LogManager.getLogger(ResourceSharingFeatureFlagSetting.class); @@ -59,3 +60,4 @@ protected String getClusterChangeMessage(final Boolean isEnabled) { ); } } +// CS-ENFORCE-SINGLE From ea333b988afda4f2d9bd1110d7970064456b5f1a Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 8 Oct 2025 14:33:36 -0400 Subject: [PATCH 17/40] Adds tests for the new dynamic settings and update existing tests to consume new changes Signed-off-by: Darshit Chanpura --- .../opensearch/sample/resource/TestUtils.java | 14 +- .../feature/FeatureFlagSettingTests.java | 437 +++++++++++++++++ .../feature/ProtectedTypesSettingTests.java | 446 ++++++++++++++++++ .../enabled/ExcludedResourceTypeTests.java | 6 +- .../transport/GetResourceTransportAction.java | 22 +- 5 files changed, 897 insertions(+), 28 deletions(-) create mode 100644 sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/FeatureFlagSettingTests.java create mode 100644 sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/ProtectedTypesSettingTests.java diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java index 8c2fbfa25a..919deec80e 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java @@ -304,7 +304,6 @@ public void wipeOutResourceEntries() { String jsonBody = "{ \"query\": { \"match_all\": {} } }"; TestRestClient.HttpResponse resp = client.postJson(endpoint, jsonBody); resp.assertStatusCode(HttpStatus.SC_OK); - } } @@ -545,24 +544,13 @@ public void assertApiRevoke( TestSecurityConfig.User target, String accessLevel, int status - ) { - assertRevoke(SECURITY_SHARE_ENDPOINT, resourceId, user, target, accessLevel, status); - } - - private void assertRevoke( - String endpoint, - String resourceId, - TestSecurityConfig.User user, - TestSecurityConfig.User target, - String accessLevel, - int status ) { PatchSharingInfoPayloadBuilder patchBuilder = new PatchSharingInfoPayloadBuilder(); patchBuilder.resourceType(RESOURCE_TYPE); patchBuilder.resourceId(resourceId); patchBuilder.revoke(new Recipients(Map.of(Recipient.USERS, Set.of(target.getName()))), accessLevel); try (TestRestClient client = cluster.getRestClient(user)) { - TestRestClient.HttpResponse response = client.patch(endpoint, patchBuilder.build()); + TestRestClient.HttpResponse response = client.patch(TestUtils.SECURITY_SHARE_ENDPOINT, patchBuilder.build()); response.assertStatusCode(status); } } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/FeatureFlagSettingTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/FeatureFlagSettingTests.java new file mode 100644 index 0000000000..a355cb7742 --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/FeatureFlagSettingTests.java @@ -0,0 +1,437 @@ +/* + * SPDX-License-Identifier: Apache-2.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.feature; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import com.carrotsearch.randomizedtesting.RandomizedRunner; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.awaitility.Awaitility; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.sample.resource.TestUtils; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.TestSecurityConfig; +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.resource.TestUtils.ApiHelper.searchAllPayload; +import static org.opensearch.sample.resource.TestUtils.ApiHelper.searchByNamePayload; +import static org.opensearch.sample.resource.TestUtils.FULL_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.LIMITED_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.NO_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.RESOURCE_SHARING_MIGRATION_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_FULL_ACCESS_RESOURCE_AG; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_READ_ONLY_RESOURCE_AG; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_CREATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SEARCH_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.migrationPayload_valid; +import static org.opensearch.sample.resource.TestUtils.newCluster; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; +import static org.awaitility.Awaitility.await; + +/** + * Verifies dynamic behavior of cluster setting: + * {@link ConfigConstants#OPENSEARCH_RESOURCE_SHARING_ENABLED} + * + * Phase 1: feature disabled + * Phase 2: flip setting via _cluster/settings and verify enabled behavior + */ +@RunWith(RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class FeatureFlagSettingTests { + + @ClassRule + public static LocalCluster cluster = newCluster(false, true); + + private final TestUtils.ApiHelper api = new TestUtils.ApiHelper(cluster); + private String adminResId; + + // --------- Lifecycle --------- + + @Before + public void setup() { + // Starting with disabled for deterministic ordering + setResourceSharingEnabled(false); + awaitResourceSharingEnabled(false); + + adminResId = createSampleResourceAs(USER_ADMIN); + } + + @After + public void cleanup() { + api.wipeOutResourceEntries(); + // Flip the dynamic cluster setting to false + setResourceSharingEnabled(false); + awaitResourceSharingEnabled(false); + } + + private String createSampleResourceAs(TestSecurityConfig.User user) { + try (TestRestClient client = cluster.getRestClient(user)) { + String sampleResource = """ + { + "name":"sample", + "store_user": true + } + """; + + TestRestClient.HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + + String resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + + Awaitility.await() + .alias("Wait until resource data is populated") + .until(() -> client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId).getStatusCode(), equalTo(200)); + return resourceId; + } + } + + // --------- Phase 1: Disabled behavior --------- + + @Test + public void testBehaviorWhenDisabled() { + // “Disabled” expectations: + // - Share api handler not exist -> 501 on endpoints that would be implemented by the feature + // - Access relies purely on existing index/cluster perms ("legacy" RBAC behavior) + + assertNoAccessUser_Disabled(); + assertLimitedUser_Disabled(); + assertFullUser_Disabled(); + assertAdminCert_Disabled(); + } + + // --------- Assertions: Disabled behavior --------- + + private void assertNoAccessUser_Disabled() { + // cannot create + try (TestRestClient c = cluster.getRestClient(NO_ACCESS_USER)) { + TestRestClient.HttpResponse r = c.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, "{\"name\":\"sampleUser\"}"); + r.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + // cannot get, update, delete + api.assertApiGet(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiUpdate(adminResId, NO_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + // share/revoke endpoints exist? -> when disabled we expect 403 (no perm) or 501 (no handler). + api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + + // search forbidden + api.assertApiGetSearchForbidden(NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchAllPayload(), NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchByNamePayload("sample"), NO_ACCESS_USER); + } + + private void assertLimitedUser_Disabled() { + // create own + String userResId = createSampleResourceAs(LIMITED_ACCESS_USER); + + // see admin resource (disabled path follows index perms) per your “disabled” expectations + api.assertApiGet(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); + api.assertApiGetAll(LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); + + // cannot update (no perm); cannot delete + api.assertApiUpdate(adminResId, LIMITED_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiUpdate(userResId, LIMITED_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + + // share/revoke should be NOT IMPLEMENTED when disabled (your “last 4 tests”) + api.assertApiShare( + adminResId, + LIMITED_ACCESS_USER, + LIMITED_ACCESS_USER, + SAMPLE_READ_ONLY_RESOURCE_AG, + HttpStatus.SC_NOT_IMPLEMENTED + ); + api.assertApiRevoke(adminResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + + // can search both resources + api.assertApiGetSearch(LIMITED_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); + api.assertApiPostSearch(searchAllPayload(), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); + api.assertApiPostSearch(searchByNamePayload("sample"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); + } + + private void assertFullUser_Disabled() { + String userResId = createSampleResourceAs(FULL_ACCESS_USER); + + // full * perms when disabled -> can see & update both + api.assertApiGet(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); + api.assertApiGetAll(FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); + api.assertApiUpdate(adminResId, FULL_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_OK); + api.assertApiUpdate(userResId, FULL_ACCESS_USER, "sampleUpdateUser", HttpStatus.SC_OK); + + // share/revoke not implemented + api.assertApiShare(adminResId, FULL_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + api.assertApiRevoke(adminResId, FULL_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + + // search sees both + api.assertApiGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 3, "sampleUpdateAdmin"); // admin + full user + limited user created + // above + api.assertApiPostSearch(searchAllPayload(), FULL_ACCESS_USER, HttpStatus.SC_OK, 3, "sampleUpdateAdmin"); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateAdmin"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateAdmin"); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // can delete both + api.assertApiDelete(userResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + api.assertApiDelete(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + } + + private void assertAdminCert_Disabled() { + adminResId = createSampleResourceAs(USER_ADMIN); + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // read / update ok + TestRestClient.HttpResponse resp = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + + resp = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + adminResId, "{\"name\":\"sampleUpdated\"}"); + resp.assertStatusCode(HttpStatus.SC_OK); + + // share/revoke not implemented + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + FULL_ACCESS_USER.getName() + ) + ); + resp.assertStatusCode(HttpStatus.SC_NOT_IMPLEMENTED); + + TestUtils.PatchSharingInfoPayloadBuilder payloadBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(FULL_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG + ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, payloadBuilder.build()); + resp.assertStatusCode(HttpStatus.SC_NOT_IMPLEMENTED); + + // search works + resp = client.get(SAMPLE_RESOURCE_SEARCH_ENDPOINT); + resp.assertStatusCode(HttpStatus.SC_OK); + + // delete ok + resp = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } + + // --------- Phase 2: Flip to Enabled then verify enabled behavior --------- + + @Test + public void testBehaviorAfterEnabling() { + // Flip the dynamic cluster setting to true + setResourceSharingEnabled(true); + awaitResourceSharingEnabled(true); + + // migrate existing resources to new records + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse migrateResponse = client.postJson(RESOURCE_SHARING_MIGRATION_ENDPOINT, migrationPayload_valid()); + migrateResponse.assertStatusCode(HttpStatus.SC_OK); + assertThat(migrateResponse.bodyAsMap().get("summary"), equalTo("Migration complete. migrated 1; skippedNoUser 0; failed 0")); + } + + // “Enabled” expectations: + // - Share/Revoke handlers exist -> return permission-based 200/403 (not 501) + // - Search and read access follow resource-sharing rules + // - Owners can share/revoke; others constrained + + assertNoAccessUser_Enabled(); + assertLimitedUser_Enabled(); + assertFullUser_Enabled(); + assertAdminCert_Enabled(); + } + + // --------- Helpers: cluster setting flips & awaits --------- + private void setResourceSharingEnabled(boolean enabled) { + String body = String.format("{\"transient\":{\"%s\":%s}}", ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, enabled); + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse resp = client.putJson("_cluster/settings", body); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } + + /** + * Confirm the setting took effect + */ + private void awaitResourceSharingEnabled(boolean expected) { + // Wait for cluster setting to reflect desired value + await().atMost(30, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS).until(() -> readSettingEquals(expected)); + } + + private boolean readSettingEquals(boolean expected) { + try (var client = cluster.getRestClient(cluster.getAdminCertificate())) { + var r = client.get("_cluster/settings?include_defaults=true&flat_settings=true"); + r.assertStatusCode(200); + String key = "\"" + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED + "\":\"" + expected + "\""; + return r.getBody().contains(key); + } catch (Exception e) { + return false; + } + } + + // --------- Assertions: Enabled behavior --------- + + private void assertNoAccessUser_Enabled() { + // cannot create + try (TestRestClient c = cluster.getRestClient(NO_ACCESS_USER)) { + TestRestClient.HttpResponse r = c.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, "{\"name\":\"sampleUser\"}"); + r.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + // cannot get/update/delete + api.assertApiGet(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiUpdate(adminResId, NO_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + + // share/revoke forbidden (handlers now exist → 403 vs 501) + api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + + // search forbidden + api.assertApiGetSearchForbidden(NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchAllPayload(), NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchByNamePayload("sample"), NO_ACCESS_USER); + } + + private void assertLimitedUser_Enabled() { + String userResId = createSampleResourceAs(LIMITED_ACCESS_USER); + + // cannot see admin resource under sharing rules + api.assertApiGet(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiGetAll(LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); + + // can update own; not others + api.assertApiUpdate(adminResId, LIMITED_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_FORBIDDEN); + api.assertApiUpdate(userResId, LIMITED_ACCESS_USER, "sampleUpdateUser", HttpStatus.SC_OK); + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiGetSearch(LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // cannot share/revoke others, can share own + api.assertApiShare(adminResId, LIMITED_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiRevoke(adminResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + + api.assertApiGet(userResId, USER_ADMIN, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiShare(userResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, USER_ADMIN, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiRevoke(userResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, USER_ADMIN, HttpStatus.SC_FORBIDDEN, ""); + + // searches aligned with ownership + api.assertApiGetSearch(LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchAllPayload(), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchByNamePayload("sample"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 0, ""); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // can delete own + api.assertApiDelete(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK); + // cannot delete admin's + api.assertApiDelete(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + } + + private void assertFullUser_Enabled() { + String userResId = createSampleResourceAs(FULL_ACCESS_USER); + + // even with * perms, sharing rules restrict access to others’ resources + api.assertApiGet(adminResId, FULL_ACCESS_USER, HttpStatus.SC_FORBIDDEN, "sample"); + api.assertApiGetAll(FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); + + // can update own + api.assertApiUpdate(adminResId, FULL_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_FORBIDDEN); + api.assertApiUpdate(userResId, FULL_ACCESS_USER, "sampleUpdateUser", HttpStatus.SC_OK); + api.assertApiGet(userResId, FULL_ACCESS_USER, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // cannot share/revoke others’ resources; can share own + api.assertApiShare(adminResId, FULL_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiRevoke(adminResId, FULL_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiShare(userResId, FULL_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiRevoke(userResId, FULL_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + + // search visibility matches sharing state + api.assertApiGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchAllPayload(), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchByNamePayload("sample"), FULL_ACCESS_USER, HttpStatus.SC_OK, 0, ""); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // can delete own; cannot delete admin’s under sharing rules + api.assertApiDelete(userResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + api.assertApiDelete(adminResId, FULL_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + } + + private void assertAdminCert_Enabled() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // read/update + TestRestClient.HttpResponse resp = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + + resp = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + adminResId, "{\"name\":\"sampleUpdated\"}"); + resp.assertStatusCode(HttpStatus.SC_OK); + assertThat(resp.getBody(), containsString("sampleUpdated")); + + // share/revoke handlers exist → expect 200 for admin path + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, Recipient.USERS, NO_ACCESS_USER.getName()) + ); + resp.assertStatusCode(HttpStatus.SC_OK); + + TestUtils.PatchSharingInfoPayloadBuilder payloadBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(NO_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG + ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, payloadBuilder.build()); + resp.assertStatusCode(HttpStatus.SC_OK); + + // search works + resp = client.get(SAMPLE_RESOURCE_SEARCH_ENDPOINT); + resp.assertStatusCode(HttpStatus.SC_OK); + assertThat(resp.getBody(), containsString("sampleUpdated")); + + resp = client.postJson(SAMPLE_RESOURCE_SEARCH_ENDPOINT, searchAllPayload()); + resp.assertStatusCode(HttpStatus.SC_OK); + + resp = client.postJson(SAMPLE_RESOURCE_SEARCH_ENDPOINT, searchByNamePayload("sampleUpdated")); + resp.assertStatusCode(HttpStatus.SC_OK); + + // delete ok + resp = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } +} diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/ProtectedTypesSettingTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/ProtectedTypesSettingTests.java new file mode 100644 index 0000000000..df25522ead --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/ProtectedTypesSettingTests.java @@ -0,0 +1,446 @@ +/* + * SPDX-License-Identifier: Apache-2.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.feature; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import com.carrotsearch.randomizedtesting.RandomizedRunner; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.awaitility.Awaitility; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.sample.resource.TestUtils; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.TestSecurityConfig; +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.resource.TestUtils.ApiHelper.searchAllPayload; +import static org.opensearch.sample.resource.TestUtils.ApiHelper.searchByNamePayload; +import static org.opensearch.sample.resource.TestUtils.FULL_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.LIMITED_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.NO_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.RESOURCE_SHARING_MIGRATION_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_FULL_ACCESS_RESOURCE_AG; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_READ_ONLY_RESOURCE_AG; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_CREATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SEARCH_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.migrationPayload_valid; +import static org.opensearch.sample.resource.TestUtils.newCluster; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; +import static org.awaitility.Awaitility.await; + +/** + * Test the dynamic nature of feature settings: + * {@link ConfigConstants#OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES} + * + * Phase 1: empty list + * Phase 2: add sample resource as an entry + */ +@RunWith(RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class ProtectedTypesSettingTests { + + // do not include sample resource as protected resource, should behave as if feature was disabled for that resource + @ClassRule + public static LocalCluster cluster = newCluster(true, true, List.of()); + + private final TestUtils.ApiHelper api = new TestUtils.ApiHelper(cluster); + private String adminResId; + + // --------- Lifecycle --------- + + @Before + public void setup() { + // Starting with empty for deterministic ordering + removeSampleResourceAsProtectedType(); + awaitSetting(""); + + adminResId = createSampleResourceAs(USER_ADMIN); + } + + @After + public void cleanup() { + api.wipeOutResourceEntries(); + // Mark resource as not protected + removeSampleResourceAsProtectedType(); + awaitSetting(""); + } + + private String createSampleResourceAs(TestSecurityConfig.User user) { + try (TestRestClient client = cluster.getRestClient(user)) { + String sampleResource = """ + { + "name":"sample", + "store_user": true + } + """; + + TestRestClient.HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + + String resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + + Awaitility.await() + .alias("Wait until resource data is populated") + .until(() -> client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId).getStatusCode(), equalTo(200)); + return resourceId; + } + } + + // --------- Phase 1: Resource not protected --------- + + @Test + public void testResourceNotProtected() { + // Not marked as protected type; expectations: + // - Share api handler not exist -> 501 on endpoints that would be implemented by the feature + // - Access relies purely on existing index/cluster perms ("legacy" RBAC behavior) + + assertNoAccessUser_LegacyProtection(); + assertLimitedUser_LegacyProtection(); + assertFullUser_LegacyProtection(); + assertAdminCert_LegacyProtection(); + } + + // --------- Assertions: Disabled behavior --------- + + private void assertNoAccessUser_LegacyProtection() { + // cannot create + try (TestRestClient c = cluster.getRestClient(NO_ACCESS_USER)) { + TestRestClient.HttpResponse r = c.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, "{\"name\":\"sampleUser\"}"); + r.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + // cannot get, update, delete + api.assertApiGet(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiUpdate(adminResId, NO_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + // share/revoke endpoints exist? -> when not protected we expect 400 since no corresponding types exist in ResourcePluginInfo + api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_BAD_REQUEST); + api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_BAD_REQUEST); + + // search forbidden + api.assertApiGetSearchForbidden(NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchAllPayload(), NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchByNamePayload("sample"), NO_ACCESS_USER); + } + + private void assertLimitedUser_LegacyProtection() { + // create own + String userResId = createSampleResourceAs(LIMITED_ACCESS_USER); + + // see admin resource (disabled path follows index perms) per your “disabled” expectations + api.assertApiGet(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); + api.assertApiGetAll(LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); + + // cannot update (no perm); cannot delete + api.assertApiUpdate(adminResId, LIMITED_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiUpdate(userResId, LIMITED_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + + // share/revoke endpoints exist? -> when not protected we expect 400 since no corresponding types exist in ResourcePluginInfo + api.assertApiShare(adminResId, LIMITED_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_BAD_REQUEST); + api.assertApiRevoke(adminResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_BAD_REQUEST); + + // can search both resources + api.assertApiGetSearch(LIMITED_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); + api.assertApiPostSearch(searchAllPayload(), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); + api.assertApiPostSearch(searchByNamePayload("sample"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); + } + + private void assertFullUser_LegacyProtection() { + String userResId = createSampleResourceAs(FULL_ACCESS_USER); + + // full * perms when disabled -> can see & update both + api.assertApiGet(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); + api.assertApiGetAll(FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); + api.assertApiUpdate(adminResId, FULL_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_OK); + api.assertApiUpdate(userResId, FULL_ACCESS_USER, "sampleUpdateUser", HttpStatus.SC_OK); + + // share/revoke endpoints exist? -> when not protected we expect 400 since no corresponding types exist in ResourcePluginInfo + api.assertApiShare(adminResId, FULL_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_BAD_REQUEST); + api.assertApiRevoke(adminResId, FULL_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_BAD_REQUEST); + + // search sees both + api.assertApiGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 3, "sampleUpdateAdmin"); // admin + full user + limited user created + // above + api.assertApiPostSearch(searchAllPayload(), FULL_ACCESS_USER, HttpStatus.SC_OK, 3, "sampleUpdateAdmin"); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateAdmin"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateAdmin"); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // can delete both + api.assertApiDelete(userResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + api.assertApiDelete(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + } + + private void assertAdminCert_LegacyProtection() { + adminResId = createSampleResourceAs(USER_ADMIN); + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // read / update ok + TestRestClient.HttpResponse resp = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + + resp = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + adminResId, "{\"name\":\"sampleUpdated\"}"); + resp.assertStatusCode(HttpStatus.SC_OK); + + // share/revoke endpoints exist? -> when not protected we expect 400 since no corresponding types exist in ResourcePluginInfo + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + FULL_ACCESS_USER.getName() + ) + ); + resp.assertStatusCode(HttpStatus.SC_BAD_REQUEST); + + TestUtils.PatchSharingInfoPayloadBuilder payloadBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(FULL_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG + ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, payloadBuilder.build()); + resp.assertStatusCode(HttpStatus.SC_BAD_REQUEST); + + // search works + resp = client.get(SAMPLE_RESOURCE_SEARCH_ENDPOINT); + resp.assertStatusCode(HttpStatus.SC_OK); + + // delete ok + resp = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } + + // --------- Phase 2: Mark resource as protected --------- + + @Test + public void testResourceProtected() { + // Mark sample resource as protected + addSampleResourceAsProtectedType(); + awaitSetting(RESOURCE_TYPE); + + // migrate existing resources to new records + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse migrateResponse = client.postJson(RESOURCE_SHARING_MIGRATION_ENDPOINT, migrationPayload_valid()); + migrateResponse.assertStatusCode(HttpStatus.SC_OK); + assertThat(migrateResponse.bodyAsMap().get("summary"), equalTo("Migration complete. migrated 1; skippedNoUser 0; failed 0")); + } + + // Marked as protected type; expectations: + // - Share/Revoke handlers -> return permission-based 200/403 (not 400) + // - Search and read access follow resource-sharing rules + // - Owners can share/revoke; others constrained + + assertNoAccessUser_ResourceProtection(); + assertLimitedUser_ResourceProtection(); + assertFullUser_ResourceProtection(); + assertAdminCert_ResourceProtection(); + } + + // --------- Helpers: cluster setting flips & awaits --------- + private void addSampleResourceAsProtectedType() { + String body = String.format( + "{\"transient\":{\"%s\":[\"%s\"]}}", + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES, + RESOURCE_TYPE + ); + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse resp = client.putJson("_cluster/settings", body); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } + + private void removeSampleResourceAsProtectedType() { + String body = String.format("{\"transient\":{\"%s\":[]}}", ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES); + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse resp = client.putJson("_cluster/settings", body); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } + + /** + * Confirm the setting took effect + */ + private void awaitSetting(String expected) { + // Wait for cluster setting to reflect desired value + await().atMost(30, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS).until(() -> readSettingEquals(expected)); + } + + private boolean readSettingEquals(String expected) { + try (var client = cluster.getRestClient(cluster.getAdminCertificate())) { + var r = client.get("_cluster/settings?include_defaults=true&flat_settings=true"); + r.assertStatusCode(200); + String expectedValue = expected.isEmpty() ? "[]" : ("[\"" + expected + "\"]"); + String key = "\"" + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES + "\":" + expectedValue; + return r.getBody().contains(key); + } catch (Exception e) { + return false; + } + } + + // --------- Assertions: Enabled behavior --------- + + private void assertNoAccessUser_ResourceProtection() { + // cannot create + try (TestRestClient c = cluster.getRestClient(NO_ACCESS_USER)) { + TestRestClient.HttpResponse r = c.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, "{\"name\":\"sampleUser\"}"); + r.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + // cannot get/update/delete + api.assertApiGet(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiUpdate(adminResId, NO_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + + // share/revoke forbidden (handlers now exist → 403 vs 501) + api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + + // search forbidden + api.assertApiGetSearchForbidden(NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchAllPayload(), NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchByNamePayload("sample"), NO_ACCESS_USER); + } + + private void assertLimitedUser_ResourceProtection() { + String userResId = createSampleResourceAs(LIMITED_ACCESS_USER); + + // cannot see admin resource under sharing rules + api.assertApiGet(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiGetAll(LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); + + // can update own; not others + api.assertApiUpdate(adminResId, LIMITED_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_FORBIDDEN); + api.assertApiUpdate(userResId, LIMITED_ACCESS_USER, "sampleUpdateUser", HttpStatus.SC_OK); + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiGetSearch(LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // cannot share/revoke others, can share own + api.assertApiShare(adminResId, LIMITED_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiRevoke(adminResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + + api.assertApiGet(userResId, USER_ADMIN, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiShare(userResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, USER_ADMIN, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiRevoke(userResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, USER_ADMIN, HttpStatus.SC_FORBIDDEN, ""); + + // searches aligned with ownership + api.assertApiGetSearch(LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchAllPayload(), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchByNamePayload("sample"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 0, ""); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // can delete own + api.assertApiDelete(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK); + // cannot delete admin's + api.assertApiDelete(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + } + + private void assertFullUser_ResourceProtection() { + String userResId = createSampleResourceAs(FULL_ACCESS_USER); + + // even with * perms, sharing rules restrict access to others’ resources + api.assertApiGet(adminResId, FULL_ACCESS_USER, HttpStatus.SC_FORBIDDEN, "sample"); + api.assertApiGetAll(FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); + + // can update own + api.assertApiUpdate(adminResId, FULL_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_FORBIDDEN); + api.assertApiUpdate(userResId, FULL_ACCESS_USER, "sampleUpdateUser", HttpStatus.SC_OK); + api.assertApiGet(userResId, FULL_ACCESS_USER, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // cannot share/revoke others’ resources; can share own + api.assertApiShare(adminResId, FULL_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiRevoke(adminResId, FULL_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiShare(userResId, FULL_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiRevoke(userResId, FULL_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + + // search visibility matches sharing state + api.assertApiGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchAllPayload(), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchByNamePayload("sample"), FULL_ACCESS_USER, HttpStatus.SC_OK, 0, ""); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // can delete own; cannot delete admin’s under sharing rules + api.assertApiDelete(userResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + api.assertApiDelete(adminResId, FULL_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + } + + private void assertAdminCert_ResourceProtection() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // read/update + TestRestClient.HttpResponse resp = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + + resp = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + adminResId, "{\"name\":\"sampleUpdated\"}"); + resp.assertStatusCode(HttpStatus.SC_OK); + assertThat(resp.getBody(), containsString("sampleUpdated")); + + // share/revoke handlers exist → expect 200 for admin path + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, Recipient.USERS, NO_ACCESS_USER.getName()) + ); + resp.assertStatusCode(HttpStatus.SC_OK); + + TestUtils.PatchSharingInfoPayloadBuilder payloadBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(NO_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG + ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, payloadBuilder.build()); + resp.assertStatusCode(HttpStatus.SC_OK); + + // search works + resp = client.get(SAMPLE_RESOURCE_SEARCH_ENDPOINT); + resp.assertStatusCode(HttpStatus.SC_OK); + assertThat(resp.getBody(), containsString("sampleUpdated")); + + resp = client.postJson(SAMPLE_RESOURCE_SEARCH_ENDPOINT, searchAllPayload()); + resp.assertStatusCode(HttpStatus.SC_OK); + + resp = client.postJson(SAMPLE_RESOURCE_SEARCH_ENDPOINT, searchByNamePayload("sampleUpdated")); + resp.assertStatusCode(HttpStatus.SC_OK); + + // delete ok + resp = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } +} diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ExcludedResourceTypeTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ExcludedResourceTypeTests.java index a62a0d3762..f778f6dd78 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ExcludedResourceTypeTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ExcludedResourceTypeTests.java @@ -25,7 +25,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.not; import static org.opensearch.sample.resource.TestUtils.FULL_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.LIMITED_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.NO_ACCESS_USER; @@ -58,11 +57,12 @@ public void cleanup() { } @Test - public void testSampleResourceSharingIndexDoesNotExist() { + public void testSampleResourceSharingIndexExists() { + // we create resource-sharing index as we need to add index operation listener and we cannot add that dynamically try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { TestRestClient.HttpResponse response = client.get("_cat/indices?expand_wildcards=all"); response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), not(containsString(RESOURCE_SHARING_INDEX))); + assertThat(response.getBody(), containsString(RESOURCE_SHARING_INDEX)); } } 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 3ab5e2fe59..126503faf8 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 @@ -68,18 +68,16 @@ private void fetchAllResources(ActionListener listener) { SearchRequest req = new SearchRequest(RESOURCE_INDEX_NAME).source(ssb); pluginClient.search(req, ActionListener.wrap(searchResponse -> { SearchHit[] hits = searchResponse.getHits().getHits(); - if (hits.length == 0) { - listener.onFailure(new ResourceNotFoundException("No resources found in index: " + RESOURCE_INDEX_NAME)); - } else { - Set resources = Arrays.stream(hits).map(hit -> { - try { - return parseResource(hit.getSourceAsString()); - } catch (IOException e) { - throw new RuntimeException(e); - } - }).collect(Collectors.toSet()); - listener.onResponse(new GetResourceResponse(resources)); - } + + Set resources = Arrays.stream(hits).map(hit -> { + try { + return parseResource(hit.getSourceAsString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toSet()); + listener.onResponse(new GetResourceResponse(resources)); + }, listener::onFailure)); } From 7c2c896a03720c197bc86c3617a782f5c39c522c Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 8 Oct 2025 15:52:36 -0400 Subject: [PATCH 18/40] Adds documentation Signed-off-by: Darshit Chanpura --- RESOURCE_SHARING_AND_ACCESS_CONTROL.md | 54 +++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/RESOURCE_SHARING_AND_ACCESS_CONTROL.md b/RESOURCE_SHARING_AND_ACCESS_CONTROL.md index 0eb99b14d2..e03fd2b21d 100644 --- a/RESOURCE_SHARING_AND_ACCESS_CONTROL.md +++ b/RESOURCE_SHARING_AND_ACCESS_CONTROL.md @@ -499,7 +499,7 @@ Since no entities are listed, the resource is accessible **only by its creator a ### **Additional Notes** - **Feature Flag:** These APIs are available only when `plugins.security.experimental.resource_sharing.enabled` is set to `true` in the configuration. - +- **Protected Types:** These APIs will only come into effect if concerned resources are marked as protected: `plugins.security.experimental.resource_sharing.protected_types: [, ]`. --- @@ -529,6 +529,58 @@ The list of protected types are controlled through following opensearch setting ``` NOTE: These types will be available on documentation website. +### **Dynamic Updates** + +The settings described above can be dynamically updated at runtime using the OpenSearch `_cluster/settings` API. +This allows administrators to enable or disable the **Resource Sharing** feature and modify the list of **protected types** without restarting the cluster. + +#### **Example 1: Enable Resource Sharing Feature** + +```bash +PUT _cluster/settings +{ + "transient": { + "plugins.security.experimental.resource_sharing.enabled": true + } +} +``` + +#### **Example 2: Update Protected Types** + +```bash +PUT _cluster/settings +{ + "transient": { + "plugins.security.experimental.resource_sharing.protected_types": ["sample-resource", "ml-model"] + } +} +``` + +#### **Example 3: Clear Protected Types** + +```bash +PUT _cluster/settings +{ + "transient": { + "plugins.security.experimental.resource_sharing.protected_types": [] + } +} +``` + +#### **Notes** + +* Both settings support **dynamic updates**, meaning the changes take effect immediately without requiring a node restart. +* You can use either **transient** (temporary until restart) or **persistent** (survive restarts) settings. +* To verify the current values, use: + + ```bash + GET _cluster/settings?include_defaults=true + ``` +* Feature toggles and protected type lists can also be modified through configuration files before cluster startup if preferred. + + + + ## **2. 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. From 8d186fedaf3448bb7ba233bbe2b427d0c36f8489 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 13 Oct 2025 10:23:59 -0400 Subject: [PATCH 19/40] Fixes resource search request evaluation Signed-off-by: Darshit Chanpura --- .../security/OpenSearchSecurityPlugin.java | 11 +++-- .../configuration/DlsFlsValveImpl.java | 48 +++++++++++-------- .../privileges/ResourceAccessEvaluator.java | 11 +++-- .../ResourceAccessControlClient.java | 15 ++++-- .../resources/ResourcePluginInfo.java | 14 +++++- .../ResourceAccessEvaluatorTest.java | 8 ++-- 6 files changed, 70 insertions(+), 37 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 3885d6d40d..9cc10c2273 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -1217,7 +1217,8 @@ public Collection createComponents( dlsFlsBaseContext, adminDns, resourcePluginInfo, - resourceSharingEnabledSetting + resourceSharingEnabledSetting, + resourceSharingProtectedResourceTypesSetting ); cr.subscribeOnChange(configMap -> { ((DlsFlsValveImpl) dlsFlsValve).updateConfiguration(cr.getConfiguration(CType.ROLES)); }); } @@ -1227,7 +1228,11 @@ public Collection createComponents( // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions // Assign resource sharing client to each extension // Using the non-gated client (i.e. no additional permissions required) - ResourceSharingClient resourceAccessControlClient = new ResourceAccessControlClient(resourceAccessHandler, resourcePluginInfo); + ResourceSharingClient resourceAccessControlClient = new ResourceAccessControlClient( + resourceAccessHandler, + resourcePluginInfo, + resourceSharingProtectedResourceTypesSetting + ); resourcePluginInfo.setResourceSharingClient(resourceAccessControlClient); resourcePluginInfo.getResourceSharingExtensions().forEach(extension -> { extension.assignResourceSharingClient(resourceAccessControlClient); @@ -1237,7 +1242,7 @@ public Collection createComponents( // CS-ENFORCE-SINGLE resourceAccessEvaluator = new ResourceAccessEvaluator( - resourcePluginInfo.getResourceIndices(), + resourcePluginInfo, resourceAccessHandler, resourceSharingEnabledSetting, resourceSharingProtectedResourceTypesSetting diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 8b714e0146..67ff90f5f4 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; @@ -109,6 +110,7 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve { private final Settings settings; private final AdminDNs adminDNs; private final OpensearchDynamicSetting resourceSharingEnabledSetting; + private final OpensearchDynamicSetting> resourceSharingProtectedTypesSetting; private final ResourcePluginInfo resourcePluginInfo; public DlsFlsValveImpl( @@ -121,7 +123,8 @@ public DlsFlsValveImpl( DlsFlsBaseContext dlsFlsBaseContext, AdminDNs adminDNs, ResourcePluginInfo resourcePluginInfo, - OpensearchDynamicSetting resourceSharingEnabledSetting + OpensearchDynamicSetting resourceSharingEnabledSetting, + OpensearchDynamicSetting> resourceSharingProtectedTypesSetting ) { super(); this.nodeClient = nodeClient; @@ -144,6 +147,7 @@ public DlsFlsValveImpl( } }); this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; + this.resourceSharingProtectedTypesSetting = resourceSharingProtectedTypesSetting; } /** @@ -163,27 +167,31 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< ActionRequest request = context.getRequest(); if (HeaderHelper.isInternalOrPluginRequest(threadContext)) { IndexResolverReplacer.Resolved resolved = context.getResolvedRequest(); - WildcardMatcher resourceIndicesMatcher = WildcardMatcher.from(resourcePluginInfo.getResourceIndices()); - if (resourceSharingEnabledSetting.getDynamicSettingValue() - && request instanceof SearchRequest - && resourceIndicesMatcher.matchAll(resolved.getAllIndices())) { - - IndexToRuleMap sharedResourceMap = ResourceSharingDlsUtils.resourceRestrictions( - namedXContentRegistry, - resolved, - userSubject.getUser() - ); + if (resourceSharingEnabledSetting.getDynamicSettingValue() && request instanceof SearchRequest) { - return DlsFilterLevelActionHandler.handle( - context, - sharedResourceMap, - listener, - nodeClient, - clusterService, - OpenSearchSecurityPlugin.GuiceHolder.getIndicesService(), - resolver, - threadContext + Set protectedIndices = resourcePluginInfo.getResourceIndicesForProtectedTypes( + resourceSharingProtectedTypesSetting.getDynamicSettingValue() ); + WildcardMatcher resourceIndicesMatcher = WildcardMatcher.from(protectedIndices); + if (resourceIndicesMatcher.matchAll(resolved.getAllIndices())) { + + IndexToRuleMap sharedResourceMap = ResourceSharingDlsUtils.resourceRestrictions( + namedXContentRegistry, + resolved, + userSubject.getUser() + ); + + return DlsFilterLevelActionHandler.handle( + context, + sharedResourceMap, + listener, + nodeClient, + clusterService, + OpenSearchSecurityPlugin.GuiceHolder.getIndicesService(), + resolver, + threadContext + ); + } } else { return true; } diff --git a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java index 4fae73abba..0c7156c319 100644 --- a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java @@ -11,7 +11,6 @@ package org.opensearch.security.privileges; import java.util.List; -import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -23,6 +22,7 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.Strings; import org.opensearch.security.resources.ResourceAccessHandler; +import org.opensearch.security.resources.ResourcePluginInfo; import org.opensearch.security.setting.OpensearchDynamicSetting; /** @@ -41,19 +41,19 @@ public class ResourceAccessEvaluator { private static final Logger log = LogManager.getLogger(ResourceAccessEvaluator.class); - private final Set resourceIndices; + private final ResourcePluginInfo resourcePluginInfo; private final ResourceAccessHandler resourceAccessHandler; private final OpensearchDynamicSetting resourceSharingEnabledSetting; private final OpensearchDynamicSetting> protectedResourceTypesSetting; public ResourceAccessEvaluator( - Set resourceIndices, + ResourcePluginInfo resourcePluginInfo, ResourceAccessHandler resourceAccessHandler, final OpensearchDynamicSetting resourceSharingEnabledSetting, final OpensearchDynamicSetting> protectedResourceTypesSetting ) { - this.resourceIndices = resourceIndices; + this.resourcePluginInfo = resourcePluginInfo; this.resourceAccessHandler = resourceAccessHandler; this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; this.protectedResourceTypesSetting = protectedResourceTypesSetting; @@ -130,7 +130,8 @@ public boolean shouldEvaluate(ActionRequest request) { return false; } // if requested index is not a resource sharing index, move on to the regular evaluator - if (!resourceIndices.contains(docRequest.index())) { + List currentProtectedTypes = protectedResourceTypesSetting.getDynamicSettingValue(); + if (!resourcePluginInfo.getResourceIndicesForProtectedTypes(currentProtectedTypes).contains(docRequest.index())) { log.debug("Request index {} is not a protected resource index", docRequest.index()); return false; } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java index 7bda2257ad..99f196cc68 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java @@ -8,12 +8,14 @@ package org.opensearch.security.resources; +import java.util.List; import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.core.action.ActionListener; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.spi.resources.client.ResourceSharingClient; import org.opensearch.security.spi.resources.sharing.ResourceSharing; import org.opensearch.security.spi.resources.sharing.ShareWith; @@ -28,17 +30,21 @@ public final class ResourceAccessControlClient implements ResourceSharingClient private static final Logger LOGGER = LogManager.getLogger(ResourceAccessControlClient.class); private final ResourceAccessHandler resourceAccessHandler; - private final Set resourceIndices; private final ResourcePluginInfo resourcePluginInfo; + private final OpensearchDynamicSetting> resourceSharingProtectedResourcesSetting; /** * Constructs a new ResourceAccessControlClient. * */ - public ResourceAccessControlClient(ResourceAccessHandler resourceAccessHandler, ResourcePluginInfo resourcePluginInfo) { + public ResourceAccessControlClient( + ResourceAccessHandler resourceAccessHandler, + ResourcePluginInfo resourcePluginInfo, + OpensearchDynamicSetting> resourceSharingProtectedResourcesSetting + ) { this.resourceAccessHandler = resourceAccessHandler; - this.resourceIndices = resourcePluginInfo.getResourceIndices(); this.resourcePluginInfo = resourcePluginInfo; + this.resourceSharingProtectedResourcesSetting = resourceSharingProtectedResourcesSetting; } /** @@ -52,7 +58,8 @@ public ResourceAccessControlClient(ResourceAccessHandler resourceAccessHandler, @Override public void verifyAccess(String resourceId, String resourceIndex, String action, ActionListener listener) { // following situation will arise when resource is onboarded to framework but not marked as protected - if (!resourceIndices.contains(resourceIndex)) { + List protectedResourceTypes = resourceSharingProtectedResourcesSetting.getDynamicSettingValue(); + if (!resourcePluginInfo.getResourceIndicesForProtectedTypes(protectedResourceTypes).contains(resourceIndex)) { LOGGER.warn( "Resource '{}' is onboarded to sharing framework but is not marked as protected. Action {} is allowed.", resourceId, diff --git a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index 1b5d8797c2..95ba89b383 100644 --- a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -116,7 +116,6 @@ public ResourceSharingClient getResourceAccessControlClient() { } /** Register/merge action-group names for a given resource type. */ - public record ResourceDashboardInfo(String resourceType, Set actionGroups // names only (for UI) ) implements ToXContentObject { @Override @@ -158,9 +157,22 @@ public Set getResourceTypes() { .collect(Collectors.toCollection(LinkedHashSet::new)); } + // for index public Set getResourceIndices() { return indexToType.keySet(); } + public Set getResourceIndicesForProtectedTypes(List resourceTypes) { + if (resourceTypes == null || resourceTypes.isEmpty()) { + return Collections.emptySet(); + } + + return indexToType.entrySet() + .stream() + .filter(e -> resourceTypes.contains(e.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + } // CS-ENFORCE-SINGLE diff --git a/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java index 46ad719b72..d23c9a29cb 100644 --- a/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java @@ -8,8 +8,6 @@ package org.opensearch.security.privileges; -import java.util.Collections; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -20,6 +18,7 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.resources.ResourceAccessHandler; +import org.opensearch.security.resources.ResourcePluginInfo; import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.support.ConfigConstants; @@ -41,6 +40,8 @@ public class ResourceAccessEvaluatorTest { @Mock private ResourceAccessHandler resourceAccessHandler; + @Mock + private ResourcePluginInfo resourcePluginInfo; @Mock private PrivilegesEvaluationContext context; @@ -52,10 +53,9 @@ public class ResourceAccessEvaluatorTest { @Before public void setup() { - Settings settings = Settings.builder().put(ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, true).build(); threadContext = new ThreadContext(Settings.EMPTY); evaluator = new ResourceAccessEvaluator( - Collections.singleton(IDX), + resourcePluginInfo, resourceAccessHandler, mock(OpensearchDynamicSetting.class), mock(OpensearchDynamicSetting.class) From b707959090fac89a87ee86834c2d441210c93e37 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 13 Oct 2025 10:52:21 -0400 Subject: [PATCH 20/40] Adds caching to resource indices and makes read-write operations thread-safe in resource-plugin-info class Signed-off-by: Darshit Chanpura --- .../resources/ResourcePluginInfo.java | 182 ++++++++++++------ 1 file changed, 128 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index 95ba89b383..78ce305d0b 100644 --- a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; import com.google.common.collect.ImmutableSet; @@ -52,54 +53,81 @@ public class ResourcePluginInfo { // AuthZ: resolved (flattened) groups per type private final Map typeToFlattened = new HashMap<>(); + // cache current protected types and their indices + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); // make the updates/reads thread-safe + private Set currentProtectedTypes = Collections.emptySet(); // snapshot of last set + private Set cachedProtectedTypeIndices = Collections.emptySet(); // precomputed indices + public void setResourceSharingExtensions(Set extensions) { - resourceSharingExtensions.clear(); - typeToIndex.clear(); - indexToType.clear(); - - // Enforce resource-type unique-ness - Set resourceTypes = new HashSet<>(); - for (ResourceSharingExtension extension : extensions) { - for (var rp : extension.getResourceProviders()) { - if (!resourceTypes.contains(rp.resourceType())) { - // add name seen so far to the resource-types set - resourceTypes.add(rp.resourceType()); - // also cache type->index and index->type mapping - typeToIndex.put(rp.resourceType(), rp.resourceIndexName()); - indexToType.put(rp.resourceIndexName(), rp.resourceType()); - } else { - throw new OpenSearchSecurityException( - String.format( - "Resource type [%s] is already registered. Please provide a different unique-name for the resource declared by %s.", - rp.resourceType(), - extension.getClass().getName() - ) - ); + lock.writeLock().lock(); + try { + resourceSharingExtensions.clear(); + typeToIndex.clear(); + indexToType.clear(); + + // Enforce resource-type unique-ness + Set resourceTypes = new HashSet<>(); + for (ResourceSharingExtension extension : extensions) { + for (var rp : extension.getResourceProviders()) { + if (!resourceTypes.contains(rp.resourceType())) { + // add name seen so far to the resource-types set + resourceTypes.add(rp.resourceType()); + // also cache type->index and index->type mapping + typeToIndex.put(rp.resourceType(), rp.resourceIndexName()); + indexToType.put(rp.resourceIndexName(), rp.resourceType()); + } else { + throw new OpenSearchSecurityException( + String.format( + "Resource type [%s] is already registered. Please provide a different unique-name for the resource declared by %s.", + rp.resourceType(), + extension.getClass().getName() + ) + ); + } } } + resourceSharingExtensions.addAll(extensions); + + // Whenever providers change, invalidate protected caches so next update refreshes them + currentProtectedTypes = Collections.emptySet(); + cachedProtectedTypeIndices = Collections.emptySet(); + } finally { + lock.writeLock().unlock(); } - resourceSharingExtensions.addAll(extensions); } public void updateProtectedTypes(List protectedTypes) { - // Rebuild mappings based on the current allowlist - typeToIndex.clear(); - indexToType.clear(); + lock.writeLock().lock(); + try { + // Rebuild mappings based on the current allowlist + typeToIndex.clear(); + indexToType.clear(); + + if (protectedTypes == null || protectedTypes.isEmpty()) { + // No protected types -> leave maps empty + currentProtectedTypes = Collections.emptySet(); + cachedProtectedTypeIndices = Collections.emptySet(); + return; + } - if (protectedTypes == null || protectedTypes.isEmpty()) { - // No protected types -> leave maps empty - return; - } + // Cache current protected set as an unmodifiable snapshot + currentProtectedTypes = Collections.unmodifiableSet(new LinkedHashSet<>(protectedTypes)); - for (ResourceSharingExtension extension : resourceSharingExtensions) { - for (var rp : extension.getResourceProviders()) { - final String type = rp.resourceType(); - if (!protectedTypes.contains(type)) continue; + for (ResourceSharingExtension extension : resourceSharingExtensions) { + for (var rp : extension.getResourceProviders()) { + final String type = rp.resourceType(); + if (!currentProtectedTypes.contains(type)) continue; - final String index = rp.resourceIndexName(); - typeToIndex.put(type, index); - indexToType.put(index, type); + final String index = rp.resourceIndexName(); + typeToIndex.put(type, index); + indexToType.put(index, type); + } } + + // pre-compute indices for current protected set + cachedProtectedTypeIndices = Collections.unmodifiableSet(new LinkedHashSet<>(typeToIndex.values())); + } finally { + lock.writeLock().unlock(); } } @@ -129,37 +157,73 @@ public XContentBuilder toXContent(XContentBuilder b, Params p) throws IOExceptio public void registerActionGroupNames(String resourceType, Collection names) { if (resourceType == null || names == null) return; - typeToGroupNames.computeIfAbsent(resourceType, k -> new LinkedHashSet<>()) - .addAll(names.stream().filter(Objects::nonNull).map(String::trim).filter(s -> !s.isEmpty()).toList()); + lock.writeLock().lock(); + try { + typeToGroupNames.computeIfAbsent(resourceType, k -> new LinkedHashSet<>()) + .addAll(names.stream().filter(Objects::nonNull).map(String::trim).filter(s -> !s.isEmpty()).toList()); + } finally { + lock.writeLock().unlock(); + } } public void registerFlattened(String resourceType, FlattenedActionGroups flattened) { if (resourceType == null || flattened == null) return; - typeToFlattened.put(resourceType, flattened); + lock.writeLock().lock(); + try { + typeToFlattened.put(resourceType, flattened); + } finally { + lock.writeLock().unlock(); + } } public FlattenedActionGroups flattenedForType(String resourceType) { - return typeToFlattened.getOrDefault(resourceType, FlattenedActionGroups.EMPTY); + lock.readLock().lock(); + try { + return typeToFlattened.getOrDefault(resourceType, FlattenedActionGroups.EMPTY); + } finally { + lock.readLock().unlock(); + } } public String typeByIndex(String index) { - return indexToType.get(index); + lock.readLock().lock(); + try { + return indexToType.get(index); + } finally { + lock.readLock().unlock(); + } } public String indexByType(String type) { - return typeToIndex.get(type); + lock.readLock().lock(); + try { + return typeToIndex.get(type); + } finally { + lock.readLock().unlock(); + } } public Set getResourceTypes() { - return typeToIndex.keySet() - .stream() - .map(s -> new ResourceDashboardInfo(s, Collections.unmodifiableSet(typeToGroupNames.getOrDefault(s, new LinkedHashSet<>())))) - .collect(Collectors.toCollection(LinkedHashSet::new)); + lock.readLock().lock(); + try { + return typeToIndex.keySet() + .stream() + .map( + s -> new ResourceDashboardInfo(s, Collections.unmodifiableSet(typeToGroupNames.getOrDefault(s, new LinkedHashSet<>()))) + ) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } finally { + lock.readLock().unlock(); + } } - // for index public Set getResourceIndices() { - return indexToType.keySet(); + lock.readLock().lock(); + try { + return new LinkedHashSet<>(indexToType.keySet()); + } finally { + lock.readLock().unlock(); + } } public Set getResourceIndicesForProtectedTypes(List resourceTypes) { @@ -167,11 +231,21 @@ public Set getResourceIndicesForProtectedTypes(List resourceType return Collections.emptySet(); } - return indexToType.entrySet() - .stream() - .filter(e -> resourceTypes.contains(e.getValue())) - .map(Map.Entry::getKey) - .collect(Collectors.toSet()); + lock.readLock().lock(); + try { + // If caller is asking for the current protected set, return the cache + if (new LinkedHashSet<>(resourceTypes).equals(currentProtectedTypes)) { + return cachedProtectedTypeIndices; + } + + return indexToType.entrySet() + .stream() + .filter(e -> resourceTypes.contains(e.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } finally { + lock.readLock().unlock(); + } } } From 877bb1152b229f7befbe5c43ad8ad2caae5eebdb Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 13 Oct 2025 11:56:12 -0400 Subject: [PATCH 21/40] Fixes type enabled check Signed-off-by: Darshit Chanpura --- .../security/resources/ResourceAccessControlClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java index 99f196cc68..cf6a0c4ad0 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java @@ -115,6 +115,6 @@ public void getAccessibleResourceIds(String resourceIndex, ActionListener Date: Mon, 13 Oct 2025 13:35:38 -0400 Subject: [PATCH 22/40] Refactors resource type fetch logic Signed-off-by: Darshit Chanpura --- .../opensearch/security/OpenSearchSecurityPlugin.java | 1 + .../security/configuration/DlsFlsValveImpl.java | 4 +--- .../security/privileges/ResourceAccessEvaluator.java | 3 +-- .../resources/ResourceAccessControlClient.java | 3 +-- .../security/resources/ResourcePluginInfo.java | 10 +++++++++- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 9cc10c2273..56c157ba0d 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -372,6 +372,7 @@ public OpenSearchSecurityPlugin(final Settings settings, final Path configPath) resourceSharingEnabledSetting = new ResourceSharingFeatureFlagSetting(settings, resourcePluginInfo); // not filtered resourceSharingProtectedResourceTypesSetting = new ResourceSharingProtectedResourcesSetting(settings, resourcePluginInfo); // not // filtered + resourcePluginInfo.setResourceSharingProtectedTypesSetting(resourceSharingProtectedResourceTypesSetting); if (disabled) { this.sslCertReloadEnabled = false; diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 67ff90f5f4..670034495e 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -169,9 +169,7 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< IndexResolverReplacer.Resolved resolved = context.getResolvedRequest(); if (resourceSharingEnabledSetting.getDynamicSettingValue() && request instanceof SearchRequest) { - Set protectedIndices = resourcePluginInfo.getResourceIndicesForProtectedTypes( - resourceSharingProtectedTypesSetting.getDynamicSettingValue() - ); + Set protectedIndices = resourcePluginInfo.getResourceIndicesForProtectedTypes(); WildcardMatcher resourceIndicesMatcher = WildcardMatcher.from(protectedIndices); if (resourceIndicesMatcher.matchAll(resolved.getAllIndices())) { diff --git a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java index 0c7156c319..29119a127f 100644 --- a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java @@ -130,8 +130,7 @@ public boolean shouldEvaluate(ActionRequest request) { return false; } // if requested index is not a resource sharing index, move on to the regular evaluator - List currentProtectedTypes = protectedResourceTypesSetting.getDynamicSettingValue(); - if (!resourcePluginInfo.getResourceIndicesForProtectedTypes(currentProtectedTypes).contains(docRequest.index())) { + if (!resourcePluginInfo.getResourceIndicesForProtectedTypes().contains(docRequest.index())) { log.debug("Request index {} is not a protected resource index", docRequest.index()); return false; } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java index cf6a0c4ad0..65ce3beef1 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java @@ -58,8 +58,7 @@ public ResourceAccessControlClient( @Override public void verifyAccess(String resourceId, String resourceIndex, String action, ActionListener listener) { // following situation will arise when resource is onboarded to framework but not marked as protected - List protectedResourceTypes = resourceSharingProtectedResourcesSetting.getDynamicSettingValue(); - if (!resourcePluginInfo.getResourceIndicesForProtectedTypes(protectedResourceTypes).contains(resourceIndex)) { + if (!resourcePluginInfo.getResourceIndicesForProtectedTypes().contains(resourceIndex)) { LOGGER.warn( "Resource '{}' is onboarded to sharing framework but is not marked as protected. Action {} is allowed.", resourceId, diff --git a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index 78ce305d0b..ae8ac8cda2 100644 --- a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -28,6 +28,7 @@ import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.security.securityconf.FlattenedActionGroups; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.spi.resources.ResourceSharingExtension; import org.opensearch.security.spi.resources.client.ResourceSharingClient; @@ -41,6 +42,8 @@ public class ResourcePluginInfo { private ResourceSharingClient resourceAccessControlClient; + private OpensearchDynamicSetting> resourceSharingProtectedTypesSetting; + private final Set resourceSharingExtensions = new HashSet<>(); // type <-> index @@ -58,6 +61,10 @@ public class ResourcePluginInfo { private Set currentProtectedTypes = Collections.emptySet(); // snapshot of last set private Set cachedProtectedTypeIndices = Collections.emptySet(); // precomputed indices + public void setResourceSharingProtectedTypesSetting(OpensearchDynamicSetting> resourceSharingProtectedTypesSetting) { + this.resourceSharingProtectedTypesSetting = resourceSharingProtectedTypesSetting; + } + public void setResourceSharingExtensions(Set extensions) { lock.writeLock().lock(); try { @@ -226,7 +233,8 @@ public Set getResourceIndices() { } } - public Set getResourceIndicesForProtectedTypes(List resourceTypes) { + public Set getResourceIndicesForProtectedTypes() { + List resourceTypes = this.resourceSharingProtectedTypesSetting.getDynamicSettingValue(); if (resourceTypes == null || resourceTypes.isEmpty()) { return Collections.emptySet(); } From f53bdfb801498e3e7e5fc5f9fe0924823582bd0e Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 14 Oct 2025 15:40:05 -0400 Subject: [PATCH 23/40] Back out unrelated changes Signed-off-by: Craig Perkins --- .../actions/rest/create/CreateResourceRequest.java | 10 +++++----- .../actions/rest/create/UpdateResourceRequest.java | 10 +++++----- .../actions/rest/delete/DeleteResourceRequest.java | 10 +++++----- .../resource/actions/rest/get/GetResourceRequest.java | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRequest.java index 92a7c9cc8d..5345667fb0 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/create/CreateResourceRequest.java @@ -60,6 +60,11 @@ public boolean shouldStoreUser() { return this.shouldStoreUser; } + @Override + public String type() { + return RESOURCE_TYPE; + } + @Override public String index() { return RESOURCE_INDEX_NAME; @@ -69,9 +74,4 @@ public String index() { public String id() { return null; } - - @Override - public String type() { - return RESOURCE_TYPE; - } } 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 index a4ebcb78a1..ef8e5cf077 100644 --- 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 @@ -60,6 +60,11 @@ public String getResourceId() { return this.resourceId; } + @Override + public String type() { + return RESOURCE_TYPE; + } + @Override public String index() { return RESOURCE_INDEX_NAME; @@ -69,9 +74,4 @@ public String index() { public String id() { return resourceId; } - - @Override - public String type() { - return RESOURCE_TYPE; - } } 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 7c25d8d59d..7ab9606f74 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 @@ -51,6 +51,11 @@ public String getResourceId() { return this.resourceId; } + @Override + public String type() { + return RESOURCE_TYPE; + } + @Override public String index() { return RESOURCE_INDEX_NAME; @@ -60,9 +65,4 @@ public String index() { public String id() { return resourceId; } - - @Override - public String type() { - return RESOURCE_TYPE; - } } 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 index b41175d865..132114d011 100644 --- 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 @@ -51,6 +51,11 @@ public String getResourceId() { return this.resourceId; } + @Override + public String type() { + return RESOURCE_TYPE; + } + @Override public String index() { return RESOURCE_INDEX_NAME; @@ -60,9 +65,4 @@ public String index() { public String id() { return resourceId; } - - @Override - public String type() { - return RESOURCE_TYPE; - } } From 7df24dcc1293b89b61e1fe894267ebdfdae04240 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 14 Oct 2025 15:43:15 -0400 Subject: [PATCH 24/40] Clean up changes Signed-off-by: Craig Perkins --- .../resources/ResourceAccessControlClient.java | 2 +- .../resources/api/share/ShareRequest.java | 18 ++++++------------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java index d025c57565..25b3f2eb24 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java @@ -51,7 +51,7 @@ public ResourceAccessControlClient( * Verifies whether the current user has access to the specified resource. * * @param resourceId The ID of the resource to verify. - * @param resourceType The resource tupe. + * @param resourceType The resource type. * @param action The action to be evaluated against * @param listener Callback that receives {@code true} if access is granted, {@code false} otherwise. */ diff --git a/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java b/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java index b89482a435..851272937d 100644 --- a/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java +++ b/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java @@ -62,8 +62,8 @@ public ShareRequest(StreamInput in) throws IOException { super(in); this.method = in.readEnum(RestRequest.Method.class); this.resourceId = in.readString(); - this.resourceType = in.readString(); this.resourceIndex = in.readString(); + this.resourceType = in.readString(); this.shareWith = in.readOptionalWriteable(ShareWith::new); this.add = in.readOptionalWriteable(ShareWith::new); this.revoke = in.readOptionalWriteable(ShareWith::new); @@ -73,7 +73,6 @@ public ShareRequest(StreamInput in) throws IOException { public void writeTo(StreamOutput out) throws IOException { out.writeEnum(method); out.writeString(resourceId); - out.writeString(resourceType); out.writeString(resourceIndex); out.writeString(resourceType); out.writeOptionalWriteable(shareWith); @@ -121,6 +120,11 @@ public RestRequest.Method getMethod() { return method; } + @Override + public String type() { + return resourceType; + } + /** * Get the index that this request operates on * @@ -131,16 +135,6 @@ public String index() { return resourceIndex; } - /** - * Get the type - * - * @return the resource type - */ - @Override - public String type() { - return resourceType; - } - /** * Get the id of the document for this request * From 4dc058aac78752eebb5e68cc36cf1f5a7c0de850 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Tue, 14 Oct 2025 15:46:28 -0400 Subject: [PATCH 25/40] Use correct method Signed-off-by: Craig Perkins --- .../opensearch/security/resources/ResourceIndexListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java index e4cbf4a555..02fe5aa23e 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java @@ -132,7 +132,7 @@ public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResul } String resourceIndex = shardId.getIndexName(); - if (!resourcePluginInfo.getResourceIndices().contains(resourceIndex)) { + if (!resourcePluginInfo.getResourceIndicesForProtectedTypes().contains(resourceIndex)) { // type is marked as not protected return; } From eaefa32d29c75fe4e43883a1abc5e7edfbca966a Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 15 Oct 2025 17:43:46 -0400 Subject: [PATCH 26/40] Add actions around sample-resource-group Signed-off-by: Craig Perkins --- .../opensearch/sample/resource/TestUtils.java | 88 +++++- .../SampleResourceGroupTests.java | 271 ++++++++++++++++++ .../sample/SampleResourceGroup.java | 97 +++++++ .../sample/SampleResourceGroupExtension.java | 27 ++ .../sample/SampleResourcePlugin.java | 23 ++ .../create/CreateResourceGroupAction.java | 29 ++ .../create/CreateResourceGroupRequest.java | 69 +++++ .../create/CreateResourceGroupResponse.java | 55 ++++ .../create/CreateResourceGroupRestAction.java | 97 +++++++ .../create/UpdateResourceGroupAction.java | 29 ++ .../create/UpdateResourceGroupRequest.java | 77 +++++ .../delete/DeleteResourceGroupAction.java | 29 ++ .../delete/DeleteResourceGroupRequest.java | 68 +++++ .../delete/DeleteResourceGroupResponse.java | 55 ++++ .../delete/DeleteResourceGroupRestAction.java | 53 ++++ .../rest/get/GetResourceGroupAction.java | 29 ++ .../rest/get/GetResourceGroupRequest.java | 68 +++++ .../rest/get/GetResourceGroupResponse.java | 54 ++++ .../rest/get/GetResourceGroupRestAction.java | 48 ++++ .../search/SearchResourceGroupAction.java | 26 ++ .../search/SearchResourceGroupRestAction.java | 65 +++++ .../CreateResourceGroupTransportAction.java | 133 +++++++++ .../DeleteResourceGroupTransportAction.java | 77 +++++ .../GetResourceGroupTransportAction.java | 105 +++++++ .../SearchResourceGroupTransportAction.java | 43 +++ .../UpdateResourceGroupTransportAction.java | 86 ++++++ .../opensearch/sample/utils/Constants.java | 1 + ...ity.spi.resources.ResourceSharingExtension | 3 +- .../main/resources/resource-action-groups.yml | 13 + .../resources/ResourcePluginInfo.java | 2 +- 30 files changed, 1817 insertions(+), 3 deletions(-) create mode 100644 sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resourcegroup/SampleResourceGroupTests.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceGroup.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceGroupExtension.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupRequest.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupResponse.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupRestAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/CreateResourceGroupTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/DeleteResourceGroupTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/GetResourceGroupTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/SearchResourceGroupTransportAction.java create mode 100644 sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/UpdateResourceGroupTransportAction.java diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java index 919deec80e..3029700246 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java @@ -40,6 +40,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.is; +import static org.opensearch.sample.utils.Constants.RESOURCE_GROUP_TYPE; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_PREFIX; @@ -80,19 +81,29 @@ public final class TestUtils { public static final String SAMPLE_READ_WRITE_RESOURCE_AG = "sample_read_write"; public static final String SAMPLE_FULL_ACCESS_RESOURCE_AG = "sample_full_access"; + public static final String SAMPLE_GROUP_READ_ONLY_RESOURCE_AG = "sample_group_read_only"; + public static final String SAMPLE_GROUP_READ_WRITE_RESOURCE_AG = "sample_group_read_write"; + public static final String SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG = "sample_group_full_access"; + public static final String SAMPLE_RESOURCE_CREATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/create"; public static final String SAMPLE_RESOURCE_GET_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/get"; public static final String SAMPLE_RESOURCE_UPDATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/update"; public static final String SAMPLE_RESOURCE_DELETE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/delete"; public static final String SAMPLE_RESOURCE_SEARCH_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/search"; + public static final String SAMPLE_RESOURCE_GROUP_CREATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/group/create"; + public static final String SAMPLE_RESOURCE_GROUP_GET_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/group/get"; + public static final String SAMPLE_RESOURCE_GROUP_UPDATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/group/update"; + public static final String SAMPLE_RESOURCE_GROUP_DELETE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/group/delete"; + public static final String SAMPLE_RESOURCE_GROUP_SEARCH_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/group/search"; + public static final String RESOURCE_SHARING_MIGRATION_ENDPOINT = "_plugins/_security/api/resources/migrate"; public static final String SECURITY_SHARE_ENDPOINT = "_plugins/_security/api/resource/share"; public static final String SECURITY_TYPES_ENDPOINT = "_plugins/_security/api/resource/types"; public static final String SECURITY_LIST_ENDPOINT = "_plugins/_security/api/resource/list"; public static LocalCluster newCluster(boolean featureEnabled, boolean systemIndexEnabled) { - return newCluster(featureEnabled, systemIndexEnabled, List.of(RESOURCE_TYPE)); + return newCluster(featureEnabled, systemIndexEnabled, List.of(RESOURCE_TYPE, RESOURCE_GROUP_TYPE)); } public static LocalCluster newCluster(boolean featureEnabled, boolean systemIndexEnabled, List protectedResourceTypes) { @@ -317,6 +328,15 @@ public String createSampleResourceAs(TestSecurityConfig.User user, Header... hea } } + public String createSampleResourceGroupAs(TestSecurityConfig.User user, Header... headers) { + try (TestRestClient client = cluster.getRestClient(user)) { + String sample = "{\"name\":\"samplegroup\"}"; + TestRestClient.HttpResponse resp = client.putJson(SAMPLE_RESOURCE_GROUP_CREATE_ENDPOINT, sample, headers); + resp.assertStatusCode(HttpStatus.SC_OK); + return resp.getTextFromJsonBody("/message").split(":")[1].trim(); + } + } + public String createRawResourceAs(CertificateData adminCert) { try (TestRestClient client = cluster.getRestClient(adminCert)) { String sample = "{\"name\":\"sample\"}"; @@ -338,6 +358,15 @@ public void assertApiGet(String resourceId, TestSecurityConfig.User user, int st assertGet(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId, user, status, expectedResourceName); } + public void getResourceGroupAndAssert( + String resourceGroupId, + TestSecurityConfig.User user, + int status, + String expectedResourceName + ) { + assertGet(SAMPLE_RESOURCE_GROUP_GET_ENDPOINT + "/" + resourceGroupId, user, status, expectedResourceName); + } + private void assertGet(String endpoint, TestSecurityConfig.User user, int status, String expectedString) { try (TestRestClient client = cluster.getRestClient(user)) { TestRestClient.HttpResponse response = client.get(endpoint); @@ -478,6 +507,10 @@ public void assertApiUpdate(String resourceId, TestSecurityConfig.User user, Str assertUpdate(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, newName, user, status); } + public void updateResourceGroupAndAssert(String resourceGroupId, TestSecurityConfig.User user, String newName, int status) { + assertUpdate(SAMPLE_RESOURCE_GROUP_UPDATE_ENDPOINT + "/" + resourceGroupId, newName, user, status); + } + public void assertDirectUpdate(String resourceId, TestSecurityConfig.User user, String newName, int status) { assertUpdate(RESOURCE_INDEX_NAME + "/_doc/" + resourceId + "?refresh=true", newName, user, status); } @@ -522,6 +555,22 @@ public void assertApiShare( } } + public void shareResourceGroupAndAssert( + String resourceId, + TestSecurityConfig.User user, + TestSecurityConfig.User target, + String accessLevel, + int status + ) { + try (TestRestClient client = cluster.getRestClient(user)) { + TestRestClient.HttpResponse response = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(resourceId, RESOURCE_GROUP_TYPE, accessLevel, Recipient.USERS, target.getName()) + ); + response.assertStatusCode(status); + } + } + public void assertApiShareByRole( String resourceId, TestSecurityConfig.User user, @@ -538,6 +587,22 @@ public void assertApiShareByRole( } } + public void shareResourceGroupByRoleAndAssert( + String resourceId, + TestSecurityConfig.User user, + String targetRole, + String accessLevel, + int status + ) { + try (TestRestClient client = cluster.getRestClient(user)) { + TestRestClient.HttpResponse response = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(resourceId, RESOURCE_GROUP_TYPE, accessLevel, Recipient.ROLES, targetRole) + ); + response.assertStatusCode(status); + } + } + public void assertApiRevoke( String resourceId, TestSecurityConfig.User user, @@ -555,6 +620,23 @@ public void assertApiRevoke( } } + public void revokeResourceGroupAndAssert( + String resourceId, + TestSecurityConfig.User user, + TestSecurityConfig.User target, + String accessLevel, + int status + ) { + PatchSharingInfoPayloadBuilder patchBuilder = new PatchSharingInfoPayloadBuilder(); + patchBuilder.resourceType(RESOURCE_GROUP_TYPE); + patchBuilder.resourceId(resourceId); + patchBuilder.revoke(new Recipients(Map.of(Recipient.USERS, Set.of(target.getName()))), accessLevel); + try (TestRestClient client = cluster.getRestClient(user)) { + TestRestClient.HttpResponse response = client.patch(TestUtils.SECURITY_SHARE_ENDPOINT, patchBuilder.build()); + response.assertStatusCode(status); + } + } + public void assertDirectDelete(String resourceId, TestSecurityConfig.User user, int status) { assertDelete(RESOURCE_INDEX_NAME + "/_doc/" + resourceId, user, status); } @@ -567,6 +649,10 @@ public void assertApiDelete(String resourceId, TestSecurityConfig.User user, int assertDelete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId, user, status); } + public void deleteResourceGroupAndAssert(String resourceGroupId, TestSecurityConfig.User user, int status) { + assertDelete(SAMPLE_RESOURCE_GROUP_DELETE_ENDPOINT + "/" + resourceGroupId, user, status); + } + private void assertDelete(String endpoint, TestSecurityConfig.User user, int status) { try (TestRestClient client = cluster.getRestClient(user)) { TestRestClient.HttpResponse response = client.delete(endpoint); diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resourcegroup/SampleResourceGroupTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resourcegroup/SampleResourceGroupTests.java new file mode 100644 index 0000000000..32b1e0bc8c --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resourcegroup/SampleResourceGroupTests.java @@ -0,0 +1,271 @@ +/* + * SPDX-License-Identifier: Apache-2.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.resourcegroup; + +import com.carrotsearch.randomizedtesting.RandomizedRunner; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.sample.resource.TestUtils; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.opensearch.sample.resource.TestUtils.FULL_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.LIMITED_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_GROUP_READ_ONLY_RESOURCE_AG; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.newCluster; +import static org.opensearch.sample.utils.Constants.RESOURCE_GROUP_TYPE; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +/** + * Test resource access to a resource shared with mixed access-levels. Some users are shared at read_only, others at full_access. + * All tests are against USER_ADMIN's resource created during setup. + */ +@RunWith(RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class SampleResourceGroupTests { + + @ClassRule + public static LocalCluster cluster = newCluster(true, true); + + private final TestUtils.ApiHelper api = new TestUtils.ApiHelper(cluster); + private String resourceGroupId; + + @Before + public void setup() { + resourceGroupId = api.createSampleResourceGroupAs(USER_ADMIN); + api.awaitSharingEntry(resourceGroupId); // wait until sharing entry is created + } + + @After + public void cleanup() { + api.wipeOutResourceEntries(); + } + + private void assertNoAccessBeforeSharing(TestSecurityConfig.User user) { + api.getResourceGroupAndAssert(resourceGroupId, user, HttpStatus.SC_FORBIDDEN, ""); + api.updateResourceGroupAndAssert(resourceGroupId, user, "sampleUpdateAdmin", HttpStatus.SC_FORBIDDEN); + api.deleteResourceGroupAndAssert(resourceGroupId, user, HttpStatus.SC_FORBIDDEN); + + api.shareResourceGroupAndAssert(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.revokeResourceGroupAndAssert(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + } + + private void assertReadOnly(TestSecurityConfig.User user) { + api.getResourceGroupAndAssert(resourceGroupId, user, HttpStatus.SC_OK, "sample"); + api.updateResourceGroupAndAssert(resourceGroupId, user, "sampleUpdateAdmin", HttpStatus.SC_FORBIDDEN); + api.deleteResourceGroupAndAssert(resourceGroupId, user, HttpStatus.SC_FORBIDDEN); + + api.shareResourceGroupAndAssert(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.revokeResourceGroupAndAssert(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + } + + private void assertFullAccess(TestSecurityConfig.User user) { + api.getResourceGroupAndAssert(resourceGroupId, user, HttpStatus.SC_OK, "sample"); + api.updateResourceGroupAndAssert(resourceGroupId, user, "sampleUpdateAdmin", HttpStatus.SC_OK); + api.shareResourceGroupAndAssert(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_OK); + api.revokeResourceGroupAndAssert(resourceGroupId, user, USER_ADMIN, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_OK); + api.awaitSharingEntry(resourceGroupId); + api.deleteResourceGroupAndAssert(resourceGroupId, user, HttpStatus.SC_OK); + } + + @Test + public void multipleUsers_multipleLevels() { + assertNoAccessBeforeSharing(FULL_ACCESS_USER); + assertNoAccessBeforeSharing(LIMITED_ACCESS_USER); + // 1. share at read-only for full-access user and at full-access for limited-perms user + api.shareResourceGroupAndAssert( + resourceGroupId, + USER_ADMIN, + FULL_ACCESS_USER, + SAMPLE_GROUP_READ_ONLY_RESOURCE_AG, + HttpStatus.SC_OK + ); + api.shareResourceGroupAndAssert( + resourceGroupId, + USER_ADMIN, + LIMITED_ACCESS_USER, + SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, + HttpStatus.SC_OK + ); + api.awaitSharingEntry(resourceGroupId, FULL_ACCESS_USER.getName()); + api.awaitSharingEntry(resourceGroupId, LIMITED_ACCESS_USER.getName()); + + // 2. check read-only access for full-access user + assertReadOnly(FULL_ACCESS_USER); + + // 3. limited access user shares with full-access user at sampleAllAG + api.shareResourceGroupAndAssert( + resourceGroupId, + LIMITED_ACCESS_USER, + FULL_ACCESS_USER, + SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, + HttpStatus.SC_OK + ); + api.awaitSharingEntry(resourceGroupId, FULL_ACCESS_USER.getName()); + + // 4. full-access user now has full-access to admin's resource + assertFullAccess(FULL_ACCESS_USER); + } + + @Test + public void multipleUsers_sameLevel() { + assertNoAccessBeforeSharing(FULL_ACCESS_USER); + assertNoAccessBeforeSharing(LIMITED_ACCESS_USER); + + // 1. share with both users at read-only level + api.shareResourceGroupAndAssert( + resourceGroupId, + USER_ADMIN, + FULL_ACCESS_USER, + SAMPLE_GROUP_READ_ONLY_RESOURCE_AG, + HttpStatus.SC_OK + ); + api.shareResourceGroupAndAssert( + resourceGroupId, + USER_ADMIN, + LIMITED_ACCESS_USER, + SAMPLE_GROUP_READ_ONLY_RESOURCE_AG, + HttpStatus.SC_OK + ); + api.awaitSharingEntry(resourceGroupId, SAMPLE_GROUP_READ_ONLY_RESOURCE_AG); + + // 2. assert both now have read-only access + assertReadOnly(LIMITED_ACCESS_USER); + } + + @Test + public void sameUser_multipleLevels() { + assertNoAccessBeforeSharing(LIMITED_ACCESS_USER); + + // 1. share with user at read-only level + api.shareResourceGroupAndAssert( + resourceGroupId, + USER_ADMIN, + LIMITED_ACCESS_USER, + SAMPLE_GROUP_READ_ONLY_RESOURCE_AG, + HttpStatus.SC_OK + ); + api.awaitSharingEntry(resourceGroupId, LIMITED_ACCESS_USER.getName()); + + // 2. assert user now has read-only access + assertReadOnly(LIMITED_ACCESS_USER); + + // 3. share with user at full-access level + api.shareResourceGroupAndAssert( + resourceGroupId, + USER_ADMIN, + LIMITED_ACCESS_USER, + SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, + HttpStatus.SC_OK + ); + api.awaitSharingEntry(resourceGroupId, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG); + + // 4. assert user now has full access + assertFullAccess(LIMITED_ACCESS_USER); + } + + private String getActualRoleName(TestSecurityConfig.User user, String baseRoleName) { + return "user_" + user.getName() + "__" + baseRoleName; + } + + @Test + public void multipleRoles_multipleLevels() { + assertNoAccessBeforeSharing(FULL_ACCESS_USER); + assertNoAccessBeforeSharing(LIMITED_ACCESS_USER); + + String fullAccessUserRole = getActualRoleName(FULL_ACCESS_USER, "shared_role"); + String limitedAccessUserRole = getActualRoleName(LIMITED_ACCESS_USER, "shared_role_limited_perms"); + + // 1. share at read-only for shared_role and at full-access for shared_role_limited_perms + api.shareResourceGroupByRoleAndAssert( + resourceGroupId, + USER_ADMIN, + fullAccessUserRole, + SAMPLE_GROUP_READ_ONLY_RESOURCE_AG, + HttpStatus.SC_OK + ); + api.shareResourceGroupByRoleAndAssert( + resourceGroupId, + USER_ADMIN, + limitedAccessUserRole, + SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, + HttpStatus.SC_OK + ); + api.awaitSharingEntry(resourceGroupId, fullAccessUserRole); + api.awaitSharingEntry(resourceGroupId, limitedAccessUserRole); + + // 2. check read-only access for FULL_ACCESS_USER (has shared_role) + assertReadOnly(FULL_ACCESS_USER); + + // 3. LIMITED_ACCESS_USER (has shared_role_limited_perms) shares with shared_role at sampleAllAG + api.shareResourceGroupByRoleAndAssert( + resourceGroupId, + LIMITED_ACCESS_USER, + fullAccessUserRole, + SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, + HttpStatus.SC_OK + ); + api.awaitSharingEntry(resourceGroupId, fullAccessUserRole); + + // 4. FULL_ACCESS_USER now has full-access to admin's resource + assertFullAccess(FULL_ACCESS_USER); + } + + @Test + public void initialShare_multipleLevels() { + assertNoAccessBeforeSharing(FULL_ACCESS_USER); + assertNoAccessBeforeSharing(LIMITED_ACCESS_USER); + + String shareWithPayload = """ + { + "resource_id": "%s", + "resource_type": "%s", + "share_with": { + "%s" : { + "users": ["%s"] + }, + "%s" : { + "users": ["%s"] + } + } + } + """.formatted( + resourceGroupId, + RESOURCE_GROUP_TYPE, + SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, + LIMITED_ACCESS_USER.getName(), + SAMPLE_GROUP_READ_ONLY_RESOURCE_AG, + FULL_ACCESS_USER.getName() + ); + + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + TestRestClient.HttpResponse response = client.putJson(SECURITY_SHARE_ENDPOINT, shareWithPayload); + response.assertStatusCode(HttpStatus.SC_OK); + // wait for one of the users to be populated + api.awaitSharingEntry(resourceGroupId, FULL_ACCESS_USER.getName()); + } + + // full-access user has read-only perm + assertReadOnly(FULL_ACCESS_USER); + + // limited access user has full-access + assertFullAccess(LIMITED_ACCESS_USER); + + } + +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceGroup.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceGroup.java new file mode 100644 index 0000000000..fd566af87e --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceGroup.java @@ -0,0 +1,97 @@ +/* + * SPDX-License-Identifier: Apache-2.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 org.opensearch.core.ParseField; +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.ConstructingObjectParser; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +import static org.opensearch.core.xcontent.ConstructingObjectParser.constructorArg; +import static org.opensearch.core.xcontent.ConstructingObjectParser.optionalConstructorArg; +import static org.opensearch.sample.utils.Constants.RESOURCE_GROUP_TYPE; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; + +/** + * Sample resource group declared by this plugin. + */ +public class SampleResourceGroup implements NamedWriteable, ToXContentObject { + + private String name; + private String description; + + public SampleResourceGroup() throws IOException { + super(); + } + + public SampleResourceGroup(StreamInput in) throws IOException { + this.name = in.readString(); + this.description = in.readString(); + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + RESOURCE_TYPE, + true, + a -> { + SampleResourceGroup s; + try { + s = new SampleResourceGroup(); + } catch (IOException e) { + throw new RuntimeException(e); + } + s.setName((String) a[0]); + s.setDescription((String) a[1]); + return s; + } + ); + + static { + PARSER.declareString(constructorArg(), new ParseField("name")); + PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("description")); + } + + public static SampleResourceGroup fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().field("name", name).field("description", description).endObject(); + } + + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeString(description); + } + + public void setName(String name) { + this.name = name; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getName() { + return name; + } + + @Override + public String getWriteableName() { + return RESOURCE_GROUP_TYPE; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceGroupExtension.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceGroupExtension.java new file mode 100644 index 0000000000..822ae612e7 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourceGroupExtension.java @@ -0,0 +1,27 @@ +package org.opensearch.sample; + +import java.util.Set; + +import org.opensearch.sample.client.ResourceSharingClientAccessor; +import org.opensearch.security.spi.resources.ResourceProvider; +import org.opensearch.security.spi.resources.ResourceSharingExtension; +import org.opensearch.security.spi.resources.client.ResourceSharingClient; + +import static org.opensearch.sample.utils.Constants.RESOURCE_GROUP_TYPE; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +/** + * Responsible for parsing the XContent into a SampleResourceGroup object. + */ +public class SampleResourceGroupExtension implements ResourceSharingExtension { + + @Override + public Set getResourceProviders() { + return Set.of(new ResourceProvider(RESOURCE_GROUP_TYPE, RESOURCE_INDEX_NAME)); + } + + @Override + public void assignResourceSharingClient(ResourceSharingClient resourceSharingClient) { + ResourceSharingClientAccessor.getInstance().setResourceSharingClient(resourceSharingClient); + } +} 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 e1b63cadd1..3cddcaeefd 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 @@ -50,6 +50,20 @@ import org.opensearch.sample.resource.actions.transport.GetResourceTransportAction; import org.opensearch.sample.resource.actions.transport.SearchResourceTransportAction; import org.opensearch.sample.resource.actions.transport.UpdateResourceTransportAction; +import org.opensearch.sample.resourcegroup.actions.rest.create.CreateResourceGroupAction; +import org.opensearch.sample.resourcegroup.actions.rest.create.CreateResourceGroupRestAction; +import org.opensearch.sample.resourcegroup.actions.rest.create.UpdateResourceGroupAction; +import org.opensearch.sample.resourcegroup.actions.rest.delete.DeleteResourceGroupAction; +import org.opensearch.sample.resourcegroup.actions.rest.delete.DeleteResourceGroupRestAction; +import org.opensearch.sample.resourcegroup.actions.rest.get.GetResourceGroupAction; +import org.opensearch.sample.resourcegroup.actions.rest.get.GetResourceGroupRestAction; +import org.opensearch.sample.resourcegroup.actions.rest.search.SearchResourceGroupAction; +import org.opensearch.sample.resourcegroup.actions.rest.search.SearchResourceGroupRestAction; +import org.opensearch.sample.resourcegroup.actions.transport.CreateResourceGroupTransportAction; +import org.opensearch.sample.resourcegroup.actions.transport.DeleteResourceGroupTransportAction; +import org.opensearch.sample.resourcegroup.actions.transport.GetResourceGroupTransportAction; +import org.opensearch.sample.resourcegroup.actions.transport.SearchResourceGroupTransportAction; +import org.opensearch.sample.resourcegroup.actions.transport.UpdateResourceGroupTransportAction; import org.opensearch.sample.secure.actions.rest.create.SecurePluginAction; import org.opensearch.sample.secure.actions.rest.create.SecurePluginRestAction; import org.opensearch.sample.secure.actions.transport.SecurePluginTransportAction; @@ -105,6 +119,10 @@ public List getRestHandlers( handlers.add(new GetResourceRestAction()); handlers.add(new DeleteResourceRestAction()); handlers.add(new SearchResourceRestAction()); + handlers.add(new CreateResourceGroupRestAction()); + handlers.add(new GetResourceGroupRestAction()); + handlers.add(new DeleteResourceGroupRestAction()); + handlers.add(new SearchResourceGroupRestAction()); handlers.add(new SecurePluginRestAction()); return handlers; @@ -118,6 +136,11 @@ public List getRestHandlers( actions.add(new ActionHandler<>(UpdateResourceAction.INSTANCE, UpdateResourceTransportAction.class)); actions.add(new ActionHandler<>(DeleteResourceAction.INSTANCE, DeleteResourceTransportAction.class)); actions.add(new ActionHandler<>(SearchResourceAction.INSTANCE, SearchResourceTransportAction.class)); + actions.add(new ActionHandler<>(CreateResourceGroupAction.INSTANCE, CreateResourceGroupTransportAction.class)); + actions.add(new ActionHandler<>(GetResourceGroupAction.INSTANCE, GetResourceGroupTransportAction.class)); + actions.add(new ActionHandler<>(UpdateResourceGroupAction.INSTANCE, UpdateResourceGroupTransportAction.class)); + actions.add(new ActionHandler<>(DeleteResourceGroupAction.INSTANCE, DeleteResourceGroupTransportAction.class)); + actions.add(new ActionHandler<>(SearchResourceGroupAction.INSTANCE, SearchResourceGroupTransportAction.class)); actions.add(new ActionHandler<>(SecurePluginAction.INSTANCE, SecurePluginTransportAction.class)); return actions; } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupAction.java new file mode 100644 index 0000000000..d1a0a4c6d9 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupAction.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.resourcegroup.actions.rest.create; + +import org.opensearch.action.ActionType; + +/** + * Action to create a sample resource + */ +public class CreateResourceGroupAction extends ActionType { + /** + * Create sample resource action instance + */ + public static final CreateResourceGroupAction INSTANCE = new CreateResourceGroupAction(); + /** + * Create sample resource action name + */ + public static final String NAME = "cluster:admin/sample-resource-plugin/group/create"; + + private CreateResourceGroupAction() { + super(NAME, CreateResourceGroupResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupRequest.java new file mode 100644 index 0000000000..0e7db2dc75 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupRequest.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.resourcegroup.actions.rest.create; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.action.DocRequest; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.sample.SampleResource; + +import static org.opensearch.sample.utils.Constants.RESOURCE_GROUP_TYPE; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +/** + * Request object for CreateSampleResourceGroup transport action + */ +public class CreateResourceGroupRequest extends ActionRequest implements DocRequest { + + private final SampleResource resource; + + /** + * Default constructor + */ + public CreateResourceGroupRequest(SampleResource resource) { + this.resource = resource; + } + + public CreateResourceGroupRequest(StreamInput in) throws IOException { + this.resource = in.readNamedWriteable(SampleResource.class); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + resource.writeTo(out); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public SampleResource getResource() { + return this.resource; + } + + @Override + public String type() { + return RESOURCE_GROUP_TYPE; + } + + @Override + public String index() { + return RESOURCE_INDEX_NAME; + } + + @Override + public String id() { + return null; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupResponse.java new file mode 100644 index 0000000000..8cf259e661 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupResponse.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.resourcegroup.actions.rest.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 CreateResourceGroupResponse extends ActionResponse implements ToXContentObject { + private final String message; + + /** + * Default constructor + * + * @param message The message + */ + public CreateResourceGroupResponse(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 CreateResourceGroupResponse(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/resourcegroup/actions/rest/create/CreateResourceGroupRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupRestAction.java new file mode 100644 index 0000000000..3cd4a632b0 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupRestAction.java @@ -0,0 +1,97 @@ +/* + * SPDX-License-Identifier: Apache-2.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.resourcegroup.actions.rest.create; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +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; +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 CreateResourceGroupRestAction extends BaseRestHandler { + + public CreateResourceGroupRestAction() {} + + @Override + public List routes() { + return List.of( + new Route(PUT, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/group/create"), + new Route(POST, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/group/update/{resource_id}") + ); + } + + @Override + public String getName() { + return "create_update_sample_resource_group"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + return switch (request.method()) { + case PUT -> createResource(source, client); + case POST -> updateResource(source, request.param("resource_id"), 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 = getAttributes(source); + SampleResource resource = new SampleResource(); + resource.setName(name); + resource.setDescription(description); + resource.setAttributes(attributes); + final UpdateResourceGroupRequest updateResourceRequest = new UpdateResourceGroupRequest(resourceId, resource); + return channel -> client.executeLocally( + UpdateResourceGroupAction.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 = getAttributes(source); + SampleResource resource = new SampleResource(); + resource.setName(name); + resource.setDescription(description); + resource.setAttributes(attributes); + final CreateResourceGroupRequest createSampleResourceRequest = new CreateResourceGroupRequest(resource); + return channel -> client.executeLocally( + CreateResourceGroupAction.INSTANCE, + createSampleResourceRequest, + new RestToXContentListener<>(channel) + ); + } + + // NOTE: Do NOT use @SuppressWarnings("unchecked") on untrusted data in production code. This is used here only to keep the code simple + @SuppressWarnings("unchecked") + private Map getAttributes(Map source) { + return source.containsKey("attributes") ? (Map) source.get("attributes") : null; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupAction.java new file mode 100644 index 0000000000..7dfbc8b57d --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupAction.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.resourcegroup.actions.rest.create; + +import org.opensearch.action.ActionType; + +/** + * Action to update a sample resource + */ +public class UpdateResourceGroupAction extends ActionType { + /** + * Update sample resource action instance + */ + public static final UpdateResourceGroupAction INSTANCE = new UpdateResourceGroupAction(); + /** + * Update sample resource action name + */ + public static final String NAME = "cluster:admin/sample-resource-plugin/group/update"; + + private UpdateResourceGroupAction() { + super(NAME, CreateResourceGroupResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupRequest.java new file mode 100644 index 0000000000..c13dfcb864 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupRequest.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.resourcegroup.actions.rest.create; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.action.DocRequest; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.sample.SampleResource; + +import static org.opensearch.sample.utils.Constants.RESOURCE_GROUP_TYPE; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +/** + * Request object for UpdateResource transport action + */ +public class UpdateResourceGroupRequest extends ActionRequest implements DocRequest { + + private final String resourceId; + private final SampleResource resource; + + /** + * Default constructor + */ + public UpdateResourceGroupRequest(String resourceId, SampleResource resource) { + this.resourceId = resourceId; + this.resource = resource; + } + + public UpdateResourceGroupRequest(StreamInput in) throws IOException { + this.resourceId = in.readString(); + this.resource = in.readNamedWriteable(SampleResource.class); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(resourceId); + resource.writeTo(out); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public SampleResource getResource() { + return this.resource; + } + + public String getResourceId() { + return this.resourceId; + } + + @Override + public String type() { + return RESOURCE_GROUP_TYPE; + } + + @Override + public String index() { + return RESOURCE_INDEX_NAME; + } + + @Override + public String id() { + return resourceId; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupAction.java new file mode 100644 index 0000000000..921d10ebde --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupAction.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.resourcegroup.actions.rest.delete; + +import org.opensearch.action.ActionType; + +/** + * Action to delete a sample resource + */ +public class DeleteResourceGroupAction extends ActionType { + /** + * Delete sample resource action instance + */ + public static final DeleteResourceGroupAction INSTANCE = new DeleteResourceGroupAction(); + /** + * Delete sample resource action name + */ + public static final String NAME = "cluster:admin/sample-resource-plugin/group/delete"; + + private DeleteResourceGroupAction() { + super(NAME, DeleteResourceGroupResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRequest.java new file mode 100644 index 0000000000..c122d0c2be --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRequest.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.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.resourcegroup.actions.rest.delete; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.action.DocRequest; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +import static org.opensearch.sample.utils.Constants.RESOURCE_GROUP_TYPE; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +/** + * Request object for DeleteSampleResource transport action + */ +public class DeleteResourceGroupRequest extends ActionRequest implements DocRequest { + + private final String resourceId; + + /** + * Default constructor + */ + public DeleteResourceGroupRequest(String resourceId) { + this.resourceId = resourceId; + } + + public DeleteResourceGroupRequest(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; + } + + @Override + public String type() { + return RESOURCE_GROUP_TYPE; + } + + @Override + public String index() { + return RESOURCE_INDEX_NAME; + } + + @Override + public String id() { + return resourceId; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupResponse.java new file mode 100644 index 0000000000..ac70bd9bb6 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupResponse.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.resourcegroup.actions.rest.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; + +/** + * Response to a DeleteSampleResourceRequest + */ +public class DeleteResourceGroupResponse extends ActionResponse implements ToXContentObject { + private final String message; + + /** + * Default constructor + * + * @param message The message + */ + public DeleteResourceGroupResponse(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 DeleteResourceGroupResponse(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/resourcegroup/actions/rest/delete/DeleteResourceGroupRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRestAction.java new file mode 100644 index 0000000000..ce945c6056 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRestAction.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.resourcegroup.actions.rest.delete; + +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.DELETE; +import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; + +/** + * Rest Action to delete a Sample Resource. + */ +public class DeleteResourceGroupRestAction extends BaseRestHandler { + + public DeleteResourceGroupRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(DELETE, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/group/delete/{resource_id}")); + } + + @Override + public String getName() { + return "delete_sample_resource_group"; + } + + @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 DeleteResourceGroupRequest createSampleResourceRequest = new DeleteResourceGroupRequest(resourceId); + return channel -> client.executeLocally( + DeleteResourceGroupAction.INSTANCE, + createSampleResourceRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupAction.java new file mode 100644 index 0000000000..3442bf1f70 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupAction.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.resourcegroup.actions.rest.get; + +import org.opensearch.action.ActionType; + +/** + * Action to get a sample resource + */ +public class GetResourceGroupAction extends ActionType { + /** + * Get sample resource action instance + */ + public static final GetResourceGroupAction INSTANCE = new GetResourceGroupAction(); + /** + * Get sample resource action name + */ + public static final String NAME = "cluster:admin/sample-resource-plugin/group/get"; + + private GetResourceGroupAction() { + super(NAME, GetResourceGroupResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupRequest.java new file mode 100644 index 0000000000..c10e9f25ae --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupRequest.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.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.resourcegroup.actions.rest.get; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.action.DocRequest; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +import static org.opensearch.sample.utils.Constants.RESOURCE_GROUP_TYPE; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +/** + * Request object for GetSampleResourceGroup transport action + */ +public class GetResourceGroupRequest extends ActionRequest implements DocRequest { + + private final String resourceId; + + /** + * Default constructor + */ + public GetResourceGroupRequest(String resourceId) { + this.resourceId = resourceId; + } + + public GetResourceGroupRequest(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; + } + + @Override + public String type() { + return RESOURCE_GROUP_TYPE; + } + + @Override + public String index() { + return RESOURCE_INDEX_NAME; + } + + @Override + public String id() { + return resourceId; + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupResponse.java new file mode 100644 index 0000000000..8cfd10a05f --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupResponse.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.sample.resourcegroup.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; +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 GetResourceGroupResponse extends ActionResponse implements ToXContentObject { + private final Set resources; + + /** + * Default constructor + * + * @param resources The resources + */ + public GetResourceGroupResponse(Set resources) { + this.resources = resources; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeCollection(resources, (o, r) -> r.writeTo(o)); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public GetResourceGroupResponse(final StreamInput in) throws IOException { + 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/resourcegroup/actions/rest/get/GetResourceGroupRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupRestAction.java new file mode 100644 index 0000000000..4a56c311b9 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupRestAction.java @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.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.resourcegroup.actions.rest.get; + +import java.util.List; + +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 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 GetResourceGroupRestAction extends BaseRestHandler { + + public GetResourceGroupRestAction() {} + + @Override + public List routes() { + return List.of( + new Route(GET, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/group/get/{resource_id}"), + new Route(GET, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/group/get") + ); + } + + @Override + public String getName() { + return "get_sample_resource_group"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + String resourceId = request.param("resource_id"); + + final GetResourceGroupRequest getResourceRequest = new GetResourceGroupRequest(resourceId); + return channel -> client.executeLocally(GetResourceGroupAction.INSTANCE, getResourceRequest, new RestToXContentListener<>(channel)); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupAction.java new file mode 100644 index 0000000000..c49d31c719 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupAction.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.resourcegroup.actions.rest.search; + +import org.opensearch.action.ActionType; +import org.opensearch.action.search.SearchResponse; + +/** + * Action to search sample resources + */ +public class SearchResourceGroupAction extends ActionType { + + public static final SearchResourceGroupAction INSTANCE = new SearchResourceGroupAction(); + + public static final String NAME = "cluster:admin/sample-resource-plugin/group/search"; + + private SearchResourceGroupAction() { + super(NAME, SearchResponse::new); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupRestAction.java new file mode 100644 index 0000000000..9a28bb18cc --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupRestAction.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.resourcegroup.actions.rest.search; + +import java.io.IOException; +import java.util.List; + +import org.opensearch.action.search.SearchRequest; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.search.builder.SearchSourceBuilder; +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.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; + +/** + * Rest action to search sample resource(s) + */ +public class SearchResourceGroupRestAction extends BaseRestHandler { + + public SearchResourceGroupRestAction() {} + + @Override + public List routes() { + return List.of( + new Route(GET, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/group/search"), + new Route(POST, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/group/search") + ); + } + + @Override + public String getName() { + return "search_sample_resource_group"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + + if (request.hasContentOrSourceParam()) { + try (XContentParser parser = request.contentOrSourceParamParser()) { + searchSourceBuilder.parseXContent(parser); + } + } else { + // Optional: default query if no body is provided + searchSourceBuilder.query(QueryBuilders.matchAllQuery()); + } + + SearchRequest searchRequest = new SearchRequest().indices(RESOURCE_INDEX_NAME).source(searchSourceBuilder); + + return channel -> client.executeLocally(SearchResourceGroupAction.INSTANCE, searchRequest, new RestToXContentListener<>(channel)); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/CreateResourceGroupTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/CreateResourceGroupTransportAction.java new file mode 100644 index 0000000000..6856417839 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/CreateResourceGroupTransportAction.java @@ -0,0 +1,133 @@ +/* + * SPDX-License-Identifier: Apache-2.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.resourcegroup.actions.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.sample.SampleResource; +import org.opensearch.sample.resourcegroup.actions.rest.create.CreateResourceGroupAction; +import org.opensearch.sample.resourcegroup.actions.rest.create.CreateResourceGroupRequest; +import org.opensearch.sample.resourcegroup.actions.rest.create.CreateResourceGroupResponse; +import org.opensearch.sample.utils.PluginClient; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +/** + * Transport action for creating a new resource. + */ +public class CreateResourceGroupTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(CreateResourceGroupTransportAction.class); + + private final TransportService transportService; + private final PluginClient pluginClient; + + @Inject + public CreateResourceGroupTransportAction(TransportService transportService, ActionFilters actionFilters, PluginClient pluginClient) { + super(CreateResourceGroupAction.NAME, transportService, actionFilters, CreateResourceGroupRequest::new); + this.transportService = transportService; + this.pluginClient = pluginClient; + } + + @Override + protected void doExecute(Task task, CreateResourceGroupRequest request, ActionListener listener) { + createResource(request, listener); + } + + private void createResource(CreateResourceGroupRequest request, ActionListener listener) { + SampleResource sample = request.getResource(); + + // 1. Read mapping JSON from the config file + final String mappingJson; + try { + URL url = CreateResourceGroupTransportAction.class.getClassLoader().getResource("mappings.json"); + if (url == null) { + listener.onFailure(new IllegalStateException("mappings.json not found on classpath")); + return; + } + try (InputStream is = url.openStream()) { + mappingJson = new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } catch (IOException e) { + listener.onFailure(new RuntimeException("Failed to read mappings.json from classpath", e)); + return; + } + + // 2. Ensure index exists with mapping, then index the doc + ensureIndexWithMapping(pluginClient, mappingJson, ActionListener.wrap(v -> { + try (XContentBuilder builder = org.opensearch.common.xcontent.XContentFactory.jsonBuilder()) { + IndexRequest ir = pluginClient.prepareIndex(RESOURCE_INDEX_NAME) + .setWaitForActiveShards(1) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .setSource(sample.toXContent(builder, ToXContent.EMPTY_PARAMS)) + .request(); + + log.debug("Index Request: {}", ir); + + pluginClient.index(ir, ActionListener.wrap(idxResponse -> { + log.debug("Created resource: {}", idxResponse.getId()); + listener.onResponse(new CreateResourceGroupResponse("Created resource: " + idxResponse.getId())); + }, listener::onFailure)); + } catch (IOException e) { + listener.onFailure(new RuntimeException(e)); + } + }, listener::onFailure)); + } + + /** + * Ensures the index exists with the provided mapping. + * - If the index does not exist: creates it with the mapping. + * - If the index exists: updates (puts) the mapping. + */ + private void ensureIndexWithMapping(PluginClient pluginClient, String mappingJson, ActionListener listener) { + String indexName = RESOURCE_INDEX_NAME; + pluginClient.admin().indices().prepareExists(indexName).execute(ActionListener.wrap(existsResp -> { + if (!existsResp.isExists()) { + // Create index with mapping + pluginClient.admin().indices().prepareCreate(indexName).setMapping(mappingJson).execute(ActionListener.wrap(createResp -> { + if (!createResp.isAcknowledged()) { + listener.onFailure(new IllegalStateException("CreateIndex not acknowledged for " + indexName)); + return; + } + listener.onResponse(null); + }, listener::onFailure)); + } else { + // Update mapping on existing index + pluginClient.admin() + .indices() + .preparePutMapping(indexName) + .setSource(mappingJson, XContentType.JSON) + .execute(ActionListener.wrap(ack -> { + if (!ack.isAcknowledged()) { + listener.onFailure(new IllegalStateException("PutMapping not acknowledged for " + indexName)); + return; + } + listener.onResponse(null); + }, listener::onFailure)); + } + }, listener::onFailure)); + } + +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/DeleteResourceGroupTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/DeleteResourceGroupTransportAction.java new file mode 100644 index 0000000000..31e9cdefff --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/DeleteResourceGroupTransportAction.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.resourcegroup.actions.transport; + +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.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.resourcegroup.actions.rest.delete.DeleteResourceGroupAction; +import org.opensearch.sample.resourcegroup.actions.rest.delete.DeleteResourceGroupRequest; +import org.opensearch.sample.resourcegroup.actions.rest.delete.DeleteResourceGroupResponse; +import org.opensearch.sample.utils.PluginClient; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +/** + * Transport action for deleting a resource + */ +public class DeleteResourceGroupTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(DeleteResourceGroupTransportAction.class); + + private final TransportService transportService; + private final PluginClient pluginClient; + + @Inject + public DeleteResourceGroupTransportAction(TransportService transportService, ActionFilters actionFilters, PluginClient pluginClient) { + super(DeleteResourceGroupAction.NAME, transportService, actionFilters, DeleteResourceGroupRequest::new); + this.transportService = transportService; + this.pluginClient = pluginClient; + } + + @Override + protected void doExecute(Task task, DeleteResourceGroupRequest request, ActionListener listener) { + String resourceId = request.getResourceId(); + if (resourceId == null || resourceId.isEmpty()) { + listener.onFailure(new IllegalArgumentException("Resource group ID cannot be null or empty")); + return; + } + ActionListener deleteResponseListener = ActionListener.wrap(deleteResponse -> { + if (deleteResponse.getResult() == DocWriteResponse.Result.NOT_FOUND) { + listener.onFailure(new ResourceNotFoundException("Resource group " + resourceId + " not found.")); + } else { + listener.onResponse(new DeleteResourceGroupResponse("Resource group " + resourceId + " deleted successfully.")); + } + }, exception -> { + log.error("Failed to delete resource group: " + resourceId, exception); + listener.onFailure(exception); + }); + + deleteResource(resourceId, deleteResponseListener); + } + + private void deleteResource(String resourceId, ActionListener listener) { + DeleteRequest deleteRequest = new DeleteRequest(RESOURCE_INDEX_NAME, resourceId).setRefreshPolicy( + WriteRequest.RefreshPolicy.IMMEDIATE + ); + + pluginClient.delete(deleteRequest, listener); + } + +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/GetResourceGroupTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/GetResourceGroupTransportAction.java new file mode 100644 index 0000000000..e071bdb3ed --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/GetResourceGroupTransportAction.java @@ -0,0 +1,105 @@ +/* + * SPDX-License-Identifier: Apache-2.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.resourcegroup.actions.transport; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import org.opensearch.ResourceNotFoundException; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.Strings; +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.resourcegroup.actions.rest.get.GetResourceGroupAction; +import org.opensearch.sample.resourcegroup.actions.rest.get.GetResourceGroupRequest; +import org.opensearch.sample.resourcegroup.actions.rest.get.GetResourceGroupResponse; +import org.opensearch.sample.utils.PluginClient; +import org.opensearch.search.SearchHit; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; + +/** + * Transport action for getting a resource + */ +public class GetResourceGroupTransportAction extends HandledTransportAction { + + private final PluginClient pluginClient; + + @Inject + public GetResourceGroupTransportAction(TransportService transportService, ActionFilters actionFilters, PluginClient pluginClient) { + super(GetResourceGroupAction.NAME, transportService, actionFilters, GetResourceGroupRequest::new); + this.pluginClient = pluginClient; + } + + @Override + protected void doExecute(Task task, GetResourceGroupRequest request, ActionListener listener) { + String resourceId = request.getResourceId(); + + if (Strings.isNullOrEmpty(resourceId)) { + fetchAllResources(listener); + } else { + fetchResourceById(resourceId, listener); + } + } + + private void fetchAllResources(ActionListener listener) { + SearchSourceBuilder ssb = new SearchSourceBuilder().size(1000).query(QueryBuilders.matchAllQuery()); + + SearchRequest req = new SearchRequest(RESOURCE_INDEX_NAME).source(ssb); + pluginClient.search(req, ActionListener.wrap(searchResponse -> { + SearchHit[] hits = searchResponse.getHits().getHits(); + + Set resources = Arrays.stream(hits).map(hit -> { + try { + return parseResource(hit.getSourceAsString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toSet()); + listener.onResponse(new GetResourceGroupResponse(resources)); + + }, listener::onFailure)); + } + + private void fetchResourceById(String resourceId, ActionListener listener) { + GetRequest req = new GetRequest(RESOURCE_INDEX_NAME, resourceId); + pluginClient.get(req, ActionListener.wrap(resp -> { + if (resp.isSourceEmpty()) { + listener.onFailure(new ResourceNotFoundException("Resource group " + resourceId + " not found.")); + } else { + SampleResource resource = parseResource(resp.getSourceAsString()); + listener.onResponse(new GetResourceGroupResponse(Set.of(resource))); + } + }, listener::onFailure)); + } + + private SampleResource parseResource(String json) throws IOException { + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, json) + ) { + return SampleResource.fromXContent(parser); + } + } + +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/SearchResourceGroupTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/SearchResourceGroupTransportAction.java new file mode 100644 index 0000000000..ed4d359e97 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/SearchResourceGroupTransportAction.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.resourcegroup.actions.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +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; +import org.opensearch.core.action.ActionListener; +import org.opensearch.sample.resourcegroup.actions.rest.search.SearchResourceGroupAction; +import org.opensearch.sample.utils.PluginClient; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +/** + * Transport action for searching sample resources + */ +public class SearchResourceGroupTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(SearchResourceGroupTransportAction.class); + + private final PluginClient pluginClient; + + @Inject + public SearchResourceGroupTransportAction(TransportService transportService, ActionFilters actionFilters, PluginClient pluginClient) { + super(SearchResourceGroupAction.NAME, transportService, actionFilters, SearchRequest::new); + this.pluginClient = pluginClient; + } + + @Override + protected void doExecute(Task task, SearchRequest request, ActionListener listener) { + pluginClient.search(request, listener); + } +} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/UpdateResourceGroupTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/UpdateResourceGroupTransportAction.java new file mode 100644 index 0000000000..b6fd7a67c0 --- /dev/null +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/transport/UpdateResourceGroupTransportAction.java @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.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.resourcegroup.actions.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.sample.SampleResource; +import org.opensearch.sample.resourcegroup.actions.rest.create.CreateResourceGroupResponse; +import org.opensearch.sample.resourcegroup.actions.rest.create.UpdateResourceGroupAction; +import org.opensearch.sample.resourcegroup.actions.rest.create.UpdateResourceGroupRequest; +import org.opensearch.sample.utils.PluginClient; +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; + +/** + * Transport action for updating a resource. + */ +public class UpdateResourceGroupTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(UpdateResourceGroupTransportAction.class); + + private final TransportService transportService; + private final PluginClient pluginClient; + + @Inject + public UpdateResourceGroupTransportAction(TransportService transportService, ActionFilters actionFilters, PluginClient pluginClient) { + super(UpdateResourceGroupAction.NAME, transportService, actionFilters, UpdateResourceGroupRequest::new); + this.transportService = transportService; + this.pluginClient = pluginClient; + } + + @Override + protected void doExecute(Task task, UpdateResourceGroupRequest request, ActionListener listener) { + if (request.getResourceId() == null || request.getResourceId().isEmpty()) { + listener.onFailure(new IllegalArgumentException("Resource Group ID cannot be null or empty")); + return; + } + // Check permission to resource + updateResource(request, listener); + } + + private void updateResource(UpdateResourceGroupRequest request, ActionListener listener) { + try { + String resourceId = request.getResourceId(); + SampleResource sample = request.getResource(); + try (XContentBuilder builder = jsonBuilder()) { + sample.toXContent(builder, ToXContent.EMPTY_PARAMS); + + // because some plugins seem to treat update API calls as index request + IndexRequest ir = new IndexRequest(RESOURCE_INDEX_NAME).id(resourceId) + .setRefreshPolicy(WriteRequest.RefreshPolicy.WAIT_UNTIL) // WAIT_UNTIL because we don't want tests to fail, as they + // execute search right after update + .source(builder); + + log.debug("Update Request: {}", ir.toString()); + + pluginClient.index(ir, ActionListener.wrap(updateResponse -> { + listener.onResponse( + new CreateResourceGroupResponse("Resource " + request.getResource().getName() + " updated successfully.") + ); + }, listener::onFailure)); + } + } catch (Exception e) { + log.error("Failed to update resource: {}", request.getResourceId(), e); + listener.onFailure(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 d2136b7ed7..876bdef2e0 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 @@ -14,6 +14,7 @@ public class Constants { public static final String RESOURCE_INDEX_NAME = ".sample_resource"; public static final String RESOURCE_TYPE = "sample-resource"; + public static final String RESOURCE_GROUP_TYPE = "sample-resource-group"; public static final String SAMPLE_RESOURCE_PLUGIN_PREFIX = "_plugins/sample_plugin"; public static final String SAMPLE_RESOURCE_PLUGIN_API_PREFIX = "/" + SAMPLE_RESOURCE_PLUGIN_PREFIX; diff --git a/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.security.spi.resources.ResourceSharingExtension b/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.security.spi.resources.ResourceSharingExtension index d8a7415020..034acfc2a7 100644 --- a/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.security.spi.resources.ResourceSharingExtension +++ b/sample-resource-plugin/src/main/resources/META-INF/services/org.opensearch.security.spi.resources.ResourceSharingExtension @@ -1 +1,2 @@ -org.opensearch.sample.SampleResourceExtension \ No newline at end of file +org.opensearch.sample.SampleResourceExtension +org.opensearch.sample.SampleResourceGroupExtension \ No newline at end of file diff --git a/sample-resource-plugin/src/main/resources/resource-action-groups.yml b/sample-resource-plugin/src/main/resources/resource-action-groups.yml index bcd0fcda1d..c373634be9 100644 --- a/sample-resource-plugin/src/main/resources/resource-action-groups.yml +++ b/sample-resource-plugin/src/main/resources/resource-action-groups.yml @@ -12,3 +12,16 @@ resource_types: allowed_actions: - "cluster:admin/sample-resource-plugin/*" - "cluster:admin/security/resource/share" + sample-resource-group: + sample_group_read_only: + allowed_actions: + - "cluster:admin/sample-resource-plugin/group/get" + + sample_group_read_write: + allowed_actions: + - "cluster:admin/sample-resource-plugin/group/*" + + sample_group_full_access: + allowed_actions: + - "cluster:admin/sample-resource-plugin/group/*" + - "cluster:admin/security/resource/share" diff --git a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index 34acf2a97e..94785b5478 100644 --- a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -213,7 +213,7 @@ public Set getResourceTypes() { public Collection getResourceIndices() { lock.readLock().lock(); try { - return typeToIndex.values(); + return new HashSet<>(typeToIndex.values()); } finally { lock.readLock().unlock(); } From 287bfefb2733d2dbc0af1c24ce37d5240a742b10 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 15 Oct 2025 21:41:28 -0400 Subject: [PATCH 27/40] Fix test Signed-off-by: Craig Perkins --- .../ShareableResourceTypesInfoApiTests.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ShareableResourceTypesInfoApiTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ShareableResourceTypesInfoApiTests.java index bccb542133..770b588eb9 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ShareableResourceTypesInfoApiTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ShareableResourceTypesInfoApiTests.java @@ -22,6 +22,7 @@ import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.opensearch.sample.resource.TestUtils.NO_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.RESOURCE_SHARING_INDEX; @@ -54,10 +55,19 @@ public void testTypesApi_mustListSampleResourceAsAType() { TestRestClient.HttpResponse response = client.get(SECURITY_TYPES_ENDPOINT); response.assertStatusCode(HttpStatus.SC_OK); List types = (List) response.bodyAsMap().get("types"); - assertThat(types.size(), equalTo(1)); - Map responseBody = (Map) types.getFirst(); - assertThat(responseBody.get("type"), equalTo("sample-resource")); - assertThat(responseBody.get("action_groups"), equalTo(List.of("sample_read_only", "sample_read_write", "sample_full_access"))); + assertThat(types.size(), equalTo(2)); + Map firstType = (Map) types.get(0); + assertThat(firstType.get("type"), equalTo("sample-resource")); + assertThat( + (List) firstType.get("action_groups"), + containsInAnyOrder("sample_read_only", "sample_read_write", "sample_full_access") + ); + Map secondType = (Map) types.get(1); + assertThat(secondType.get("type"), equalTo("sample-resource-group")); + assertThat( + (List) secondType.get("action_groups"), + containsInAnyOrder("sample_group_read_only", "sample_group_read_write", "sample_group_full_access") + ); } } From 5b3f561d193f4e4e275a1de5bb08c0ed84566e13 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 16 Oct 2025 09:42:15 -0400 Subject: [PATCH 28/40] Handle case where index is passed explicitly Signed-off-by: Craig Perkins --- .../resources/ResourceAccessHandler.java | 81 ++++++++++++++++--- 1 file changed, 72 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 91a4042a0b..a0b711dae9 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -89,7 +89,14 @@ public void getOwnAndSharedResourceIdsForCurrentUser(@NonNull String resourceTyp return; } - String resourceIndex = resourcePluginInfo.indexByType(resourceType); + // ResourceAccessHandler will be removed when hierarchy is supported. + // This support passing resourceType as either the index name or the type to handle existing instances. + String resourceIndex; + if (resourceType.startsWith(".")) { + resourceIndex = resourceType; + } else { + resourceIndex = resourcePluginInfo.indexByType(resourceType); + } if (adminDNs.isAdmin(user)) { loadAllResourceIds(resourceType, ActionListener.wrap(listener::onResponse, listener::onFailure)); @@ -124,7 +131,14 @@ public void getResourceSharingInfoForCurrentUser(@NonNull String resourceType, A Set flatPrincipals = getFlatPrincipals(user); - String resourceIndex = resourcePluginInfo.indexByType(resourceType); + // ResourceAccessHandler will be removed when hierarchy is supported. + // This support passing resourceType as either the index name or the type to handle existing instances. + String resourceIndex; + if (resourceType.startsWith(".")) { + resourceIndex = resourceType; + } else { + resourceIndex = resourcePluginInfo.indexByType(resourceType); + } // 3) Fetch all accessible resource sharing records resourceSharingIndexHandler.fetchAccessibleResourceSharingRecords(resourceIndex, resourceType, user, flatPrincipals, listener); @@ -181,7 +195,14 @@ public void hasPermission( return; } - String resourceIndex = resourcePluginInfo.indexByType(resourceType); + // ResourceAccessHandler will be removed when hierarchy is supported. + // This support passing resourceType as either the index name or the type to handle existing instances. + String resourceIndex; + if (resourceType.startsWith(".")) { + resourceIndex = resourceType; + } else { + resourceIndex = resourcePluginInfo.indexByType(resourceType); + } if (resourceIndex == null) { LOGGER.debug("No resourceIndex mapping found for type '{}'; denying action {}", resourceType, action); listener.onResponse(false); @@ -262,7 +283,14 @@ public void patchSharingInfo( return; } - String resourceIndex = resourcePluginInfo.indexByType(resourceType); + // ResourceAccessHandler will be removed when hierarchy is supported. + // This support passing resourceType as either the index name or the type to handle existing instances. + String resourceIndex; + if (resourceType.startsWith(".")) { + resourceIndex = resourceType; + } else { + resourceIndex = resourcePluginInfo.indexByType(resourceType); + } if (resourceIndex == null) { LOGGER.debug("No resourceIndex mapping found for type '{}';", resourceType); return; @@ -318,7 +346,14 @@ public void getSharingInfo(@NonNull String resourceId, @NonNull String resourceT LOGGER.debug("User {} is fetching sharing info for resource {} in index {}", user.getName(), resourceId, resourceType); - String resourceIndex = resourcePluginInfo.indexByType(resourceType); + // ResourceAccessHandler will be removed when hierarchy is supported. + // This support passing resourceType as either the index name or the type to handle existing instances. + String resourceIndex; + if (resourceType.startsWith(".")) { + resourceIndex = resourceType; + } else { + resourceIndex = resourcePluginInfo.indexByType(resourceType); + } if (resourceIndex == null) { LOGGER.debug("No resourceIndex mapping found for type '{}';", resourceType); return; @@ -365,7 +400,14 @@ public void share( LOGGER.debug("Sharing resource {} created by {} with {}", resourceId, user.getName(), target.toString()); - String resourceIndex = resourcePluginInfo.indexByType(resourceType); + // ResourceAccessHandler will be removed when hierarchy is supported. + // This support passing resourceType as either the index name or the type to handle existing instances. + String resourceIndex; + if (resourceType.startsWith(".")) { + resourceIndex = resourceType; + } else { + resourceIndex = resourcePluginInfo.indexByType(resourceType); + } this.resourceSharingIndexHandler.share(resourceId, resourceIndex, target, ActionListener.wrap(sharingInfo -> { LOGGER.debug("Successfully shared resource {} with {}", resourceId, target.toString()); @@ -408,7 +450,14 @@ public void revoke( LOGGER.debug("User {} revoking access to resource {} for {}.", user.getName(), resourceId, target); - String resourceIndex = resourcePluginInfo.indexByType(resourceType); + // ResourceAccessHandler will be removed when hierarchy is supported. + // This support passing resourceType as either the index name or the type to handle existing instances. + String resourceIndex; + if (resourceType.startsWith(".")) { + resourceIndex = resourceType; + } else { + resourceIndex = resourcePluginInfo.indexByType(resourceType); + } this.resourceSharingIndexHandler.revoke(resourceId, resourceIndex, target, ActionListener.wrap(listener::onResponse, exception -> { LOGGER.error("Failed to revoke access to resource {} in index {}: {}", resourceId, resourceIndex, exception.getMessage()); @@ -423,7 +472,14 @@ public void revoke( * @param listener The listener to be notified with the set of resource IDs. */ private void loadAllResourceIds(String resourceType, ActionListener> listener) { - String resourceIndex = resourcePluginInfo.indexByType(resourceType); + // ResourceAccessHandler will be removed when hierarchy is supported. + // This support passing resourceType as either the index name or the type to handle existing instances. + String resourceIndex; + if (resourceType.startsWith(".")) { + resourceIndex = resourceType; + } else { + resourceIndex = resourcePluginInfo.indexByType(resourceType); + } this.resourceSharingIndexHandler.fetchAllResourceIds(resourceIndex, listener); } @@ -434,7 +490,14 @@ private void loadAllResourceIds(String resourceType, ActionListener> * @param listener The listener to be notified with the set of resource-sharing records. */ private void loadAllResourceSharingRecords(String resourceType, ActionListener> listener) { - String resourceIndex = resourcePluginInfo.indexByType(resourceType); + // ResourceAccessHandler will be removed when hierarchy is supported. + // This support passing resourceType as either the index name or the type to handle existing instances. + String resourceIndex; + if (resourceType.startsWith(".")) { + resourceIndex = resourceType; + } else { + resourceIndex = resourcePluginInfo.indexByType(resourceType); + } this.resourceSharingIndexHandler.fetchAllResourceSharingRecords(resourceIndex, resourceType, listener); } From 812c428a73674857bf2f05e61ec0d29356f10270 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 16 Oct 2025 10:34:41 -0400 Subject: [PATCH 29/40] Refactor to use methods from AbstractApiIntegrationTest Signed-off-by: Craig Perkins --- .../opensearch/sample/resource/TestUtils.java | 54 +++---- .../SampleResourceGroupTests.java | 144 ++++++------------ .../api/AbstractApiIntegrationTest.java | 18 ++- 3 files changed, 81 insertions(+), 135 deletions(-) diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java index 3029700246..8538a99d9e 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java @@ -81,9 +81,9 @@ public final class TestUtils { public static final String SAMPLE_READ_WRITE_RESOURCE_AG = "sample_read_write"; public static final String SAMPLE_FULL_ACCESS_RESOURCE_AG = "sample_full_access"; - public static final String SAMPLE_GROUP_READ_ONLY_RESOURCE_AG = "sample_group_read_only"; - public static final String SAMPLE_GROUP_READ_WRITE_RESOURCE_AG = "sample_group_read_write"; - public static final String SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG = "sample_group_full_access"; + public static final String SAMPLE_GROUP_READ_ONLY = "sample_group_read_only"; + public static final String SAMPLE_GROUP_READ_WRITE = "sample_group_read_write"; + public static final String SAMPLE_GROUP_FULL_ACCESS = "sample_group_full_access"; public static final String SAMPLE_RESOURCE_CREATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/create"; public static final String SAMPLE_RESOURCE_GET_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/get"; @@ -358,13 +358,10 @@ public void assertApiGet(String resourceId, TestSecurityConfig.User user, int st assertGet(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId, user, status, expectedResourceName); } - public void getResourceGroupAndAssert( - String resourceGroupId, - TestSecurityConfig.User user, - int status, - String expectedResourceName - ) { - assertGet(SAMPLE_RESOURCE_GROUP_GET_ENDPOINT + "/" + resourceGroupId, user, status, expectedResourceName); + public TestRestClient.HttpResponse getResourceGroup(String resourceGroupId, TestSecurityConfig.User user) { + try (TestRestClient client = cluster.getRestClient(user)) { + return client.get(SAMPLE_RESOURCE_GROUP_GET_ENDPOINT + "/" + resourceGroupId); + } } private void assertGet(String endpoint, TestSecurityConfig.User user, int status, String expectedString) { @@ -507,8 +504,11 @@ public void assertApiUpdate(String resourceId, TestSecurityConfig.User user, Str assertUpdate(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, newName, user, status); } - public void updateResourceGroupAndAssert(String resourceGroupId, TestSecurityConfig.User user, String newName, int status) { - assertUpdate(SAMPLE_RESOURCE_GROUP_UPDATE_ENDPOINT + "/" + resourceGroupId, newName, user, status); + public TestRestClient.HttpResponse updateResourceGroup(String resourceGroupId, TestSecurityConfig.User user, String newName) { + try (TestRestClient client = cluster.getRestClient(user)) { + String updatePayload = "{" + "\"name\": \"" + newName + "\"}"; + return client.postJson(SAMPLE_RESOURCE_GROUP_UPDATE_ENDPOINT + "/" + resourceGroupId, updatePayload); + } } public void assertDirectUpdate(String resourceId, TestSecurityConfig.User user, String newName, int status) { @@ -555,19 +555,17 @@ public void assertApiShare( } } - public void shareResourceGroupAndAssert( + public TestRestClient.HttpResponse shareResourceGroup( String resourceId, TestSecurityConfig.User user, TestSecurityConfig.User target, - String accessLevel, - int status + String accessLevel ) { try (TestRestClient client = cluster.getRestClient(user)) { - TestRestClient.HttpResponse response = client.putJson( + return client.putJson( SECURITY_SHARE_ENDPOINT, putSharingInfoPayload(resourceId, RESOURCE_GROUP_TYPE, accessLevel, Recipient.USERS, target.getName()) ); - response.assertStatusCode(status); } } @@ -587,19 +585,17 @@ public void assertApiShareByRole( } } - public void shareResourceGroupByRoleAndAssert( + public TestRestClient.HttpResponse shareResourceGroupByRole( String resourceId, TestSecurityConfig.User user, String targetRole, - String accessLevel, - int status + String accessLevel ) { try (TestRestClient client = cluster.getRestClient(user)) { - TestRestClient.HttpResponse response = client.putJson( + return client.putJson( SECURITY_SHARE_ENDPOINT, putSharingInfoPayload(resourceId, RESOURCE_GROUP_TYPE, accessLevel, Recipient.ROLES, targetRole) ); - response.assertStatusCode(status); } } @@ -620,20 +616,18 @@ public void assertApiRevoke( } } - public void revokeResourceGroupAndAssert( + public TestRestClient.HttpResponse revokeResourceGroup( String resourceId, TestSecurityConfig.User user, TestSecurityConfig.User target, - String accessLevel, - int status + String accessLevel ) { PatchSharingInfoPayloadBuilder patchBuilder = new PatchSharingInfoPayloadBuilder(); patchBuilder.resourceType(RESOURCE_GROUP_TYPE); patchBuilder.resourceId(resourceId); patchBuilder.revoke(new Recipients(Map.of(Recipient.USERS, Set.of(target.getName()))), accessLevel); try (TestRestClient client = cluster.getRestClient(user)) { - TestRestClient.HttpResponse response = client.patch(TestUtils.SECURITY_SHARE_ENDPOINT, patchBuilder.build()); - response.assertStatusCode(status); + return client.patch(TestUtils.SECURITY_SHARE_ENDPOINT, patchBuilder.build()); } } @@ -649,8 +643,10 @@ public void assertApiDelete(String resourceId, TestSecurityConfig.User user, int assertDelete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId, user, status); } - public void deleteResourceGroupAndAssert(String resourceGroupId, TestSecurityConfig.User user, int status) { - assertDelete(SAMPLE_RESOURCE_GROUP_DELETE_ENDPOINT + "/" + resourceGroupId, user, status); + public TestRestClient.HttpResponse deleteResourceGroup(String resourceGroupId, TestSecurityConfig.User user) { + try (TestRestClient client = cluster.getRestClient(user)) { + return client.delete(SAMPLE_RESOURCE_GROUP_DELETE_ENDPOINT + "/" + resourceGroupId); + } } private void assertDelete(String endpoint, TestSecurityConfig.User user, int status) { diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resourcegroup/SampleResourceGroupTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resourcegroup/SampleResourceGroupTests.java index 32b1e0bc8c..d7798dc111 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resourcegroup/SampleResourceGroupTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resourcegroup/SampleResourceGroupTests.java @@ -22,13 +22,17 @@ 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.opensearch.sample.resource.TestUtils.FULL_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.LIMITED_ACCESS_USER; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_GROUP_READ_ONLY_RESOURCE_AG; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_GROUP_FULL_ACCESS; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_GROUP_READ_ONLY; import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.newCluster; import static org.opensearch.sample.utils.Constants.RESOURCE_GROUP_TYPE; +import static org.opensearch.security.api.AbstractApiIntegrationTest.forbidden; +import static org.opensearch.security.api.AbstractApiIntegrationTest.ok; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** @@ -56,52 +60,42 @@ public void cleanup() { api.wipeOutResourceEntries(); } - private void assertNoAccessBeforeSharing(TestSecurityConfig.User user) { - api.getResourceGroupAndAssert(resourceGroupId, user, HttpStatus.SC_FORBIDDEN, ""); - api.updateResourceGroupAndAssert(resourceGroupId, user, "sampleUpdateAdmin", HttpStatus.SC_FORBIDDEN); - api.deleteResourceGroupAndAssert(resourceGroupId, user, HttpStatus.SC_FORBIDDEN); + private void assertNoAccessBeforeSharing(TestSecurityConfig.User user) throws Exception { + forbidden(() -> api.getResourceGroup(resourceGroupId, user)); + forbidden(() -> api.updateResourceGroup(resourceGroupId, user, "sampleUpdateAdmin")); + forbidden(() -> api.deleteResourceGroup(resourceGroupId, user)); - api.shareResourceGroupAndAssert(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); - api.revokeResourceGroupAndAssert(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + forbidden(() -> api.shareResourceGroup(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS)); + forbidden(() -> api.revokeResourceGroup(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS)); } - private void assertReadOnly(TestSecurityConfig.User user) { - api.getResourceGroupAndAssert(resourceGroupId, user, HttpStatus.SC_OK, "sample"); - api.updateResourceGroupAndAssert(resourceGroupId, user, "sampleUpdateAdmin", HttpStatus.SC_FORBIDDEN); - api.deleteResourceGroupAndAssert(resourceGroupId, user, HttpStatus.SC_FORBIDDEN); + private void assertReadOnly(TestSecurityConfig.User user) throws Exception { + TestRestClient.HttpResponse response = ok(() -> api.getResourceGroup(resourceGroupId, user)); + assertThat(response.getBody(), containsString("sample")); + forbidden(() -> api.updateResourceGroup(resourceGroupId, user, "sampleUpdateAdmin")); + forbidden(() -> api.deleteResourceGroup(resourceGroupId, user)); - api.shareResourceGroupAndAssert(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); - api.revokeResourceGroupAndAssert(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + forbidden(() -> api.shareResourceGroup(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS)); + forbidden(() -> api.revokeResourceGroup(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS)); } - private void assertFullAccess(TestSecurityConfig.User user) { - api.getResourceGroupAndAssert(resourceGroupId, user, HttpStatus.SC_OK, "sample"); - api.updateResourceGroupAndAssert(resourceGroupId, user, "sampleUpdateAdmin", HttpStatus.SC_OK); - api.shareResourceGroupAndAssert(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_OK); - api.revokeResourceGroupAndAssert(resourceGroupId, user, USER_ADMIN, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_OK); + private void assertFullAccess(TestSecurityConfig.User user) throws Exception { + TestRestClient.HttpResponse response = ok(() -> api.getResourceGroup(resourceGroupId, user)); + assertThat(response.getBody(), containsString("sample")); + ok(() -> api.updateResourceGroup(resourceGroupId, user, "sampleUpdateAdmin")); + ok(() -> api.shareResourceGroup(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS)); + ok(() -> api.revokeResourceGroup(resourceGroupId, user, USER_ADMIN, SAMPLE_GROUP_FULL_ACCESS)); api.awaitSharingEntry(resourceGroupId); - api.deleteResourceGroupAndAssert(resourceGroupId, user, HttpStatus.SC_OK); + ok(() -> api.deleteResourceGroup(resourceGroupId, user)); } @Test - public void multipleUsers_multipleLevels() { + public void multipleUsers_multipleLevels() throws Exception { assertNoAccessBeforeSharing(FULL_ACCESS_USER); assertNoAccessBeforeSharing(LIMITED_ACCESS_USER); // 1. share at read-only for full-access user and at full-access for limited-perms user - api.shareResourceGroupAndAssert( - resourceGroupId, - USER_ADMIN, - FULL_ACCESS_USER, - SAMPLE_GROUP_READ_ONLY_RESOURCE_AG, - HttpStatus.SC_OK - ); - api.shareResourceGroupAndAssert( - resourceGroupId, - USER_ADMIN, - LIMITED_ACCESS_USER, - SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, - HttpStatus.SC_OK - ); + ok(() -> api.shareResourceGroup(resourceGroupId, USER_ADMIN, FULL_ACCESS_USER, SAMPLE_GROUP_READ_ONLY)); + ok(() -> api.shareResourceGroup(resourceGroupId, USER_ADMIN, LIMITED_ACCESS_USER, SAMPLE_GROUP_FULL_ACCESS)); api.awaitSharingEntry(resourceGroupId, FULL_ACCESS_USER.getName()); api.awaitSharingEntry(resourceGroupId, LIMITED_ACCESS_USER.getName()); @@ -109,13 +103,7 @@ public void multipleUsers_multipleLevels() { assertReadOnly(FULL_ACCESS_USER); // 3. limited access user shares with full-access user at sampleAllAG - api.shareResourceGroupAndAssert( - resourceGroupId, - LIMITED_ACCESS_USER, - FULL_ACCESS_USER, - SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, - HttpStatus.SC_OK - ); + ok(() -> api.shareResourceGroup(resourceGroupId, LIMITED_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_GROUP_FULL_ACCESS)); api.awaitSharingEntry(resourceGroupId, FULL_ACCESS_USER.getName()); // 4. full-access user now has full-access to admin's resource @@ -123,57 +111,33 @@ public void multipleUsers_multipleLevels() { } @Test - public void multipleUsers_sameLevel() { + public void multipleUsers_sameLevel() throws Exception { assertNoAccessBeforeSharing(FULL_ACCESS_USER); assertNoAccessBeforeSharing(LIMITED_ACCESS_USER); // 1. share with both users at read-only level - api.shareResourceGroupAndAssert( - resourceGroupId, - USER_ADMIN, - FULL_ACCESS_USER, - SAMPLE_GROUP_READ_ONLY_RESOURCE_AG, - HttpStatus.SC_OK - ); - api.shareResourceGroupAndAssert( - resourceGroupId, - USER_ADMIN, - LIMITED_ACCESS_USER, - SAMPLE_GROUP_READ_ONLY_RESOURCE_AG, - HttpStatus.SC_OK - ); - api.awaitSharingEntry(resourceGroupId, SAMPLE_GROUP_READ_ONLY_RESOURCE_AG); + ok(() -> api.shareResourceGroup(resourceGroupId, USER_ADMIN, FULL_ACCESS_USER, SAMPLE_GROUP_READ_ONLY)); + ok(() -> api.shareResourceGroup(resourceGroupId, USER_ADMIN, LIMITED_ACCESS_USER, SAMPLE_GROUP_READ_ONLY)); + api.awaitSharingEntry(resourceGroupId, SAMPLE_GROUP_READ_ONLY); // 2. assert both now have read-only access assertReadOnly(LIMITED_ACCESS_USER); } @Test - public void sameUser_multipleLevels() { + public void sameUser_multipleLevels() throws Exception { assertNoAccessBeforeSharing(LIMITED_ACCESS_USER); // 1. share with user at read-only level - api.shareResourceGroupAndAssert( - resourceGroupId, - USER_ADMIN, - LIMITED_ACCESS_USER, - SAMPLE_GROUP_READ_ONLY_RESOURCE_AG, - HttpStatus.SC_OK - ); + ok(() -> api.shareResourceGroup(resourceGroupId, USER_ADMIN, LIMITED_ACCESS_USER, SAMPLE_GROUP_READ_ONLY)); api.awaitSharingEntry(resourceGroupId, LIMITED_ACCESS_USER.getName()); // 2. assert user now has read-only access assertReadOnly(LIMITED_ACCESS_USER); // 3. share with user at full-access level - api.shareResourceGroupAndAssert( - resourceGroupId, - USER_ADMIN, - LIMITED_ACCESS_USER, - SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, - HttpStatus.SC_OK - ); - api.awaitSharingEntry(resourceGroupId, SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG); + ok(() -> api.shareResourceGroup(resourceGroupId, USER_ADMIN, LIMITED_ACCESS_USER, SAMPLE_GROUP_FULL_ACCESS)); + api.awaitSharingEntry(resourceGroupId, SAMPLE_GROUP_FULL_ACCESS); // 4. assert user now has full access assertFullAccess(LIMITED_ACCESS_USER); @@ -184,7 +148,7 @@ private String getActualRoleName(TestSecurityConfig.User user, String baseRoleNa } @Test - public void multipleRoles_multipleLevels() { + public void multipleRoles_multipleLevels() throws Exception { assertNoAccessBeforeSharing(FULL_ACCESS_USER); assertNoAccessBeforeSharing(LIMITED_ACCESS_USER); @@ -192,20 +156,8 @@ public void multipleRoles_multipleLevels() { String limitedAccessUserRole = getActualRoleName(LIMITED_ACCESS_USER, "shared_role_limited_perms"); // 1. share at read-only for shared_role and at full-access for shared_role_limited_perms - api.shareResourceGroupByRoleAndAssert( - resourceGroupId, - USER_ADMIN, - fullAccessUserRole, - SAMPLE_GROUP_READ_ONLY_RESOURCE_AG, - HttpStatus.SC_OK - ); - api.shareResourceGroupByRoleAndAssert( - resourceGroupId, - USER_ADMIN, - limitedAccessUserRole, - SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, - HttpStatus.SC_OK - ); + ok(() -> api.shareResourceGroupByRole(resourceGroupId, USER_ADMIN, fullAccessUserRole, SAMPLE_GROUP_READ_ONLY)); + ok(() -> api.shareResourceGroupByRole(resourceGroupId, USER_ADMIN, limitedAccessUserRole, SAMPLE_GROUP_FULL_ACCESS)); api.awaitSharingEntry(resourceGroupId, fullAccessUserRole); api.awaitSharingEntry(resourceGroupId, limitedAccessUserRole); @@ -213,13 +165,7 @@ public void multipleRoles_multipleLevels() { assertReadOnly(FULL_ACCESS_USER); // 3. LIMITED_ACCESS_USER (has shared_role_limited_perms) shares with shared_role at sampleAllAG - api.shareResourceGroupByRoleAndAssert( - resourceGroupId, - LIMITED_ACCESS_USER, - fullAccessUserRole, - SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, - HttpStatus.SC_OK - ); + ok(() -> api.shareResourceGroupByRole(resourceGroupId, LIMITED_ACCESS_USER, fullAccessUserRole, SAMPLE_GROUP_FULL_ACCESS)); api.awaitSharingEntry(resourceGroupId, fullAccessUserRole); // 4. FULL_ACCESS_USER now has full-access to admin's resource @@ -227,7 +173,7 @@ public void multipleRoles_multipleLevels() { } @Test - public void initialShare_multipleLevels() { + public void initialShare_multipleLevels() throws Exception { assertNoAccessBeforeSharing(FULL_ACCESS_USER); assertNoAccessBeforeSharing(LIMITED_ACCESS_USER); @@ -247,9 +193,9 @@ public void initialShare_multipleLevels() { """.formatted( resourceGroupId, RESOURCE_GROUP_TYPE, - SAMPLE_GROUP_FULL_ACCESS_RESOURCE_AG, + SAMPLE_GROUP_FULL_ACCESS, LIMITED_ACCESS_USER.getName(), - SAMPLE_GROUP_READ_ONLY_RESOURCE_AG, + SAMPLE_GROUP_READ_ONLY, FULL_ACCESS_USER.getName() ); diff --git a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java index a69ca83378..4bf962393d 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java @@ -326,13 +326,16 @@ TestRestClient.HttpResponse created(final CheckedSupplier endpointCallback, final String expectedMessage) - throws Exception { + public static void forbidden( + final CheckedSupplier endpointCallback, + final String expectedMessage + ) throws Exception { final var response = forbidden(endpointCallback); assertThat(response.getBody(), response.getTextFromJsonBody("/message"), is(expectedMessage)); } - TestRestClient.HttpResponse forbidden(final CheckedSupplier endpointCallback) throws Exception { + public static TestRestClient.HttpResponse forbidden(final CheckedSupplier endpointCallback) + throws Exception { final var response = endpointCallback.get(); assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); assertResponseBody(response.getBody()); @@ -368,14 +371,15 @@ void notFound(final CheckedSupplier endp assertThat(response.getBody(), response.getTextFromJsonBody("/message"), is(expectedMessage)); } - TestRestClient.HttpResponse ok(final CheckedSupplier endpointCallback) throws Exception { + public static TestRestClient.HttpResponse ok(final CheckedSupplier endpointCallback) + throws Exception { final var response = endpointCallback.get(); assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); assertResponseBody(response.getBody()); return response; } - TestRestClient.HttpResponse ok( + public static TestRestClient.HttpResponse ok( final CheckedSupplier endpointCallback, final String expectedMessage ) throws Exception { @@ -393,12 +397,12 @@ TestRestClient.HttpResponse unauthorized(final CheckedSupplier Date: Thu, 16 Oct 2025 10:51:18 -0400 Subject: [PATCH 30/40] Remove calls to awaitSharingEntry Signed-off-by: Craig Perkins --- .../resourcegroup/SampleResourceGroupTests.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resourcegroup/SampleResourceGroupTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resourcegroup/SampleResourceGroupTests.java index d7798dc111..46b7fc93fa 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resourcegroup/SampleResourceGroupTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resourcegroup/SampleResourceGroupTests.java @@ -85,7 +85,6 @@ private void assertFullAccess(TestSecurityConfig.User user) throws Exception { ok(() -> api.updateResourceGroup(resourceGroupId, user, "sampleUpdateAdmin")); ok(() -> api.shareResourceGroup(resourceGroupId, user, user, SAMPLE_GROUP_FULL_ACCESS)); ok(() -> api.revokeResourceGroup(resourceGroupId, user, USER_ADMIN, SAMPLE_GROUP_FULL_ACCESS)); - api.awaitSharingEntry(resourceGroupId); ok(() -> api.deleteResourceGroup(resourceGroupId, user)); } @@ -96,15 +95,12 @@ public void multipleUsers_multipleLevels() throws Exception { // 1. share at read-only for full-access user and at full-access for limited-perms user ok(() -> api.shareResourceGroup(resourceGroupId, USER_ADMIN, FULL_ACCESS_USER, SAMPLE_GROUP_READ_ONLY)); ok(() -> api.shareResourceGroup(resourceGroupId, USER_ADMIN, LIMITED_ACCESS_USER, SAMPLE_GROUP_FULL_ACCESS)); - api.awaitSharingEntry(resourceGroupId, FULL_ACCESS_USER.getName()); - api.awaitSharingEntry(resourceGroupId, LIMITED_ACCESS_USER.getName()); // 2. check read-only access for full-access user assertReadOnly(FULL_ACCESS_USER); // 3. limited access user shares with full-access user at sampleAllAG ok(() -> api.shareResourceGroup(resourceGroupId, LIMITED_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_GROUP_FULL_ACCESS)); - api.awaitSharingEntry(resourceGroupId, FULL_ACCESS_USER.getName()); // 4. full-access user now has full-access to admin's resource assertFullAccess(FULL_ACCESS_USER); @@ -118,7 +114,6 @@ public void multipleUsers_sameLevel() throws Exception { // 1. share with both users at read-only level ok(() -> api.shareResourceGroup(resourceGroupId, USER_ADMIN, FULL_ACCESS_USER, SAMPLE_GROUP_READ_ONLY)); ok(() -> api.shareResourceGroup(resourceGroupId, USER_ADMIN, LIMITED_ACCESS_USER, SAMPLE_GROUP_READ_ONLY)); - api.awaitSharingEntry(resourceGroupId, SAMPLE_GROUP_READ_ONLY); // 2. assert both now have read-only access assertReadOnly(LIMITED_ACCESS_USER); @@ -130,14 +125,12 @@ public void sameUser_multipleLevels() throws Exception { // 1. share with user at read-only level ok(() -> api.shareResourceGroup(resourceGroupId, USER_ADMIN, LIMITED_ACCESS_USER, SAMPLE_GROUP_READ_ONLY)); - api.awaitSharingEntry(resourceGroupId, LIMITED_ACCESS_USER.getName()); // 2. assert user now has read-only access assertReadOnly(LIMITED_ACCESS_USER); // 3. share with user at full-access level ok(() -> api.shareResourceGroup(resourceGroupId, USER_ADMIN, LIMITED_ACCESS_USER, SAMPLE_GROUP_FULL_ACCESS)); - api.awaitSharingEntry(resourceGroupId, SAMPLE_GROUP_FULL_ACCESS); // 4. assert user now has full access assertFullAccess(LIMITED_ACCESS_USER); @@ -158,15 +151,12 @@ public void multipleRoles_multipleLevels() throws Exception { // 1. share at read-only for shared_role and at full-access for shared_role_limited_perms ok(() -> api.shareResourceGroupByRole(resourceGroupId, USER_ADMIN, fullAccessUserRole, SAMPLE_GROUP_READ_ONLY)); ok(() -> api.shareResourceGroupByRole(resourceGroupId, USER_ADMIN, limitedAccessUserRole, SAMPLE_GROUP_FULL_ACCESS)); - api.awaitSharingEntry(resourceGroupId, fullAccessUserRole); - api.awaitSharingEntry(resourceGroupId, limitedAccessUserRole); // 2. check read-only access for FULL_ACCESS_USER (has shared_role) assertReadOnly(FULL_ACCESS_USER); // 3. LIMITED_ACCESS_USER (has shared_role_limited_perms) shares with shared_role at sampleAllAG ok(() -> api.shareResourceGroupByRole(resourceGroupId, LIMITED_ACCESS_USER, fullAccessUserRole, SAMPLE_GROUP_FULL_ACCESS)); - api.awaitSharingEntry(resourceGroupId, fullAccessUserRole); // 4. FULL_ACCESS_USER now has full-access to admin's resource assertFullAccess(FULL_ACCESS_USER); @@ -202,8 +192,6 @@ public void initialShare_multipleLevels() throws Exception { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { TestRestClient.HttpResponse response = client.putJson(SECURITY_SHARE_ENDPOINT, shareWithPayload); response.assertStatusCode(HttpStatus.SC_OK); - // wait for one of the users to be populated - api.awaitSharingEntry(resourceGroupId, FULL_ACCESS_USER.getName()); } // full-access user has read-only perm From bbe3fbe1f8492d3a1af2d3b83d1907993317d7f2 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 16 Oct 2025 14:20:05 -0400 Subject: [PATCH 31/40] Revert "Handle case where index is passed explicitly" This reverts commit 5b3f561d193f4e4e275a1de5bb08c0ed84566e13. Signed-off-by: Craig Perkins --- .../resources/ResourceAccessHandler.java | 81 +++---------------- 1 file changed, 9 insertions(+), 72 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index a0b711dae9..91a4042a0b 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -89,14 +89,7 @@ public void getOwnAndSharedResourceIdsForCurrentUser(@NonNull String resourceTyp return; } - // ResourceAccessHandler will be removed when hierarchy is supported. - // This support passing resourceType as either the index name or the type to handle existing instances. - String resourceIndex; - if (resourceType.startsWith(".")) { - resourceIndex = resourceType; - } else { - resourceIndex = resourcePluginInfo.indexByType(resourceType); - } + String resourceIndex = resourcePluginInfo.indexByType(resourceType); if (adminDNs.isAdmin(user)) { loadAllResourceIds(resourceType, ActionListener.wrap(listener::onResponse, listener::onFailure)); @@ -131,14 +124,7 @@ public void getResourceSharingInfoForCurrentUser(@NonNull String resourceType, A Set flatPrincipals = getFlatPrincipals(user); - // ResourceAccessHandler will be removed when hierarchy is supported. - // This support passing resourceType as either the index name or the type to handle existing instances. - String resourceIndex; - if (resourceType.startsWith(".")) { - resourceIndex = resourceType; - } else { - resourceIndex = resourcePluginInfo.indexByType(resourceType); - } + String resourceIndex = resourcePluginInfo.indexByType(resourceType); // 3) Fetch all accessible resource sharing records resourceSharingIndexHandler.fetchAccessibleResourceSharingRecords(resourceIndex, resourceType, user, flatPrincipals, listener); @@ -195,14 +181,7 @@ public void hasPermission( return; } - // ResourceAccessHandler will be removed when hierarchy is supported. - // This support passing resourceType as either the index name or the type to handle existing instances. - String resourceIndex; - if (resourceType.startsWith(".")) { - resourceIndex = resourceType; - } else { - resourceIndex = resourcePluginInfo.indexByType(resourceType); - } + String resourceIndex = resourcePluginInfo.indexByType(resourceType); if (resourceIndex == null) { LOGGER.debug("No resourceIndex mapping found for type '{}'; denying action {}", resourceType, action); listener.onResponse(false); @@ -283,14 +262,7 @@ public void patchSharingInfo( return; } - // ResourceAccessHandler will be removed when hierarchy is supported. - // This support passing resourceType as either the index name or the type to handle existing instances. - String resourceIndex; - if (resourceType.startsWith(".")) { - resourceIndex = resourceType; - } else { - resourceIndex = resourcePluginInfo.indexByType(resourceType); - } + String resourceIndex = resourcePluginInfo.indexByType(resourceType); if (resourceIndex == null) { LOGGER.debug("No resourceIndex mapping found for type '{}';", resourceType); return; @@ -346,14 +318,7 @@ public void getSharingInfo(@NonNull String resourceId, @NonNull String resourceT LOGGER.debug("User {} is fetching sharing info for resource {} in index {}", user.getName(), resourceId, resourceType); - // ResourceAccessHandler will be removed when hierarchy is supported. - // This support passing resourceType as either the index name or the type to handle existing instances. - String resourceIndex; - if (resourceType.startsWith(".")) { - resourceIndex = resourceType; - } else { - resourceIndex = resourcePluginInfo.indexByType(resourceType); - } + String resourceIndex = resourcePluginInfo.indexByType(resourceType); if (resourceIndex == null) { LOGGER.debug("No resourceIndex mapping found for type '{}';", resourceType); return; @@ -400,14 +365,7 @@ public void share( LOGGER.debug("Sharing resource {} created by {} with {}", resourceId, user.getName(), target.toString()); - // ResourceAccessHandler will be removed when hierarchy is supported. - // This support passing resourceType as either the index name or the type to handle existing instances. - String resourceIndex; - if (resourceType.startsWith(".")) { - resourceIndex = resourceType; - } else { - resourceIndex = resourcePluginInfo.indexByType(resourceType); - } + String resourceIndex = resourcePluginInfo.indexByType(resourceType); this.resourceSharingIndexHandler.share(resourceId, resourceIndex, target, ActionListener.wrap(sharingInfo -> { LOGGER.debug("Successfully shared resource {} with {}", resourceId, target.toString()); @@ -450,14 +408,7 @@ public void revoke( LOGGER.debug("User {} revoking access to resource {} for {}.", user.getName(), resourceId, target); - // ResourceAccessHandler will be removed when hierarchy is supported. - // This support passing resourceType as either the index name or the type to handle existing instances. - String resourceIndex; - if (resourceType.startsWith(".")) { - resourceIndex = resourceType; - } else { - resourceIndex = resourcePluginInfo.indexByType(resourceType); - } + String resourceIndex = resourcePluginInfo.indexByType(resourceType); this.resourceSharingIndexHandler.revoke(resourceId, resourceIndex, target, ActionListener.wrap(listener::onResponse, exception -> { LOGGER.error("Failed to revoke access to resource {} in index {}: {}", resourceId, resourceIndex, exception.getMessage()); @@ -472,14 +423,7 @@ public void revoke( * @param listener The listener to be notified with the set of resource IDs. */ private void loadAllResourceIds(String resourceType, ActionListener> listener) { - // ResourceAccessHandler will be removed when hierarchy is supported. - // This support passing resourceType as either the index name or the type to handle existing instances. - String resourceIndex; - if (resourceType.startsWith(".")) { - resourceIndex = resourceType; - } else { - resourceIndex = resourcePluginInfo.indexByType(resourceType); - } + String resourceIndex = resourcePluginInfo.indexByType(resourceType); this.resourceSharingIndexHandler.fetchAllResourceIds(resourceIndex, listener); } @@ -490,14 +434,7 @@ private void loadAllResourceIds(String resourceType, ActionListener> * @param listener The listener to be notified with the set of resource-sharing records. */ private void loadAllResourceSharingRecords(String resourceType, ActionListener> listener) { - // ResourceAccessHandler will be removed when hierarchy is supported. - // This support passing resourceType as either the index name or the type to handle existing instances. - String resourceIndex; - if (resourceType.startsWith(".")) { - resourceIndex = resourceType; - } else { - resourceIndex = resourcePluginInfo.indexByType(resourceType); - } + String resourceIndex = resourcePluginInfo.indexByType(resourceType); this.resourceSharingIndexHandler.fetchAllResourceSharingRecords(resourceIndex, resourceType, listener); } From 4499615113ac86b34d07be5d560513e5744d3802 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 16 Oct 2025 15:11:18 -0400 Subject: [PATCH 32/40] Fix compilation errors Signed-off-by: Craig Perkins --- .../resources/ResourcePluginInfo.java | 13 +++++++++++ .../MigrateResourceSharingInfoApiAction.java | 23 +++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index 94785b5478..70c5bb908c 100644 --- a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -196,6 +196,19 @@ public String indexByType(String type) { } } + public Set typesByIndex(String index) { + lock.readLock().lock(); + try { + return typeToIndex.entrySet() + .stream() + .filter(entry -> Objects.equals(entry.getValue(), index)) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } finally { + lock.readLock().unlock(); + } + } + public Set getResourceTypes() { lock.readLock().lock(); try { diff --git a/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java b/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java index 56fd591dfa..9b2bae7197 100644 --- a/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java +++ b/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java @@ -148,22 +148,35 @@ private ValidationResult>> loadCurrentSha String backendRolesPath = body.get("backend_roles_path").asText(); String defaultAccessLevel = body.get("default_access_level").asText(); + // TODO: once we support multiple resource types within an index, this needs to be revisited + // Default access level as part of the resource-action-groups.yml file + Set types = resourcePluginInfo.typesByIndex(sourceIndex); + if (types.size() != 1) { + LOGGER.error( + "Invalid resource index [{}]. Either no resource type or multiple resource types are associated with this index", + sourceIndex + ); + String badRequestMessage = "Invalid resource index [" + + sourceIndex + + "]. Either no resource type or multiple resource types are associated with this index"; + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(badRequestMessage)); + } + String type = types.iterator().next(); // check that access level exists for given resource-index - String type = resourcePluginInfo.typeByIndex(sourceIndex); - var availableAGs = resourcePluginInfo.flattenedForType(type).actionGroups(); - if (!availableAGs.contains(defaultAccessLevel)) { + var accessLevels = resourcePluginInfo.flattenedForType(type).actionGroups(); + if (!accessLevels.contains(defaultAccessLevel)) { LOGGER.error( "Invalid access level {} for resource sharing for resource type [{}]. Available access-levels are [{}]", defaultAccessLevel, sourceIndex, - availableAGs + accessLevels ); String badRequestMessage = "Invalid access level " + defaultAccessLevel + " for resource sharing for resource type [" + type + "]. Available access-levels are [" - + availableAGs + + accessLevels + "]"; return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(badRequestMessage)); } From d487a01c8dcd7b8150311a9bd0f16faffc9c8805 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 16 Oct 2025 17:07:58 -0400 Subject: [PATCH 33/40] Fix tests Signed-off-by: Craig Perkins --- .../MigrateResourceSharingInfoApiAction.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java b/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java index 9b2bae7197..304040818e 100644 --- a/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java +++ b/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java @@ -151,17 +151,21 @@ private ValidationResult>> loadCurrentSha // TODO: once we support multiple resource types within an index, this needs to be revisited // Default access level as part of the resource-action-groups.yml file Set types = resourcePluginInfo.typesByIndex(sourceIndex); - if (types.size() != 1) { - LOGGER.error( - "Invalid resource index [{}]. Either no resource type or multiple resource types are associated with this index", - sourceIndex - ); + if (types.isEmpty()) { + LOGGER.error("Invalid resource index [{}]. No resource types are associated with this index", sourceIndex); String badRequestMessage = "Invalid resource index [" + sourceIndex + "]. Either no resource type or multiple resource types are associated with this index"; return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(badRequestMessage)); } String type = types.iterator().next(); + if (types.size() > 1) { + LOGGER.warn( + "Invalid resource index [{}]. Multiple resource types are associated with this index, choosing {}.", + sourceIndex, + type + ); + } // check that access level exists for given resource-index var accessLevels = resourcePluginInfo.flattenedForType(type).actionGroups(); if (!accessLevels.contains(defaultAccessLevel)) { From d3c22c045d218e00bcef5327d649617000f2f431 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 20 Oct 2025 14:52:28 -0400 Subject: [PATCH 34/40] Use Set Signed-off-by: Craig Perkins --- .../opensearch/security/configuration/DlsFlsValveImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 08fd04ac19..35b2e9397b 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -14,10 +14,10 @@ import java.lang.reflect.Field; import java.security.AccessController; import java.security.PrivilegedAction; -import java.util.Collection; 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; @@ -165,7 +165,7 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< if (HeaderHelper.isInternalOrPluginRequest(threadContext)) { if (resourceSharingEnabledSetting.getDynamicSettingValue() && request instanceof SearchRequest) { IndexResolverReplacer.Resolved resolved = context.getResolvedRequest(); - Collection protectedIndices = resourcePluginInfo.getResourceIndicesForProtectedTypes(); + Set protectedIndices = resourcePluginInfo.getResourceIndicesForProtectedTypes(); WildcardMatcher resourceIndicesMatcher = WildcardMatcher.from(protectedIndices); if (resourceIndicesMatcher.matchAll(resolved.getAllIndices())) { IndexToRuleMap sharedResourceMap = ResourceSharingDlsUtils.resourceRestrictions( From 3d25b31e11518a0fd7379b95039ec8909444d805 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 20 Oct 2025 15:07:04 -0400 Subject: [PATCH 35/40] Address review feedback Signed-off-by: Craig Perkins --- .../create/CreateResourceGroupAction.java | 6 ++--- .../create/CreateResourceGroupResponse.java | 2 +- .../create/CreateResourceGroupRestAction.java | 2 +- .../create/UpdateResourceGroupAction.java | 6 ++--- .../create/UpdateResourceGroupRequest.java | 2 +- .../delete/DeleteResourceGroupAction.java | 6 ++--- .../delete/DeleteResourceGroupRequest.java | 2 +- .../delete/DeleteResourceGroupResponse.java | 2 +- .../delete/DeleteResourceGroupRestAction.java | 2 +- .../rest/get/GetResourceGroupAction.java | 6 ++--- .../rest/get/GetResourceGroupRestAction.java | 2 +- .../search/SearchResourceGroupAction.java | 2 +- .../search/SearchResourceGroupRestAction.java | 2 +- .../security/OpenSearchSecurityPlugin.java | 4 ++-- .../security/filter/SecurityFilter.java | 2 +- .../privileges/ResourceAccessEvaluator.java | 4 +--- .../ResourceAccessControlClient.java | 2 +- .../resources/ResourceAccessHandler.java | 24 +++++-------------- .../resources/ResourcePluginInfo.java | 2 +- .../resources/api/share/ShareRequest.java | 1 - .../ResourceAccessEvaluatorTest.java | 4 ++-- .../resources/ResourceAccessHandlerTest.java | 12 +++++----- 22 files changed, 41 insertions(+), 56 deletions(-) diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupAction.java index d1a0a4c6d9..a5eba1cea5 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupAction.java @@ -11,15 +11,15 @@ import org.opensearch.action.ActionType; /** - * Action to create a sample resource + * Action to create a sample resource group */ public class CreateResourceGroupAction extends ActionType { /** - * Create sample resource action instance + * Create sample resource group action instance */ public static final CreateResourceGroupAction INSTANCE = new CreateResourceGroupAction(); /** - * Create sample resource action name + * Create sample resource group action name */ public static final String NAME = "cluster:admin/sample-resource-plugin/group/create"; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupResponse.java index 8cf259e661..d0a44e15c1 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupResponse.java @@ -17,7 +17,7 @@ import org.opensearch.core.xcontent.XContentBuilder; /** - * Response to a CreateSampleResourceRequest + * Response to a CreateSampleResourceGroupRequest */ public class CreateResourceGroupResponse extends ActionResponse implements ToXContentObject { private final String message; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupRestAction.java index 3cd4a632b0..b3e5317966 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/CreateResourceGroupRestAction.java @@ -24,7 +24,7 @@ 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. + * Rest Action to create a Sample Resource Group. Registers Create and Update REST APIs. */ public class CreateResourceGroupRestAction extends BaseRestHandler { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupAction.java index 7dfbc8b57d..956f6b4592 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupAction.java @@ -11,15 +11,15 @@ import org.opensearch.action.ActionType; /** - * Action to update a sample resource + * Action to update a sample resource group */ public class UpdateResourceGroupAction extends ActionType { /** - * Update sample resource action instance + * Update sample resource group action instance */ public static final UpdateResourceGroupAction INSTANCE = new UpdateResourceGroupAction(); /** - * Update sample resource action name + * Update sample resource group action name */ public static final String NAME = "cluster:admin/sample-resource-plugin/group/update"; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupRequest.java index c13dfcb864..ad1d6eac9e 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/create/UpdateResourceGroupRequest.java @@ -21,7 +21,7 @@ import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; /** - * Request object for UpdateResource transport action + * Request object for UpdateResourceGroup transport action */ public class UpdateResourceGroupRequest extends ActionRequest implements DocRequest { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupAction.java index 921d10ebde..7b317fcf64 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupAction.java @@ -11,15 +11,15 @@ import org.opensearch.action.ActionType; /** - * Action to delete a sample resource + * Action to delete a sample resource group */ public class DeleteResourceGroupAction extends ActionType { /** - * Delete sample resource action instance + * Delete sample resource group action instance */ public static final DeleteResourceGroupAction INSTANCE = new DeleteResourceGroupAction(); /** - * Delete sample resource action name + * Delete sample resource group action name */ public static final String NAME = "cluster:admin/sample-resource-plugin/group/delete"; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRequest.java index c122d0c2be..f7994f3352 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRequest.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRequest.java @@ -20,7 +20,7 @@ import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; /** - * Request object for DeleteSampleResource transport action + * Request object for DeleteSampleResourceGroup transport action */ public class DeleteResourceGroupRequest extends ActionRequest implements DocRequest { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupResponse.java index ac70bd9bb6..1d1bb9b6af 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupResponse.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupResponse.java @@ -17,7 +17,7 @@ import org.opensearch.core.xcontent.XContentBuilder; /** - * Response to a DeleteSampleResourceRequest + * Response to a DeleteSampleResourceGroupRequest */ public class DeleteResourceGroupResponse extends ActionResponse implements ToXContentObject { private final String message; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRestAction.java index ce945c6056..9031258426 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/delete/DeleteResourceGroupRestAction.java @@ -21,7 +21,7 @@ import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; /** - * Rest Action to delete a Sample Resource. + * Rest Action to delete a Sample Resource Group. */ public class DeleteResourceGroupRestAction extends BaseRestHandler { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupAction.java index 3442bf1f70..40285009a8 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupAction.java @@ -11,15 +11,15 @@ import org.opensearch.action.ActionType; /** - * Action to get a sample resource + * Action to get a sample resource group */ public class GetResourceGroupAction extends ActionType { /** - * Get sample resource action instance + * Get sample resource group action instance */ public static final GetResourceGroupAction INSTANCE = new GetResourceGroupAction(); /** - * Get sample resource action name + * Get sample resource group action name */ public static final String NAME = "cluster:admin/sample-resource-plugin/group/get"; diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupRestAction.java index 4a56c311b9..fec6ccf942 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/get/GetResourceGroupRestAction.java @@ -19,7 +19,7 @@ import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; /** - * Rest action to get a sample resource + * Rest action to get a sample resource group */ public class GetResourceGroupRestAction extends BaseRestHandler { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupAction.java index c49d31c719..8b0a2c6b37 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupAction.java @@ -12,7 +12,7 @@ import org.opensearch.action.search.SearchResponse; /** - * Action to search sample resources + * Action to search sample resource groups */ public class SearchResourceGroupAction extends ActionType { diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupRestAction.java index 9a28bb18cc..2b463ddc6f 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupRestAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resourcegroup/actions/rest/search/SearchResourceGroupRestAction.java @@ -26,7 +26,7 @@ import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; /** - * Rest action to search sample resource(s) + * Rest action to search sample resource group(s) */ public class SearchResourceGroupRestAction extends BaseRestHandler { diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 90d3378a36..475b225808 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -786,7 +786,7 @@ public void onIndexModule(IndexModule indexModule) { resourceSharingEnabledSetting ); // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions - Collection resourceIndices = resourcePluginInfo.getResourceIndices(); + Set resourceIndices = resourcePluginInfo.getResourceIndices(); // CS-ENFORCE-SINGLE if (resourceIndices.contains(indexModule.getIndex().getName())) { indexModule.addIndexOperationListener(resourceIndexListener); @@ -2318,7 +2318,7 @@ public void onNodeStarted(DiscoveryNode localNode) { // create resource sharing index if absent // TODO check if this should be wrapped in an atomic completable future log.debug("Attempting to create Resource Sharing index"); - Collection resourceIndices = new HashSet<>(); + Set resourceIndices = new HashSet<>(); if (resourcePluginInfo != null) { resourceIndices = resourcePluginInfo.getResourceIndices(); } diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index 8d8c6cbfe0..4e99f06a54 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -419,7 +419,7 @@ private void ap // require blocking transport threads leading to thread exhaustion and request timeouts // We perform the rest of the evaluation as normal if the request is not for resource-access or if the feature is disabled if (resourceAccessEvaluator.shouldEvaluate(request)) { - resourceAccessEvaluator.evaluateAsync(request, action, context, ActionListener.wrap(response -> { + resourceAccessEvaluator.evaluateAsync(request, action, ActionListener.wrap(response -> { if (handlePermissionCheckRequest(listener, response, action)) { return; } diff --git a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java index e58aeccd11..b83e174600 100644 --- a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java @@ -71,13 +71,11 @@ public ResourceAccessEvaluator( * * @param request may contain information about the index and the resource being requested * @param action the action being requested to be performed on the resource - * @param context the evaluation context to be used when performing authorization * @param pResponseListener the response listener which tells whether the action is allowed for user, or should the request be checked with another evaluator */ public void evaluateAsync( final ActionRequest request, final String action, - final PrivilegesEvaluationContext context, final ActionListener pResponseListener ) { PrivilegesEvaluatorResponse pResponse = new PrivilegesEvaluatorResponse(); @@ -87,7 +85,7 @@ public void evaluateAsync( // if it reached this evaluator, it is safe to assume that the request if of DocRequest type DocRequest req = (DocRequest) request; - resourceAccessHandler.hasPermission(req.id(), req.type(), action, context, ActionListener.wrap(hasAccess -> { + resourceAccessHandler.hasPermission(req.id(), req.type(), action, ActionListener.wrap(hasAccess -> { if (hasAccess) { pResponse.allowed = true; pResponseListener.onResponse(pResponse.markComplete()); diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java index fca9b0c35e..f02c5b3d25 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java @@ -65,7 +65,7 @@ public void verifyAccess(String resourceId, String resourceType, String action, listener.onResponse(true); return; } - resourceAccessHandler.hasPermission(resourceId, resourceType, action, null, listener); + resourceAccessHandler.hasPermission(resourceId, resourceType, action, listener); } /** diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 0ed7dfa65d..e547732a9f 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -28,9 +28,7 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluator; -import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; import org.opensearch.security.resources.sharing.Recipient; import org.opensearch.security.resources.sharing.ResourceSharing; import org.opensearch.security.resources.sharing.ShareWith; @@ -136,14 +134,12 @@ public void getResourceSharingInfoForCurrentUser(@NonNull String resourceType, A * @param resourceId The resource ID to check access for. * @param resourceType The resource type. * @param action The action to check permission for - * @param context The evaluation context to be used. Will be null when used by {@link ResourceAccessControlClient}. * @param listener The listener to be notified with the permission check result. */ public void hasPermission( @NonNull String resourceId, @NonNull String resourceType, @NonNull String action, - PrivilegesEvaluationContext context, ActionListener listener ) { final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( @@ -164,23 +160,9 @@ public void hasPermission( listener.onResponse(true); return; } - - PrivilegesEvaluationContext effectiveContext = context != null ? context : privilegesEvaluator.createContext(user, action); - Set userRoles = new HashSet<>(user.getSecurityRoles()); Set userBackendRoles = new HashSet<>(user.getRoles()); - // At present, plugins and tokens are not supported for access to resources - if (!(effectiveContext.getActionPrivileges() instanceof RoleBasedActionPrivileges)) { - LOGGER.debug( - "Plugin/Token access to resources is currently not supported. {} is not authorized to access resource {}.", - user.getName(), - resourceId - ); - listener.onResponse(false); - return; - } - String resourceIndex = resourcePluginInfo.indexByType(resourceType); if (resourceIndex == null) { LOGGER.debug("No resourceIndex mapping found for type '{}'; denying action {}", resourceType, action); @@ -265,6 +247,9 @@ public void patchSharingInfo( String resourceIndex = resourcePluginInfo.indexByType(resourceType); if (resourceIndex == null) { LOGGER.debug("No resourceIndex mapping found for type '{}';", resourceType); + listener.onFailure( + new OpenSearchStatusException("No resourceIndex mapping found for type '{}';" + resourceType, RestStatus.UNAUTHORIZED) + ); return; } @@ -321,6 +306,9 @@ public void getSharingInfo(@NonNull String resourceId, @NonNull String resourceT String resourceIndex = resourcePluginInfo.indexByType(resourceType); if (resourceIndex == null) { LOGGER.debug("No resourceIndex mapping found for type '{}';", resourceType); + listener.onFailure( + new OpenSearchStatusException("No resourceIndex mapping found for type '{}';" + resourceType, RestStatus.UNAUTHORIZED) + ); return; } this.resourceSharingIndexHandler.fetchSharingInfo(resourceIndex, resourceId, ActionListener.wrap(sharingInfo -> { diff --git a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index 70c5bb908c..a44287bbfe 100644 --- a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -223,7 +223,7 @@ public Set getResourceTypes() { } } - public Collection getResourceIndices() { + public Set getResourceIndices() { lock.readLock().lock(); try { return new HashSet<>(typeToIndex.values()); diff --git a/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java b/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java index 445d06ef6e..e4962282f4 100644 --- a/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java +++ b/src/main/java/org/opensearch/security/resources/api/share/ShareRequest.java @@ -33,7 +33,6 @@ public class ShareRequest extends ActionRequest implements DocRequest { private final String resourceId; @JsonProperty("resource_type") private final String resourceType; - @JsonProperty("resource_index") private final String resourceIndex; @JsonProperty("share_with") private final ShareWith shareWith; diff --git a/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java index e33545a71c..3a59d447f5 100644 --- a/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java @@ -77,11 +77,11 @@ private void assertEvaluateAsync(boolean hasPermission, boolean expectedAllowed) ActionListener listener = inv.getArgument(4); listener.onResponse(hasPermission); return null; - }).when(resourceAccessHandler).hasPermission(eq("anyId"), eq("indices"), eq("read"), any(), any()); + }).when(resourceAccessHandler).hasPermission(eq("anyId"), eq("indices"), eq("read"), any()); ActionListener callback = mock(ActionListener.class); - evaluator.evaluateAsync(req, "read", context, callback); + evaluator.evaluateAsync(req, "read", callback); ArgumentCaptor captor = ArgumentCaptor.forClass(PrivilegesEvaluatorResponse.class); verify(callback).onResponse(captor.capture()); diff --git a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java index 68588cc371..06a4d80589 100644 --- a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java +++ b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java @@ -97,7 +97,7 @@ public void testHasPermission_adminUserAllowed() { when(adminDNs.isAdmin(user)).thenReturn(true); ActionListener listener = mock(ActionListener.class); - handler.hasPermission(RESOURCE_ID, TYPE, ACTION, context, listener); + handler.hasPermission(RESOURCE_ID, TYPE, ACTION, listener); verify(listener).onResponse(true); } @@ -120,7 +120,7 @@ public void testHasPermission_ownerAllowed() { }).when(sharingIndexHandler).fetchSharingInfo(eq(INDEX), eq(RESOURCE_ID), any()); ActionListener listener = mock(ActionListener.class); - handler.hasPermission(RESOURCE_ID, TYPE, ACTION, null, listener); + handler.hasPermission(RESOURCE_ID, TYPE, ACTION, listener); verify(listener).onResponse(true); } @@ -153,7 +153,7 @@ public void testHasPermission_sharedWithUserAllowed() { }).when(sharingIndexHandler).fetchSharingInfo(eq(INDEX), eq(RESOURCE_ID), any()); ActionListener listener = mock(ActionListener.class); - handler.hasPermission(RESOURCE_ID, TYPE, ACTION, null, listener); + handler.hasPermission(RESOURCE_ID, TYPE, ACTION, listener); verify(listener).onResponse(true); } @@ -177,7 +177,7 @@ public void testHasPermission_noAccessLevelsDenied() { }).when(sharingIndexHandler).fetchSharingInfo(eq(INDEX), eq(RESOURCE_ID), any()); ActionListener listener = mock(ActionListener.class); - handler.hasPermission(RESOURCE_ID, TYPE, ACTION, null, listener); + handler.hasPermission(RESOURCE_ID, TYPE, ACTION, listener); verify(listener).onResponse(false); } @@ -197,7 +197,7 @@ public void testHasPermission_nullDocumentDenied() { }).when(sharingIndexHandler).fetchSharingInfo(eq(INDEX), eq(RESOURCE_ID), any()); ActionListener listener = mock(ActionListener.class); - handler.hasPermission(RESOURCE_ID, TYPE, ACTION, null, listener); + handler.hasPermission(RESOURCE_ID, TYPE, ACTION, listener); verify(listener).onResponse(false); } @@ -211,7 +211,7 @@ public void testHasPermission_pluginUserDenied() { when(privilegesEvaluator.createContext(user, ACTION)).thenReturn(subjectContext); ActionListener listener = mock(ActionListener.class); - handler.hasPermission(RESOURCE_ID, TYPE, ACTION, null, listener); + handler.hasPermission(RESOURCE_ID, TYPE, ACTION, listener); verify(listener).onResponse(false); } From 06bb26f9ac55b02dffe624396e8d10f23dad032d Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 20 Oct 2025 16:15:14 -0400 Subject: [PATCH 36/40] Support multiple resource types in migrate api payload Signed-off-by: Craig Perkins --- RESOURCE_SHARING_AND_ACCESS_CONTROL.md | 4 +- .../opensearch/sample/resource/TestUtils.java | 34 ++++-- .../security/dlic/rest/support/Utils.java | 5 + .../MigrateResourceSharingInfoApiAction.java | 109 +++++++++++------- 4 files changed, 95 insertions(+), 57 deletions(-) diff --git a/RESOURCE_SHARING_AND_ACCESS_CONTROL.md b/RESOURCE_SHARING_AND_ACCESS_CONTROL.md index 400002c185..fd328daf6d 100644 --- a/RESOURCE_SHARING_AND_ACCESS_CONTROL.md +++ b/RESOURCE_SHARING_AND_ACCESS_CONTROL.md @@ -610,6 +610,7 @@ Read documents from a plugin’s index and migrate ownership and backend role-ba | `source_index` | string | yes | Name of the plugin index containing the existing resource documents | | `username_path` | string | yes | JSON Pointer to the username field inside each document | | `backend_roles_path` | string | yes | JSON Pointer to the backend_roles field (must point to a JSON array) | +| `type_path` | string | no | JSON Pointer to the resource type field inside each document (required if multiple resource types in same resource index) | | `default_access_level` | string | yes | Default access level to assign migrated backend_roles. Must be one from the available action-groups for this type. See `resource-action-groups.yml`. | **Example Request** @@ -619,7 +620,8 @@ Read documents from a plugin’s index and migrate ownership and backend role-ba { "source_index": ".sample_resource", "username_path": "/owner", - "backend_roles_path": "/access/backend_roles", + "backend_roles_path": "/backend_roles", + "type_path": "/type", "default_access_level": "read_only" } ``` diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java index 88a8ba25ca..f61440b288 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java @@ -155,10 +155,12 @@ public static String directSharePayload(String resourceId, String creator, Strin public static String migrationPayload_valid() { return """ { - "source_index": "%s", - "username_path": "%s", - "backend_roles_path": "%s", - "default_access_level": "%s" + "source_index": "%s", + "username_path": "%s", + "backend_roles_path": "%s", + "default_access_level": { + "sample-resource": "%s" + } } """.formatted(RESOURCE_INDEX_NAME, "user/name", "user/backend_roles", "sample_read_only"); } @@ -166,12 +168,14 @@ public static String migrationPayload_valid() { public static String migrationPayload_valid_withSpecifiedAccessLevel(String accessLevel) { return """ { - "source_index": "%s", - "username_path": "%s", - "backend_roles_path": "%s", - "default_access_level": "%s" + "source_index": "%s", + "username_path": "%s", + "backend_roles_path": "%s", + "default_access_level": { + "sample-resource": "%s" + } } - """.formatted(RESOURCE_INDEX_NAME, "user/name", "user/backend_roles", accessLevel); + """.formatted(RESOURCE_INDEX_NAME, "user/name", "user/backend_roles", accessLevel); } public static String migrationPayload_missingSourceIndex() { @@ -179,7 +183,9 @@ public static String migrationPayload_missingSourceIndex() { { "username_path": "%s", "backend_roles_path": "%s", - "default_access_level": "%s" + "default_access_level": { + "sample-resource": "%s" + } } """.formatted("user/name", "user/backend_roles", "sample_read_only"); } @@ -189,7 +195,9 @@ public static String migrationPayload_missingUserName() { { "source_index": "%s", "backend_roles_path": "%s", - "default_access_level": "%s" + "default_access_level": { + "sample-resource": "%s" + } } """.formatted(RESOURCE_INDEX_NAME, "user/backend_roles", "sample_read_only"); } @@ -199,7 +207,9 @@ public static String migrationPayload_missingBackendRoles() { { "source_index": "%s", "username_path": "%s", - "default_access_level": "%s" + "default_access_level": { + "sample-resource": "%s" + } } """.formatted(RESOURCE_INDEX_NAME, "user/name", "sample_read_only"); } 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 0e99b6aece..5967f25e15 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 @@ -127,6 +127,11 @@ public static JsonNode toJsonNode(final String content) throws IOException { return DefaultObjectMapper.readTree(content); } + public static Map toMapOfStrings(final JsonNode jsonNode) { + return internalMapper.convertValue(jsonNode, new TypeReference>() { + }); + } + public static Object toConfigObject(final JsonNode content, final Class clazz) throws IOException { return DefaultObjectMapper.readTree(content, clazz); } diff --git a/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java b/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java index 304040818e..20344d6157 100644 --- a/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java +++ b/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java @@ -139,52 +139,59 @@ private void handleMigrate(RestChannel channel, RestRequest request, Client clie * 3. Create a SourceDoc for each raw doc * 4. Returns a triple of the source index name, the default access level and the list of source docs. */ - private ValidationResult>> loadCurrentSharingInfo(RestRequest request, Client client) - throws IOException { + private ValidationResult, List>> loadCurrentSharingInfo( + RestRequest request, + Client client + ) throws IOException { JsonNode body = Utils.toJsonNode(request.content().utf8ToString()); String sourceIndex = body.get("source_index").asText(); String userNamePath = body.get("username_path").asText(); String backendRolesPath = body.get("backend_roles_path").asText(); - String defaultAccessLevel = body.get("default_access_level").asText(); - - // TODO: once we support multiple resource types within an index, this needs to be revisited - // Default access level as part of the resource-action-groups.yml file - Set types = resourcePluginInfo.typesByIndex(sourceIndex); - if (types.isEmpty()) { - LOGGER.error("Invalid resource index [{}]. No resource types are associated with this index", sourceIndex); - String badRequestMessage = "Invalid resource index [" - + sourceIndex - + "]. Either no resource type or multiple resource types are associated with this index"; - return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(badRequestMessage)); - } - String type = types.iterator().next(); - if (types.size() > 1) { - LOGGER.warn( - "Invalid resource index [{}]. Multiple resource types are associated with this index, choosing {}.", - sourceIndex, - type - ); + JsonNode node = body.get("default_access_level"); + Map typeToDefaultAccessLevel = Utils.toMapOfStrings(node); + String typePath = null; + if (body.has("type_path")) { + typePath = body.get("type_path").asText(); + } else { + LOGGER.info("No type_path provided, assuming single resource-type for all records."); + if (typeToDefaultAccessLevel.size() > 1) { + String badRequestMessage = "type_path must be provided when multiple resource types are specified in default_access_level."; + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(badRequestMessage)); + } } - // check that access level exists for given resource-index - var accessLevels = resourcePluginInfo.flattenedForType(type).actionGroups(); - if (!accessLevels.contains(defaultAccessLevel)) { - LOGGER.error( - "Invalid access level {} for resource sharing for resource type [{}]. Available access-levels are [{}]", - defaultAccessLevel, - sourceIndex, - accessLevels - ); - String badRequestMessage = "Invalid access level " - + defaultAccessLevel - + " for resource sharing for resource type [" - + type - + "]. Available access-levels are [" - + accessLevels - + "]"; + if (!resourcePluginInfo.getResourceIndices().contains(sourceIndex)) { + String badRequestMessage = "Invalid resource index " + sourceIndex + "."; return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(badRequestMessage)); } + for (String type : typeToDefaultAccessLevel.keySet()) { + String defaultAccessLevelForType = typeToDefaultAccessLevel.get(type); + LOGGER.info("Default access level for resource type [{}] is [{}]", type, typeToDefaultAccessLevel.get(type)); + // check that access level exists for given resource-index + if (resourcePluginInfo.indexByType(type) == null) { + String badRequestMessage = "Invalid resource type " + type + "."; + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(badRequestMessage)); + } + var accessLevels = resourcePluginInfo.flattenedForType(type).actionGroups(); + if (!accessLevels.contains(defaultAccessLevelForType)) { + LOGGER.error( + "Invalid access level {} for resource sharing for resource type [{}]. Available access-levels are [{}]", + defaultAccessLevelForType, + type, + accessLevels + ); + String badRequestMessage = "Invalid access level " + + defaultAccessLevelForType + + " for resource sharing for resource type [" + + type + + "]. Available access-levels are [" + + accessLevels + + "]"; + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(badRequestMessage)); + } + } + List results = new ArrayList<>(); // 1) configure a 1-minute scroll @@ -218,7 +225,19 @@ private ValidationResult>> loadCurrentSha } } - results.add(new SourceDoc(id, username, backendRoles)); + String type = null; + + if (typePath != null) { + type = rec.at(typePath.startsWith("/") ? typePath : ("/" + typePath)).asText(null); + if (type == null) { + LOGGER.debug("Record without associated type, skipping entirely: {}", hit.getId()); + continue; + } + } else { + type = typeToDefaultAccessLevel.keySet().iterator().next(); + } + + results.add(new SourceDoc(id, username, backendRoles, type)); } // 4) fetch next batch SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId).scroll(scroll); @@ -231,7 +250,7 @@ private ValidationResult>> loadCurrentSha clear.addScrollId(scrollId); client.clearScroll(clear).actionGet(); - return ValidationResult.success(Triple.of(sourceIndex, defaultAccessLevel, results)); + return ValidationResult.success(Triple.of(sourceIndex, typeToDefaultAccessLevel, results)); } /** @@ -239,7 +258,7 @@ private ValidationResult>> loadCurrentSha * 1. Parses existing sharing info to a new ResourceSharing records * 2. Indexes the new record into corresponding resource-sharing index */ - private ValidationResult createNewSharingRecords(Triple> sourceInfo) + private ValidationResult createNewSharingRecords(Triple, List> sourceInfo) throws IOException { AtomicInteger migratedCount = new AtomicInteger(); AtomicReference> skippedNoUser = new AtomicReference<>(); @@ -278,7 +297,7 @@ private ValidationResult createNewSharingRecords(Triple(backendRoles))); - shareWith = new ShareWith(Map.of(sourceInfo.getMiddle(), recipients)); + shareWith = new ShareWith(Map.of(sourceInfo.getMiddle().get(doc.type), recipients)); } // 5) index the new record @@ -359,8 +378,8 @@ public Map allowedKeys() { .put("source_index", RequestContentValidator.DataType.STRING) // name of the resource plugin index .put("username_path", RequestContentValidator.DataType.STRING) // path to resource creator's name .put("backend_roles_path", RequestContentValidator.DataType.STRING) // path to backend_roles - .put("default_access_level", RequestContentValidator.DataType.STRING) // default access level for the new - // structure + .put("type_path", RequestContentValidator.DataType.STRING) // path to resource type + .put("default_access_level", RequestContentValidator.DataType.OBJECT) // default access level by type .build(); } }); @@ -372,11 +391,13 @@ static class SourceDoc { String resourceId; String username; List backendRoles; + String type; - public SourceDoc(String id, String username, List backendRoles) { + public SourceDoc(String id, String username, List backendRoles, String type) { this.resourceId = id; this.username = username; this.backendRoles = backendRoles; + this.type = type; } } From 67f3a2bb1012835b519fc1c33f5ad949178ffae9 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 20 Oct 2025 16:38:40 -0400 Subject: [PATCH 37/40] Fix tests Signed-off-by: Craig Perkins --- .../privileges/ResourceAccessEvaluatorTest.java | 2 +- .../resources/ResourceAccessHandlerTest.java | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java index 3a59d447f5..ff70fe8d9f 100644 --- a/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java @@ -74,7 +74,7 @@ private void assertEvaluateAsync(boolean hasPermission, boolean expectedAllowed) // TODO check to see if type can be something other than indices doAnswer(inv -> { - ActionListener listener = inv.getArgument(4); + ActionListener listener = inv.getArgument(3); listener.onResponse(hasPermission); return null; }).when(resourceAccessHandler).hasPermission(eq("anyId"), eq("indices"), eq("read"), any()); diff --git a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java index 06a4d80589..da75c9a5ac 100644 --- a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java +++ b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java @@ -26,7 +26,6 @@ import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; -import org.opensearch.security.privileges.actionlevel.SubjectBasedActionPrivileges; import org.opensearch.security.resources.sharing.Recipient; import org.opensearch.security.resources.sharing.ResourceSharing; import org.opensearch.security.resources.sharing.ShareWith; @@ -202,20 +201,6 @@ public void testHasPermission_nullDocumentDenied() { verify(listener).onResponse(false); } - @Test - public void testHasPermission_pluginUserDenied() { - User user = new User("plugin_user", ImmutableSet.of(), ImmutableSet.of(), null, ImmutableMap.of(), false); - injectUser(user); - PrivilegesEvaluationContext subjectContext = mock(PrivilegesEvaluationContext.class); - when(subjectContext.getActionPrivileges()).thenReturn(mock(SubjectBasedActionPrivileges.class)); - when(privilegesEvaluator.createContext(user, ACTION)).thenReturn(subjectContext); - - ActionListener listener = mock(ActionListener.class); - handler.hasPermission(RESOURCE_ID, TYPE, ACTION, listener); - - verify(listener).onResponse(false); - } - @Test public void testGetOwnAndSharedResources_asAdmin() { User admin = new User("admin", ImmutableSet.of(), ImmutableSet.of(), null, ImmutableMap.of(), false); From 2dcde2e7f0b6416ddaaa0502177b3376b32889ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:46:04 -0400 Subject: [PATCH 38/40] Bump com.autonomousapps.build-health from 3.0.4 to 3.1.0 (#5726) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + settings.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55e2cae4cd..91dbaf93e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Bump `derek-ho/start-opensearch` from 7 to 9 ([#5630](https://github.com/opensearch-project/security/pull/5630), [#5679](https://github.com/opensearch-project/security/pull/5679)) - Bump `github/codeql-action` from 3 to 4 ([#5702](https://github.com/opensearch-project/security/pull/5702)) - Bump `com.github.spotbugs` from 6.4.2 to 6.4.4 ([#5727](https://github.com/opensearch-project/security/pull/5727)) +- Bump `com.autonomousapps.build-health` from 3.0.4 to 3.1.0 ([#5726](https://github.com/opensearch-project/security/pull/5726)) ### Documentation diff --git a/settings.gradle b/settings.gradle index 2d9e2b9b4f..e309543ad6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,7 +5,7 @@ */ plugins { - id("com.autonomousapps.build-health") version "3.0.4" + id("com.autonomousapps.build-health") version "3.1.0" } rootProject.name = 'opensearch-security' From dab8bb05846269c8d1f1b2c878c520f0645d5c69 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Mon, 20 Oct 2025 17:08:53 -0400 Subject: [PATCH 39/40] Address review feedback Signed-off-by: Craig Perkins --- RESOURCE_SHARING_AND_ACCESS_CONTROL.md | 19 ++++++++++-------- .../MigrateResourceSharingInfoApiAction.java | 20 +++++++++++-------- .../resources/ResourceAccessHandlerTest.java | 14 ------------- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/RESOURCE_SHARING_AND_ACCESS_CONTROL.md b/RESOURCE_SHARING_AND_ACCESS_CONTROL.md index fd328daf6d..ed41ac8f7b 100644 --- a/RESOURCE_SHARING_AND_ACCESS_CONTROL.md +++ b/RESOURCE_SHARING_AND_ACCESS_CONTROL.md @@ -605,13 +605,13 @@ Read documents from a plugin’s index and migrate ownership and backend role-ba **Request Body** -| Parameter | Type | Required | Description | -|------------------------|---------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------| -| `source_index` | string | yes | Name of the plugin index containing the existing resource documents | -| `username_path` | string | yes | JSON Pointer to the username field inside each document | -| `backend_roles_path` | string | yes | JSON Pointer to the backend_roles field (must point to a JSON array) | -| `type_path` | string | no | JSON Pointer to the resource type field inside each document (required if multiple resource types in same resource index) | -| `default_access_level` | string | yes | Default access level to assign migrated backend_roles. Must be one from the available action-groups for this type. See `resource-action-groups.yml`. | +| Parameter | Type | Required | Description | +|------------------------|--------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------| +| `source_index` | string | yes | Name of the plugin index containing the existing resource documents | +| `username_path` | string | yes | JSON Pointer to the username field inside each document | +| `backend_roles_path` | string | yes | JSON Pointer to the backend_roles field (must point to a JSON array) | +| `type_path` | string | no | JSON Pointer to the resource type field inside each document (required if multiple resource types in same resource index) | +| `default_access_level` | object | yes | Default access level to assign migrated backend_roles. Must be one from the available action-groups for this type. See `resource-action-groups.yml`. | **Example Request** `POST /_plugins/_security/api/resources/migrate` @@ -622,7 +622,10 @@ Read documents from a plugin’s index and migrate ownership and backend role-ba "username_path": "/owner", "backend_roles_path": "/backend_roles", "type_path": "/type", - "default_access_level": "read_only" + "default_access_level": { + "sample-resource": "read_only", + "sample-resource-group": "read-only-group" + } } ``` diff --git a/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java b/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java index 20344d6157..70bdf61de3 100644 --- a/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java +++ b/src/main/java/org/opensearch/security/resources/api/migrate/MigrateResourceSharingInfoApiAction.java @@ -160,7 +160,7 @@ private ValidationResult, List>> l return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(badRequestMessage)); } } - if (!resourcePluginInfo.getResourceIndices().contains(sourceIndex)) { + if (!resourcePluginInfo.getResourceIndicesForProtectedTypes().contains(sourceIndex)) { String badRequestMessage = "Invalid resource index " + sourceIndex + "."; return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage(badRequestMessage)); } @@ -225,14 +225,9 @@ private ValidationResult, List>> l } } - String type = null; - + String type; if (typePath != null) { type = rec.at(typePath.startsWith("/") ? typePath : ("/" + typePath)).asText(null); - if (type == null) { - LOGGER.debug("Record without associated type, skipping entirely: {}", hit.getId()); - continue; - } } else { type = typeToDefaultAccessLevel.keySet().iterator().next(); } @@ -279,7 +274,7 @@ private ValidationResult createNewSharingRecords(Triple createNewSharingRecords(Triple { ActionListener l = inv.getArgument(2); From ba937a6f5ef9e7eb061744fd85c8dff48416890c Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 20 Oct 2025 17:27:59 -0400 Subject: [PATCH 40/40] Adds changelog entry Signed-off-by: Darshit Chanpura --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55e2cae4cd..d7d6906faa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Enhancements - Makes resource settings dynamic ([#5677](https://github.com/opensearch-project/security/pull/5677)) +- [Resource Sharing] Allow multiple sharable resource types in single resource index ([#5713](https://github.com/opensearch-project/security/pull/5713)) ### Bug Fixes - Create a WildcardMatcher.NONE when creating a WildcardMatcher with an empty string ([#5694](https://github.com/opensearch-project/security/pull/5694))