Skip to content

Commit 7ab5686

Browse files
Updates PATCH api request content signature and adds handler for GET endpoint
Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
1 parent 1d7678d commit 7ab5686

File tree

7 files changed

+246
-80
lines changed

7 files changed

+246
-80
lines changed

spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipients.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,22 @@ public Map<Recipient, Set<String>> getRecipients() {
6060
public void share(Recipients target) {
6161
Map<Recipient, Set<String>> targetRecipients = target.getRecipients();
6262
for (Recipient recipientType : targetRecipients.keySet()) {
63-
Set<String> updatedRecipients = recipients.get(recipientType);
64-
updatedRecipients.addAll(targetRecipients.get(recipientType));
63+
if (!recipients.containsKey(recipientType)) { // will not be present in case of very first share
64+
recipients.put(recipientType, targetRecipients.get(recipientType));
65+
} else {
66+
Set<String> updatedRecipients = recipients.get(recipientType);
67+
updatedRecipients.addAll(targetRecipients.get(recipientType));
68+
}
6569
}
6670
}
6771

6872
public void revoke(Recipients target) {
6973
Map<Recipient, Set<String>> targetRecipients = target.getRecipients();
7074
for (Recipient recipientType : targetRecipients.keySet()) {
71-
Set<String> updatedRecipients = recipients.get(recipientType);
72-
updatedRecipients.removeAll(targetRecipients.get(recipientType));
75+
if (recipients.containsKey(recipientType)) { // no need to revoke if access was not granted in first place
76+
Set<String> updatedRecipients = recipients.get(recipientType);
77+
updatedRecipients.removeAll(targetRecipients.get(recipientType));
78+
}
7379
}
7480
}
7581

@@ -117,7 +123,7 @@ public static Recipients fromXContent(XContentParser parser) throws IOException
117123

118124
@Override
119125
public String toString() {
120-
return "{" + recipients + '}';
126+
return recipients.toString();
121127
}
122128

123129
@Override

src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313

1414
import java.util.Collections;
1515
import java.util.HashSet;
16+
import java.util.Map;
1617
import java.util.Set;
1718
import java.util.stream.Collectors;
1819
import java.util.stream.Stream;
1920

20-
import com.fasterxml.jackson.databind.JsonNode;
2121
import org.apache.logging.log4j.LogManager;
2222
import org.apache.logging.log4j.Logger;
2323

@@ -128,7 +128,7 @@ public void getOwnAndSharedResourceIdsForCurrentUser(@NonNull String resourceInd
128128
public void patchSharingInfo(
129129
@NonNull String resourceId,
130130
@NonNull String resourceIndex,
131-
@NonNull JsonNode patchContent,
131+
@NonNull Map<String, Object> patchContent,
132132
ActionListener<ResourceSharing> listener
133133
) {
134134
final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent(
@@ -165,6 +165,41 @@ public void patchSharingInfo(
165165

166166
}
167167

168+
/**
169+
* Get sharing info for this record
170+
* @param resourceId id of the resource whose sharing info is to be fetched
171+
* @param resourceIndex name of the resource index
172+
* @param listener listener to be notified of final resource sharing record
173+
*/
174+
public void getSharingInfo(@NonNull String resourceId, @NonNull String resourceIndex, ActionListener<ResourceSharing> listener) {
175+
final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent(
176+
ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER
177+
);
178+
final User user = (userSubject == null) ? null : userSubject.getUser();
179+
180+
if (user == null) {
181+
LOGGER.warn("No authenticated user found. Failed to fetch resource sharing info {}", resourceId);
182+
listener.onFailure(
183+
new OpenSearchStatusException(
184+
"No authenticated user found. Failed to fetch resource sharing info " + resourceId,
185+
RestStatus.UNAUTHORIZED
186+
)
187+
);
188+
return;
189+
}
190+
191+
LOGGER.debug("User {} is fetching sharing info for resource {} in index {}", user.getName(), resourceId, resourceIndex);
192+
193+
this.resourceSharingIndexHandler.fetchSharingInfo(resourceIndex, resourceId, ActionListener.wrap(sharingInfo -> {
194+
LOGGER.debug("Successfully fetched sharing info for resource {} in index {}", resourceId, resourceIndex);
195+
listener.onResponse(sharingInfo);
196+
}, e -> {
197+
LOGGER.error("Failed to fetched sharing info for resource {} in index {}: {}", resourceId, resourceIndex, e.getMessage());
198+
listener.onFailure(e);
199+
}));
200+
201+
}
202+
168203
/**
169204
* Shares a resource with the specified users, roles, and backend roles.
170205
*

src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java

Lines changed: 108 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
package org.opensearch.security.resources;
1111

1212
import java.io.IOException;
13+
import java.util.HashMap;
1314
import java.util.HashSet;
15+
import java.util.List;
16+
import java.util.Locale;
1417
import java.util.Map;
1518
import java.util.Set;
1619

17-
import com.fasterxml.jackson.databind.JsonNode;
18-
import com.fasterxml.jackson.databind.node.ObjectNode;
1920
import org.apache.commons.lang3.StringUtils;
2021
import org.apache.logging.log4j.LogManager;
2122
import org.apache.logging.log4j.Logger;
@@ -37,6 +38,7 @@
3738
import org.opensearch.action.search.SearchResponse;
3839
import org.opensearch.action.search.SearchScrollRequest;
3940
import org.opensearch.action.support.WriteRequest;
41+
import org.opensearch.common.Nullable;
4042
import org.opensearch.common.unit.TimeValue;
4143
import org.opensearch.common.util.concurrent.ThreadContext;
4244
import org.opensearch.common.xcontent.LoggingDeprecationHandler;
@@ -56,7 +58,6 @@
5658
import org.opensearch.search.Scroll;
5759
import org.opensearch.search.SearchHit;
5860
import org.opensearch.search.builder.SearchSourceBuilder;
59-
import org.opensearch.security.dlic.rest.support.Utils;
6061
import org.opensearch.security.spi.resources.sharing.CreatedBy;
6162
import org.opensearch.security.spi.resources.sharing.Recipient;
6263
import org.opensearch.security.spi.resources.sharing.Recipients;
@@ -65,8 +66,6 @@
6566
import org.opensearch.threadpool.ThreadPool;
6667
import org.opensearch.transport.client.Client;
6768

68-
import com.flipkart.zjsonpatch.JsonPatch;
69-
7069
import 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

Comments
 (0)