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]

Reply via email to