This is an automated email from the ASF dual-hosted git repository.
cecemei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new d9b6c4e4727 feat: add auth metrics emission (#19552)
d9b6c4e4727 is described below
commit d9b6c4e472712744dd3ade8bfc00070c8398d42a
Author: Cece Mei <[email protected]>
AuthorDate: Fri Jun 26 14:56:41 2026 -0700
feat: add auth metrics emission (#19552)
* auth-metric
* test
* test2
* static
* Update
server/src/main/java/org/apache/druid/server/security/AuthorizationUtils.java
Co-authored-by: Lucas Capistrant <[email protected]>
* forbidden
* doc
* indent
* test
---------
Co-authored-by: Lucas Capistrant <[email protected]>
---
docs/configuration/index.md | 3 +-
docs/operations/metrics.md | 9 +
.../http/OverlordCompactionResourceTest.java | 2 +-
.../overlord/http/OverlordResourceTest.java | 2 +-
.../security/SupervisorResourceFilterTest.java | 9 +-
.../overlord/sampler/SamplerResourceTest.java | 2 +-
.../msq/indexing/MSQCompactionRunnerTest.java | 5 +
.../initialization/AuthorizerMapperModule.java | 26 ++-
.../apache/druid/server/security/AuthConfig.java | 29 ++-
.../druid/server/security/AuthorizationUtils.java | 79 ++++++--
.../druid/server/security/AuthorizerMapper.java | 22 ++-
.../druid/server/http/DataSourcesResourceTest.java | 10 +-
.../druid/server/http/IntervalsResourceTest.java | 8 +-
.../http/security/ResourceFilterTestHelper.java | 3 +-
.../initialization/AuthorizerMapperModuleTest.java | 52 ++++-
.../druid/server/security/AuthConfigTest.java | 14 ++
.../server/security/AuthorizationUtilsTest.java | 211 +++++++++++++++++++++
.../org/apache/druid/sql/SqlStatementTest.java | 6 +-
18 files changed, 438 insertions(+), 54 deletions(-)
diff --git a/docs/configuration/index.md b/docs/configuration/index.md
index 070f1a40859..f25e1bdd99f 100644
--- a/docs/configuration/index.md
+++ b/docs/configuration/index.md
@@ -218,7 +218,8 @@ values for the above mentioned configs among others
provided by Java implementat
|`druid.escalator.type`|String|Type of the Escalator that should be used for
internal Druid communications. This Escalator must use an authentication scheme
that is supported by an Authenticator in
`druid.auth.authenticatorChain`.|`noop`|no|
|`druid.auth.authorizers`|JSON List of Strings|List of Authorizer type names
|["allowAll"]|no|
|`druid.auth.unsecuredPaths`| List of Strings|List of paths for which security
checks will not be performed. All requests to these paths will be
allowed.|[]|no|
-|`druid.auth.allowUnauthenticatedHttpOptions`|Boolean|If true, skip
authentication checks for HTTP OPTIONS requests. This is needed for certain use
cases, such as supporting CORS pre-flight requests. Note that disabling
authentication checks for OPTIONS requests will allow unauthenticated users to
determine what Druid endpoints are valid (by checking if the OPTIONS request
returns a 200 instead of 404), so enabling this option may reveal information
about server configuration, including [...]
+|`druid.auth.allowUnauthenticatedHttpOptions`|Boolean|If true, skip
authentication checks for HTTP OPTIONS requests. This is needed for certain use
cases, such as supporting CORS pre-flight requests. Note that disabling
authentication checks for OPTIONS requests will allow unauthenticated users to
determine what Druid endpoints are valid (by checking if the OPTIONS request
returns a 200 instead of 404), so enabling this option may reveal information
about server configuration, including [...]
+|`druid.auth.emitAuthMetrics`|Boolean|If true, emit service metrics for
authorization events. When enabled, Druid emits `auth/forbidden` when access is
denied and `auth/exception` when an internal authorization error occurs. See
[Security metrics](#security) for details.|false|no|
For more information, please see [Authentication and
Authorization](../operations/auth.md).
diff --git a/docs/operations/metrics.md b/docs/operations/metrics.md
index 4ce8e85061f..8fa59a1a55f 100644
--- a/docs/operations/metrics.md
+++ b/docs/operations/metrics.md
@@ -490,6 +490,15 @@ These metrics are emitted by the Druid Coordinator in
every run of the correspon
|`segment/schemaCache/deepStorageOnly/count`|Number of deep storage only
segments with cached schema.|`dataSource`||
|`segment/schemaCache/deepStorageOnly/refresh/time`|Time taken in milliseconds
to refresh schemas of deep storage only segments.||Under a minute|
+## Security
+
+These metrics are emitted when `druid.auth.emitAuthMetrics` is set to `true`.
+
+|Metric|Description|Dimensions|Normal value|
+|------|-----------|----------|------------|
+|`auth/forbidden`|Emitted when a request is denied due to insufficient
permissions. This includes both outright access denials and cases where basic
access is granted but a row-level policy restricts full access.|`identity`,
`authorizerName`, `resourceName`, `resourceType`, `action`, `errorMessage`|1|
+|`auth/exception`|Emitted when an internal authorization error occurs, such as
a missing authorizer, duplicate resource policies, or a double-authorization
check on the same request.|`identity`, `authorizerName`, `resourceName` (when
available), `resourceType` (when available), `action` (when available),
`errorMessage` (when available)|1|
+
## General Health
### Service Health
diff --git
a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordCompactionResourceTest.java
b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordCompactionResourceTest.java
index 3518e1dea40..7872ffa4f2d 100644
---
a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordCompactionResourceTest.java
+++
b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordCompactionResourceTest.java
@@ -461,10 +461,10 @@ public class OverlordCompactionResourceTest
private void setupMockRequestForUser(String user)
{
EasyMock.expect(httpRequest.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).atLeastOnce();
-
EasyMock.expect(httpRequest.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).atLeastOnce();
EasyMock.expect(httpRequest.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT))
.andReturn(new AuthenticationResult(user, "druid", null, null))
.atLeastOnce();
+
EasyMock.expect(httpRequest.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).atLeastOnce();
httpRequest.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().anyTimes();
}
diff --git
a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordResourceTest.java
b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordResourceTest.java
index 288ae1bf387..dd295fc979c 100644
---
a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordResourceTest.java
+++
b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordResourceTest.java
@@ -1495,10 +1495,10 @@ public class OverlordResourceTest
{
AuthenticationResult authenticationResult = new
AuthenticationResult(username, "druid", null, null);
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).anyTimes();
-
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).atLeastOnce();
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT))
.andReturn(authenticationResult)
.atLeastOnce();
+
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).atLeastOnce();
req.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, false);
EasyMock.expectLastCall().anyTimes();
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 f8935a70e3c..dcf1d26e6d1 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
@@ -180,14 +180,14 @@ public class SupervisorResourceFilterTest
HttpServletRequest servletRequest =
EasyMock.createMock(HttpServletRequest.class);
expect(servletRequest.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH))
.andReturn(null).anyTimes();
- expect(servletRequest.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED))
- .andReturn(null).anyTimes();
- servletRequest.setAttribute(isA(String.class), anyObject());
-
final String authorizerName = "authorizer";
AuthenticationResult authResult =
EasyMock.createMock(AuthenticationResult.class);
expect(authResult.getAuthorizerName()).andReturn(authorizerName).anyTimes();
+ expect(servletRequest.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED))
+ .andReturn(null).anyTimes();
+ servletRequest.setAttribute(isA(String.class), anyObject());
+
Authorizer authorizer = EasyMock.createMock(Authorizer.class);
expect(
authorizer.authorize(
@@ -200,6 +200,7 @@ public class SupervisorResourceFilterTest
expect(authorizerMapper.getAuthorizer(authorizerName))
.andReturn(authorizer)
.atLeastOnce();
+ expect(authorizerMapper.getServiceEmitter()).andReturn(null).anyTimes();
expect(servletRequest.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT))
.andReturn(authResult)
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 68c0d729a33..5566d7dd670 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
@@ -181,10 +181,10 @@ public class SamplerResourceTest
{
AuthenticationResult authenticationResult = new
AuthenticationResult(username, "druid", null, null);
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).anyTimes();
-
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).atLeastOnce();
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT))
.andReturn(authenticationResult)
.atLeastOnce();
+
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).atLeastOnce();
req.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, false);
EasyMock.expectLastCall().anyTimes();
diff --git
a/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/MSQCompactionRunnerTest.java
b/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/MSQCompactionRunnerTest.java
index f2f6f97a180..10d9b09d81a 100644
---
a/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/MSQCompactionRunnerTest.java
+++
b/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/MSQCompactionRunnerTest.java
@@ -34,6 +34,7 @@ import org.apache.druid.data.input.impl.LongDimensionSchema;
import org.apache.druid.data.input.impl.StringDimensionSchema;
import org.apache.druid.data.input.impl.TimestampSpec;
import org.apache.druid.guice.StartupInjectorBuilder;
+import org.apache.druid.guice.security.DruidAuthModule;
import org.apache.druid.guice.security.EscalatorModule;
import org.apache.druid.guice.security.PolicyModule;
import org.apache.druid.indexer.TaskStatus;
@@ -52,6 +53,7 @@ import org.apache.druid.java.util.common.Intervals;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.granularity.Granularities;
import org.apache.druid.java.util.common.granularity.GranularityType;
+import org.apache.druid.java.util.emitter.service.ServiceEmitter;
import org.apache.druid.msq.indexing.destination.DataSourceMSQDestination;
import org.apache.druid.msq.kernel.WorkerAssignmentStrategy;
import org.apache.druid.msq.util.MultiStageQueryContext;
@@ -83,6 +85,7 @@ import
org.apache.druid.segment.transform.CompactionTransformSpec;
import org.apache.druid.segment.transform.TransformSpec;
import org.apache.druid.server.coordinator.CompactionConfigValidationResult;
import org.apache.druid.server.initialization.AuthorizerMapperModule;
+import org.apache.druid.server.metrics.NoopServiceEmitter;
import org.apache.druid.sql.calcite.parser.DruidSqlInsert;
import org.joda.time.Interval;
import org.junit.Assert;
@@ -162,6 +165,8 @@ public class MSQCompactionRunnerTest
new CoreInjectorBuilder(new
StartupInjectorBuilder().forTests().build()).addModules(
new EscalatorModule(),
new AuthorizerMapperModule(),
+ new DruidAuthModule(),
+ binder ->
binder.bind(ServiceEmitter.class).to(NoopServiceEmitter.class),
new PolicyModule()
).build()
);
diff --git
a/server/src/main/java/org/apache/druid/server/initialization/AuthorizerMapperModule.java
b/server/src/main/java/org/apache/druid/server/initialization/AuthorizerMapperModule.java
index 9e35b32d8f1..5ae8a35f8f5 100644
---
a/server/src/main/java/org/apache/druid/server/initialization/AuthorizerMapperModule.java
+++
b/server/src/main/java/org/apache/druid/server/initialization/AuthorizerMapperModule.java
@@ -21,7 +21,6 @@ package org.apache.druid.server.initialization;
import com.google.inject.Binder;
import com.google.inject.Inject;
-import com.google.inject.Injector;
import com.google.inject.Provider;
import org.apache.druid.guice.JsonConfigProvider;
import org.apache.druid.guice.JsonConfigurator;
@@ -31,6 +30,7 @@ import org.apache.druid.initialization.DruidModule;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.emitter.service.ServiceEmitter;
import org.apache.druid.server.security.AllowAllAuthorizer;
import org.apache.druid.server.security.AuthConfig;
import org.apache.druid.server.security.AuthValidator;
@@ -62,15 +62,20 @@ public class AuthorizerMapperModule implements DruidModule
private static class AuthorizerMapperProvider implements
Provider<AuthorizerMapper>
{
private AuthConfig authConfig;
- private Injector injector;
+ private ServiceEmitter serviceEmitter;
private Properties props;
private JsonConfigurator configurator;
@Inject
- public void inject(Injector injector, Properties props, JsonConfigurator
configurator)
+ public void inject(
+ AuthConfig authConfig,
+ ServiceEmitter serviceEmitter,
+ Properties props,
+ JsonConfigurator configurator
+ )
{
- this.authConfig = injector.getInstance(AuthConfig.class);
- this.injector = injector;
+ this.authConfig = authConfig;
+ this.serviceEmitter = serviceEmitter;
this.props = props;
this.configurator = configurator;
}
@@ -83,12 +88,19 @@ public class AuthorizerMapperModule implements DruidModule
validateAuthorizers(authorizers);
+ final ServiceEmitter emitter;
+ if (authConfig.isEmitAuthMetrics()) {
+ emitter = serviceEmitter;
+ } else {
+ emitter = null;
+ }
+
// Default is allow all
if (authorizers == null) {
AllowAllAuthorizer allowAllAuthorizer = new AllowAllAuthorizer(null);
authorizerMap.put(AuthConfig.ALLOW_ALL_NAME, allowAllAuthorizer);
- return new AuthorizerMapper(null)
+ return new AuthorizerMapper(null, emitter)
{
@Override
public Authorizer getAuthorizer(String name)
@@ -125,7 +137,7 @@ public class AuthorizerMapperModule implements DruidModule
authorizerMap.put(authorizerName, authorizer);
}
- return new AuthorizerMapper(authorizerMap);
+ return new AuthorizerMapper(authorizerMap, emitter);
}
}
diff --git
a/server/src/main/java/org/apache/druid/server/security/AuthConfig.java
b/server/src/main/java/org/apache/druid/server/security/AuthConfig.java
index 3a664594d89..092eb69528b 100644
--- a/server/src/main/java/org/apache/druid/server/security/AuthConfig.java
+++ b/server/src/main/java/org/apache/druid/server/security/AuthConfig.java
@@ -65,7 +65,7 @@ public class AuthConfig
public AuthConfig()
{
- this(null, null, null, false, false, null, null, false);
+ this(null, null, null, false, false, null, null, false, false);
}
@JsonProperty
@@ -102,6 +102,9 @@ public class AuthConfig
@JsonProperty
private final boolean enableInputSourceSecurity;
+ @JsonProperty
+ private final boolean emitAuthMetrics;
+
@JsonCreator
public AuthConfig(
@JsonProperty("authenticatorChain") List<String> authenticatorChain,
@@ -111,7 +114,8 @@ public class AuthConfig
@JsonProperty("authorizeQueryContextParams") boolean
authorizeQueryContextParams,
@JsonProperty("unsecuredContextKeys") Set<String> unsecuredContextKeys,
@JsonProperty("securedContextKeys") Set<String> securedContextKeys,
- @JsonProperty("enableInputSourceSecurity") boolean
enableInputSourceSecurity
+ @JsonProperty("enableInputSourceSecurity") boolean
enableInputSourceSecurity,
+ @JsonProperty("emitAuthMetrics") boolean emitAuthMetrics
)
{
this.authenticatorChain = authenticatorChain;
@@ -124,6 +128,7 @@ public class AuthConfig
: unsecuredContextKeys;
this.securedContextKeys = securedContextKeys;
this.enableInputSourceSecurity = enableInputSourceSecurity;
+ this.emitAuthMetrics = emitAuthMetrics;
}
public List<String> getAuthenticatorChain()
@@ -156,6 +161,11 @@ public class AuthConfig
return enableInputSourceSecurity;
}
+ public boolean isEmitAuthMetrics()
+ {
+ return emitAuthMetrics;
+ }
+
/**
* Filter the user-supplied context keys based on the context key security
* rules. If context key security is disabled, then allow all keys. Else,
@@ -198,6 +208,7 @@ public class AuthConfig
AuthConfig that = (AuthConfig) o;
return allowUnauthenticatedHttpOptions ==
that.allowUnauthenticatedHttpOptions
&& authorizeQueryContextParams == that.authorizeQueryContextParams
+ && emitAuthMetrics == that.emitAuthMetrics
&& Objects.equals(authenticatorChain, that.authenticatorChain)
&& Objects.equals(authorizers, that.authorizers)
&& Objects.equals(unsecuredPaths, that.unsecuredPaths)
@@ -217,7 +228,8 @@ public class AuthConfig
authorizeQueryContextParams,
unsecuredContextKeys,
securedContextKeys,
- enableInputSourceSecurity
+ enableInputSourceSecurity,
+ emitAuthMetrics
);
}
@@ -233,6 +245,7 @@ public class AuthConfig
", unsecuredContextKeys=" + unsecuredContextKeys +
", securedContextKeys=" + securedContextKeys +
", enableInputSourceSecurity=" + enableInputSourceSecurity +
+ ", emitAuthMetrics=" + emitAuthMetrics +
'}';
}
@@ -254,6 +267,7 @@ public class AuthConfig
private Set<String> unsecuredContextKeys;
private Set<String> securedContextKeys;
private boolean enableInputSourceSecurity;
+ private boolean emitAuthMetrics;
public Builder setAuthenticatorChain(List<String> authenticatorChain)
{
@@ -303,6 +317,12 @@ public class AuthConfig
return this;
}
+ public Builder setEmitAuthMetrics(boolean emitAuthMetrics)
+ {
+ this.emitAuthMetrics = emitAuthMetrics;
+ return this;
+ }
+
public AuthConfig build()
{
return new AuthConfig(
@@ -313,7 +333,8 @@ public class AuthConfig
authorizeQueryContextParams,
unsecuredContextKeys,
securedContextKeys,
- enableInputSourceSecurity
+ enableInputSourceSecurity,
+ emitAuthMetrics
);
}
}
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 9bcff9bdc1d..687685f45f1 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
@@ -28,6 +28,9 @@ import org.apache.druid.audit.AuditManager;
import org.apache.druid.audit.RequestInfo;
import org.apache.druid.error.DruidException;
import org.apache.druid.java.util.common.ISE;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.emitter.service.ServiceEmitter;
+import org.apache.druid.java.util.emitter.service.ServiceMetricEvent;
import org.apache.druid.query.policy.Policy;
import javax.annotation.Nullable;
@@ -50,6 +53,9 @@ public class AuthorizationUtils
ResourceType.DATASOURCE
);
+ private static final String METRIC_FORBIDDEN = "auth/forbidden";
+ private static final String METRIC_EXCEPTION = "auth/exception";
+
/**
* Performs authorization check on a single resource-action based on the
authentication fields from the request.
* <p>
@@ -90,6 +96,18 @@ public class AuthorizationUtils
ResourceAction resourceAction = createDatasourceResourceAction(datasource,
req);
AuthorizationResult authResult = authorizeResourceAction(req,
resourceAction, authorizerMapper);
if (!authResult.allowAccessWithNoRestriction()) {
+ if (authResult.allowBasicAccess()) {
+ // Basic access was granted, but access was restricted by a policy.
+ // This is checked to avoid double emitting the forbidden metric if
the basic access was denied,
+ // since the authorizeResourceAction method already emits the metric
in that case.
+ emitAuthMetric(
+ authorizerMapper.getServiceEmitter(),
+ authenticationResultFromRequest(req),
+ resourceAction,
+ METRIC_FORBIDDEN,
+ authResult.getErrorMessage()
+ );
+ }
throw new ForbiddenException(authResult.getErrorMessage());
}
}
@@ -184,7 +202,10 @@ public class AuthorizationUtils
{
final Authorizer authorizer =
authorizerMapper.getAuthorizer(authenticationResult.getAuthorizerName());
if (authorizer == null) {
- throw new ISE("No authorizer found with name: [%s].",
authenticationResult.getAuthorizerName());
+ final String msg =
+ StringUtils.format("No authorizer found with name: [%s].",
authenticationResult.getAuthorizerName());
+ emitAuthMetric(authorizerMapper.getServiceEmitter(),
authenticationResult, null, METRIC_EXCEPTION, null);
+ throw new ISE(msg);
}
// this method returns on first failure, so only successful Access results
are kept in the cache
@@ -201,6 +222,7 @@ public class AuthorizationUtils
resourceAction.getAction()
);
if (!access.isAllowed()) {
+ emitAuthMetric(authorizerMapper.getServiceEmitter(),
authenticationResult, resourceAction, METRIC_FORBIDDEN, access.getMessage());
return AuthorizationResult.deny(access.getMessage());
} else {
resultCache.add(resourceAction);
@@ -212,18 +234,22 @@ public class AuthorizationUtils
if (prevPolicy != null) {
// Shouldn't have two policies for the same resource name, because
only tables have policies and
// each table should only be processed one time. If it does
happen, throw a defensive exception.
- throw DruidException.defensive(
+ final String twoPoliciesMsg = StringUtils.format(
"Cannot have two policies on the same resourceName[%s]:
policyA[%s], policyB[%s]",
resourceName,
prevPolicy,
access.getPolicy()
);
+ emitAuthMetric(authorizerMapper.getServiceEmitter(),
authenticationResult, resourceAction, METRIC_EXCEPTION, twoPoliciesMsg);
+ throw DruidException.defensive(twoPoliciesMsg);
}
} else if (access.getPolicy().isPresent()) {
- throw DruidException.defensive(
+ final String policyPresentMsg = StringUtils.format(
"Policy should only present when reading a table, but was
present for a different kind of resource action [%s]",
resourceAction
);
+ emitAuthMetric(authorizerMapper.getServiceEmitter(),
authenticationResult, resourceAction, METRIC_EXCEPTION, policyPresentMsg);
+ throw DruidException.defensive(policyPresentMsg);
} else {
// Not a read table action, access doesn't have a filter, do nothing.
}
@@ -265,12 +291,15 @@ public class AuthorizationUtils
return AuthorizationResult.ALLOW_NO_RESTRICTION;
}
+ final AuthenticationResult authenticationResult =
authenticationResultFromRequest(request);
if (request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED) != null) {
- throw new ISE("Request already had authorization check.");
+ final String msg = "Request already had authorization check.";
+ emitAuthMetric(authorizerMapper.getServiceEmitter(),
authenticationResult, null, METRIC_EXCEPTION, msg);
+ throw new ISE(msg);
}
AuthorizationResult authResult = authorizeAllResourceActions(
- authenticationResultFromRequest(request),
+ authenticationResult,
resourceActions,
authorizerMapper
);
@@ -326,12 +355,13 @@ public class AuthorizationUtils
return resources;
}
+ final AuthenticationResult authenticationResult =
authenticationResultFromRequest(request);
if (request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED) != null) {
- throw new ISE("Request already had authorization check.");
+ final String msg = "Request already had authorization check.";
+ emitAuthMetric(authorizerMapper.getServiceEmitter(),
authenticationResult, null, METRIC_EXCEPTION, msg);
+ throw new ISE(msg);
}
- final AuthenticationResult authenticationResult =
authenticationResultFromRequest(request);
-
final Iterable<ResType> filteredResources = filterAuthorizedResources(
authenticationResult,
resources,
@@ -370,7 +400,9 @@ public class AuthorizationUtils
{
final Authorizer authorizer =
authorizerMapper.getAuthorizer(authenticationResult.getAuthorizerName());
if (authorizer == null) {
- throw new ISE("No authorizer found with name: [%s].",
authenticationResult.getAuthorizerName());
+ final String msg = StringUtils.format("No authorizer found with name:
[%s].", authenticationResult.getAuthorizerName());
+ emitAuthMetric(authorizerMapper.getServiceEmitter(),
authenticationResult, null, METRIC_EXCEPTION, msg);
+ throw new ISE(msg);
}
final Map<ResourceAction, Access> resultCache = new HashMap<>();
@@ -433,11 +465,13 @@ public class AuthorizationUtils
return unfilteredResources;
}
+ final AuthenticationResult authenticationResult =
AuthorizationUtils.authenticationResultFromRequest(request);
if (request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED) != null) {
- throw new ISE("Request already had authorization check.");
+ final String msg = "Request already had authorization check.";
+ emitAuthMetric(authorizerMapper.getServiceEmitter(),
authenticationResult, null, METRIC_EXCEPTION, msg);
+ throw new ISE(msg);
}
- final AuthenticationResult authenticationResult =
AuthorizationUtils.authenticationResultFromRequest(request);
Map<KeyType, List<ResType>> filteredResources = new HashMap<>();
for (Map.Entry<KeyType, List<ResType>> entry :
unfilteredResources.entrySet()) {
@@ -560,4 +594,27 @@ public class AuthorizationUtils
Action.WRITE
);
+ private static void emitAuthMetric(
+ @Nullable ServiceEmitter emitter,
+ AuthenticationResult authenticationResult,
+ @Nullable ResourceAction resourceAction,
+ String metricName,
+ @Nullable String errorMessage
+ )
+ {
+ if (emitter == null) {
+ return;
+ }
+ final ServiceMetricEvent.Builder builder =
ServiceMetricEvent.builder().setDimension("identity",
authenticationResult.getIdentity()).setDimension("authorizerName",
authenticationResult.getAuthorizerName());
+
+ if (resourceAction != null) {
+ builder.setDimension("resourceName",
resourceAction.getResource().getName())
+ .setDimension("resourceType",
resourceAction.getResource().getType())
+ .setDimension("action", resourceAction.getAction().toString());
+ }
+ if (errorMessage != null) {
+ builder.setDimension("errorMessage", errorMessage);
+ }
+ emitter.emit(builder.setMetric(metricName, 1));
+ }
}
diff --git
a/server/src/main/java/org/apache/druid/server/security/AuthorizerMapper.java
b/server/src/main/java/org/apache/druid/server/security/AuthorizerMapper.java
index 5ab2d1d2e10..f98e7ce24c7 100644
---
a/server/src/main/java/org/apache/druid/server/security/AuthorizerMapper.java
+++
b/server/src/main/java/org/apache/druid/server/security/AuthorizerMapper.java
@@ -19,17 +19,27 @@
package org.apache.druid.server.security;
+import org.apache.druid.java.util.emitter.service.ServiceEmitter;
+
+import javax.annotation.Nullable;
import java.util.Map;
public class AuthorizerMapper
{
private Map<String, Authorizer> authorizerMap;
- public AuthorizerMapper(
- Map<String, Authorizer> authorizerMap
- )
+ @Nullable
+ private final ServiceEmitter serviceEmitter;
+
+ public AuthorizerMapper(Map<String, Authorizer> authorizerMap)
+ {
+ this(authorizerMap, null);
+ }
+
+ public AuthorizerMapper(Map<String, Authorizer> authorizerMap, @Nullable
ServiceEmitter serviceEmitter)
{
this.authorizerMap = authorizerMap;
+ this.serviceEmitter = serviceEmitter;
}
public Authorizer getAuthorizer(String name)
@@ -41,4 +51,10 @@ public class AuthorizerMapper
{
return authorizerMap;
}
+
+ @Nullable
+ public ServiceEmitter getServiceEmitter()
+ {
+ return serviceEmitter;
+ }
}
diff --git
a/server/src/test/java/org/apache/druid/server/http/DataSourcesResourceTest.java
b/server/src/test/java/org/apache/druid/server/http/DataSourcesResourceTest.java
index 8b117834d36..4f57d6cb31f 100644
---
a/server/src/test/java/org/apache/druid/server/http/DataSourcesResourceTest.java
+++
b/server/src/test/java/org/apache/druid/server/http/DataSourcesResourceTest.java
@@ -192,10 +192,10 @@ public class DataSourcesResourceTest
ImmutableList.of(server)
).once();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).once();
-
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("druid", "druid", null, null)
).atLeastOnce();
+
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().times(1);
@@ -207,10 +207,10 @@ public class DataSourcesResourceTest
ImmutableList.of(server)
).once();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).once();
-
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("druid", "druid", null, null)
).once();
+
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().times(1);
@@ -243,10 +243,10 @@ public class DataSourcesResourceTest
).once();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).once();
-
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
authenticationResult
).once();
+
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().times(1);
@@ -258,10 +258,10 @@ public class DataSourcesResourceTest
).once();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).once();
-
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
authenticationResult
).once();
+
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().times(1);
@@ -319,10 +319,10 @@ public class DataSourcesResourceTest
EasyMock.expect(server.getDataSource(TestDataSource.KOALA)).andReturn(listDataSources.get(1)).atLeastOnce();
EasyMock.expect(inventoryView.getInventory()).andReturn(ImmutableList.of(server)).atLeastOnce();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).once();
-
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("druid", "druid", null, null)
).atLeastOnce();
+
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().times(1);
diff --git
a/server/src/test/java/org/apache/druid/server/http/IntervalsResourceTest.java
b/server/src/test/java/org/apache/druid/server/http/IntervalsResourceTest.java
index a506c27f273..19b3b6b84d8 100644
---
a/server/src/test/java/org/apache/druid/server/http/IntervalsResourceTest.java
+++
b/server/src/test/java/org/apache/druid/server/http/IntervalsResourceTest.java
@@ -109,10 +109,10 @@ public class IntervalsResourceTest
ImmutableList.of(server)
).atLeastOnce();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).once();
-
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("druid", "druid", null, null)
).once();
+
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().times(1);
EasyMock.replay(inventoryView, request);
@@ -147,10 +147,10 @@ public class IntervalsResourceTest
ImmutableList.of(server)
).atLeastOnce();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).once();
-
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("druid", "druid", null, null)
).once();
+
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().times(1);
EasyMock.replay(inventoryView, request);
@@ -179,10 +179,10 @@ public class IntervalsResourceTest
ImmutableList.of(server)
).atLeastOnce();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).once();
-
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("druid", "druid", null, null)
).once();
+
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().times(1);
EasyMock.replay(inventoryView, request);
@@ -213,10 +213,10 @@ public class IntervalsResourceTest
ImmutableList.of(server)
).atLeastOnce();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).once();
-
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)).andReturn(
new AuthenticationResult("druid", "druid", null, null)
).once();
+
EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once();
request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
EasyMock.expectLastCall().times(1);
EasyMock.replay(inventoryView, request);
diff --git
a/server/src/test/java/org/apache/druid/server/http/security/ResourceFilterTestHelper.java
b/server/src/test/java/org/apache/druid/server/http/security/ResourceFilterTestHelper.java
index 82b15bd453a..6d13b5c4bc4 100644
---
a/server/src/test/java/org/apache/druid/server/http/security/ResourceFilterTestHelper.java
+++
b/server/src/test/java/org/apache/druid/server/http/security/ResourceFilterTestHelper.java
@@ -112,11 +112,11 @@ public class ResourceFilterTestHelper
).anyTimes();
EasyMock.expect(request.getMethod()).andReturn(requestMethod).anyTimes();
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).anyTimes();
-
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).anyTimes();
AuthenticationResult authenticationResult = new
AuthenticationResult("druid", "druid", null, null);
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT))
.andReturn(authenticationResult)
.atLeastOnce();
+
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).anyTimes();
req.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, authCheckResult);
EasyMock.expectLastCall().anyTimes();
EasyMock.expect(authorizerMapper.getAuthorizer(
@@ -132,6 +132,7 @@ public class ResourceFilterTestHelper
}
).atLeastOnce();
+
EasyMock.expect(authorizerMapper.getServiceEmitter()).andReturn(null).anyTimes();
}
public static Collection<Object[]> getRequestPathsWithAuthorizer(final
AnnotatedElement classOrMethod)
diff --git
a/server/src/test/java/org/apache/druid/server/initialization/AuthorizerMapperModuleTest.java
b/server/src/test/java/org/apache/druid/server/initialization/AuthorizerMapperModuleTest.java
index 5bc2231063d..a350b6a6ccc 100644
---
a/server/src/test/java/org/apache/druid/server/initialization/AuthorizerMapperModuleTest.java
+++
b/server/src/test/java/org/apache/druid/server/initialization/AuthorizerMapperModuleTest.java
@@ -22,40 +22,76 @@ package org.apache.druid.server.initialization;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Scopes;
+import org.apache.druid.guice.JsonConfigurator;
import org.apache.druid.guice.LazySingleton;
+import org.apache.druid.jackson.JacksonModule;
+import org.apache.druid.java.util.emitter.service.ServiceEmitter;
+import org.apache.druid.java.util.metrics.StubServiceEmitter;
+import org.apache.druid.server.metrics.NoopServiceEmitter;
+import org.apache.druid.server.security.AuthConfig;
import org.apache.druid.server.security.AuthValidator;
+import org.apache.druid.server.security.AuthorizerMapper;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import javax.validation.Validation;
import javax.validation.Validator;
+import java.util.Properties;
public class AuthorizerMapperModuleTest
{
- private AuthorizerMapperModule target;
private Injector injector;
@Before
public void setUp()
{
- target = new AuthorizerMapperModule();
injector = Guice.createInjector(
- (binder) -> {
+ new JacksonModule(),
+ binder -> {
binder.bind(Validator.class).toInstance(Validation.buildDefaultValidatorFactory().getValidator());
+ binder.bind(JsonConfigurator.class).in(LazySingleton.class);
binder.bindScope(LazySingleton.class, Scopes.SINGLETON);
+ binder.bind(Properties.class).toInstance(new Properties());
+ binder.bind(AuthConfig.class).toInstance(new AuthConfig());
+ binder.bind(ServiceEmitter.class).toInstance(new
NoopServiceEmitter());
},
- target
+ new AuthorizerMapperModule()
);
}
@Test
public void testAuthorizerNameValidatorIsInjectedAsSingleton()
{
- AuthValidator authValidator =
- injector.getInstance(AuthValidator.class);
- AuthValidator other =
- injector.getInstance(AuthValidator.class);
+ AuthValidator authValidator = injector.getInstance(AuthValidator.class);
+ AuthValidator other = injector.getInstance(AuthValidator.class);
Assert.assertSame(authValidator, other);
}
+
+ @Test
+ public void testEmitAuthMetrics_defaultsFalse_emitterNullInMapper()
+ {
+ AuthorizerMapper mapper = injector.getInstance(AuthorizerMapper.class);
+ Assert.assertNull(mapper.getServiceEmitter());
+ }
+
+ @Test
+ public void testEmitAuthMetrics_true_emitterBoundToMapper()
+ {
+ StubServiceEmitter emitter = StubServiceEmitter.createStarted();
+ Injector inj = Guice.createInjector(
+ new JacksonModule(),
+ binder -> {
+
binder.bind(Validator.class).toInstance(Validation.buildDefaultValidatorFactory().getValidator());
+ binder.bind(JsonConfigurator.class).in(LazySingleton.class);
+ binder.bindScope(LazySingleton.class, Scopes.SINGLETON);
+ binder.bind(Properties.class).toInstance(new Properties());
+
binder.bind(AuthConfig.class).toInstance(AuthConfig.newBuilder().setEmitAuthMetrics(true).build());
+ binder.bind(ServiceEmitter.class).toInstance(emitter);
+ },
+ new AuthorizerMapperModule()
+ );
+ AuthorizerMapper mapper = inj.getInstance(AuthorizerMapper.class);
+ Assert.assertSame(emitter, mapper.getServiceEmitter());
+ }
}
diff --git
a/server/src/test/java/org/apache/druid/server/security/AuthConfigTest.java
b/server/src/test/java/org/apache/druid/server/security/AuthConfigTest.java
index 3987c4d9cda..fd6cb8cb18e 100644
--- a/server/src/test/java/org/apache/druid/server/security/AuthConfigTest.java
+++ b/server/src/test/java/org/apache/druid/server/security/AuthConfigTest.java
@@ -27,6 +27,7 @@ import org.junit.Test;
import java.util.Set;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class AuthConfigTest
@@ -37,6 +38,19 @@ public class AuthConfigTest
EqualsVerifier.configure().usingGetClass().forClass(AuthConfig.class).verify();
}
+ @Test
+ public void testEmitAuthMetricsDefaultsFalse()
+ {
+ assertFalse(new AuthConfig().isEmitAuthMetrics());
+ assertFalse(AuthConfig.newBuilder().build().isEmitAuthMetrics());
+ }
+
+ @Test
+ public void testEmitAuthMetricsCanBeEnabled()
+ {
+
assertTrue(AuthConfig.newBuilder().setEmitAuthMetrics(true).build().isEmitAuthMetrics());
+ }
+
@Test
public void testContextSecurity()
{
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 50e5fce688e..0d69297da39 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
@@ -21,6 +21,8 @@ package org.apache.druid.server.security;
import com.google.common.base.Function;
import org.apache.druid.error.DruidException;
+import org.apache.druid.java.util.emitter.service.ServiceMetricEvent;
+import org.apache.druid.java.util.metrics.StubServiceEmitter;
import org.apache.druid.query.filter.EqualityFilter;
import org.apache.druid.query.policy.NoRestrictionPolicy;
import org.apache.druid.query.policy.RowFilterPolicy;
@@ -211,6 +213,215 @@ public class AuthorizationUtilsTest
Assert.assertEquals(Map.of("test", Optional.of(policy)),
result.getPolicyMap());
}
+ @Test
+ public void testAuthorizeAllResourceActions_emitsOnDeny()
+ {
+ final String authorizerName = "testAuthorizer";
+ final AuthenticationResult authenticationResult = new AuthenticationResult(
+ "someUser",
+ authorizerName,
+ "authenticator",
+ null
+ );
+ final Authorizer denyAll = (authResult, resource, action) -> Access.DENIED;
+
+ final Map<String, Authorizer> authorizerMap = new HashMap<>();
+ authorizerMap.put(authorizerName, denyAll);
+
+ final StubServiceEmitter emitter = StubServiceEmitter.createStarted();
+ final AuthorizerMapper mapper = new AuthorizerMapper(authorizerMap,
emitter);
+
+ final ResourceAction resourceAction = new ResourceAction(
+ new Resource("myDatasource", ResourceType.DATASOURCE),
+ Action.READ
+ );
+
+ final AuthorizationResult result =
AuthorizationUtils.authorizeAllResourceActions(
+ authenticationResult,
+ Collections.singletonList(resourceAction),
+ mapper
+ );
+
+ Assert.assertFalse(result.allowBasicAccess());
+
+ final List<ServiceMetricEvent> events =
emitter.getMetricEvents("auth/forbidden");
+ Assert.assertEquals(1, events.size());
+ final Map<String, Object> dims = events.get(0).getUserDims();
+ Assert.assertEquals("someUser", dims.get("identity"));
+ Assert.assertEquals(authorizerName, dims.get("authorizerName"));
+ Assert.assertEquals("myDatasource", dims.get("resourceName"));
+ Assert.assertEquals(ResourceType.DATASOURCE, dims.get("resourceType"));
+ Assert.assertEquals(Action.READ.toString(), dims.get("action"));
+ }
+
+ @Test
+ public void testAuthorizeAllResourceActions_noEmitOnAllow()
+ {
+ final String authorizerName = "testAuthorizer";
+ final AuthenticationResult authenticationResult = new AuthenticationResult(
+ "someUser",
+ authorizerName,
+ "authenticator",
+ null
+ );
+ final Authorizer allowAll = (authResult, resource, action) -> Access.OK;
+
+ final Map<String, Authorizer> authorizerMap = new HashMap<>();
+ authorizerMap.put(authorizerName, allowAll);
+
+ final StubServiceEmitter emitter = StubServiceEmitter.createStarted();
+ final AuthorizerMapper mapper = new AuthorizerMapper(authorizerMap,
emitter);
+
+ final AuthorizationResult result =
AuthorizationUtils.authorizeAllResourceActions(
+ authenticationResult,
+ Collections.singletonList(
+ new ResourceAction(new Resource("myDatasource",
ResourceType.DATASOURCE), Action.READ)
+ ),
+ mapper
+ );
+
+ Assert.assertTrue(result.allowBasicAccess());
+ Assert.assertEquals(0, emitter.getMetricEventCount("auth/forbidden"));
+ }
+
+ @Test
+ public void testAuthorizeAllResourceActions_noEmitWhenEmitterNull()
+ {
+ final String authorizerName = "testAuthorizer";
+ final AuthenticationResult authenticationResult = new AuthenticationResult(
+ "someUser",
+ authorizerName,
+ "authenticator",
+ null
+ );
+ final Authorizer denyAll = (authResult, resource, action) -> Access.DENIED;
+
+ final Map<String, Authorizer> authorizerMap = new HashMap<>();
+ authorizerMap.put(authorizerName, denyAll);
+
+ // no emitter — default constructor
+ final AuthorizerMapper mapper = new AuthorizerMapper(authorizerMap);
+
+ final AuthorizationResult result =
AuthorizationUtils.authorizeAllResourceActions(
+ authenticationResult,
+ Collections.singletonList(
+ new ResourceAction(new Resource("myDatasource",
ResourceType.DATASOURCE), Action.READ)
+ ),
+ mapper
+ );
+
+ Assert.assertFalse(result.allowBasicAccess());
+ Assert.assertNull(mapper.getServiceEmitter());
+ }
+
+ @Test
+ public void
testAuthorizeAllResourceActions_emitsExceptionOnPolicyForNonReadAction()
+ {
+ final String authorizerName = "testAuthorizer";
+ final AuthenticationResult authResult = new
AuthenticationResult("someUser", authorizerName, "authenticator", null);
+ final Map<String, Authorizer> authorizerMap = new HashMap<>();
+ // Broken authorizer: returns a policy for a WRITE action (should never
happen)
+ authorizerMap.put(authorizerName, (ar, resource, action) ->
Access.allowWithRestriction(NoRestrictionPolicy.instance()));
+
+ final StubServiceEmitter emitter = StubServiceEmitter.createStarted();
+ final AuthorizerMapper mapper = new AuthorizerMapper(authorizerMap,
emitter);
+ final ResourceAction resourceAction =
+ new ResourceAction(new Resource("myDatasource",
ResourceType.DATASOURCE), Action.WRITE);
+
+ Assert.assertThrows(
+ DruidException.class,
+ () -> AuthorizationUtils.authorizeAllResourceActions(
+ authResult,
+ Collections.singletonList(resourceAction),
+ mapper
+ )
+ );
+
+ Assert.assertEquals(1, emitter.getMetricEventCount("auth/exception"));
+ Assert.assertEquals(0, emitter.getMetricEventCount("auth/forbidden"));
+ final Map<String, Object> dims =
emitter.getMetricEvents("auth/exception").get(0).getUserDims();
+ Assert.assertEquals("someUser", dims.get("identity"));
+ Assert.assertEquals(authorizerName, dims.get("authorizerName"));
+ Assert.assertEquals("myDatasource", dims.get("resourceName"));
+ Assert.assertEquals(Action.WRITE.toString(), dims.get("action"));
+ }
+
+ @Test
+ public void
testAuthorizeAllResourceActions_emitsExceptionOnUnexpectedAuthorizerBehavior()
+ {
+ final String authorizerName = "testAuthorizer";
+ final AuthenticationResult authResult = new
AuthenticationResult("someUser", authorizerName, "authenticator", null);
+ final Map<String, Authorizer> authorizerMap = new HashMap<>();
+ // Broken authorizer: returns a policy for a WRITE action (should never
happen)
+ authorizerMap.put(authorizerName, (ar, resource, action) ->
Access.allowWithRestriction(NoRestrictionPolicy.instance()));
+
+ final StubServiceEmitter emitter = StubServiceEmitter.createStarted();
+ final AuthorizerMapper mapper = new AuthorizerMapper(authorizerMap,
emitter);
+ final ResourceAction resourceAction =
+ new ResourceAction(new Resource("myDatasource",
ResourceType.DATASOURCE), Action.WRITE);
+
+ final DruidException e = Assert.assertThrows(
+ DruidException.class,
+ () -> AuthorizationUtils.authorizeAllResourceActions(
+ authResult,
+ Collections.singletonList(resourceAction),
+ mapper
+ )
+ );
+
+ final List<ServiceMetricEvent> events =
emitter.getMetricEvents("auth/exception");
+ Assert.assertEquals(1, events.size());
+ Assert.assertEquals(0, emitter.getMetricEventCount("auth/forbidden"));
+ final Map<String, Object> dims = events.get(0).getUserDims();
+ Assert.assertEquals("someUser", dims.get("identity"));
+ Assert.assertEquals(authorizerName, dims.get("authorizerName"));
+ Assert.assertEquals("myDatasource", dims.get("resourceName"));
+ Assert.assertEquals(Action.WRITE.toString(), dims.get("action"));
+ Assert.assertEquals(e.getMessage(), dims.get("errorMessage"));
+ }
+
+ @Test
+ public void
testVerifyUnrestrictedAccessToDatasource_emitsForbiddenOnPolicyDeny()
+ {
+ final String authorizerName = "testAuthorizer";
+ final AuthenticationResult authenticationResult = new AuthenticationResult(
+ "someUser",
+ authorizerName,
+ "authenticator",
+ null
+ );
+ // Authorizer grants basic access but attaches a row-filter policy —
triggering the policy-deny path.
+ final Authorizer policyAuthorizer = (ar, resource, action) ->
+ Access.allowWithRestriction(RowFilterPolicy.from(new
EqualityFilter("col", ColumnType.STRING, "val", null)));
+
+ final Map<String, Authorizer> authorizerMap = new HashMap<>();
+ authorizerMap.put(authorizerName, policyAuthorizer);
+
+ final StubServiceEmitter emitter = StubServiceEmitter.createStarted();
+ final AuthorizerMapper mapper = new AuthorizerMapper(authorizerMap,
emitter);
+
+ final MockHttpServletRequest request = new MockHttpServletRequest();
+ request.method = "GET";
+ request.attributes.put(AuthConfig.DRUID_AUTHENTICATION_RESULT,
authenticationResult);
+
+ Assert.assertThrows(
+ ForbiddenException.class,
+ () -> AuthorizationUtils.verifyUnrestrictedAccessToDatasource(request,
"myDatasource", mapper)
+ );
+
+ // The policy deny path in verifyUnrestrictedAccessToDatasource should
emit auth/forbidden.
+ // auth/forbidden is NOT emitted by authorizeAllResourceActions here
because basic access was allowed.
+ Assert.assertEquals(1, emitter.getMetricEventCount("auth/forbidden"));
+ Assert.assertEquals(0, emitter.getMetricEventCount("auth/exception"));
+
+ final Map<String, Object> dims =
emitter.getMetricEvents("auth/forbidden").get(0).getUserDims();
+ Assert.assertEquals("someUser", dims.get("identity"));
+ Assert.assertEquals(authorizerName, dims.get("authorizerName"));
+ Assert.assertEquals("myDatasource", dims.get("resourceName"));
+ Assert.assertEquals(ResourceType.DATASOURCE, dims.get("resourceType"));
+ Assert.assertEquals(Action.READ.toString(), dims.get("action"));
+ }
+
@Test
public void
testAuthorizeAllResourceActions_policyForNonReadDatasourceThrows()
{
diff --git a/sql/src/test/java/org/apache/druid/sql/SqlStatementTest.java
b/sql/src/test/java/org/apache/druid/sql/SqlStatementTest.java
index b7152fd9d33..c4ecd97f507 100644
--- a/sql/src/test/java/org/apache/druid/sql/SqlStatementTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/SqlStatementTest.java
@@ -161,12 +161,12 @@ public class SqlStatementTest
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH))
.andReturn(null)
.anyTimes();
- EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED))
- .andReturn(null)
- .anyTimes();
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT))
.andReturn(CalciteTests.REGULAR_USER_AUTH_RESULT)
.anyTimes();
+ EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED))
+ .andReturn(null)
+ .anyTimes();
req.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, ok);
EasyMock.expectLastCall().anyTimes();
EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT))
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]