1010package org .opensearch .security .resources ;
1111
1212import java .io .IOException ;
13+ import java .util .HashMap ;
1314import java .util .HashSet ;
15+ import java .util .List ;
16+ import java .util .Locale ;
1417import java .util .Map ;
1518import java .util .Set ;
1619
17- import com .fasterxml .jackson .databind .JsonNode ;
18- import com .fasterxml .jackson .databind .node .ObjectNode ;
1920import org .apache .commons .lang3 .StringUtils ;
2021import org .apache .logging .log4j .LogManager ;
2122import org .apache .logging .log4j .Logger ;
3738import org .opensearch .action .search .SearchResponse ;
3839import org .opensearch .action .search .SearchScrollRequest ;
3940import org .opensearch .action .support .WriteRequest ;
41+ import org .opensearch .common .Nullable ;
4042import org .opensearch .common .unit .TimeValue ;
4143import org .opensearch .common .util .concurrent .ThreadContext ;
4244import org .opensearch .common .xcontent .LoggingDeprecationHandler ;
5658import org .opensearch .search .Scroll ;
5759import org .opensearch .search .SearchHit ;
5860import org .opensearch .search .builder .SearchSourceBuilder ;
59- import org .opensearch .security .dlic .rest .support .Utils ;
6061import org .opensearch .security .spi .resources .sharing .CreatedBy ;
6162import org .opensearch .security .spi .resources .sharing .Recipient ;
6263import org .opensearch .security .spi .resources .sharing .Recipients ;
6566import org .opensearch .threadpool .ThreadPool ;
6667import org .opensearch .transport .client .Client ;
6768
68- import com .flipkart .zjsonpatch .JsonPatch ;
69-
7069import static org .opensearch .common .xcontent .XContentFactory .jsonBuilder ;
7170
7271/**
@@ -608,17 +607,109 @@ public void revoke(String resourceId, String resourceIndex, ShareWith revokeAcce
608607 }
609608
610609 /**
611- * Fetch existing share_with, apply JSON-Patch ops, and update the document.
612- * Three ops are supported:
613- * 1. Move -> upgrade or downgrade access
614- * 2. Add -> share with new entities
615- * 3. Remove -> revoke access
610+ * Helper method to build shareWith object from the supplied patch to then be used to apply the patch
611+ * @param patch source content
612+ * @return the parsed content map of entities to share and to revoke
613+ */
614+ @ SuppressWarnings ("unchecked" )
615+ private Map <String , ShareWith > patchParser (Map <String , Object > patch ) {
616+ Map <String , ShareWith > patches = new HashMap <>(2 );
617+ patches .put ("share_with" , buildShareWithObject ((Map <String , Object >) patch .get ("share_with" )));
618+ patches .put ("revoke" , buildShareWithObject ((Map <String , Object >) patch .get ("revoke" )));
619+ return patches ;
620+ }
621+
622+ /**
623+ * Helper to build the shareWith structure to be used to apply the patch
624+ */
625+ @ SuppressWarnings ("unchecked" )
626+ private ShareWith buildShareWithObject (Map <String , Object > rawMap ) {
627+ ShareWith sw = new ShareWith (new HashMap <>());
628+ try {
629+ if (rawMap == null ) return sw ;
630+
631+ for (var e : rawMap .entrySet ()) {
632+ String level = e .getKey ();
633+
634+ Map <String , Object > recMap = (Map <String , Object >) e .getValue ();
635+
636+ Map <Recipient , Set <String >> recipients = new HashMap <>();
637+ for (var rec : recMap .entrySet ()) {
638+ recipients .put (Recipient .valueOf (rec .getKey ().toUpperCase (Locale .ROOT )), new HashSet <>((List <String >) rec .getValue ()));
639+ }
640+
641+ sw .getSharingInfo ().put (level , new Recipients (recipients ));
642+ }
643+ } catch (Exception e ) {
644+ LOGGER .debug ("Failed to build share with object" , e );
645+ }
646+ return sw ;
647+ }
648+
649+ /**
650+ * Applies the patch to the original resource-sharing share-with record.
651+ * @param existing current share-with object
652+ * @param patches updates to be applied; share-with will be added and revoke will be removed
653+ * @return updated share-with object
654+ */
655+ private ShareWith applyPatch (@ Nullable ShareWith existing , Map <String , ShareWith > patches ) {
656+ Map <String , Recipients > updated = new HashMap <>();
657+
658+ // 1) put all current values into the update map
659+ if (existing != null ) {
660+ updated .putAll (existing .getSharingInfo ());
661+ }
662+
663+ // 2) apply all the "share_with" patches
664+ ShareWith sharePatch = patches .get ("share_with" );
665+ if (sharePatch != null ) {
666+ for (var entry : sharePatch .getSharingInfo ().entrySet ()) {
667+ String level = entry .getKey ();
668+ Recipients toShare = entry .getValue ();
669+ // if there’s already a Recipients at that level, merge into it;
670+ // otherwise insert a fresh copy of the patch
671+ updated .merge (level , toShare , (origRecipients , patchCopy ) -> {
672+ origRecipients .share (toShare );
673+ return origRecipients ;
674+ });
675+ }
676+ }
677+
678+ // 3) apply all the "revoke" patches
679+ ShareWith revokePatch = patches .get ("revoke" );
680+ if (revokePatch != null ) {
681+ for (var entry : revokePatch .getSharingInfo ().entrySet ()) {
682+ String level = entry .getKey ();
683+ Recipients toRevoke = entry .getValue ();
684+ // we revoke only if we already had any recipients at that level
685+ updated .computeIfPresent (level , (lvl , origRecipients ) -> {
686+ origRecipients .revoke (toRevoke );
687+ return origRecipients ;
688+ });
689+ }
690+ }
691+
692+ // 4) return a new ShareWith object with the updated recipients
693+ return new ShareWith (updated );
694+ }
695+
696+ /**
697+ * Fetch existing share_with, apply the patch ops in-memory, and update the sharing record.
698+ * Two ops are supported:
699+ * 1. share_with -> upgrade or downgrade access; share with new entities
700+ * 2. revoke -> revoke access for existing entities
616701 * @param resourceId id of the resource to apply the patch to
617702 * @param resourceIndex name of the index where resource is present
618703 * @param patchContent the patch to be applied
619704 * @param listener listener to be notified in case of success or failure
620705 */
621- public void patchSharingInfo (String resourceId , String resourceIndex , JsonNode patchContent , ActionListener <ResourceSharing > listener ) {
706+ public void patchSharingInfo (
707+ String resourceId ,
708+ String resourceIndex ,
709+ Map <String , Object > patchContent ,
710+ ActionListener <ResourceSharing > listener
711+ ) {
712+
622713 StepListener <ResourceSharing > sharingInfoListener = new StepListener <>();
623714 String resourceSharingIndex = getSharingIndex (resourceIndex );
624715
@@ -627,20 +718,13 @@ public void patchSharingInfo(String resourceId, String resourceIndex, JsonNode p
627718
628719 // Apply patch and update the document
629720 sharingInfoListener .whenComplete (resourceSharing -> {
630- // TODO see if .getShareWith() call should be removed, if so patch content path must be "/share_with"
631- final var sharingNode = (ObjectNode ) Utils .convertJsonToJackson (resourceSharing .getShareWith (), true );
632- JsonNode updatedSharingNode = JsonPatch .apply (sharingNode , patchContent );
633-
634- ResourceSharing updatedSharingInfo ;
635-
636- try (
637- XContentParser parser = XContentType .JSON .xContent ()
638- .createParser (NamedXContentRegistry .EMPTY , LoggingDeprecationHandler .INSTANCE , updatedSharingNode .toString ())
639- ) {
640- ShareWith shareWith = ShareWith .fromXContent (parser );
641- updatedSharingInfo = new ResourceSharing (resourceId , resourceSharing .getCreatedBy (), shareWith );
642- }
721+ // parse and apply the patch
722+ Map <String , ShareWith > patches = patchParser (patchContent );
723+ ShareWith updatedShareWith = applyPatch (resourceSharing .getShareWith (), patches );
724+
725+ ResourceSharing updatedSharingInfo = new ResourceSharing (resourceId , resourceSharing .getCreatedBy (), updatedShareWith );
643726
727+ // update the record
644728 IndexRequest ir = client .prepareIndex (resourceSharingIndex )
645729 .setId (resourceId )
646730 .setRefreshPolicy (WriteRequest .RefreshPolicy .IMMEDIATE )
0 commit comments