diff --git a/server/src/internalClusterTest/java/org/opensearch/accesscontrol/resources/fallback/DefaultResourceAccessControlPluginIT.java b/server/src/internalClusterTest/java/org/opensearch/accesscontrol/resources/fallback/DefaultResourceAccessControlPluginIT.java new file mode 100644 index 0000000000000..4abf412159ea6 --- /dev/null +++ b/server/src/internalClusterTest/java/org/opensearch/accesscontrol/resources/fallback/DefaultResourceAccessControlPluginIT.java @@ -0,0 +1,107 @@ +/* + * 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.accesscontrol.resources.fallback; + +import org.opensearch.client.Client; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.ResourceAccessControlPlugin; +import org.opensearch.test.InternalTestCluster; +import org.opensearch.test.OpenSearchIntegTestCase; +import org.opensearch.threadpool.ThreadPool; +import org.hamcrest.MatcherAssert; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static org.opensearch.accesscontrol.resources.fallback.SampleTestResourcePlugin.SAMPLE_TEST_INDEX; +import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +public class DefaultResourceAccessControlPluginIT extends OpenSearchIntegTestCase { + @Override + protected Collection> nodePlugins() { + return List.of(SampleTestResourcePlugin.class); + } + + public void testGetResources() throws IOException { + final Client client = client(); + + createIndex(SAMPLE_TEST_INDEX); + indexSampleDocuments(); + + Set resources; + try ( + InternalTestCluster cluster = internalCluster(); + DefaultResourceAccessControlPlugin plugin = new DefaultResourceAccessControlPlugin( + client, + cluster.getInstance(ThreadPool.class) + ) + ) { + + resources = plugin.getAccessibleResourcesForCurrentUser(SAMPLE_TEST_INDEX, TestResource.class); + } + + assertNotNull(resources); + MatcherAssert.assertThat(resources, hasSize(2)); + + MatcherAssert.assertThat(resources, hasItem(hasProperty("id", is("1")))); + MatcherAssert.assertThat(resources, hasItem(hasProperty("id", is("2")))); + } + + public void testSampleResourcePluginCallsDefaultPlugin() throws IOException { + createIndex(SAMPLE_TEST_INDEX); + indexSampleDocuments(); + + ResourceAccessControlPlugin racPlugin = SampleTestResourcePlugin.GuiceHolder.getResourceService().getResourceAccessControlPlugin(); + MatcherAssert.assertThat(racPlugin.getClass(), is(DefaultResourceAccessControlPlugin.class)); + + Set resources = racPlugin.getAccessibleResourcesForCurrentUser(SAMPLE_TEST_INDEX, TestResource.class); + + assertNotNull(resources); + MatcherAssert.assertThat(resources, hasSize(2)); + MatcherAssert.assertThat(resources, hasItem(hasProperty("id", is("1")))); + MatcherAssert.assertThat(resources, hasItem(hasProperty("id", is("2")))); + } + + private void indexSampleDocuments() throws IOException { + XContentBuilder doc1 = jsonBuilder().startObject().field("id", "1").field("name", "Test Document 1").endObject(); + + XContentBuilder doc2 = jsonBuilder().startObject().field("id", "2").field("name", "Test Document 2").endObject(); + + try (Client client = client()) { + + client.prepareIndex(SAMPLE_TEST_INDEX).setId("1").setSource(doc1).get(); + + client.prepareIndex(SAMPLE_TEST_INDEX).setId("2").setSource(doc2).get(); + + client.admin().indices().prepareRefresh(SAMPLE_TEST_INDEX).get(); + } + } + + public static class TestResource { + public String id; + public String name; + + public TestResource() {} + + public String getId() { + return id; + } + + public String getName() { + return name; + } + } +} diff --git a/server/src/internalClusterTest/java/org/opensearch/accesscontrol/resources/fallback/SampleTestResourcePlugin.java b/server/src/internalClusterTest/java/org/opensearch/accesscontrol/resources/fallback/SampleTestResourcePlugin.java new file mode 100644 index 0000000000000..20251e7ac7ba3 --- /dev/null +++ b/server/src/internalClusterTest/java/org/opensearch/accesscontrol/resources/fallback/SampleTestResourcePlugin.java @@ -0,0 +1,79 @@ +/* + * 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.accesscontrol.resources.fallback; + +import org.opensearch.accesscontrol.resources.ResourceService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.lifecycle.Lifecycle; +import org.opensearch.common.lifecycle.LifecycleComponent; +import org.opensearch.common.lifecycle.LifecycleListener; +import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.ResourcePlugin; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +// Sample test resource plugin +public class SampleTestResourcePlugin extends Plugin implements ResourcePlugin { + + public static final String SAMPLE_TEST_INDEX = ".sample_test_resource"; + + @Override + public String getResourceType() { + return ""; + } + + @Override + public String getResourceIndex() { + return SAMPLE_TEST_INDEX; + } + + @Override + public Collection> getGuiceServiceClasses() { + final List> services = new ArrayList<>(1); + services.add(GuiceHolder.class); + return services; + } + + public static class GuiceHolder implements LifecycleComponent { + + private static ResourceService resourceService; + + @Inject + public GuiceHolder(final ResourceService resourceService) { + GuiceHolder.resourceService = resourceService; + } + + public static ResourceService getResourceService() { + return resourceService; + } + + @Override + public void close() {} + + @Override + public Lifecycle.State lifecycleState() { + return null; + } + + @Override + public void addLifecycleListener(LifecycleListener listener) {} + + @Override + public void removeLifecycleListener(LifecycleListener listener) {} + + @Override + public void start() {} + + @Override + public void stop() {} + + } +} diff --git a/server/src/main/java/org/opensearch/accesscontrol/resources/ResourceService.java b/server/src/main/java/org/opensearch/accesscontrol/resources/ResourceService.java index c20a144f6384d..70d9c837931d8 100644 --- a/server/src/main/java/org/opensearch/accesscontrol/resources/ResourceService.java +++ b/server/src/main/java/org/opensearch/accesscontrol/resources/ResourceService.java @@ -66,6 +66,6 @@ public ResourceAccessControlPlugin getResourceAccessControlPlugin() { * List active plugins that define resources */ public List listResourcePlugins() { - return resourcePlugins; + return List.copyOf(resourcePlugins); } } diff --git a/server/src/test/java/org/opensearch/accesscontrol/resources/CreatedByTests.java b/server/src/test/java/org/opensearch/accesscontrol/resources/CreatedByTests.java new file mode 100644 index 0000000000000..52b0bfd01c7fc --- /dev/null +++ b/server/src/test/java/org/opensearch/accesscontrol/resources/CreatedByTests.java @@ -0,0 +1,296 @@ +package org.opensearch.accesscontrol.resources; + +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.test.OpenSearchTestCase; +import org.hamcrest.MatcherAssert; + +import java.io.IOException; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CreatedByTests extends OpenSearchTestCase { + + public void testCreatedByConstructorWithValidUser() { + String expectedUser = "testUser"; + CreatedBy createdBy = new CreatedBy(expectedUser); + + MatcherAssert.assertThat(expectedUser, is(equalTo(createdBy.getUser()))); + } + + public void testCreatedByFromStreamInput() throws IOException { + String expectedUser = "testUser"; + + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.writeString(expectedUser); + + StreamInput in = out.bytes().streamInput(); + + CreatedBy createdBy = new CreatedBy(in); + + MatcherAssert.assertThat(expectedUser, is(equalTo(createdBy.getUser()))); + } + } + + public void testCreatedByWithEmptyStreamInput() throws IOException { + + try (StreamInput mockStreamInput = mock(StreamInput.class)) { + when(mockStreamInput.readString()).thenThrow(new IOException("EOF")); + + assertThrows(IOException.class, () -> new CreatedBy(mockStreamInput)); + } + } + + public void testCreatedByWithEmptyUser() { + + CreatedBy createdBy = new CreatedBy(""); + MatcherAssert.assertThat("", equalTo(createdBy.getUser())); + } + + public void testCreatedByWithIOException() throws IOException { + + try (StreamInput mockStreamInput = mock(StreamInput.class)) { + when(mockStreamInput.readString()).thenThrow(new IOException("Test IOException")); + + assertThrows(IOException.class, () -> new CreatedBy(mockStreamInput)); + } + } + + public void testCreatedByWithLongUsername() { + String longUsername = "a".repeat(10000); + CreatedBy createdBy = new CreatedBy(longUsername); + MatcherAssert.assertThat(longUsername, equalTo(createdBy.getUser())); + } + + public void testCreatedByWithUnicodeCharacters() { + String unicodeUsername = "用户こんにちは"; + CreatedBy createdBy = new CreatedBy(unicodeUsername); + MatcherAssert.assertThat(unicodeUsername, equalTo(createdBy.getUser())); + } + + public void testFromXContentThrowsExceptionWhenUserFieldIsMissing() throws IOException { + String json = "{}"; + IllegalArgumentException exception; + try (XContentParser parser = createParser(XContentType.JSON.xContent(), json)) { + + exception = assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + + MatcherAssert.assertThat("user field is required", equalTo(exception.getMessage())); + } + + public void testFromXContentWithEmptyInput() throws IOException { + String emptyJson = "{}"; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, emptyJson)) { + + assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + } + + public void testFromXContentWithExtraFields() throws IOException { + String jsonWithExtraFields = "{\"user\": \"testUser\", \"extraField\": \"value\"}"; + XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, jsonWithExtraFields); + + CreatedBy.fromXContent(parser); + } + + public void testFromXContentWithIncorrectFieldType() throws IOException { + String jsonWithIncorrectType = "{\"user\": 12345}"; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, jsonWithIncorrectType)) { + + assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + } + + public void testFromXContentWithMissingUser() throws IOException { + String json = "{}"; + IllegalArgumentException exception; + try (XContentParser parser = createParser(JsonXContent.jsonXContent, json)) { + parser.nextToken(); // Move to the start object token + + exception = assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + + MatcherAssert.assertThat("user field is required", equalTo(exception.getMessage())); + } + + public void testFromXContentWithMissingUserField() throws IOException { + String jsonWithoutUser = "{\"someOtherField\": \"value\"}"; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, jsonWithoutUser)) { + + assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + } + + public void testFromXContentWithNullUserValue() throws IOException { + String jsonWithNullUser = "{\"user\": null}"; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, jsonWithNullUser)) { + + assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + } + + public void testFromXContentWithValidUser() throws IOException { + String json = "{\"user\":\"testUser\"}"; + XContentParser parser = XContentType.JSON.xContent().createParser(xContentRegistry(), null, json); + + CreatedBy createdBy = CreatedBy.fromXContent(parser); + + assertNotNull(createdBy); + MatcherAssert.assertThat("testUser", equalTo(createdBy.getUser())); + } + + public void testGetUserReturnsCorrectValue() { + String expectedUser = "testUser"; + CreatedBy createdBy = new CreatedBy(expectedUser); + + String actualUser = createdBy.getUser(); + + MatcherAssert.assertThat(expectedUser, equalTo(actualUser)); + } + + public void testGetUserWithNullString() { + + CreatedBy createdBy = new CreatedBy((String) null); + assertNull("getUser should return null when initialized with null", createdBy.getUser()); + } + + public void testGetWriteableNameReturnsCorrectString() { + CreatedBy createdBy = new CreatedBy("testUser"); + MatcherAssert.assertThat("created_by", equalTo(createdBy.getWriteableName())); + } + + public void testSetUserWithEmptyString() { + CreatedBy createdBy = new CreatedBy("initialUser"); + createdBy.setUser(""); + MatcherAssert.assertThat("", equalTo(createdBy.getUser())); + } + + public void testToStringWithEmptyUser() { + CreatedBy createdBy = new CreatedBy(""); + String result = createdBy.toString(); + MatcherAssert.assertThat("CreatedBy {user=''}", equalTo(result)); + } + + public void testToStringWithNullUser() { + CreatedBy createdBy = new CreatedBy((String) null); + String result = createdBy.toString(); + MatcherAssert.assertThat("CreatedBy {user='null'}", equalTo(result)); + } + + public void testToStringWithLongUserName() { + + String longUserName = "a".repeat(1000); + CreatedBy createdBy = new CreatedBy(longUserName); + String result = createdBy.toString(); + assertTrue(result.startsWith("CreatedBy {user='")); + assertTrue(result.endsWith("'}")); + MatcherAssert.assertThat(1019, equalTo(result.length())); + } + + public void testToXContentWithEmptyUser() throws IOException { + CreatedBy createdBy = new CreatedBy(""); + XContentBuilder builder = JsonXContent.contentBuilder(); + + createdBy.toXContent(builder, null); + String result = builder.toString(); + MatcherAssert.assertThat("{\"user\":\"\"}", equalTo(result)); + } + + public void testWriteToWithExceptionInStreamOutput() throws IOException { + CreatedBy createdBy = new CreatedBy("user1"); + try (StreamOutput failingOutput = new StreamOutput() { + @Override + public void writeByte(byte b) throws IOException { + throw new IOException("Simulated IO exception"); + } + + @Override + public void writeBytes(byte[] b, int offset, int length) throws IOException { + throw new IOException("Simulated IO exception"); + } + + @Override + public void flush() throws IOException { + + } + + @Override + public void close() throws IOException { + + } + + @Override + public void reset() throws IOException { + + } + }) { + + assertThrows(IOException.class, () -> createdBy.writeTo(failingOutput)); + } + } + + public void testWriteToWithLongUserName() throws IOException { + String longUserName = "a".repeat(65536); + CreatedBy createdBy = new CreatedBy(longUserName); + BytesStreamOutput out = new BytesStreamOutput(); + createdBy.writeTo(out); + assertTrue(out.size() > 65536); + } + + public void test_createdByToStringReturnsCorrectFormat() { + String testUser = "testUser"; + CreatedBy createdBy = new CreatedBy(testUser); + + String expected = "CreatedBy {user='" + testUser + "'}"; + String actual = createdBy.toString(); + + MatcherAssert.assertThat(expected, equalTo(actual)); + } + + public void test_fromXContent_missingUserField() throws IOException { + String json = "{}"; + IllegalArgumentException exception; + try (XContentParser parser = createParser(XContentType.JSON.xContent(), json)) { + parser.nextToken(); // Move to the start object token + + exception = assertThrows(IllegalArgumentException.class, () -> { CreatedBy.fromXContent(parser); }); + } + + MatcherAssert.assertThat("user field is required", equalTo(exception.getMessage())); + } + + public void test_toXContent_serializesCorrectly() throws IOException { + String expectedUser = "testUser"; + CreatedBy createdBy = new CreatedBy(expectedUser); + XContentBuilder builder = XContentFactory.jsonBuilder(); + + createdBy.toXContent(builder, null); + + String expectedJson = "{\"user\":\"testUser\"}"; + MatcherAssert.assertThat(expectedJson, equalTo(builder.toString())); + } + + public void test_writeTo_writesUserCorrectly() throws IOException { + String expectedUser = "testUser"; + CreatedBy createdBy = new CreatedBy(expectedUser); + + BytesStreamOutput out = new BytesStreamOutput(); + createdBy.writeTo(out); + + StreamInput in = out.bytes().streamInput(); + String actualUser = in.readString(); + + MatcherAssert.assertThat(expectedUser, equalTo(actualUser)); + } + +} diff --git a/server/src/test/java/org/opensearch/accesscontrol/resources/ResourceServiceTests.java b/server/src/test/java/org/opensearch/accesscontrol/resources/ResourceServiceTests.java new file mode 100644 index 0000000000000..63c2cd16253b3 --- /dev/null +++ b/server/src/test/java/org/opensearch/accesscontrol/resources/ResourceServiceTests.java @@ -0,0 +1,213 @@ +package org.opensearch.accesscontrol.resources; + +import org.opensearch.OpenSearchException; +import org.opensearch.accesscontrol.resources.fallback.DefaultResourceAccessControlPlugin; +import org.opensearch.client.Client; +import org.opensearch.plugins.ResourceAccessControlPlugin; +import org.opensearch.plugins.ResourcePlugin; +import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.threadpool.ThreadPool; +import org.hamcrest.MatcherAssert; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.mock; + +public class ResourceServiceTests extends OpenSearchTestCase { + + @Mock + private Client client; + + @Mock + private ThreadPool threadPool; + + public void setup() { + MockitoAnnotations.openMocks(this); + } + + public void testGetResourceAccessControlPluginReturnsInitializedPlugin() { + setup(); + Client mockClient = mock(Client.class); + ThreadPool mockThreadPool = mock(ThreadPool.class); + + ResourceAccessControlPlugin mockPlugin = mock(ResourceAccessControlPlugin.class); + List plugins = new ArrayList<>(); + plugins.add(mockPlugin); + + List resourcePlugins = new ArrayList<>(); + + ResourceService resourceService = new ResourceService(plugins, resourcePlugins, mockClient, mockThreadPool); + + ResourceAccessControlPlugin result = resourceService.getResourceAccessControlPlugin(); + + assertEquals(mockPlugin, result); + } + + public void testGetResourceAccessControlPlugin_NoPlugins() { + setup(); + List emptyPlugins = new ArrayList<>(); + List resourcePlugins = new ArrayList<>(); + + ResourceService resourceService = new ResourceService(emptyPlugins, resourcePlugins, client, threadPool); + + ResourceAccessControlPlugin result = resourceService.getResourceAccessControlPlugin(); + + assertNotNull(result); + assertTrue(result instanceof DefaultResourceAccessControlPlugin); + } + + public void testGetResourceAccessControlPlugin_SinglePlugin() { + setup(); + ResourceAccessControlPlugin mockPlugin = mock(ResourceAccessControlPlugin.class); + List singlePlugin = Arrays.asList(mockPlugin); + List resourcePlugins = new ArrayList<>(); + + ResourceService resourceService = new ResourceService(singlePlugin, resourcePlugins, client, threadPool); + + ResourceAccessControlPlugin result = resourceService.getResourceAccessControlPlugin(); + + assertNotNull(result); + assertSame(mockPlugin, result); + } + + public void testListResourcePluginsReturnsPluginList() { + setup(); + List resourceACPlugins = new ArrayList<>(); + List expectedResourcePlugins = new ArrayList<>(); + expectedResourcePlugins.add(mock(ResourcePlugin.class)); + expectedResourcePlugins.add(mock(ResourcePlugin.class)); + + ResourceService resourceService = new ResourceService(resourceACPlugins, expectedResourcePlugins, client, threadPool); + + List actualResourcePlugins = resourceService.listResourcePlugins(); + + MatcherAssert.assertThat(expectedResourcePlugins, equalTo(actualResourcePlugins)); + } + + public void testListResourcePlugins_concurrentModification() { + setup(); + List emptyACPlugins = Collections.emptyList(); + List resourcePlugins = new ArrayList<>(); + resourcePlugins.add(mock(ResourcePlugin.class)); + + ResourceService resourceService = new ResourceService(emptyACPlugins, resourcePlugins, client, threadPool); + + Thread modifierThread = new Thread(() -> { resourcePlugins.add(mock(ResourcePlugin.class)); }); + + modifierThread.start(); + + List result = resourceService.listResourcePlugins(); + + assertNotNull(result); + // The size could be either 1 or 2 depending on the timing of the concurrent modification + assertTrue(result.size() == 1 || result.size() == 2); + } + + public void testListResourcePlugins_emptyList() { + setup(); + List emptyACPlugins = Collections.emptyList(); + List emptyResourcePlugins = Collections.emptyList(); + + ResourceService resourceService = new ResourceService(emptyACPlugins, emptyResourcePlugins, client, threadPool); + + List result = resourceService.listResourcePlugins(); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + public void testListResourcePlugins_immutability() { + setup(); + List emptyACPlugins = Collections.emptyList(); + List resourcePlugins = new ArrayList<>(); + resourcePlugins.add(mock(ResourcePlugin.class)); + + ResourceService resourceService = new ResourceService(emptyACPlugins, resourcePlugins, client, threadPool); + + List result = resourceService.listResourcePlugins(); + + assertThrows(UnsupportedOperationException.class, () -> { result.add(mock(ResourcePlugin.class)); }); + } + + public void testResourceServiceConstructorWithMultiplePlugins() { + setup(); + ResourceAccessControlPlugin plugin1 = mock(ResourceAccessControlPlugin.class); + ResourceAccessControlPlugin plugin2 = mock(ResourceAccessControlPlugin.class); + List resourceACPlugins = Arrays.asList(plugin1, plugin2); + List resourcePlugins = Arrays.asList(mock(ResourcePlugin.class)); + + assertThrows(OpenSearchException.class, () -> { new ResourceService(resourceACPlugins, resourcePlugins, client, threadPool); }); + } + + public void testResourceServiceConstructor_MultiplePlugins() { + setup(); + ResourceAccessControlPlugin mockPlugin1 = mock(ResourceAccessControlPlugin.class); + ResourceAccessControlPlugin mockPlugin2 = mock(ResourceAccessControlPlugin.class); + List multiplePlugins = Arrays.asList(mockPlugin1, mockPlugin2); + List resourcePlugins = new ArrayList<>(); + + assertThrows( + org.opensearch.OpenSearchException.class, + () -> { new ResourceService(multiplePlugins, resourcePlugins, client, threadPool); } + ); + } + + public void testResourceServiceWithMultipleResourceACPlugins() { + setup(); + List multipleResourceACPlugins = Arrays.asList( + mock(ResourceAccessControlPlugin.class), + mock(ResourceAccessControlPlugin.class) + ); + List resourcePlugins = new ArrayList<>(); + + assertThrows( + OpenSearchException.class, + () -> { new ResourceService(multipleResourceACPlugins, resourcePlugins, client, threadPool); } + ); + } + + public void testResourceServiceWithNoAccessControlPlugin() { + setup(); + List resourceACPlugins = new ArrayList<>(); + List resourcePlugins = new ArrayList<>(); + Client client = mock(Client.class); + ThreadPool threadPool = mock(ThreadPool.class); + + ResourceService resourceService = new ResourceService(resourceACPlugins, resourcePlugins, client, threadPool); + + assertTrue(resourceService.getResourceAccessControlPlugin() instanceof DefaultResourceAccessControlPlugin); + assertEquals(resourcePlugins, resourceService.listResourcePlugins()); + } + + public void testResourceServiceWithNoResourceACPlugins() { + setup(); + List emptyResourceACPlugins = new ArrayList<>(); + List resourcePlugins = new ArrayList<>(); + + ResourceService resourceService = new ResourceService(emptyResourceACPlugins, resourcePlugins, client, threadPool); + + assertNotNull(resourceService.getResourceAccessControlPlugin()); + } + + public void testResourceServiceWithSingleResourceAccessControlPlugin() { + setup(); + List resourceACPlugins = new ArrayList<>(); + ResourceAccessControlPlugin mockPlugin = mock(ResourceAccessControlPlugin.class); + resourceACPlugins.add(mockPlugin); + + List resourcePlugins = new ArrayList<>(); + + ResourceService resourceService = new ResourceService(resourceACPlugins, resourcePlugins, client, threadPool); + + assertNotNull(resourceService); + assertEquals(mockPlugin, resourceService.getResourceAccessControlPlugin()); + assertEquals(resourcePlugins, resourceService.listResourcePlugins()); + } +} diff --git a/server/src/test/java/org/opensearch/accesscontrol/resources/ShareWithTests.java b/server/src/test/java/org/opensearch/accesscontrol/resources/ShareWithTests.java new file mode 100644 index 0000000000000..c1c7d48802fc8 --- /dev/null +++ b/server/src/test/java/org/opensearch/accesscontrol/resources/ShareWithTests.java @@ -0,0 +1,201 @@ +package org.opensearch.accesscontrol.resources; + +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.test.OpenSearchTestCase; +import org.hamcrest.MatcherAssert; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.mockito.Mockito; + +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ShareWithTests extends OpenSearchTestCase { + + public void testFromXContentWhenCurrentTokenIsNotStartObject() throws IOException { + String json = "{\"read_only\": {\"users\": [\"user1\"], \"roles\": [], \"backend_roles\": []}}"; + XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, json); + + parser.nextToken(); + + ShareWith shareWith = ShareWith.fromXContent(parser); + + assertNotNull(shareWith); + Set sharedWithScopes = shareWith.getSharedWithScopes(); + assertNotNull(sharedWithScopes); + MatcherAssert.assertThat(1, equalTo(sharedWithScopes.size())); + + SharedWithScope scope = sharedWithScopes.iterator().next(); + MatcherAssert.assertThat("read_only", equalTo(scope.getScope())); + + SharedWithScope.SharedWithPerScope sharedWithPerScope = scope.getSharedWithPerScope(); + assertNotNull(sharedWithPerScope); + MatcherAssert.assertThat(1, equalTo(sharedWithPerScope.getUsers().size())); + MatcherAssert.assertThat("user1", equalTo(sharedWithPerScope.getUsers().iterator().next())); + MatcherAssert.assertThat(0, equalTo(sharedWithPerScope.getRoles().size())); + MatcherAssert.assertThat(0, equalTo(sharedWithPerScope.getBackendRoles().size())); + } + + public void testFromXContentWithEmptyInput() throws IOException { + String emptyJson = "{}"; + XContentParser parser = XContentType.JSON.xContent().createParser(xContentRegistry(), null, emptyJson); + + ShareWith result = ShareWith.fromXContent(parser); + + assertNotNull(result); + MatcherAssert.assertThat(result.getSharedWithScopes(), is(empty())); + } + + public void testFromXContentWithStartObject() throws IOException { + XContentParser parser; + try (XContentBuilder builder = XContentFactory.jsonBuilder()) { + builder.startObject() + .startObject(ResourceAccessScope.READ_ONLY) + .array("users", "user1", "user2") + .array("roles", "role1") + .array("backend_roles", "backend_role1") + .endObject() + .startObject(ResourceAccessScope.READ_WRITE) + .array("users", "user3") + .array("roles", "role2", "role3") + .array("backend_roles") + .endObject() + .endObject(); + + parser = JsonXContent.jsonXContent.createParser(null, null, builder.toString()); + } + + parser.nextToken(); + + ShareWith shareWith = ShareWith.fromXContent(parser); + + assertNotNull(shareWith); + Set scopes = shareWith.getSharedWithScopes(); + assertEquals(2, scopes.size()); + + for (SharedWithScope scope : scopes) { + SharedWithScope.SharedWithPerScope perScope = scope.getSharedWithPerScope(); + if (scope.getScope().equals(ResourceAccessScope.READ_ONLY)) { + assertEquals(2, perScope.getUsers().size()); + assertEquals(1, perScope.getRoles().size()); + assertEquals(1, perScope.getBackendRoles().size()); + } else if (scope.getScope().equals(ResourceAccessScope.READ_WRITE)) { + assertEquals(1, perScope.getUsers().size()); + assertEquals(2, perScope.getRoles().size()); + assertEquals(0, perScope.getBackendRoles().size()); + } + } + } + + public void testFromXContentWithUnexpectedEndOfInput() throws IOException { + XContentParser mockParser = mock(XContentParser.class); + when(mockParser.currentToken()).thenReturn(XContentParser.Token.START_OBJECT); + when(mockParser.nextToken()).thenReturn(XContentParser.Token.END_OBJECT, (XContentParser.Token) null); + + ShareWith result = ShareWith.fromXContent(mockParser); + + assertNotNull(result); + assertTrue(result.getSharedWithScopes().isEmpty()); + } + + public void testToXContentBuildsCorrectly() throws IOException { + SharedWithScope scope = new SharedWithScope("scope1", new SharedWithScope.SharedWithPerScope(Set.of(), Set.of(), Set.of())); + + Set scopes = new HashSet<>(); + scopes.add(scope); + + ShareWith shareWith = new ShareWith(scopes); + + XContentBuilder builder = JsonXContent.contentBuilder(); + + shareWith.toXContent(builder, null); + + String result = builder.toString(); + + String expected = "{\"scope1\":{\"users\":[],\"roles\":[],\"backend_roles\":[]}}"; + + MatcherAssert.assertThat(expected.length(), equalTo(result.length())); + MatcherAssert.assertThat(expected, equalTo(result)); + } + + public void testWriteToWithEmptySet() throws IOException { + Set emptySet = Collections.emptySet(); + ShareWith shareWith = new ShareWith(emptySet); + StreamOutput mockOutput = Mockito.mock(StreamOutput.class); + + shareWith.writeTo(mockOutput); + + verify(mockOutput).writeCollection(emptySet); + } + + public void testWriteToWithIOException() throws IOException { + Set set = new HashSet<>(); + set.add(new SharedWithScope("test", new SharedWithScope.SharedWithPerScope(Set.of(), Set.of(), Set.of()))); + ShareWith shareWith = new ShareWith(set); + StreamOutput mockOutput = Mockito.mock(StreamOutput.class); + + doThrow(new IOException("Simulated IO exception")).when(mockOutput).writeCollection(set); + + assertThrows(IOException.class, () -> shareWith.writeTo(mockOutput)); + } + + public void testWriteToWithLargeSet() throws IOException { + Set largeSet = new HashSet<>(); + for (int i = 0; i < 10000; i++) { + largeSet.add(new SharedWithScope("scope" + i, new SharedWithScope.SharedWithPerScope(Set.of(), Set.of(), Set.of()))); + } + ShareWith shareWith = new ShareWith(largeSet); + StreamOutput mockOutput = Mockito.mock(StreamOutput.class); + + shareWith.writeTo(mockOutput); + + verify(mockOutput).writeCollection(largeSet); + } + + public void test_fromXContent_emptyObject() throws IOException { + XContentParser parser; + try (XContentBuilder builder = XContentFactory.jsonBuilder()) { + builder.startObject().endObject(); + parser = XContentType.JSON.xContent().createParser(null, null, builder.toString()); + } + + ShareWith shareWith = ShareWith.fromXContent(parser); + + assertTrue(shareWith.getSharedWithScopes().isEmpty()); + } + + public void test_writeSharedWithScopesToStream() throws IOException { + StreamOutput mockStreamOutput = Mockito.mock(StreamOutput.class); + + Set sharedWithScopes = new HashSet<>(); + sharedWithScopes.add( + new SharedWithScope(ResourceAccessScope.READ_ONLY, new SharedWithScope.SharedWithPerScope(Set.of(), Set.of(), Set.of())) + ); + sharedWithScopes.add( + new SharedWithScope(ResourceAccessScope.READ_WRITE, new SharedWithScope.SharedWithPerScope(Set.of(), Set.of(), Set.of())) + ); + + ShareWith shareWith = new ShareWith(sharedWithScopes); + + shareWith.writeTo(mockStreamOutput); + + verify(mockStreamOutput, times(1)).writeCollection(eq(sharedWithScopes)); + } + +}