diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilterTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilterTest.java index f8935a70e3c0..d7d5c3c2494d 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilterTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilterTest.java @@ -46,6 +46,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import static org.easymock.EasyMock.anyObject; import static org.easymock.EasyMock.expect; @@ -193,7 +194,8 @@ private void setExpectations( authorizer.authorize( authResult, new Resource(datasource, ResourceType.DATASOURCE), - expectedAction + expectedAction, + Map.of() ) ).andReturn(new Access(userHasAccess)).anyTimes(); diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/sampler/SamplerResourceTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/sampler/SamplerResourceTest.java index 68c0d729a338..3ff7fd9fc2f7 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/sampler/SamplerResourceTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/sampler/SamplerResourceTest.java @@ -38,6 +38,7 @@ import javax.servlet.http.HttpServletRequest; import java.util.Collections; +import java.util.Map; public class SamplerResourceTest { @@ -98,7 +99,8 @@ public void test_post_properResourcesAuthorized() EasyMock.expect(mockAuthorizer.authorize( EasyMock.anyObject(AuthenticationResult.class), EasyMock.eq(Resource.STATE_RESOURCE), - EasyMock.eq(Action.WRITE))).andReturn(Access.OK); + EasyMock.eq(Action.WRITE), + EasyMock.eq(Map.of()))).andReturn(Access.OK); EasyMock.expect(authConfig.isEnableInputSourceSecurity()).andReturn(false); EasyMock.expect(samplerSpec.sample()).andReturn(null); EasyMock.replay( diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationUtils.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationUtils.java index 9bcff9bdc1de..6124d3bc84a2 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationUtils.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationUtils.java @@ -46,6 +46,11 @@ */ public class AuthorizationUtils { + /** + * Key used in the authorization context map to indicate the caller path that triggered the authorization check. + */ + public static final String AUTHORIZATION_CONTEXT_CALLER_PATH_CONTEXT_KEY = "callerPath"; + public static final ImmutableSet RESTRICTION_APPLICABLE_RESOURCE_TYPES = ImmutableSet.of( ResourceType.DATASOURCE ); @@ -74,6 +79,35 @@ public static AuthorizationResult authorizeResourceAction( ); } + /** + * Performs authorization check on a single resource-action based on the authentication fields from the request, + * with additional context about the authorization request. + *

+ * This function will set the DRUID_AUTHORIZATION_CHECKED attribute in the request. If this attribute is already set + * when this function is called, an exception is thrown. + * + * @param request HTTP request to be authorized + * @param resourceAction A resource identifier and the action to be taken the resource. + * @param authorizerMapper The singleton AuthorizerMapper instance + * @param context Additional context about the authorization request, such as information about the + * caller path to be authorized. + * @return AuthorizationResult containing allow/deny access to the resource action, along with policy restrictions. + */ + public static AuthorizationResult authorizeResourceAction( + final HttpServletRequest request, + final ResourceAction resourceAction, + final AuthorizerMapper authorizerMapper, + final Map context + ) + { + return authorizeAllResourceActions( + request, + Collections.singletonList(resourceAction), + authorizerMapper, + context + ); + } + /** * Verifies that the user has unrestricted access to perform the required * action on the given datasource. @@ -181,6 +215,29 @@ public static AuthorizationResult authorizeAllResourceActions( final Iterable resourceActions, final AuthorizerMapper authorizerMapper ) + { + return authorizeAllResourceActions(authenticationResult, resourceActions, authorizerMapper, Map.of()); + } + + /** + * Performs authorization check on a list of resource-actions based on the authenticationResult, with additional + * context about the authorization request. + *

+ * If one of the resource-actions denys access, returns deny access immediately. + * + * @param authenticationResult Authentication result representing identity of requester + * @param resourceActions An Iterable of resource-actions to authorize + * @param authorizerMapper The singleton AuthorizerMapper instance + * @param context Additional context about the authorization request, such as information about the + * caller path to be authorized. + * @return AuthorizationResult containing allow/deny access to the resource actions, along with policy restrictions. + */ + public static AuthorizationResult authorizeAllResourceActions( + final AuthenticationResult authenticationResult, + final Iterable resourceActions, + final AuthorizerMapper authorizerMapper, + final Map context + ) { final Authorizer authorizer = authorizerMapper.getAuthorizer(authenticationResult.getAuthorizerName()); if (authorizer == null) { @@ -198,7 +255,8 @@ public static AuthorizationResult authorizeAllResourceActions( final Access access = authorizer.authorize( authenticationResult, resourceAction.getResource(), - resourceAction.getAction() + resourceAction.getAction(), + context ); if (!access.isAllowed()) { return AuthorizationResult.deny(access.getMessage()); @@ -260,6 +318,32 @@ public static AuthorizationResult authorizeAllResourceActions( final Iterable resourceActions, final AuthorizerMapper authorizerMapper ) + { + return authorizeAllResourceActions(request, resourceActions, authorizerMapper, Map.of()); + } + + /** + * Performs authorization check on a list of resource-actions based on the authentication fields from the request, + * with additional context about the authorization request. + *

+ * If one of the resource-actions denys access, returns deny access immediately. + *

+ * This function will set the DRUID_AUTHORIZATION_CHECKED attribute in the request. If this attribute is already set + * when this function is called, an exception is thrown. + * + * @param request HTTP request to be authorized + * @param resourceActions An Iterable of resource-actions to authorize + * @param authorizerMapper The singleton AuthorizerMapper instance + * @param context Additional context about the authorization request, such as information about the + * caller path to be authorized. + * @return AuthorizationResult containing allow/deny access to the resource actions, along with policy restrictions. + */ + public static AuthorizationResult authorizeAllResourceActions( + final HttpServletRequest request, + final Iterable resourceActions, + final AuthorizerMapper authorizerMapper, + final Map context + ) { if (request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH) != null) { return AuthorizationResult.ALLOW_NO_RESTRICTION; @@ -272,7 +356,8 @@ public static AuthorizationResult authorizeAllResourceActions( AuthorizationResult authResult = authorizeAllResourceActions( authenticationResultFromRequest(request), resourceActions, - authorizerMapper + authorizerMapper, + context ); request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, authResult.allowBasicAccess()); @@ -321,6 +406,38 @@ public static Iterable filterAuthorizedResources( final Function> resourceActionGenerator, final AuthorizerMapper authorizerMapper ) + { + return filterAuthorizedResources(request, resources, resourceActionGenerator, authorizerMapper, Map.of()); + } + + /** + * Return an iterable of authorized resources, by filtering the input resources with authorization checks based on the + * authentication fields from the request, with additional context about the authorization request. This method does: + *

  • + * For every resource, resourceActionGenerator generates an Iterable of ResourceAction or null. + *
  • + * If null, continue with next resource. If any resource-action in the iterable has deny-access, continue with next + * resource. Only when every resource-action has allow-access, add the resource to the result. + *
  • + *

    + * This function will set the DRUID_AUTHORIZATION_CHECKED attribute in the request. If this attribute is already set + * when this function is called, an exception is thrown. + * + * @param request HTTP request to be authorized + * @param resources resources to be processed into resource-actions + * @param resourceActionGenerator Function that creates an iterable of resource-actions from a resource + * @param authorizerMapper authorizer mapper + * @param context Additional context about the authorization request, such as information about the + * caller path to be authorized. + * @return Iterable containing resources that were authorized + */ + public static Iterable filterAuthorizedResources( + final HttpServletRequest request, + final Iterable resources, + final Function> resourceActionGenerator, + final AuthorizerMapper authorizerMapper, + final Map context + ) { if (request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH) != null) { return resources; @@ -336,7 +453,8 @@ public static Iterable filterAuthorizedResources( authenticationResult, resources, resourceActionGenerator, - authorizerMapper + authorizerMapper, + context ); // We're filtering, so having access to none of the objects isn't an authorization failure (in terms of whether @@ -367,6 +485,34 @@ public static Iterable filterAuthorizedResources( final Function> resourceActionGenerator, final AuthorizerMapper authorizerMapper ) + { + return filterAuthorizedResources(authenticationResult, resources, resourceActionGenerator, authorizerMapper, Map.of()); + } + + /** + * Return an iterable of authorized resources, by filtering the input resources with authorization checks based on + * authenticationResult, with additional context about the authorization request. This method does: + *

  • + * For every resource, resourceActionGenerator generates an Iterable of ResourceAction or null. + *
  • + * If null, continue with next resource. If any resource-action in the iterable has deny-access, continue with next + * resource. Only when every resource-action has allow-access, add the resource to the result. + * + * @param authenticationResult Authentication result representing identity of requester + * @param resources resources to be processed into resource-actions + * @param resourceActionGenerator Function that creates an iterable of resource-actions from a resource + * @param authorizerMapper authorizer mapper + * @param context Additional context about the authorization request, such as information about the + * caller path to be authorized. + * @return Iterable containing resources that were authorized + */ + public static Iterable filterAuthorizedResources( + final AuthenticationResult authenticationResult, + final Iterable resources, + final Function> resourceActionGenerator, + final AuthorizerMapper authorizerMapper, + final Map context + ) { final Authorizer authorizer = authorizerMapper.getAuthorizer(authenticationResult.getAuthorizerName()); if (authorizer == null) { @@ -390,7 +536,8 @@ public static Iterable filterAuthorizedResources( ra -> authorizer.authorize( authenticationResult, ra.getResource(), - ra.getAction() + ra.getAction(), + context ) ); if (!access.isAllowed()) { @@ -427,6 +574,38 @@ public static Map> filterAuthorizedRes final Function> resourceActionGenerator, final AuthorizerMapper authorizerMapper ) + { + return filterAuthorizedResources(request, unfilteredResources, resourceActionGenerator, authorizerMapper, Map.of()); + } + + /** + * Return a map of authorized resources, by filtering the input resources with authorization checks based on the + * authentication fields from the request, with additional context about the authorization request. This method does: + *
  • + * For every resource, resourceActionGenerator generates an Iterable of ResourceAction or null. + *
  • + * If null, continue with next resource. If any resource-action in the iterable has deny-access, continue with next + * resource. Only when every resource-action has allow-access, add the resource to the result. + *
  • + *

    + * This function will set the DRUID_AUTHORIZATION_CHECKED attribute in the request. If this attribute is already set + * when this function is called, an exception is thrown. + * + * @param request HTTP request to be authorized + * @param unfilteredResources Map of resource lists to be filtered + * @param resourceActionGenerator Function that creates an iterable of resource-actions from a resource + * @param authorizerMapper authorizer mapper + * @param context Additional context about the authorization request, such as information about the + * caller path to be authorized. + * @return Map containing lists of resources that were authorized + */ + public static Map> filterAuthorizedResources( + final HttpServletRequest request, + final Map> unfilteredResources, + final Function> resourceActionGenerator, + final AuthorizerMapper authorizerMapper, + final Map context + ) { if (request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH) != null) { @@ -450,7 +629,8 @@ public static Map> filterAuthorizedRes authenticationResult, entry.getValue(), resourceActionGenerator, - authorizerMapper + authorizerMapper, + context ) ); diff --git a/server/src/main/java/org/apache/druid/server/security/Authorizer.java b/server/src/main/java/org/apache/druid/server/security/Authorizer.java index 43ff43681c90..2046c2f74423 100644 --- a/server/src/main/java/org/apache/druid/server/security/Authorizer.java +++ b/server/src/main/java/org/apache/druid/server/security/Authorizer.java @@ -22,6 +22,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.util.Map; + /** * An Authorizer is responsible for performing authorization checks for resource accesses. *

    @@ -51,4 +53,33 @@ public interface Authorizer * @return An {@link Access} object representing the result of the authorization check. Must not be null. */ Access authorize(AuthenticationResult authenticationResult, Resource resource, Action action); + + /** + * Check if the entity represented by the authentication result is authorized to perform the given action on the + * given resource, with additional context about the authorization request. + *

    + * The {@code context} map can be used to provide supplementary information about the caller path to be authorized. + * For example, it may contain details about the API endpoint or query context that triggered the authorization + * check, allowing authorizer implementations to make more informed decisions. + *

    + * If the action involves reading a table, the outcome could include {@link org.apache.druid.query.policy.Policy} restrictions. + * However, if the action does not involve reading a table, there must be no {@link org.apache.druid.query.policy.Policy} restrictions. + * + * @param authenticationResult The authentication result of the request + * @param resource The resource to be accessed + * @param action The action to perform on the resource + * @param context Additional context about the authorization request, such as information about the + * caller path to be authorized. Implementations that do not need this context can + * safely ignore it. + * @return An {@link Access} object representing the result of the authorization check. Must not be null. + */ + default Access authorize( + AuthenticationResult authenticationResult, + Resource resource, + Action action, + Map context + ) + { + return authorize(authenticationResult, resource, action); + } } diff --git a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java index b478ab9befc0..64ed5ff11fa6 100644 --- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java @@ -403,7 +403,7 @@ public void testAuthorized_withPolicyRestriction() EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize(authenticationResult, RESOURCE, Action.READ)) + EasyMock.expect(authorizer.authorize(authenticationResult, RESOURCE, Action.READ, Map.of())) .andReturn(access).anyTimes(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) .andReturn(toolChest).anyTimes(); @@ -443,7 +443,7 @@ public void testAuthorized_withPolicyRestriction_failedSecurityValidation() EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize(authenticationResult, RESOURCE, Action.READ)) + EasyMock.expect(authorizer.authorize(authenticationResult, RESOURCE, Action.READ, Map.of())) .andReturn(access).anyTimes(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) .andReturn(toolChest).anyTimes(); @@ -477,7 +477,7 @@ public void testAuthorized_queryWithRestrictedDataSource_runWithSuperUserPermiss EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize(authenticationResult, RESOURCE, Action.READ)) + EasyMock.expect(authorizer.authorize(authenticationResult, RESOURCE, Action.READ, Map.of())) .andReturn(access).anyTimes(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) .andReturn(toolChest).anyTimes(); @@ -506,19 +506,22 @@ public void testAuthorizeQueryContext_authorized() EasyMock.expect(authorizer.authorize( authenticationResult, new Resource(DATASOURCE, ResourceType.DATASOURCE), - Action.READ + Action.READ, + Map.of() )) .andReturn(Access.OK).times(2); EasyMock.expect(authorizer.authorize( authenticationResult, new Resource("foo", ResourceType.QUERY_CONTEXT), - Action.WRITE + Action.WRITE, + Map.of() )) .andReturn(Access.OK).times(2); EasyMock.expect(authorizer.authorize( authenticationResult, new Resource("baz", ResourceType.QUERY_CONTEXT), - Action.WRITE + Action.WRITE, + Map.of() )) .andReturn(Access.OK).times(2); @@ -564,14 +567,16 @@ public void testAuthorizeQueryContext_notAuthorized() EasyMock.expect(authorizer.authorize( authenticationResult, new Resource(DATASOURCE, ResourceType.DATASOURCE), - Action.READ + Action.READ, + Map.of() )) .andReturn(Access.OK) .times(2); EasyMock.expect(authorizer.authorize( authenticationResult, new Resource("foo", ResourceType.QUERY_CONTEXT), - Action.WRITE + Action.WRITE, + Map.of() )) .andReturn(Access.DENIED) .times(2); @@ -605,7 +610,7 @@ public void testAuthorizeQueryContext_unsecuredKeys() EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize(authenticationResult, RESOURCE, Action.READ)) + EasyMock.expect(authorizer.authorize(authenticationResult, RESOURCE, Action.READ, Map.of())) .andReturn(Access.OK) .times(2); @@ -654,7 +659,8 @@ public void testAuthorizeQueryContext_securedKeys() EasyMock.expect(authorizer.authorize( authenticationResult, new Resource(DATASOURCE, ResourceType.DATASOURCE), - Action.READ + Action.READ, + Map.of() )) .andReturn(Access.OK) .times(2); @@ -705,14 +711,16 @@ public void testAuthorizeQueryContext_securedKeysNotAuthorized() EasyMock.expect(authorizer.authorize( authenticationResult, new Resource(DATASOURCE, ResourceType.DATASOURCE), - Action.READ + Action.READ, + Map.of() )) .andReturn(Access.OK) .times(2); EasyMock.expect(authorizer.authorize( authenticationResult, new Resource("foo", ResourceType.QUERY_CONTEXT), - Action.WRITE + Action.WRITE, + Map.of() )) .andReturn(Access.DENIED) .times(2); @@ -754,21 +762,24 @@ public void testAuthorizeLegacyQueryContext_authorized() EasyMock.expect(authorizer.authorize( authenticationResult, new Resource("fake", ResourceType.DATASOURCE), - Action.READ + Action.READ, + Map.of() )) .andReturn(Access.OK) .times(2); EasyMock.expect(authorizer.authorize( authenticationResult, new Resource("foo", ResourceType.QUERY_CONTEXT), - Action.WRITE + Action.WRITE, + Map.of() )) .andReturn(Access.OK) .times(2); EasyMock.expect(authorizer.authorize( authenticationResult, new Resource("baz", ResourceType.QUERY_CONTEXT), - Action.WRITE + Action.WRITE, + Map.of() )) .andReturn(Access.OK) .times(2); diff --git a/server/src/test/java/org/apache/druid/server/security/AuthorizationUtilsTest.java b/server/src/test/java/org/apache/druid/server/security/AuthorizationUtilsTest.java index 50e5fce688ed..827e32c9114d 100644 --- a/server/src/test/java/org/apache/druid/server/security/AuthorizationUtilsTest.java +++ b/server/src/test/java/org/apache/druid/server/security/AuthorizationUtilsTest.java @@ -20,6 +20,7 @@ package org.apache.druid.server.security; import com.google.common.base.Function; +import com.google.common.collect.ImmutableMap; import org.apache.druid.error.DruidException; import org.apache.druid.query.filter.EqualityFilter; import org.apache.druid.query.policy.NoRestrictionPolicy; @@ -39,6 +40,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; public class AuthorizationUtilsTest { @@ -243,4 +245,182 @@ public void testAuthorizeAllResourceActions_policyForNonReadDatasourceThrows() ); Assert.assertTrue(exception.getMessage().contains("Policy should only present when reading a table")); } + + + @Test + public void testAuthorizeAllResourceActions_contextPassedToAuthorizer() + { + final String authorizerName = "testAuthorizer"; + final AuthenticationResult authenticationResult = + new AuthenticationResult("identity", authorizerName, "authenticator", null); + + final Map expectedContext = ImmutableMap.of("testKey", "testValue"); + final AtomicReference> capturedContext = new AtomicReference<>(); + + final Authorizer authorizer = new Authorizer() + { + @Override + public Access authorize(AuthenticationResult authResult, Resource resource, Action action) + { + return Access.OK; + } + + @Override + public Access authorize( + AuthenticationResult authResult, + Resource resource, + Action action, + Map context + ) + { + capturedContext.set(context); + return Access.OK; + } + }; + + final Map authorizerMap = new HashMap<>(); + authorizerMap.put(authorizerName, authorizer); + final AuthorizerMapper mapper = new AuthorizerMapper(authorizerMap); + + final List resourceActions = Collections.singletonList( + new ResourceAction(Resource.STATE_RESOURCE, Action.READ) + ); + + AuthorizationUtils.authorizeAllResourceActions( + authenticationResult, + resourceActions, + mapper, + expectedContext + ); + + Assert.assertEquals(expectedContext, capturedContext.get()); + } + + @Test + public void testAuthorizeAllResourceActions_emptyContextByDefault() + { + final String authorizerName = "testAuthorizer"; + final AuthenticationResult authenticationResult = + new AuthenticationResult("identity", authorizerName, "authenticator", null); + + final AtomicReference> capturedContext = new AtomicReference<>(); + + final Authorizer authorizer = new Authorizer() + { + @Override + public Access authorize(AuthenticationResult authResult, Resource resource, Action action) + { + return Access.OK; + } + + @Override + public Access authorize( + AuthenticationResult authResult, + Resource resource, + Action action, + Map context + ) + { + capturedContext.set(context); + return Access.OK; + } + }; + + final Map authorizerMap = new HashMap<>(); + authorizerMap.put(authorizerName, authorizer); + final AuthorizerMapper mapper = new AuthorizerMapper(authorizerMap); + + final List resourceActions = Collections.singletonList( + new ResourceAction(Resource.STATE_RESOURCE, Action.READ) + ); + + AuthorizationUtils.authorizeAllResourceActions( + authenticationResult, + resourceActions, + mapper + ); + + Assert.assertEquals(Map.of(), capturedContext.get()); + } + + @Test + public void testFilterAuthorizedResources_contextPassedToAuthorizer() + { + final String authorizerName = "testAuthorizer"; + final AuthenticationResult authenticationResult = + new AuthenticationResult("identity", authorizerName, "authenticator", null); + + final Map expectedContext = ImmutableMap.of("testKey", "testValue"); + final List> capturedContext = new ArrayList<>(); + + final Authorizer authorizer = new Authorizer() + { + @Override + public Access authorize(AuthenticationResult authResult, Resource resource, Action action) + { + return Access.OK; + } + + @Override + public Access authorize( + AuthenticationResult authResult, + Resource resource, + Action action, + Map context + ) + { + capturedContext.add(context); + return Access.OK; + } + }; + + final Map authorizerMap = new HashMap<>(); + authorizerMap.put(authorizerName, authorizer); + final AuthorizerMapper mapper = new AuthorizerMapper(authorizerMap); + + final List resources = Arrays.asList("resource1", "resource2"); + final Function> raGenerator = + input -> Collections.singletonList(new ResourceAction(new Resource(input, ResourceType.DATASOURCE), Action.READ)); + + Iterable filtered = AuthorizationUtils.filterAuthorizedResources( + authenticationResult, + resources, + raGenerator, + mapper, + expectedContext + ); + + // Consume the iterable to trigger authorization + Iterator itr = filtered.iterator(); + while (itr.hasNext()) { + itr.next(); + } + Assert.assertEquals(2, capturedContext.size()); + for (Map context : capturedContext) { + Assert.assertEquals(expectedContext, context); + } + } + + @Test + public void testAuthorizerDefaultMethodDelegatesToThreeArgMethod() + { + final AtomicReference capturedResource = new AtomicReference<>(); + + // An authorizer that only implements the 3-arg method (simulating existing subclasses) + final Authorizer authorizer = (authResult, resource, action) -> { + capturedResource.set(resource.getName()); + return Access.OK; + }; + + final AuthenticationResult authResult = + new AuthenticationResult("identity", "authorizerName", "authenticator", null); + final Resource resource = new Resource("testResource", ResourceType.DATASOURCE); + final Map context = ImmutableMap.of("callerPath", "TestCaller"); + + // Calling the 4-arg default method should delegate to the 3-arg method + Access access = authorizer.authorize(authResult, resource, Action.READ, context); + + Assert.assertTrue(access.isAllowed()); + Assert.assertEquals("testResource", capturedResource.get()); + } } diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/InformationSchema.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/InformationSchema.java index a9e3d2e31d2c..9fd3ba66750c 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/InformationSchema.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/InformationSchema.java @@ -79,6 +79,14 @@ public class InformationSchema extends AbstractSchema private static final String COLUMNS_TABLE = "COLUMNS"; private static final String ROUTINES_TABLE = "ROUTINES"; + /** + * Context map passed to {@link AuthorizationUtils} methods to indicate that authorization + * is being performed from the InformationSchema. + */ + static final String AUTHORIZATION_CONTEXT_CALLER_PATH_VALUE = InformationSchema.class.getSimpleName(); + private static final Map INFORMATION_SCHEMA_AUTHORIZATION_CONTEXT = + ImmutableMap.of(AuthorizationUtils.AUTHORIZATION_CONTEXT_CALLER_PATH_CONTEXT_KEY, AUTHORIZATION_CONTEXT_CALLER_PATH_VALUE); + private static class RowTypeBuilder { final RelDataTypeFactory typeFactory = DruidTypeSystem.TYPE_FACTORY; @@ -615,7 +623,8 @@ private Set getAuthorizedNamesFromNamedSchema( new ResourceAction(new Resource(name, resourseType), Action.READ) ); }, - authorizerMapper + authorizerMapper, + INFORMATION_SCHEMA_AUTHORIZATION_CONTEXT ) ); } diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java index 15a54d16ff9d..83bc8140bd5d 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java @@ -121,6 +121,14 @@ public class SystemSchema extends AbstractSchema segment.getDataSource()) ); + /** + * Context map passed to {@link AuthorizationUtils} methods to indicate that authorization + * is being performed from the SystemSchema. + */ + static final String AUTHORIZATION_CONTEXT_CALLER_PATH_VALUE = SystemSchema.class.getSimpleName(); + private static final Map SYSTEM_SCHEMA_AUTHORIZATION_CONTEXT = + ImmutableMap.of(AuthorizationUtils.AUTHORIZATION_CONTEXT_CALLER_PATH_CONTEXT_KEY, AUTHORIZATION_CONTEXT_CALLER_PATH_VALUE); + private static final long REPLICATION_FACTOR_UNKNOWN = -1L; /** @@ -486,7 +494,8 @@ private Iterator getAuthorizedPublishedSegments( authenticationResult, () -> it, SEGMENT_STATUS_IN_CLUSTER_RA_GENERATOR, - authorizerMapper + authorizerMapper, + SYSTEM_SCHEMA_AUTHORIZATION_CONTEXT ); return authorizedSegments.iterator(); } @@ -511,7 +520,8 @@ private Iterator getAuthorizedAvailableSegments( authenticationResult, () -> availableSegmentEntries, raGenerator, - authorizerMapper + authorizerMapper, + SYSTEM_SCHEMA_AUTHORIZATION_CONTEXT ); return authorizedSegments.iterator(); @@ -848,7 +858,8 @@ public Enumerable scan(DataContext root) authenticationResult, druidServer.iterateAllSegments(), SEGMENT_RA_GENERATOR, - authorizerMapper + authorizerMapper, + SYSTEM_SCHEMA_AUTHORIZATION_CONTEXT ); for (DataSegment segment : authorizedServerSegments) { @@ -983,7 +994,8 @@ private CloseableIterator getAuthorizedTasks( authenticationResult, () -> it, raGenerator, - authorizerMapper + authorizerMapper, + SYSTEM_SCHEMA_AUTHORIZATION_CONTEXT ); return wrap(authorizedTasks.iterator(), it); @@ -1107,7 +1119,8 @@ private CloseableIterator getAuthorizedSupervisors( authenticationResult, () -> it, raGenerator, - authorizerMapper + authorizerMapper, + SYSTEM_SCHEMA_AUTHORIZATION_CONTEXT ); return wrap(authorizedSupervisors.iterator(), it); @@ -1276,7 +1289,8 @@ public Enumerable scan( final AuthorizationResult stateReadAuthorization = AuthorizationUtils.authorizeAllResourceActions( authenticationResult, Collections.singletonList(new ResourceAction(Resource.STATE_RESOURCE, Action.READ)), - authorizerMapper + authorizerMapper, + SYSTEM_SCHEMA_AUTHORIZATION_CONTEXT ); // Get queries from all engines