Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8575576
WIP on multiple sharable resource types per index
cwperks Sep 26, 2025
1663969
Fix compilation issues
cwperks Sep 26, 2025
c46cd2d
Make more generic collection
cwperks Sep 26, 2025
6e4b679
Fix unit tests
cwperks Sep 26, 2025
f37c54c
Make DocRequests specify type
cwperks Sep 26, 2025
14869cf
Few test fixes
cwperks Sep 26, 2025
9a66864
Fix Share API tests
cwperks Sep 26, 2025
4c83aa5
Fix tests
cwperks Sep 26, 2025
ebfd5bd
Makes resources settings dynamically updateable
DarshitChanpura Oct 2, 2025
32f903b
Allows plugin to be control codepath based on protected resource types
DarshitChanpura Oct 2, 2025
5f7beb4
Fix setting registration
DarshitChanpura Oct 2, 2025
696482e
Merge remote-tracking branch 'upstream/main' into dynamic-resource-se…
DarshitChanpura Oct 2, 2025
c2988ea
Adds changelog entry
DarshitChanpura Oct 2, 2025
fbdae9f
Corrects usage of resource-sharing flag
DarshitChanpura Oct 2, 2025
a3493a6
Fix share action rest channel consumer
DarshitChanpura Oct 7, 2025
9dd02a1
Fix sample plugin tests
DarshitChanpura Oct 7, 2025
9930ae3
Merge remote-tracking branch 'upstream/main' into dynamic-resource-se…
DarshitChanpura Oct 7, 2025
1a94f40
Merge remote-tracking branch 'upstream/main' into dynamic-resource-se…
DarshitChanpura Oct 7, 2025
42b7230
Merge remote-tracking branch 'upstream/main' into dynamic-resource-se…
DarshitChanpura Oct 8, 2025
b333d12
Registers extensions regardless of whether their resources are marked…
DarshitChanpura Oct 8, 2025
ea333b9
Adds tests for the new dynamic settings and update existing tests to …
DarshitChanpura Oct 8, 2025
7c2c896
Adds documentation
DarshitChanpura Oct 8, 2025
258229d
Merge branch 'main' into dynamic-resource-settings
DarshitChanpura Oct 9, 2025
13204e2
Merge remote-tracking branch 'upstream/main' into dynamic-resource-se…
DarshitChanpura Oct 9, 2025
09b16eb
Merge remote-tracking branch 'upstream/main' into dynamic-resource-se…
DarshitChanpura Oct 13, 2025
8d186fe
Fixes resource search request evaluation
DarshitChanpura Oct 13, 2025
b707959
Adds caching to resource indices and makes read-write operations thre…
DarshitChanpura Oct 13, 2025
877bb11
Fixes type enabled check
DarshitChanpura Oct 13, 2025
f9fb61a
Refactors resource type fetch logic
DarshitChanpura Oct 13, 2025
0bd05b9
Rebase with main
cwperks Oct 13, 2025
1bc85b4
Merge branch 'dynamic-resource-settings' into multiple-resource-type
cwperks Oct 14, 2025
3a3cf8c
Merge with main
cwperks Oct 14, 2025
5a569d3
Merge branch 'main' into multiple-resource-type
cwperks Oct 14, 2025
f53bdfb
Back out unrelated changes
cwperks Oct 14, 2025
7df24dc
Clean up changes
cwperks Oct 14, 2025
4dc058a
Use correct method
cwperks Oct 14, 2025
17c0384
Merge branch 'main' into multiple-resource-type
cwperks Oct 15, 2025
eaefa32
Add actions around sample-resource-group
cwperks Oct 15, 2025
287bfef
Fix test
cwperks Oct 16, 2025
5b3f561
Handle case where index is passed explicitly
cwperks Oct 16, 2025
812c428
Refactor to use methods from AbstractApiIntegrationTest
cwperks Oct 16, 2025
19bbd61
Remove calls to awaitSharingEntry
cwperks Oct 16, 2025
bbe3fbe
Revert "Handle case where index is passed explicitly"
cwperks Oct 16, 2025
115e9d4
Merge branch 'main' into multiple-resource-type
cwperks Oct 16, 2025
4499615
Fix compilation errors
cwperks Oct 16, 2025
d487a01
Fix tests
cwperks Oct 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = "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";
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<String> protectedResourceTypes) {
Expand Down Expand Up @@ -331,6 +342,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\"}";
Expand All @@ -352,6 +372,12 @@ public void assertApiGet(String resourceId, TestSecurityConfig.User user, int st
assertGet(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId, 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) {
try (TestRestClient client = cluster.getRestClient(user)) {
TestRestClient.HttpResponse response = client.get(endpoint);
Expand Down Expand Up @@ -492,6 +518,13 @@ public void assertApiUpdate(String resourceId, TestSecurityConfig.User user, Str
assertUpdate(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, 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) {
assertUpdate(RESOURCE_INDEX_NAME + "/_doc/" + resourceId + "?refresh=true", newName, user, status);
}
Expand Down Expand Up @@ -536,6 +569,20 @@ public void assertApiShare(
}
}

public TestRestClient.HttpResponse shareResourceGroup(
String resourceId,
TestSecurityConfig.User user,
TestSecurityConfig.User target,
String accessLevel
) {
try (TestRestClient client = cluster.getRestClient(user)) {
return client.putJson(
SECURITY_SHARE_ENDPOINT,
putSharingInfoPayload(resourceId, RESOURCE_GROUP_TYPE, accessLevel, Recipient.USERS, target.getName())
);
}
}

public void assertApiShareByRole(
String resourceId,
TestSecurityConfig.User user,
Expand All @@ -552,6 +599,20 @@ public void assertApiShareByRole(
}
}

public TestRestClient.HttpResponse shareResourceGroupByRole(
String resourceId,
TestSecurityConfig.User user,
String targetRole,
String accessLevel
) {
try (TestRestClient client = cluster.getRestClient(user)) {
return client.putJson(
SECURITY_SHARE_ENDPOINT,
putSharingInfoPayload(resourceId, RESOURCE_GROUP_TYPE, accessLevel, Recipient.ROLES, targetRole)
);
}
}

public void assertApiRevoke(
String resourceId,
TestSecurityConfig.User user,
Expand All @@ -569,6 +630,21 @@ public void assertApiRevoke(
}
}

public TestRestClient.HttpResponse revokeResourceGroup(
String resourceId,
TestSecurityConfig.User user,
TestSecurityConfig.User target,
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)) {
return client.patch(TestUtils.SECURITY_SHARE_ENDPOINT, patchBuilder.build());
}
}

public void assertDirectDelete(String resourceId, TestSecurityConfig.User user, int status) {
assertDelete(RESOURCE_INDEX_NAME + "/_doc/" + resourceId, user, status);
}
Expand All @@ -581,6 +657,12 @@ public void assertApiDelete(String resourceId, TestSecurityConfig.User user, int
assertDelete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId, 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) {
try (TestRestClient client = cluster.getRestClient(user)) {
TestRestClient.HttpResponse response = client.delete(endpoint);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,10 +55,19 @@ public void testTypesApi_mustListSampleResourceAsAType() {
TestRestClient.HttpResponse response = client.get(SECURITY_TYPES_ENDPOINT);
response.assertStatusCode(HttpStatus.SC_OK);
List<Object> types = (List<Object>) response.bodyAsMap().get("types");
assertThat(types.size(), equalTo(1));
Map<String, Object> responseBody = (Map<String, Object>) 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<String, Object> firstType = (Map<String, Object>) types.get(0);
assertThat(firstType.get("type"), equalTo("sample-resource"));
assertThat(
(List<String>) firstType.get("action_groups"),
containsInAnyOrder("sample_read_only", "sample_read_write", "sample_full_access")
);
Map<String, Object> secondType = (Map<String, Object>) types.get(1);
assertThat(secondType.get("type"), equalTo("sample-resource-group"));
assertThat(
(List<String>) secondType.get("action_groups"),
containsInAnyOrder("sample_group_read_only", "sample_group_read_write", "sample_group_full_access")
);
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* 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.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;
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;

/**
* 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) throws Exception {
forbidden(() -> api.getResourceGroup(resourceGroupId, user));
forbidden(() -> api.updateResourceGroup(resourceGroupId, user, "sampleUpdateAdmin"));
forbidden(() -> api.deleteResourceGroup(resourceGroupId, user));

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

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) 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));
ok(() -> api.deleteResourceGroup(resourceGroupId, user));
}

@Test
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
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));

// 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));

// 4. full-access user now has full-access to admin's resource
assertFullAccess(FULL_ACCESS_USER);
}

@Test
public void multipleUsers_sameLevel() throws Exception {
assertNoAccessBeforeSharing(FULL_ACCESS_USER);
assertNoAccessBeforeSharing(LIMITED_ACCESS_USER);

// 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));

// 2. assert both now have read-only access
assertReadOnly(LIMITED_ACCESS_USER);
}

@Test
public void sameUser_multipleLevels() throws Exception {
assertNoAccessBeforeSharing(LIMITED_ACCESS_USER);

// 1. share with user at read-only level
ok(() -> api.shareResourceGroup(resourceGroupId, USER_ADMIN, LIMITED_ACCESS_USER, SAMPLE_GROUP_READ_ONLY));

// 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));

// 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() throws Exception {
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
ok(() -> api.shareResourceGroupByRole(resourceGroupId, USER_ADMIN, fullAccessUserRole, SAMPLE_GROUP_READ_ONLY));
ok(() -> api.shareResourceGroupByRole(resourceGroupId, USER_ADMIN, limitedAccessUserRole, SAMPLE_GROUP_FULL_ACCESS));

// 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));

// 4. FULL_ACCESS_USER now has full-access to admin's resource
assertFullAccess(FULL_ACCESS_USER);
}

@Test
public void initialShare_multipleLevels() throws Exception {
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,
LIMITED_ACCESS_USER.getName(),
SAMPLE_GROUP_READ_ONLY,
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);
}

// full-access user has read-only perm
assertReadOnly(FULL_ACCESS_USER);

// limited access user has full-access
assertFullAccess(LIMITED_ACCESS_USER);

}

}
Loading
Loading