diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java index 4a2ac46681..930c92e1c7 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java @@ -98,6 +98,7 @@ public final class TestUtils { 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 final String SECURITY_ACCESS_ENDPOINT = "_plugins/_security/api/resource/access"; public static LocalCluster newCluster(boolean featureEnabled, boolean systemIndexEnabled) { return newCluster(featureEnabled, systemIndexEnabled, List.of(RESOURCE_TYPE, RESOURCE_GROUP_TYPE)); @@ -622,6 +623,12 @@ public TestRestClient.HttpResponse shareResourceGenerally(String resourceId, Tes } } + public TestRestClient.HttpResponse getResourceAccess(String resourceId, TestSecurityConfig.User user) { + try (TestRestClient client = cluster.getRestClient(user)) { + return client.get(SECURITY_ACCESS_ENDPOINT + "?resource_type=" + RESOURCE_TYPE + "&resource_id=" + resourceId); + } + } + public TestRestClient.HttpResponse revokeGeneralAccess(String resourceId, TestSecurityConfig.User user) { PatchSharingInfoPayloadBuilder patchBuilder = new PatchSharingInfoPayloadBuilder(); patchBuilder.resourceType(RESOURCE_TYPE); diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ResourceAccessApiTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ResourceAccessApiTests.java new file mode 100644 index 0000000000..2f7200decc --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ResourceAccessApiTests.java @@ -0,0 +1,227 @@ +/* + * 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.resource.securityapis; + +import java.util.List; +import java.util.Map; + +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.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.not; +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.NO_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.RESOURCE_SHARING_INDEX; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_FULL_ACCESS; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_READ_ONLY; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_READ_WRITE; +import static org.opensearch.sample.resource.TestUtils.SECURITY_ACCESS_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.newCluster; +import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class ResourceAccessApiTests { + @ClassRule + public static LocalCluster cluster = newCluster(true, true); + + private final TestUtils.ApiHelper api = new TestUtils.ApiHelper(cluster); + private String adminResId; + + @Before + public void setup() { + adminResId = api.createSampleResourceAs(USER_ADMIN); + api.awaitSharingEntry(adminResId); + } + + @After + public void clearIndices() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + client.delete(RESOURCE_INDEX_NAME); + client.delete(RESOURCE_SHARING_INDEX); + } + } + + @Test + public void testResourceAccess_invalidResourceType() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + TestRestClient.HttpResponse response = client.get( + SECURITY_ACCESS_ENDPOINT + "?resource_type=some-type&resource_id=" + adminResId + ); + response.assertStatusCode(HttpStatus.SC_BAD_REQUEST); + assertThat( + response.getBody(), + containsString("Invalid resource type: some-type. Must be one of: [sample-resource, sample-resource-group]") + ); + } + } + + @SuppressWarnings("unchecked") + @Test + public void testResourceAccess_ownerGetsResolvedCapabilities() { + TestRestClient.HttpResponse response = api.getResourceAccess(adminResId, USER_ADMIN); + response.assertStatusCode(HttpStatus.SC_OK); + + Map access = (Map) response.bodyAsMap().get("access"); + List accessLevels = (List) access.get("access_levels"); + List allowedActions = (List) access.get("allowed_actions"); + + assertThat(access.get("resource_id"), equalTo(adminResId)); + assertThat(access.get("resource_type"), equalTo(RESOURCE_TYPE)); + assertThat(access.get("is_owner"), equalTo(Boolean.TRUE)); + assertThat(access.get("is_admin"), equalTo(Boolean.FALSE)); + assertThat(access.get("effective_access_level"), equalTo(SAMPLE_FULL_ACCESS)); + assertThat(accessLevels, hasItems(SAMPLE_READ_ONLY, SAMPLE_READ_WRITE, SAMPLE_FULL_ACCESS)); + assertThat(allowedActions, hasItems("sampleresource:get", "sampleresource:*", "cluster:admin/security/resource/share")); + assertThat(access.get("can_share"), equalTo(Boolean.TRUE)); + } + + @SuppressWarnings("unchecked") + @Test + public void testResourceAccess_superAdminGetsResolvedCapabilities() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse response = client.get( + SECURITY_ACCESS_ENDPOINT + "?resource_type=" + RESOURCE_TYPE + "&resource_id=" + adminResId + ); + response.assertStatusCode(HttpStatus.SC_OK); + + Map access = (Map) response.bodyAsMap().get("access"); + List accessLevels = (List) access.get("access_levels"); + List allowedActions = (List) access.get("allowed_actions"); + + assertThat(access.get("is_owner"), equalTo(Boolean.FALSE)); + assertThat(access.get("is_admin"), equalTo(Boolean.TRUE)); + assertThat(access.get("effective_access_level"), equalTo(SAMPLE_FULL_ACCESS)); + assertThat(accessLevels, hasItems(SAMPLE_READ_ONLY, SAMPLE_READ_WRITE, SAMPLE_FULL_ACCESS)); + assertThat(allowedActions, hasItems("sampleresource:get", "sampleresource:*", "cluster:admin/security/resource/share")); + assertThat(access.get("can_share"), equalTo(Boolean.TRUE)); + } + } + + @SuppressWarnings("unchecked") + @Test + public void testResourceAccess_readOnlyUserGetsReadOnlyCapabilities() { + okShareReadOnly(NO_ACCESS_USER); + + TestRestClient.HttpResponse response = api.getResourceAccess(adminResId, NO_ACCESS_USER); + response.assertStatusCode(HttpStatus.SC_OK); + + Map access = (Map) response.bodyAsMap().get("access"); + List accessLevels = (List) access.get("access_levels"); + List allowedActions = (List) access.get("allowed_actions"); + + assertThat(access.get("is_owner"), equalTo(Boolean.FALSE)); + assertThat(access.get("is_admin"), equalTo(Boolean.FALSE)); + assertThat(access.get("effective_access_level"), equalTo(SAMPLE_READ_ONLY)); + assertThat(accessLevels, hasItem(SAMPLE_READ_ONLY)); + assertThat(allowedActions, hasItems(SAMPLE_READ_ONLY, "sampleresource:get")); + assertThat(allowedActions, not(hasItem("sampleresource:update"))); + assertThat(allowedActions, not(hasItem("cluster:admin/security/resource/share"))); + assertThat(access.get("can_share"), equalTo(Boolean.FALSE)); + } + + @SuppressWarnings("unchecked") + @Test + public void testResourceAccess_readWriteUserGetsEditButNotShare() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + TestRestClient.HttpResponse response = client.putJson( + TestUtils.SECURITY_SHARE_ENDPOINT, + TestUtils.putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_READ_WRITE, + org.opensearch.security.resources.sharing.Recipient.USERS, + LIMITED_ACCESS_USER.getName() + ) + ); + response.assertStatusCode(HttpStatus.SC_OK); + } + + TestRestClient.HttpResponse response = api.getResourceAccess(adminResId, LIMITED_ACCESS_USER); + response.assertStatusCode(HttpStatus.SC_OK); + + Map access = (Map) response.bodyAsMap().get("access"); + List allowedActions = (List) access.get("allowed_actions"); + + assertThat(access.get("effective_access_level"), equalTo(SAMPLE_READ_WRITE)); + assertThat(((List) access.get("access_levels")), hasItem(SAMPLE_READ_WRITE)); + assertThat(allowedActions, hasItems(SAMPLE_READ_WRITE, "sampleresource:*")); + assertThat(allowedActions, not(hasItem("cluster:admin/security/resource/share"))); + assertThat(access.get("can_share"), equalTo(Boolean.FALSE)); + } + + @SuppressWarnings("unchecked") + @Test + public void testResourceAccess_fullAccessUserGetsShareCapability() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + TestRestClient.HttpResponse response = client.putJson( + TestUtils.SECURITY_SHARE_ENDPOINT, + TestUtils.putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS, + org.opensearch.security.resources.sharing.Recipient.USERS, + FULL_ACCESS_USER.getName() + ) + ); + response.assertStatusCode(HttpStatus.SC_OK); + } + + TestRestClient.HttpResponse response = api.getResourceAccess(adminResId, FULL_ACCESS_USER); + response.assertStatusCode(HttpStatus.SC_OK); + + Map access = (Map) response.bodyAsMap().get("access"); + List allowedActions = (List) access.get("allowed_actions"); + + assertThat(access.get("effective_access_level"), equalTo(SAMPLE_FULL_ACCESS)); + assertThat(((List) access.get("access_levels")), hasItem(SAMPLE_FULL_ACCESS)); + assertThat(allowedActions, hasItems(SAMPLE_FULL_ACCESS, "sampleresource:*", "cluster:admin/security/resource/share")); + assertThat(access.get("can_share"), equalTo(Boolean.TRUE)); + } + + @Test + public void testResourceAccess_returnsForbiddenWhenUserHasNoAccess() { + TestRestClient.HttpResponse response = api.getResourceAccess(adminResId, NO_ACCESS_USER); + response.assertStatusCode(HttpStatus.SC_FORBIDDEN); + assertThat(response.getBody(), containsString("Not authorized to access resource")); + } + + private void okShareReadOnly(org.opensearch.test.framework.TestSecurityConfig.User user) { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + TestRestClient.HttpResponse response = client.putJson( + TestUtils.SECURITY_SHARE_ENDPOINT, + TestUtils.putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_READ_ONLY, + org.opensearch.security.resources.sharing.Recipient.USERS, + user.getName() + ) + ); + response.assertStatusCode(HttpStatus.SC_OK); + } + } +} diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index dab22a2b41..2024119e25 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -186,6 +186,7 @@ import org.opensearch.security.resources.ResourceIndexListener; import org.opensearch.security.resources.ResourcePluginInfo; import org.opensearch.security.resources.ResourceSharingIndexHandler; +import org.opensearch.security.resources.api.access.ResourceAccessRestAction; import org.opensearch.security.resources.api.list.AccessibleResourcesRestAction; import org.opensearch.security.resources.api.list.ResourceTypesRestAction; import org.opensearch.security.resources.api.share.ShareAction; @@ -713,6 +714,14 @@ public List getRestHandlers( new ShareRestAction(resourcePluginInfo, resourceSharingEnabledSetting, resourceSharingProtectedResourceTypesSetting) ); handlers.add(new ResourceTypesRestAction(resourcePluginInfo, resourceSharingEnabledSetting)); + handlers.add( + new ResourceAccessRestAction( + resourceAccessHandler, + resourcePluginInfo, + resourceSharingEnabledSetting, + resourceSharingProtectedResourceTypesSetting + ) + ); handlers.add( new AccessibleResourcesRestAction( resourceAccessHandler, diff --git a/src/main/java/org/opensearch/security/resources/ResolvedResourceAccess.java b/src/main/java/org/opensearch/security/resources/ResolvedResourceAccess.java new file mode 100644 index 0000000000..37da61ba4a --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/ResolvedResourceAccess.java @@ -0,0 +1,48 @@ +/* + * 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.security.resources; + +import java.io.IOException; +import java.util.Set; + +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +/** + * Resolved access information for the current user on a single resource. + * + * @param resourceId resource identifier + * @param resourceType resource type + * @param owner whether the current user is the owner + * @param admin whether the current user is an admin + * @param effectiveAccessLevel best matching access level for the resource type, if any + * @param accessLevels all matching access levels for the current user on this resource + * @param allowedActions resolved actions granted to the current user + * @param canShare whether the current user may update sharing for this resource + */ +public record ResolvedResourceAccess(String resourceId, String resourceType, boolean owner, boolean admin, String effectiveAccessLevel, Set< + String> accessLevels, Set allowedActions, boolean canShare) implements ToXContentObject { + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("resource_id", resourceId); + builder.field("resource_type", resourceType); + builder.field("is_owner", owner); + builder.field("is_admin", admin); + if (effectiveAccessLevel != null) { + builder.field("effective_access_level", effectiveAccessLevel); + } + builder.field("access_levels", accessLevels); + builder.field("allowed_actions", allowedActions); + builder.field("can_share", canShare); + builder.endObject(); + return builder; + } +} diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 23193e0726..8eb21644ce 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -12,6 +12,8 @@ package org.opensearch.security.resources; import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -27,6 +29,7 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.resources.api.share.ShareAction; import org.opensearch.security.resources.sharing.ResourceSharing; import org.opensearch.security.resources.sharing.ShareWith; import org.opensearch.security.securityconf.FlattenedActionGroups; @@ -332,6 +335,101 @@ public void getSharingInfo(@NonNull String resourceId, @NonNull String resourceT } + /** + * Resolves the current user's access information for a single resource. + * + * This API is intentionally UI-focused: it returns the matching access-level names for the current + * resource type plus the fully resolved allowed actions that the UI can reason about directly. + */ + public void resolveAccessForCurrentUser( + @NonNull String resourceId, + @NonNull String resourceType, + ActionListener listener + ) { + final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( + ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER + ); + final User user = (userSubject == null) ? null : userSubject.getUser(); + + if (user == null) { + listener.onFailure(new OpenSearchStatusException("No authenticated user found.", RestStatus.UNAUTHORIZED)); + return; + } + + final String resourceIndex = resourcePluginInfo.indexByType(resourceType); + if (resourceIndex == null) { + listener.onFailure( + new OpenSearchStatusException("No resourceIndex mapping found for type " + resourceType, RestStatus.BAD_REQUEST) + ); + return; + } + + resourceSharingIndexHandler.fetchSharingInfo(resourceIndex, resourceId, ActionListener.wrap(sharingInfo -> { + if (sharingInfo == null) { + listener.onFailure(new OpenSearchStatusException("Resource [" + resourceId + "] does not exist.", RestStatus.NOT_FOUND)); + return; + } + + boolean isAdmin = adminDNs.isAdmin(user); + boolean isOwner = sharingInfo.isCreatedBy(user.getName()); + Set directAccessLevels = new LinkedHashSet<>(); + Set allowedActions = new LinkedHashSet<>(); + + if (isAdmin || isOwner) { + directAccessLevels.addAll(resourcePluginInfo.getAccessLevelsForType(resourceType)); + allowedActions.addAll(resourcePluginInfo.flattenedForType(resourceType).resolve(directAccessLevels)); + } else { + directAccessLevels.addAll(sharingInfo.getAccessLevelsForUser(user)); + allowedActions.addAll(resourcePluginInfo.flattenedForType(resourceType).resolve(directAccessLevels)); + } + + if (allowedActions.isEmpty() && !isAdmin && !isOwner) { + listener.onFailure( + new OpenSearchStatusException("Not authorized to access resource [" + resourceId + "].", RestStatus.FORBIDDEN) + ); + return; + } + + String effectiveAccessLevel = resolveEffectiveAccessLevel(resourceType, directAccessLevels); + boolean canShare = isAdmin + || isOwner + || allowedActions.contains(ShareAction.NAME) + || resourceSharingIndexHandler.canUserShare(user, false, sharingInfo, resourceType); + + listener.onResponse( + new ResolvedResourceAccess( + resourceId, + resourceType, + isOwner, + isAdmin, + effectiveAccessLevel, + Set.copyOf(directAccessLevels), + Set.copyOf(allowedActions), + canShare + ) + ); + }, listener::onFailure)); + } + + private String resolveEffectiveAccessLevel(String resourceType, Set directAccessLevels) { + if (directAccessLevels == null || directAccessLevels.isEmpty()) { + return null; + } + + List orderedLevels = resourcePluginInfo.getAccessLevelsForType(resourceType); + String effectiveLevel = null; + for (String accessLevel : orderedLevels) { + if (directAccessLevels.contains(accessLevel)) { + effectiveLevel = accessLevel; + } + } + if (effectiveLevel != null) { + return effectiveLevel; + } + + return directAccessLevels.iterator().next(); + } + /** * Shares a resource with the specified users, roles, and backend roles. * diff --git a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index 49a272842a..a75879c40a 100644 --- a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -249,6 +249,15 @@ public String getDefaultAccessLevel(String resourceType) { } } + public List getAccessLevelsForType(String resourceType) { + lock.readLock().lock(); + try { + return List.copyOf(typeToAccessLevels.getOrDefault(resourceType, new LinkedHashSet<>())); + } finally { + lock.readLock().unlock(); + } + } + public ResourceProvider getResourceProvider(String type) { lock.readLock().lock(); try { diff --git a/src/main/java/org/opensearch/security/resources/api/access/ResourceAccessRestAction.java b/src/main/java/org/opensearch/security/resources/api/access/ResourceAccessRestAction.java new file mode 100644 index 0000000000..b5438c2d43 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/api/access/ResourceAccessRestAction.java @@ -0,0 +1,127 @@ +/* + * 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.security.resources.api.access; + +import java.io.IOException; +import java.util.List; + +import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchStatusException; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.security.resources.ResolvedResourceAccess; +import org.opensearch.security.resources.ResourceAccessHandler; +import org.opensearch.security.resources.ResourcePluginInfo; +import org.opensearch.security.setting.OpensearchDynamicSetting; +import org.opensearch.transport.client.node.NodeClient; + +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.security.dlic.rest.api.Responses.response; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_API_RESOURCE_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +/** + * Returns resolved access information for the current user on a single resource. + */ +public class ResourceAccessRestAction extends BaseRestHandler { + private static final Logger LOGGER = LogManager.getLogger(ResourceAccessRestAction.class); + + private final ResourceAccessHandler resourceAccessHandler; + private final ResourcePluginInfo resourcePluginInfo; + private final OpensearchDynamicSetting resourceSharingEnabledSetting; + private final OpensearchDynamicSetting> resourceSharingProtectedTypesSetting; + + public ResourceAccessRestAction( + ResourceAccessHandler resourceAccessHandler, + ResourcePluginInfo resourcePluginInfo, + OpensearchDynamicSetting resourceSharingEnabledSetting, + OpensearchDynamicSetting> resourceSharingProtectedTypesSetting + ) { + this.resourceAccessHandler = resourceAccessHandler; + this.resourcePluginInfo = resourcePluginInfo; + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; + this.resourceSharingProtectedTypesSetting = resourceSharingProtectedTypesSetting; + } + + @Override + public List routes() { + return addRoutesPrefix(ImmutableList.of(new Route(GET, "/access")), PLUGIN_API_RESOURCE_ROUTE_PREFIX); + } + + @Override + public String getName() { + return getClass().getName(); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + if (!resourceSharingEnabledSetting.getDynamicSettingValue()) { + return channel -> { channel.sendResponse(new BytesRestResponse(RestStatus.NOT_IMPLEMENTED, "Feature disabled.")); }; + } + + final String resourceType = request.param("resource_type"); + final String resourceId = request.param("resource_id"); + + if (resourceType == null || resourceId == null) { + return channel -> { + channel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, "resource_type and resource_id are required")); + }; + } + + final String resourceIndex = resourcePluginInfo.indexByType(resourceType); + + if (resourceIndex == null) { + return channel -> { + String message = String.format( + "Invalid resource type: %s. Must be one of: %s", + resourceType, + resourceSharingProtectedTypesSetting.getDynamicSettingValue() + ); + channel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, message)); + }; + } + + return channel -> resourceAccessHandler.resolveAccessForCurrentUser( + resourceId, + resourceType, + ActionListener.wrap(accessInfo -> handleResponse(channel, accessInfo), e -> handleError(channel, e)) + ); + } + + private void handleResponse(RestChannel channel, ResolvedResourceAccess accessInfo) { + try (XContentBuilder builder = channel.newBuilder()) { + builder.startObject(); + builder.field("access"); + accessInfo.toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + channel.sendResponse(new BytesRestResponse(RestStatus.OK, builder)); + } catch (IOException ioe) { + handleError(channel, ioe); + } + } + + private void handleError(RestChannel channel, Exception e) { + LOGGER.error("Error while processing request", e); + final String message = e.getMessage(); + if (e instanceof OpenSearchStatusException ex) { + response(channel, ex.status(), message); + } else { + channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, message)); + } + } +}