Skip to content

Commit bc20bf0

Browse files
Adds test for Share API
Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
1 parent 87c9676 commit bc20bf0

File tree

3 files changed

+334
-3
lines changed

3 files changed

+334
-3
lines changed
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.sample.resource;
10+
11+
import java.util.HashMap;
12+
import java.util.HashSet;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.Set;
16+
17+
import com.carrotsearch.randomizedtesting.RandomizedRunner;
18+
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
19+
import org.apache.http.HttpStatus;
20+
import org.junit.After;
21+
import org.junit.Before;
22+
import org.junit.ClassRule;
23+
import org.junit.Test;
24+
import org.junit.runner.RunWith;
25+
import org.junit.runners.Suite;
26+
27+
import org.opensearch.Version;
28+
import org.opensearch.painless.PainlessModulePlugin;
29+
import org.opensearch.plugins.PluginInfo;
30+
import org.opensearch.sample.SampleResourcePlugin;
31+
import org.opensearch.security.OpenSearchSecurityPlugin;
32+
import org.opensearch.security.spi.resources.sharing.Recipient;
33+
import org.opensearch.security.spi.resources.sharing.Recipients;
34+
import org.opensearch.test.framework.cluster.ClusterManager;
35+
import org.opensearch.test.framework.cluster.LocalCluster;
36+
import org.opensearch.test.framework.cluster.TestRestClient;
37+
38+
import static org.hamcrest.MatcherAssert.assertThat;
39+
import static org.hamcrest.Matchers.containsString;
40+
import static org.hamcrest.Matchers.equalTo;
41+
import static org.hamcrest.Matchers.not;
42+
import static org.opensearch.sample.resource.TestHelper.FULL_ACCESS_USER;
43+
import static org.opensearch.sample.resource.TestHelper.LIMITED_ACCESS_USER;
44+
import static org.opensearch.sample.resource.TestHelper.NO_ACCESS_USER;
45+
import static org.opensearch.sample.resource.TestHelper.RESOURCE_SHARING_INDEX;
46+
import static org.opensearch.sample.resource.TestHelper.SECURITY_SHARE_ENDPOINT;
47+
import static org.opensearch.sample.resource.TestHelper.putSharingInfoPayload;
48+
import static org.opensearch.sample.resource.TestHelper.sampleAllAG;
49+
import static org.opensearch.sample.resource.TestHelper.sampleReadOnlyAG;
50+
import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME;
51+
import static org.opensearch.security.spi.resources.FeatureConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED;
52+
import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY;
53+
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
54+
import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN;
55+
56+
/**
57+
* This test file tests the share API defined by the security plugin.
58+
* Resource access control feature and system index protection are assumed to be enabled
59+
*/
60+
@RunWith(Suite.class)
61+
@Suite.SuiteClasses({ ShareApiTests.RoutesTests.class })
62+
public class ShareApiTests {
63+
/**
64+
* Base test class providing shared cluster setup and teardown
65+
*/
66+
public static abstract class BaseTests {
67+
@ClassRule
68+
public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
69+
.plugin(
70+
new PluginInfo(
71+
SampleResourcePlugin.class.getName(),
72+
"classpath plugin",
73+
"NA",
74+
Version.CURRENT,
75+
"1.8",
76+
SampleResourcePlugin.class.getName(),
77+
null,
78+
List.of(OpenSearchSecurityPlugin.class.getName()),
79+
false
80+
)
81+
)
82+
.plugin(PainlessModulePlugin.class)
83+
.anonymousAuth(true)
84+
.authc(AUTHC_HTTPBASIC_INTERNAL)
85+
.users(USER_ADMIN, FULL_ACCESS_USER, LIMITED_ACCESS_USER, NO_ACCESS_USER)
86+
.actionGroups(sampleReadOnlyAG, sampleAllAG)
87+
.nodeSettings(Map.of(OPENSEARCH_RESOURCE_SHARING_ENABLED, true, SECURITY_SYSTEM_INDICES_ENABLED_KEY, true))
88+
.build();
89+
90+
@After
91+
public void clearIndices() {
92+
try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) {
93+
client.delete(RESOURCE_INDEX_NAME);
94+
client.delete(RESOURCE_SHARING_INDEX);
95+
}
96+
}
97+
}
98+
99+
/**
100+
* Tests exercising the share API endpoints, GET, PUT & PATCH
101+
*/
102+
@RunWith(RandomizedRunner.class)
103+
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
104+
public static class RoutesTests extends BaseTests {
105+
private final TestHelper.ApiHelper api = new TestHelper.ApiHelper(cluster);
106+
private String adminResId;
107+
108+
@Before
109+
public void setup() {
110+
adminResId = api.createSampleResourceAs(USER_ADMIN);
111+
api.awaitSharingEntry();
112+
}
113+
114+
@Test
115+
public void testPutSharingInfo() {
116+
// non-permission user cannot share resource
117+
try (TestRestClient client = cluster.getRestClient(LIMITED_ACCESS_USER)) {
118+
TestRestClient.HttpResponse response = client.putJson(
119+
SECURITY_SHARE_ENDPOINT,
120+
putSharingInfoPayload(adminResId, RESOURCE_INDEX_NAME, sampleReadOnlyAG.name(), NO_ACCESS_USER.getName())
121+
);
122+
response.assertStatusCode(HttpStatus.SC_FORBIDDEN);
123+
}
124+
125+
// a sharing entry should be created successfully since admin has access to share API
126+
try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) {
127+
TestRestClient.HttpResponse response = client.putJson(
128+
SECURITY_SHARE_ENDPOINT,
129+
putSharingInfoPayload(adminResId, RESOURCE_INDEX_NAME, sampleAllAG.name(), LIMITED_ACCESS_USER.getName())
130+
);
131+
response.assertStatusCode(HttpStatus.SC_OK);
132+
assertThat(response.getBody(), containsString(LIMITED_ACCESS_USER.getName()));
133+
assertThat(response.getBody(), not(containsString(NO_ACCESS_USER.getName())));
134+
}
135+
136+
// non-permission user will now have access to directly call share API
137+
try (TestRestClient client = cluster.getRestClient(LIMITED_ACCESS_USER)) {
138+
TestRestClient.HttpResponse response = client.putJson(
139+
SECURITY_SHARE_ENDPOINT,
140+
putSharingInfoPayload(adminResId, RESOURCE_INDEX_NAME, sampleReadOnlyAG.name(), NO_ACCESS_USER.getName())
141+
);
142+
response.assertStatusCode(HttpStatus.SC_OK);
143+
assertThat(response.getBody(), containsString(NO_ACCESS_USER.getName()));
144+
}
145+
}
146+
147+
@Test
148+
public void testGetSharingInfo() {
149+
// non-permission user cannot list shared resources,
150+
try (TestRestClient client = cluster.getRestClient(FULL_ACCESS_USER)) {
151+
TestRestClient.HttpResponse response = client.get(
152+
SECURITY_SHARE_ENDPOINT + "?resource_id=" + adminResId + "&resource_index=" + RESOURCE_INDEX_NAME
153+
);
154+
response.assertStatusCode(HttpStatus.SC_FORBIDDEN);
155+
}
156+
157+
// a sharing entry should be created successfully since admin has access to share API
158+
try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) {
159+
TestRestClient.HttpResponse response = client.putJson(
160+
SECURITY_SHARE_ENDPOINT,
161+
putSharingInfoPayload(adminResId, RESOURCE_INDEX_NAME, sampleAllAG.name(), FULL_ACCESS_USER.getName())
162+
);
163+
response.assertStatusCode(HttpStatus.SC_OK);
164+
assertThat(response.getBody(), containsString(FULL_ACCESS_USER.getName()));
165+
}
166+
167+
// non-permission user can now list shared_with resources by calling share API
168+
try (TestRestClient client = cluster.getRestClient(FULL_ACCESS_USER)) {
169+
TestRestClient.HttpResponse response = client.get(
170+
SECURITY_SHARE_ENDPOINT + "?resource_id=" + adminResId + "&resource_index=" + RESOURCE_INDEX_NAME
171+
);
172+
response.assertStatusCode(HttpStatus.SC_OK);
173+
assertThat(response.bodyAsJsonNode().get("sharing_info").get("resource_id").asText(), equalTo(adminResId));
174+
}
175+
}
176+
177+
@Test
178+
public void testPatchSharingInfo() {
179+
Map<Recipient, Set<String>> recs = new HashMap<>();
180+
Set<String> users = new HashSet<>();
181+
users.add(FULL_ACCESS_USER.getName());
182+
recs.put(Recipient.USERS, users);
183+
Recipients recipients = new Recipients(recs);
184+
185+
TestHelper.PatchSharingInfoPayloadBuilder patchSharingInfoPayloadBuilder = new TestHelper.PatchSharingInfoPayloadBuilder();
186+
patchSharingInfoPayloadBuilder.resourceId(adminResId).resourceIndex(RESOURCE_INDEX_NAME).share(recipients, sampleAllAG.name());
187+
188+
// full-access user cannot share with itself since user doesn't have permission to share
189+
try (TestRestClient client = cluster.getRestClient(FULL_ACCESS_USER)) {
190+
TestRestClient.HttpResponse response = client.patch(SECURITY_SHARE_ENDPOINT, patchSharingInfoPayloadBuilder.build());
191+
response.assertStatusCode(HttpStatus.SC_FORBIDDEN);
192+
}
193+
194+
// a sharing entry should be created successfully since admin has access to share API
195+
try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) {
196+
TestRestClient.HttpResponse response = client.patch(SECURITY_SHARE_ENDPOINT, patchSharingInfoPayloadBuilder.build());
197+
response.assertStatusCode(HttpStatus.SC_OK);
198+
assertThat(response.getBody(), containsString(FULL_ACCESS_USER.getName()));
199+
}
200+
201+
// limited access user will not be able to call patch endpoint
202+
try (TestRestClient client = cluster.getRestClient(LIMITED_ACCESS_USER)) {
203+
TestRestClient.HttpResponse response = client.patch(SECURITY_SHARE_ENDPOINT, patchSharingInfoPayloadBuilder.build());
204+
response.assertStatusCode(HttpStatus.SC_FORBIDDEN);
205+
}
206+
207+
// full-access user will now be able to patch and grant access to limited access user
208+
// they can also shoot themselves in the foot and remove own access
209+
try (TestRestClient client = cluster.getRestClient(FULL_ACCESS_USER)) {
210+
// add limited user
211+
users.add(LIMITED_ACCESS_USER.getName());
212+
patchSharingInfoPayloadBuilder.share(recipients, sampleAllAG.name());
213+
// remove self
214+
Set<String> revokedUsers = new HashSet<>();
215+
revokedUsers.add(FULL_ACCESS_USER.getName());
216+
recs.put(Recipient.USERS, revokedUsers);
217+
recipients = new Recipients(recs);
218+
patchSharingInfoPayloadBuilder.revoke(recipients, sampleAllAG.name());
219+
220+
TestRestClient.HttpResponse response = client.patch(SECURITY_SHARE_ENDPOINT, patchSharingInfoPayloadBuilder.build());
221+
response.assertStatusCode(HttpStatus.SC_OK);
222+
}
223+
224+
// limited access user will now be able to call patch endpoint, but full-access won't
225+
try (TestRestClient client = cluster.getRestClient(LIMITED_ACCESS_USER)) {
226+
TestRestClient.HttpResponse response = client.patch(SECURITY_SHARE_ENDPOINT, patchSharingInfoPayloadBuilder.build());
227+
response.assertStatusCode(HttpStatus.SC_OK);
228+
}
229+
try (TestRestClient client = cluster.getRestClient(FULL_ACCESS_USER)) {
230+
TestRestClient.HttpResponse response = client.patch(SECURITY_SHARE_ENDPOINT, patchSharingInfoPayloadBuilder.build());
231+
response.assertStatusCode(HttpStatus.SC_FORBIDDEN);
232+
}
233+
}
234+
}
235+
236+
}

sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,25 @@
88

99
package org.opensearch.sample.resource;
1010

11+
import java.io.IOException;
1112
import java.time.Duration;
13+
import java.util.ArrayList;
14+
import java.util.HashMap;
1215
import java.util.List;
1316
import java.util.Map;
1417

1518
import org.apache.http.HttpStatus;
1619
import org.awaitility.Awaitility;
1720

1821
import org.opensearch.Version;
22+
import org.opensearch.common.xcontent.XContentFactory;
23+
import org.opensearch.core.xcontent.ToXContent;
24+
import org.opensearch.core.xcontent.XContentBuilder;
1925
import org.opensearch.painless.PainlessModulePlugin;
2026
import org.opensearch.plugins.PluginInfo;
2127
import org.opensearch.sample.SampleResourcePlugin;
2228
import org.opensearch.security.OpenSearchSecurityPlugin;
29+
import org.opensearch.security.spi.resources.sharing.Recipients;
2330
import org.opensearch.test.framework.TestSecurityConfig;
2431
import org.opensearch.test.framework.certificate.CertificateData;
2532
import org.opensearch.test.framework.cluster.ClusterManager;
@@ -72,7 +79,8 @@ public final class TestUtils {
7279
"sample_plugin_index_all_access",
7380
TestSecurityConfig.ActionGroup.Type.INDEX,
7481
"indices:*",
75-
"cluster:admin/sample-resource-plugin/*"
82+
"cluster:admin/sample-resource-plugin/*",
83+
"cluster:admin/security/resource/share"
7684
);
7785

