This is an automated email from the ASF dual-hosted git repository.

aho 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 c8a1267d5aa feat: Add an authorizer for read only operations (#19243)
c8a1267d5aa is described below

commit c8a1267d5aa5553ecec10a1ce6d46dc078541f63
Author: aho135 <[email protected]>
AuthorDate: Thu Apr 2 15:39:36 2026 -0700

    feat: Add an authorizer for read only operations (#19243)
    
    * Add an authorizer for read only operations
    
    * Checkstyle fixes
    
    * Remove use of deprecated methods
    
    * Register ReadOnlyAuthorizer
    
    * Migrate ReadOnlyAuthorizer from druid-basic-security to druid-server
    
    * Document ReadOnly authorizer
    
    * Spell check
    
    * Hook up ReadOnlyAuthorizer with Policy
    
    * Move ReadOnlyAuthorizer back to druid-basic-security
    
    * Example configuration for basic security with readonly
    
    * Change to readOnly for consistency
    
    * Fix configuration example
    
    * Update druid-basic-security.md
---
 .../extensions-core/druid-basic-security.md        | 43 ++++++++++-
 docs/operations/auth.md                            |  2 +-
 .../security/basic/BasicSecurityDruidModule.java   |  2 +
 .../basic/authorization/ReadOnlyAuthorizer.java    | 76 ++++++++++++++++++
 .../authorization/ReadOnlyAuthorizerTest.java      | 89 ++++++++++++++++++++++
 website/.spelling                                  |  2 +
 6 files changed, 212 insertions(+), 2 deletions(-)

diff --git a/docs/development/extensions-core/druid-basic-security.md 
b/docs/development/extensions-core/druid-basic-security.md
index d2882ccc5cf..80df2d18add 100644
--- a/docs/development/extensions-core/druid-basic-security.md
+++ b/docs/development/extensions-core/druid-basic-security.md
@@ -402,6 +402,47 @@ Array of LDAP group filters used to filter out the allowed 
set of groups returne
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;**Required**: No<br />
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;**Default**: null
 
+#### ReadOnly Authorizer
+
+The ReadOnly authorizer allows all READ operations and denies all other 
operations (WRITE, DELETE, etc.).
+
+Example configuration:
+
+```
+druid.auth.authenticatorChain=["basic","anonymous"]
+
+# Basic authenticator for internal user
+druid.auth.authenticator.basic.type=basic
+druid.auth.authenticator.basic.initialAdminPassword=password
+druid.auth.authenticator.basic.initialInternalClientPassword=password
+druid.auth.authenticator.basic.credentialsValidator.type=metadata
+druid.auth.authenticator.basic.authorizerName=BasicAuthorizer
+
+# Anonymous authenticator for external users
+druid.auth.authenticator.anonymous.type=anonymous
+druid.auth.authenticator.anonymous.identity=defaultUser
+druid.auth.authenticator.anonymous.authorizerName=ReadOnlyAuthorizer
+
+# Escalator with Basic auth for internal communications
+druid.escalator.type=basic
+druid.escalator.internalClientUsername=druid_system
+druid.escalator.internalClientPassword=password
+druid.escalator.authorizerName=BasicAuthorizer
+
+# Both authorizers
+druid.auth.authorizers=["BasicAuthorizer","ReadOnlyAuthorizer"]
+
+# BasicAuthorizer configuration
+druid.auth.authorizer.BasicAuthorizer.type=basic
+
+# ReadOnlyAuthorizer configuration
+druid.auth.authorizer.ReadOnlyAuthorizer.type=readOnly
+```
+
+With this configuration:
+- Internal Druid communications use Basic authentication → AllowAll authorizer 
→ full access
+- External users with no authentication → Anonymous authenticator → ReadOnly 
authorizer → read-only access
+
 #### Properties for LDAPS
 
 Use the following properties to configure Druid authentication with LDAP over 
TLS (LDAPS). See [Configure LDAP authentication](../../operations/auth-ldap.md) 
for more information.
@@ -732,4 +773,4 @@ Please see [Defining 
permissions](../../operations/security-user-auth.md#definin
 
 ##### Cache Load Status
 `GET(/druid-ext/basic-security/authorization/loadStatus)`<br />
-Return the current load status of the local caches of the authorization Druid 
metadata store.
\ No newline at end of file
+Return the current load status of the local caches of the authorization Druid 
metadata store.
diff --git a/docs/operations/auth.md b/docs/operations/auth.md
index 95b79ef5fa5..893b9c1e3dc 100644
--- a/docs/operations/auth.md
+++ b/docs/operations/auth.md
@@ -135,7 +135,7 @@ druid.auth.authorizers=["basic"]
 
 Only a single Authorizer will authorize any given request.
 
-Druid includes one built in authorizer:
+Druid includes one built-in authorizer:
 
 ### AllowAll authorizer
 
diff --git 
a/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/BasicSecurityDruidModule.java
 
b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/BasicSecurityDruidModule.java
index c5dafae62e8..bcc16c6fc34 100644
--- 
a/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/BasicSecurityDruidModule.java
+++ 
b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/BasicSecurityDruidModule.java
@@ -49,6 +49,7 @@ import 
org.apache.druid.security.basic.authentication.endpoint.BasicAuthenticato
 import 
org.apache.druid.security.basic.authentication.endpoint.CoordinatorBasicAuthenticatorResourceHandler;
 import 
org.apache.druid.security.basic.authentication.endpoint.DefaultBasicAuthenticatorResourceHandler;
 import org.apache.druid.security.basic.authorization.BasicRoleBasedAuthorizer;
+import org.apache.druid.security.basic.authorization.ReadOnlyAuthorizer;
 import 
org.apache.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheManager;
 import 
org.apache.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheNotifier;
 import 
org.apache.druid.security.basic.authorization.db.cache.CoordinatorBasicAuthorizerCacheNotifier;
@@ -216,6 +217,7 @@ public class BasicSecurityDruidModule implements DruidModule
             BasicHTTPAuthenticator.class,
             BasicHTTPEscalator.class,
             BasicRoleBasedAuthorizer.class,
+            ReadOnlyAuthorizer.class,
             NettyHttpClient.class
         )
     );
diff --git 
a/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/ReadOnlyAuthorizer.java
 
b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/ReadOnlyAuthorizer.java
new file mode 100644
index 00000000000..bc00c35b3c0
--- /dev/null
+++ 
b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/ReadOnlyAuthorizer.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.security.basic.authorization;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.apache.druid.java.util.common.IAE;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.query.policy.Policy;
+import org.apache.druid.server.security.Access;
+import org.apache.druid.server.security.Action;
+import org.apache.druid.server.security.AuthenticationResult;
+import org.apache.druid.server.security.AuthorizationUtils;
+import org.apache.druid.server.security.Authorizer;
+import org.apache.druid.server.security.Resource;
+
+import javax.annotation.Nullable;
+
+/**
+ * An authorizer that allows all READ operations and denies all other 
operations (WRITE, DELETE, etc.).
+ */
+@JsonTypeName("readOnly")
+public class ReadOnlyAuthorizer implements Authorizer
+{
+  private static final Logger LOG = new Logger(ReadOnlyAuthorizer.class);
+
+  @Nullable
+  private final Policy policy;
+
+  @JsonCreator
+  public ReadOnlyAuthorizer(
+      @JsonProperty("policy") @Nullable Policy policy
+  )
+  {
+    this.policy = policy;
+  }
+
+  @Override
+  public Access authorize(AuthenticationResult authenticationResult, Resource 
resource, Action action)
+  {
+    if (authenticationResult == null) {
+      throw new IAE("authenticationResult is null where it should never be.");
+    }
+    if (action == Action.READ) {
+      if (shouldApplyPolicy(resource, action)) {
+        return Access.allowWithRestriction(policy);
+      }
+      return Access.OK;
+    }
+    LOG.info("Authorization failed for user=%s on action=%s, %s", 
authenticationResult.getIdentity(), action, resource);
+    return Access.deny(null);
+  }
+
+  private boolean shouldApplyPolicy(Resource resource, Action action)
+  {
+    return policy != null && AuthorizationUtils.shouldApplyPolicy(resource, 
action);
+  }
+}
diff --git 
a/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/ReadOnlyAuthorizerTest.java
 
b/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/ReadOnlyAuthorizerTest.java
new file mode 100644
index 00000000000..b8bd268314e
--- /dev/null
+++ 
b/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/ReadOnlyAuthorizerTest.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.security.authorization;
+
+import org.apache.druid.query.filter.NullFilter;
+import org.apache.druid.query.policy.RowFilterPolicy;
+import org.apache.druid.security.basic.authorization.ReadOnlyAuthorizer;
+import org.apache.druid.server.security.Access;
+import org.apache.druid.server.security.Action;
+import org.apache.druid.server.security.AuthenticationResult;
+import org.apache.druid.server.security.Resource;
+import org.apache.druid.server.security.ResourceType;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ReadOnlyAuthorizerTest
+{
+
+  @Test
+  public void testAuth()
+  {
+    ReadOnlyAuthorizer authorizer = new ReadOnlyAuthorizer(null);
+    AuthenticationResult authenticationResult = new 
AuthenticationResult("anonymous", "anonymous", null, null);
+    Access access = authorizer.authorize(
+            authenticationResult,
+            new Resource("testResource", ResourceType.DATASOURCE),
+            Action.WRITE
+    );
+    Assert.assertFalse(access.isAllowed());
+    access = authorizer.authorize(
+            authenticationResult,
+            new Resource("testResource", ResourceType.DATASOURCE),
+            Action.READ
+    );
+    Assert.assertTrue(access.isAllowed());
+  }
+
+  @Test
+  public void testAuthWithPolicy()
+  {
+    RowFilterPolicy policy = RowFilterPolicy.from(new NullFilter("column", 
null));
+    ReadOnlyAuthorizer authorizerWithPolicy = new ReadOnlyAuthorizer(policy);
+    AuthenticationResult authenticationResult = new 
AuthenticationResult("anonymous", "anonymous", null, null);
+
+    // READ on DATASOURCE should return policy restriction
+    Access access = authorizerWithPolicy.authorize(
+            authenticationResult,
+            new Resource("testResource", ResourceType.DATASOURCE),
+            Action.READ
+    );
+    Assert.assertTrue(access.isAllowed());
+    Assert.assertTrue(access.getPolicy().isPresent());
+    Assert.assertEquals(policy, access.getPolicy().get());
+
+    // WRITE should still be denied
+    access = authorizerWithPolicy.authorize(
+            authenticationResult,
+            new Resource("testResource", ResourceType.DATASOURCE),
+            Action.WRITE
+    );
+    Assert.assertFalse(access.isAllowed());
+
+    // READ on non-DATASOURCE should not have policy
+    access = authorizerWithPolicy.authorize(
+            authenticationResult,
+            new Resource("testResource", ResourceType.STATE),
+            Action.READ
+    );
+    Assert.assertTrue(access.isAllowed());
+    Assert.assertFalse(access.getPolicy().isPresent());
+  }
+}
diff --git a/website/.spelling b/website/.spelling
index a5b1e7ec5fa..99e767c4891 100644
--- a/website/.spelling
+++ b/website/.spelling
@@ -874,6 +874,7 @@ LeaderLatch
 3.4.x
 3.5.x.
 AllowAll
+ReadOnly
 AuthenticationResult
 AuthorizationLoadingLookupTest
 booleans
@@ -884,6 +885,7 @@ HttpClient
 JsonConfigurator
 KIP-297
 allowAll
+readonly
 authenticatorChain
 defaultUser
 inputSegmentSizeBytes


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to