7886
public static final String SAMPLE_RESOURCE_CREATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/create";
@@ -83,6 +91,7 @@ public final class TestUtils {
8391
public static final String SAMPLE_RESOURCE_REVOKE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/revoke";
8492

8593
static final String RESOURCE_SHARING_MIGRATION_ENDPOINT = "_plugins/_security/api/resources/migrate";
94+
static final String SECURITY_SHARE_ENDPOINT = "_plugins/_security/api/resource/share";
8695

8796
public static LocalCluster newCluster(boolean featureEnabled, boolean systemIndexEnabled) {
8897
return new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS_COORDINATOR)
@@ -199,6 +208,92 @@ static String migrationPayload_missingBackendRoles() {
199208
""".formatted(RESOURCE_INDEX_NAME, "user/name");
200209
}
201210

211+
static String putSharingInfoPayload(String resourceId, String resourceIndex, String accessLevel, String user) {
212+
return """
213+
{
214+
"resource_id": "%s",
215+
"resource_index": "%s",
216+
"share_with": {
217+
"%s" : {
218+
"users": ["%s"]
219+
}
220+
}
221+
}
222+
""".formatted(resourceId, resourceIndex, accessLevel, user);
223+
}
224+
225+
public static class PatchSharingInfoPayloadBuilder {
226+
private String resourceId;
227+
private String resourceIndex;
228+
private final Map<String, Recipients> share = new HashMap<>();
229+
private final Map<String, Recipients> revoke = new HashMap<>();
230+
231+
public PatchSharingInfoPayloadBuilder resourceId(String resourceId) {
232+
this.resourceId = resourceId;
233+
return this;
234+
}
235+
236+
public PatchSharingInfoPayloadBuilder resourceIndex(String resourceIndex) {
237+
this.resourceIndex = resourceIndex;
238+
return this;
239+
}
240+
241+
public void share(Recipients recipients, String accessLevel) {
242+
Recipients existing = share.getOrDefault(accessLevel, new Recipients(new HashMap<>()));
243+
existing.share(recipients);
244+
share.put(accessLevel, existing);
245+
}
246+
247+
public void revoke(Recipients recipients, String accessLevel) {
248+
Recipients existing = revoke.getOrDefault(accessLevel, new Recipients(new HashMap<>()));
249+
// intentionally share() is called here since we are building a shareWith object, this final object will be used to remove
250+
// access
251+
// think of it as currentShareWith.removeAll(revokeShareWith)
252+
existing.share(recipients);
253+
revoke.put(accessLevel, existing);
254+
}
255+
256+
private String buildJsonString(Map<String, Recipients> input) {
257+
258+
List<String> output = new ArrayList<>();
259+
for (Map.Entry<String, Recipients> entry : input.entrySet()) {
260+
try {
261+
XContentBuilder builder = XContentFactory.jsonBuilder();
262+
entry.getValue().toXContent(builder, ToXContent.EMPTY_PARAMS);
263+
String recipJson = builder.toString();
264+
output.add("""
265+
"%s" : %s
266+
""".formatted(entry.getKey(), recipJson));
267+
} catch (IOException e) {
268+
throw new RuntimeException(e);
269+
}
270+
271+
}
272+
273+
return String.join(",", output);
274+
275+
}
276+
277+
public String build() {
278+
String allShares = buildJsonString(share);
279+
String allRevokes = buildJsonString(revoke);
280+
return """
281+
{
282+
"resource_id": "%s",
283+
"resource_index": "%s",
284+
"patch":{
285+
"share_with": {
286+
%s
287+
},
288+
"revoke": {
289+
%s
290+
}
291+
}
292+
}
293+
""".formatted(resourceId, resourceIndex, allShares, allRevokes);
294+
}
295+
}
296+
202297
public static class ApiHelper {
203298
private final LocalCluster cluster;
204299

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -522,8 +522,8 @@ public void revoke(String resourceId, String resourceIndex, ShareWith revokeAcce
522522
// Fetch the current ResourceSharing document
523523
fetchSharingInfo(resourceIndex, resourceId, sharingInfoListener);
524524

525-
// build revoke script
526-
sharingInfoListener.whenComplete(sharingInfo -> {
525+
// build revoke script
526+
sharingInfoListener.whenComplete(sharingInfo -> {
527527

528528
assert sharingInfo != null;
529529
for (String accessLevel : revokeAccess.accessLevels()) {

0 commit comments

Comments
 (0)