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

rzo1 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/storm.git


The following commit(s) were added to refs/heads/master by this push:
     new 2c71e6527 Apply the impersonation authorizer to DRPC requests (#9183)
2c71e6527 is described below

commit 2c71e6527548cee1bcce1f104d493118dfc9eb54
Author: Richard Zowalla <[email protected]>
AuthorDate: Fri Oct 23 19:43:01 2026 +0200

    Apply the impersonation authorizer to DRPC requests (#9183)
    
    * Apply the impersonation authorizer to DRPC requests
    
    DRPC now checks nimbus.impersonation.authorizer for requests made on
    behalf of another user, like Nimbus and the UI already do. The HTTP
    request context now carries the remote address so host lists in
    nimbus.impersonation.acl apply there too. Behaviour without an
    impersonation authorizer is unchanged.
    
    * Handle doAsUser without an authenticated user in DRPC
    
    Make SingleUserPrincipal equality null-safe so such requests are treated
    as impersonation, and deny them in ImpersonationAuthorizer and the UI.
    Check host addresses before names, reduce per-request INFO logging, name
    the DRPC function in denials and document reverse proxy behaviour.
    
    * Deny doAsUser without an authenticated user in DRPC
    
    DRPC now rejects a request that sets doAsUser without an authenticated user,
    whether or not an impersonation authorizer is configured. Update tests and 
docs.
    
    * Parse the remote address as a literal and reject anonymous doAsUser 
before Nimbus
    
    ReqContextFilter uses InetAddress.ofLiteral, so a remote address that is
    not an IP literal can never cause a name lookup. The UI rejects doAsUser
    without an authenticated user before it fetches the topology conf, so such
    a request costs no call to Nimbus.
---
 SECURITY.md                                        |   3 +
 docs/SECURITY.md                                   |   3 +
 .../storm/security/auth/SingleUserPrincipal.java   |   5 +-
 .../auth/authorizer/ImpersonationAuthorizer.java   |  23 +-
 .../apache/storm/security/auth/ReqContextTest.java |  13 ++
 .../security/auth/SingleUserPrincipalTest.java     |  40 ++++
 .../authorizer/ImpersonationAuthorizerTest.java    | 108 ++++++++++
 .../java/org/apache/storm/daemon/drpc/DRPC.java    |  45 +++-
 .../org/apache/storm/daemon/drpc/DRPCTest.java     | 207 ++++++++++++++++++
 .../storm/daemon/drpc/webapp/ReqContextFilter.java |  18 +-
 .../daemon/ui/filters/AuthorizedUserFilter.java    |  22 +-
 .../apache/storm/daemon/drpc/DRPCServerTest.java   | 234 +++++++++++++++++++++
 .../daemon/drpc/webapp/ReqContextFilterTest.java   |  61 ++++++
 .../ui/filters/AuthorizedUserFilterTest.java       |  87 ++++++++
 14 files changed, 851 insertions(+), 18 deletions(-)

diff --git a/SECURITY.md b/SECURITY.md
index 366c10271..661ab24f5 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -451,6 +451,9 @@ nimbus.impersonation.acl:
             [comma separated list of groups whose users impersonating_user2 is 
allowed to impersonate]
 ```
 
+The DRPC server applies the same `nimbus.impersonation.authorizer` and 
`nimbus.impersonation.acl` to requests that impersonate another user (for 
example through the `doAsUser` HTTP header or query parameter), so the DRPC 
server should be configured with the same settings (including 
`storm.group.mapping.service`). A request that sets `doAsUser` without an 
authenticated user is rejected, whether or not an impersonation authorizer is 
configured.
+For HTTP requests the host is the address the DRPC server sees the connection 
come from, so when the DRPC HTTP endpoint is behind a reverse proxy, the hosts 
listed in `nimbus.impersonation.acl` match the proxy rather than the original 
client.
+
 To support the oozie use-case, the following config can be supplied:
 ```yaml
 nimbus.impersonation.acl:
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index 36154271f..6ab9f96c0 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -620,6 +620,9 @@ nimbus.impersonation.acl:
             [comma separated list of groups whose users impersonating_user2 is 
allowed to impersonate]
 ```
 
+The DRPC server applies the same `nimbus.impersonation.authorizer` and 
`nimbus.impersonation.acl` to requests that impersonate another user (for 
example through the `doAsUser` HTTP header or query parameter), so the DRPC 
server should be configured with the same settings (including 
`storm.group.mapping.service`). A request that sets `doAsUser` without an 
authenticated user is rejected, whether or not an impersonation authorizer is 
configured.
+For HTTP requests the host is the address the DRPC server sees the connection 
come from, so when the DRPC HTTP endpoint is behind a reverse proxy, the hosts 
listed in `nimbus.impersonation.acl` match the proxy rather than the original 
client.
+
 To support the oozie use case following config can be supplied:
 ```yaml
 nimbus.impersonation.acl:
diff --git 
a/storm-client/src/jvm/org/apache/storm/security/auth/SingleUserPrincipal.java 
b/storm-client/src/jvm/org/apache/storm/security/auth/SingleUserPrincipal.java
index 2885103aa..264b0a69b 100644
--- 
a/storm-client/src/jvm/org/apache/storm/security/auth/SingleUserPrincipal.java
+++ 
b/storm-client/src/jvm/org/apache/storm/security/auth/SingleUserPrincipal.java
@@ -13,6 +13,7 @@
 package org.apache.storm.security.auth;
 
 import java.security.Principal;
+import java.util.Objects;
 
 /**
  * A Principal that represents a user.
@@ -27,7 +28,7 @@ public class SingleUserPrincipal implements Principal {
 
     @Override
     public boolean equals(Object another) {
-        return another instanceof SingleUserPrincipal && 
userName.equals(((SingleUserPrincipal) another).userName);
+        return another instanceof SingleUserPrincipal && 
Objects.equals(userName, ((SingleUserPrincipal) another).userName);
     }
 
     @Override
@@ -42,6 +43,6 @@ public class SingleUserPrincipal implements Principal {
 
     @Override
     public int hashCode() {
-        return userName.hashCode();
+        return Objects.hashCode(userName);
     }
 }
diff --git 
a/storm-client/src/jvm/org/apache/storm/security/auth/authorizer/ImpersonationAuthorizer.java
 
b/storm-client/src/jvm/org/apache/storm/security/auth/authorizer/ImpersonationAuthorizer.java
index 22aa4e166..5d2c5facf 100644
--- 
a/storm-client/src/jvm/org/apache/storm/security/auth/authorizer/ImpersonationAuthorizer.java
+++ 
b/storm-client/src/jvm/org/apache/storm/security/auth/authorizer/ImpersonationAuthorizer.java
@@ -66,10 +66,17 @@ public class ImpersonationAuthorizer implements IAuthorizer 
{
         }
 
         String impersonatingPrincipal = context.realPrincipal().getName();
-        String impersonatingUser = ptol.toLocal(context.realPrincipal());
         String userBeingImpersonated = ptol.toLocal(context.principal());
         InetAddress remoteAddress = context.remoteAddress();
 
+        if (impersonatingPrincipal == null || 
impersonatingPrincipal.isEmpty()) {
+            LOG.info("An unauthenticated request is trying to impersonate user 
{} for operation = {} from host = {}, "
+                     + "which is not permitted.", userBeingImpersonated, 
operation, remoteAddress);
+            return false;
+        }
+
+        String impersonatingUser = ptol.toLocal(context.realPrincipal());
+
         LOG.info("user = {}, principal = {} is attempting to impersonate user 
= {} for operation = {} from host = {}",
                  impersonatingUser, impersonatingPrincipal, 
userBeingImpersonated, operation, remoteAddress);
 
@@ -120,15 +127,21 @@ public class ImpersonationAuthorizer implements 
IAuthorizer {
             return false;
         }
 
-        LOG.info("Allowing impersonation of user {} by user {}", 
userBeingImpersonated, impersonatingUser);
+        LOG.debug("Allowing impersonation of user {} by user {}", 
userBeingImpersonated, impersonatingUser);
         return true;
     }
 
     private boolean isAllowedToImpersonateFromHost(Set<String> 
authorizedHosts, InetAddress remoteAddress) {
-        return authorizedHosts.contains(WILD_CARD)
+        if (authorizedHosts.contains(WILD_CARD)) {
+            return true;
+        }
+        if (remoteAddress == null) {
+            return false;
+        }
+        // Check the address first, so that no reverse name lookup is needed 
when the ACL lists IP addresses.
+        return authorizedHosts.contains(remoteAddress.getHostAddress())
                 || 
authorizedHosts.contains(remoteAddress.getCanonicalHostName())
-                || authorizedHosts.contains(remoteAddress.getHostName())
-                || authorizedHosts.contains(remoteAddress.getHostAddress());
+                || authorizedHosts.contains(remoteAddress.getHostName());
     }
 
     private boolean isAllowedToImpersonateUser(Set<String> authorizedGroups, 
String userBeingImpersonated) {
diff --git 
a/storm-client/test/jvm/org/apache/storm/security/auth/ReqContextTest.java 
b/storm-client/test/jvm/org/apache/storm/security/auth/ReqContextTest.java
index bdb97af86..4d42be8da 100644
--- a/storm-client/test/jvm/org/apache/storm/security/auth/ReqContextTest.java
+++ b/storm-client/test/jvm/org/apache/storm/security/auth/ReqContextTest.java
@@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class ReqContextTest {
 
@@ -75,4 +76,16 @@ public class ReqContextTest {
         assertEquals(principalName, rc.principal().getName());
         rc.setSubject(null);
     }
+
+    @Test
+    public void testIsImpersonatingWithUnnamedRealPrincipal() {
+        Subject subject = new Subject();
+        subject.getPrincipals().add(new SingleUserPrincipal("alice"));
+        ReqContext context = new ReqContext(subject);
+        assertFalse(context.isImpersonating());
+        context.setRealPrincipal(new SingleUserPrincipal(null));
+        assertTrue(context.isImpersonating());
+        context.setRealPrincipal(new SingleUserPrincipal("alice"));
+        assertFalse(context.isImpersonating());
+    }
 }
diff --git 
a/storm-client/test/jvm/org/apache/storm/security/auth/SingleUserPrincipalTest.java
 
b/storm-client/test/jvm/org/apache/storm/security/auth/SingleUserPrincipalTest.java
new file mode 100644
index 000000000..1ae4d34e0
--- /dev/null
+++ 
b/storm-client/test/jvm/org/apache/storm/security/auth/SingleUserPrincipalTest.java
@@ -0,0 +1,40 @@
+/**
+ * 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.storm.security.auth;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class SingleUserPrincipalTest {
+
+    @Test
+    public void testEqualsAndHashCode() {
+        assertEquals(new SingleUserPrincipal("alice"), new 
SingleUserPrincipal("alice"));
+        assertEquals(new SingleUserPrincipal("alice").hashCode(), new 
SingleUserPrincipal("alice").hashCode());
+        assertNotEquals(new SingleUserPrincipal("alice"), new 
SingleUserPrincipal("bob"));
+        assertNotEquals(new SingleUserPrincipal("alice"), null);
+    }
+
+    @Test
+    public void testNullName() {
+        SingleUserPrincipal noName = new SingleUserPrincipal(null);
+        assertNull(noName.getName());
+        assertEquals(noName, new SingleUserPrincipal(null));
+        assertEquals(noName.hashCode(), new 
SingleUserPrincipal(null).hashCode());
+        assertNotEquals(noName, new SingleUserPrincipal("alice"));
+        assertNotEquals(new SingleUserPrincipal("alice"), noName);
+    }
+}
diff --git 
a/storm-client/test/jvm/org/apache/storm/security/auth/authorizer/ImpersonationAuthorizerTest.java
 
b/storm-client/test/jvm/org/apache/storm/security/auth/authorizer/ImpersonationAuthorizerTest.java
new file mode 100644
index 000000000..66d87c282
--- /dev/null
+++ 
b/storm-client/test/jvm/org/apache/storm/security/auth/authorizer/ImpersonationAuthorizerTest.java
@@ -0,0 +1,108 @@
+/**
+ * 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.storm.security.auth.authorizer;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.net.InetAddress;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.security.auth.Subject;
+import org.apache.storm.Config;
+import org.apache.storm.security.auth.DefaultPrincipalToLocal;
+import org.apache.storm.security.auth.ReqContext;
+import org.apache.storm.security.auth.SingleUserPrincipal;
+import org.junit.jupiter.api.Test;
+
+public class ImpersonationAuthorizerTest {
+
+    private static ImpersonationAuthorizer mkAuthorizer(String 
impersonatingUser, List<String> hosts) {
+        Map<String, List<String>> entry = new HashMap<>();
+        entry.put("groups", Collections.singletonList("*"));
+        entry.put("hosts", hosts);
+        Map<String, Object> conf = new HashMap<>();
+        conf.put(Config.NIMBUS_IMPERSONATION_ACL, 
Collections.singletonMap(impersonatingUser, entry));
+        conf.put(Config.STORM_PRINCIPAL_TO_LOCAL_PLUGIN, 
DefaultPrincipalToLocal.class.getName());
+        ImpersonationAuthorizer authorizer = new ImpersonationAuthorizer();
+        authorizer.prepare(conf);
+        return authorizer;
+    }
+
+    private static ReqContext mkContext(String realUser, String user, 
InetAddress remoteAddress) {
+        Subject subject = new Subject();
+        subject.getPrincipals().add(new SingleUserPrincipal(user));
+        ReqContext context = new ReqContext(subject);
+        context.setRealPrincipal(new SingleUserPrincipal(realUser));
+        context.setRemoteAddress(remoteAddress);
+        return context;
+    }
+
+    @Test
+    public void testRealPrincipalWithoutNameIsDenied() {
+        ImpersonationAuthorizer authorizer = mkAuthorizer("bob", 
Collections.singletonList("*"));
+        ReqContext context = mkContext(null, "alice", 
InetAddress.getLoopbackAddress());
+        assertTrue(context.isImpersonating());
+        assertFalse(authorizer.permit(context, "execute", null));
+    }
+
+    @Test
+    public void testRealPrincipalWithoutNameIsDeniedWithNullAclEntry() {
+        ImpersonationAuthorizer authorizer = mkAuthorizer(null, 
Collections.singletonList("*"));
+        ReqContext context = mkContext(null, "alice", 
InetAddress.getLoopbackAddress());
+        assertFalse(authorizer.permit(context, "execute", null));
+    }
+
+    @Test
+    public void testRealPrincipalWithEmptyNameIsDenied() {
+        ImpersonationAuthorizer authorizer = mkAuthorizer("", 
Collections.singletonList("*"));
+        ReqContext context = mkContext("", "alice", 
InetAddress.getLoopbackAddress());
+        assertTrue(context.isImpersonating());
+        assertFalse(authorizer.permit(context, "execute", null));
+    }
+
+    @Test
+    public void testHostListedByAddressSkipsNameLookup() {
+        ImpersonationAuthorizer authorizer = mkAuthorizer("bob", 
Collections.singletonList("192.0.2.10"));
+        InetAddress remoteAddress = mock(InetAddress.class);
+        when(remoteAddress.getHostAddress()).thenReturn("192.0.2.10");
+        assertTrue(authorizer.permit(mkContext("bob", "alice", remoteAddress), 
"execute", null));
+        verify(remoteAddress, never()).getCanonicalHostName();
+        verify(remoteAddress, never()).getHostName();
+    }
+
+    @Test
+    public void testHostListedByName() {
+        ImpersonationAuthorizer authorizer = mkAuthorizer("bob", 
Collections.singletonList("client.example.com"));
+        InetAddress remoteAddress = mock(InetAddress.class);
+        when(remoteAddress.getHostAddress()).thenReturn("192.0.2.10");
+        
when(remoteAddress.getCanonicalHostName()).thenReturn("client.example.com");
+        assertTrue(authorizer.permit(mkContext("bob", "alice", remoteAddress), 
"execute", null));
+    }
+
+    @Test
+    public void testUnlistedHostDenied() {
+        ImpersonationAuthorizer authorizer = mkAuthorizer("bob", 
Collections.singletonList("192.0.2.10"));
+        InetAddress remoteAddress = mock(InetAddress.class);
+        when(remoteAddress.getHostAddress()).thenReturn("192.0.2.11");
+        
when(remoteAddress.getCanonicalHostName()).thenReturn("other.example.com");
+        when(remoteAddress.getHostName()).thenReturn("other.example.com");
+        assertFalse(authorizer.permit(mkContext("bob", "alice", 
remoteAddress), "execute", null));
+    }
+}
diff --git a/storm-server/src/main/java/org/apache/storm/daemon/drpc/DRPC.java 
b/storm-server/src/main/java/org/apache/storm/daemon/drpc/DRPC.java
index df9ed364f..d5f30a370 100644
--- a/storm-server/src/main/java/org/apache/storm/daemon/drpc/DRPC.java
+++ b/storm-server/src/main/java/org/apache/storm/daemon/drpc/DRPC.java
@@ -29,6 +29,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
+import org.apache.storm.Config;
 import org.apache.storm.DaemonConfig;
 import org.apache.storm.daemon.StormCommon;
 import org.apache.storm.generated.AuthorizationException;
@@ -76,14 +77,21 @@ public class DRPC implements AutoCloseable {
     private final Timer timer = new Timer("DRPC-CLEANUP-TIMER", true);
     private final AtomicLong ctr = new AtomicLong(0);
     private final IAuthorizer auth;
+    private final IAuthorizer impersonationAuth;
 
     public DRPC(StormMetricsRegistry metricsRegistry, Map<String, Object> 
conf) {
         this(metricsRegistry, mkAuthorizationHandler((String) 
conf.get(DaemonConfig.DRPC_AUTHORIZER), conf),
+             mkAuthorizationHandler((String) 
conf.get(DaemonConfig.NIMBUS_IMPERSONATION_AUTHORIZER), conf),
              
ObjectReader.getInt(conf.get(DaemonConfig.DRPC_REQUEST_TIMEOUT_SECS), 600) * 
1000);
     }
 
     public DRPC(StormMetricsRegistry metricsRegistry, IAuthorizer auth, long 
timeoutMs) {
+        this(metricsRegistry, auth, null, timeoutMs);
+    }
+
+    public DRPC(StormMetricsRegistry metricsRegistry, IAuthorizer auth, 
IAuthorizer impersonationAuth, long timeoutMs) {
         this.auth = auth;
+        this.impersonationAuth = impersonationAuth;
         this.meterServerTimedOut = 
metricsRegistry.registerMeter("drpc:num-server-timedout-requests");
         this.meterExecuteCalls = 
metricsRegistry.registerMeter("drpc:num-execute-calls");
         this.meterResultCalls = 
metricsRegistry.registerMeter("drpc:num-result-calls");
@@ -117,17 +125,40 @@ public class DRPC implements AutoCloseable {
     @VisibleForTesting
     static void checkAuthorization(ReqContext reqContext, IAuthorizer auth, 
String operation, String function)
         throws AuthorizationException {
-        checkAuthorization(reqContext, auth, operation, function, true);
+        checkAuthorization(reqContext, auth, null, operation, function, true);
     }
 
-    private static void checkAuthorization(ReqContext reqContext, IAuthorizer 
auth, String operation, String function, boolean log)
-        throws AuthorizationException {
+    private static void checkAuthorization(ReqContext reqContext, IAuthorizer 
auth, IAuthorizer impersonationAuth, String operation,
+                                           String function, boolean log) 
throws AuthorizationException {
         if (reqContext != null && log) {
             logAccess(reqContext, operation, function);
         }
+        Map<String, Object> map = new HashMap<>();
+        map.put(DRPCAuthorizerBase.FUNCTION_NAME, function);
+        if (reqContext != null && reqContext.isImpersonating()) {
+            LOG.debug("principal: {} is trying to impersonate principal: {}", 
reqContext.realPrincipal(), reqContext.principal());
+            String realUserName = reqContext.realPrincipal().getName();
+            if (realUserName == null || realUserName.isEmpty()) {
+                Principal principal = reqContext.principal();
+                throw new WrappedAuthorizationException("DRPC request '" + 
operation + "' on function '" + function + "'"
+                                                        + " gives doAsUser '" 
+ (principal == null ? "unknown" : principal.getName())
+                                                        + "' without an 
authenticated user.");
+            }
+            if (impersonationAuth == null) {
+                LOG.warn("impersonation attempt but {} has no authorizer 
configured, so it is not checked. "
+                         + "See the impersonation section of the documentation 
to learn how to configure it.",
+                         DaemonConfig.NIMBUS_IMPERSONATION_AUTHORIZER);
+            } else if (!impersonationAuth.permit(reqContext, operation, map)) {
+                String fromHost = reqContext.remoteAddress() == null ? "" : " 
from host " + reqContext.remoteAddress();
+                throw new WrappedAuthorizationException("principal " + 
reqContext.realPrincipal()
+                                                        + " is not authorized 
to impersonate principal " + reqContext.principal()
+                                                        + fromHost
+                                                        + " for DRPC request 
'" + operation + "'"
+                                                        + " on function '" + 
function + "'."
+                                                        + " See " + 
Config.NIMBUS_IMPERSONATION_ACL + " to configure impersonation.");
+            }
+        }
         if (auth != null) {
-            Map<String, Object> map = new HashMap<>();
-            map.put(DRPCAuthorizerBase.FUNCTION_NAME, function);
             if (!auth.permit(reqContext, operation, map)) {
                 Principal principal = reqContext.principal();
                 String user = (principal != null) ? principal.getName() : 
"unknown";
@@ -137,11 +168,11 @@ public class DRPC implements AutoCloseable {
     }
 
     private void checkAuthorization(String operation, String function) throws 
AuthorizationException {
-        checkAuthorization(ReqContext.context(), auth, operation, function);
+        checkAuthorization(ReqContext.context(), auth, impersonationAuth, 
operation, function, true);
     }
 
     private void checkAuthorizationNoLog(String operation, String function) 
throws AuthorizationException {
-        checkAuthorization(ReqContext.context(), auth, operation, function, 
false);
+        checkAuthorization(ReqContext.context(), auth, impersonationAuth, 
operation, function, false);
     }
 
     private void cleanup(String id) {
diff --git 
a/storm-server/src/test/java/org/apache/storm/daemon/drpc/DRPCTest.java 
b/storm-server/src/test/java/org/apache/storm/daemon/drpc/DRPCTest.java
index a9fd7f1d1..892a28239 100644
--- a/storm-server/src/test/java/org/apache/storm/daemon/drpc/DRPCTest.java
+++ b/storm-server/src/test/java/org/apache/storm/daemon/drpc/DRPCTest.java
@@ -18,6 +18,7 @@
 
 package org.apache.storm.daemon.drpc;
 
+import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -33,6 +34,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import javax.security.auth.Subject;
 import org.apache.storm.Config;
+import org.apache.storm.DaemonConfig;
 import org.apache.storm.generated.AuthorizationException;
 import org.apache.storm.generated.DRPCExceptionType;
 import org.apache.storm.generated.DRPCExecutionException;
@@ -43,12 +45,15 @@ import org.apache.storm.security.auth.SingleUserPrincipal;
 import org.apache.storm.security.auth.authorizer.DRPCSimpleACLAuthorizer;
 import 
org.apache.storm.security.auth.authorizer.DRPCSimpleACLAuthorizer.AclFunctionEntry;
 import org.apache.storm.security.auth.authorizer.DenyAuthorizer;
+import org.apache.storm.security.auth.authorizer.ImpersonationAuthorizer;
 import org.apache.storm.utils.Time;
 import org.awaitility.Awaitility;
 import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
@@ -72,6 +77,11 @@ public class DRPCTest {
         exec.shutdownNow();
     }
 
+    @AfterEach
+    public void resetContext() {
+        ReqContext.reset();
+    }
+
     public static DRPCRequest getNextAvailableRequest(DRPC server, String 
func) {
         AtomicReference<DRPCRequest> result = new AtomicReference<>();
         Awaitility.await("DRPC request on " + func)
@@ -359,6 +369,203 @@ public class DRPCTest {
         DRPC.checkAuthorization(other, auth, "execute", "not_jump");
     }
 
+    /**
+     * Only "alice" may execute the function "sensitive".
+     */
+    public static class SensitiveFunctionAclAuthorizer extends 
DRPCSimpleACLAuthorizer {
+        @Override
+        protected Map<String, AclFunctionEntry> readAclFromConfig() {
+            Map<String, AclFunctionEntry> acl = new HashMap<>();
+            acl.put("sensitive", new 
AclFunctionEntry(Collections.singletonList("alice"), "sensitive_topo"));
+            return acl;
+        }
+    }
+
+    private static Map<String, Object> impersonationConf(boolean 
withImpersonationAuthorizer, boolean grantBob) {
+        return impersonationConf(withImpersonationAuthorizer, grantBob, "*");
+    }
+
+    private static Map<String, Object> impersonationConf(boolean 
withImpersonationAuthorizer, boolean grantBob, String bobHost) {
+        Map<String, Object> conf = new HashMap<>();
+        conf.put(DaemonConfig.DRPC_AUTHORIZER, 
SensitiveFunctionAclAuthorizer.class.getName());
+        conf.put(Config.DRPC_AUTHORIZER_ACL_STRICT, true);
+        conf.put(Config.STORM_PRINCIPAL_TO_LOCAL_PLUGIN, 
DefaultPrincipalToLocal.class.getName());
+        if (withImpersonationAuthorizer) {
+            conf.put(DaemonConfig.NIMBUS_IMPERSONATION_AUTHORIZER, 
ImpersonationAuthorizer.class.getName());
+            Map<String, Map<String, List<String>>> impersonationAcl = new 
HashMap<>();
+            if (grantBob) {
+                Map<String, List<String>> bobAcl = new HashMap<>();
+                bobAcl.put("groups", Collections.singletonList("*"));
+                bobAcl.put("hosts", Collections.singletonList(bobHost));
+                impersonationAcl.put("bob", bobAcl);
+            }
+            conf.put(Config.NIMBUS_IMPERSONATION_ACL, impersonationAcl);
+        }
+        return conf;
+    }
+
+    /**
+     * Sets up the current request context for {@code user}, optionally as 
{@code realUser} impersonating {@code user}.
+     */
+    private static void setCurrentUser(String user, String realUser) {
+        setCurrentUser(user, realUser, InetAddress.getLoopbackAddress());
+    }
+
+    private static void setCurrentUser(String user, String realUser, 
InetAddress remoteAddress) {
+        Subject subject = new Subject();
+        subject.getPrincipals().add(new SingleUserPrincipal(user));
+        ReqContext context = ReqContext.context();
+        context.setSubject(subject);
+        context.setRealPrincipal(realUser == null ? null : new 
SingleUserPrincipal(realUser));
+        context.setRemoteAddress(remoteAddress);
+    }
+
+    private static void executeSensitive(DRPC server) throws Exception {
+        server.execute("sensitive", "args", 
BlockingOutstandingRequest.FACTORY);
+    }
+
+    @Test
+    public void testImpersonationDeniedByImpersonationAuthorizer() throws 
Exception {
+        try (DRPC server = new DRPC(new StormMetricsRegistry(), 
impersonationConf(true, false))) {
+            setCurrentUser("alice", "bob");
+            assertTrue(ReqContext.context().isImpersonating());
+            assertThrows(() -> executeSensitive(server), 
AuthorizationException.class);
+            assertEquals(0, server.getNumTrackedFunctions());
+        }
+    }
+
+    @Test
+    public void testImpersonationPermittedByImpersonationAuthorizer() throws 
Exception {
+        try (DRPC server = new DRPC(new StormMetricsRegistry(), 
impersonationConf(true, true))) {
+            setCurrentUser("alice", "bob");
+            executeSensitive(server);
+            assertEquals(1, server.getNumTrackedFunctions());
+        }
+    }
+
+    @Test
+    public void testNoImpersonationAllowedUser() throws Exception {
+        try (DRPC server = new DRPC(new StormMetricsRegistry(), 
impersonationConf(true, false))) {
+            setCurrentUser("alice", null);
+            executeSensitive(server);
+            assertEquals(1, server.getNumTrackedFunctions());
+        }
+    }
+
+    @Test
+    public void testNoImpersonationOtherUserDenied() throws Exception {
+        try (DRPC server = new DRPC(new StormMetricsRegistry(), 
impersonationConf(true, true))) {
+            setCurrentUser("bob", null);
+            assertThrows(() -> executeSensitive(server), 
AuthorizationException.class);
+            assertEquals(0, server.getNumTrackedFunctions());
+        }
+    }
+
+    @Test
+    public void testImpersonationFromListedHostPermitted() throws Exception {
+        InetAddress loopback = InetAddress.getLoopbackAddress();
+        try (DRPC server = new DRPC(new StormMetricsRegistry(), 
impersonationConf(true, true, loopback.getHostAddress()))) {
+            setCurrentUser("alice", "bob", loopback);
+            executeSensitive(server);
+            assertEquals(1, server.getNumTrackedFunctions());
+        }
+    }
+
+    @Test
+    public void testImpersonationWithUnknownRemoteAddressDeniedForHostList() 
throws Exception {
+        InetAddress loopback = InetAddress.getLoopbackAddress();
+        try (DRPC server = new DRPC(new StormMetricsRegistry(), 
impersonationConf(true, true, loopback.getHostAddress()))) {
+            setCurrentUser("alice", "bob", null);
+            assertThrows(() -> executeSensitive(server), 
AuthorizationException.class);
+            assertEquals(0, server.getNumTrackedFunctions());
+        }
+    }
+
+    @Test
+    public void testImpersonationWithoutImpersonationAuthorizer() throws 
Exception {
+        try (DRPC server = new DRPC(new StormMetricsRegistry(), 
impersonationConf(false, false))) {
+            setCurrentUser("alice", "bob");
+            executeSensitive(server);
+            assertEquals(1, server.getNumTrackedFunctions());
+        }
+    }
+
+    @Test
+    public void testImpersonationDenialNamesFunction() throws Exception {
+        try (DRPC server = new DRPC(new StormMetricsRegistry(), 
impersonationConf(true, false))) {
+            setCurrentUser("alice", "bob");
+            try {
+                executeSensitive(server);
+                fail("Expected an AuthorizationException");
+            } catch (AuthorizationException e) {
+                assertTrue(e.get_msg().contains("function 'sensitive'"), 
e.get_msg());
+            }
+        }
+    }
+
+    /**
+     * Sets up the current request context for an unauthenticated request 
asking to run as {@code user}.
+     */
+    private static void setUnauthenticatedImpersonation(String user) {
+        setCurrentUser(user, null);
+        ReqContext.context().setRealPrincipal(new SingleUserPrincipal(null));
+    }
+
+    @Test
+    public void 
testUnauthenticatedImpersonationWithoutImpersonationAuthorizerDenied() throws 
Exception {
+        Map<String, Object> conf = new HashMap<>();
+        try (DRPC server = new DRPC(new StormMetricsRegistry(), conf)) {
+            setUnauthenticatedImpersonation("alice");
+            assertTrue(ReqContext.context().isImpersonating());
+            try {
+                executeSensitive(server);
+                fail("Expected an AuthorizationException");
+            } catch (AuthorizationException e) {
+                assertTrue(e.get_msg().contains("function 'sensitive'"), 
e.get_msg());
+                assertTrue(e.get_msg().contains("doAsUser 'alice'"), 
e.get_msg());
+            }
+            assertEquals(0, server.getNumTrackedFunctions());
+        }
+    }
+
+    @Test
+    public void testUnauthenticatedImpersonationWithEmptyNameDenied() throws 
Exception {
+        Map<String, Object> conf = new HashMap<>();
+        try (DRPC server = new DRPC(new StormMetricsRegistry(), conf)) {
+            setCurrentUser("alice", "");
+            assertTrue(ReqContext.context().isImpersonating());
+            assertThrows(() -> executeSensitive(server), 
AuthorizationException.class);
+            assertEquals(0, server.getNumTrackedFunctions());
+        }
+    }
+
+    @Test
+    public void testUnauthenticatedRequestWithoutImpersonation() throws 
Exception {
+        Map<String, Object> conf = new HashMap<>();
+        try (DRPC server = new DRPC(new StormMetricsRegistry(), conf)) {
+            ReqContext context = ReqContext.context();
+            context.setSubject(null);
+            context.setRealPrincipal(null);
+            assertFalse(context.isImpersonating());
+            executeSensitive(server);
+            assertEquals(1, server.getNumTrackedFunctions());
+        }
+    }
+
+    @Test
+    public void 
testUnauthenticatedImpersonationWithImpersonationAuthorizerDenied() throws 
Exception {
+        try (DRPC server = new DRPC(new StormMetricsRegistry(), 
impersonationConf(true, true))) {
+            setUnauthenticatedImpersonation("alice");
+            try {
+                executeSensitive(server);
+                fail("Expected an AuthorizationException");
+            } catch (AuthorizationException e) {
+                assertTrue(e.get_msg().contains("without an authenticated 
user"), e.get_msg());
+            }
+            assertEquals(0, server.getNumTrackedFunctions());
+        }
+    }
+
     public interface ThrowStuff {
         void run() throws Exception;
     }
diff --git 
a/storm-webapp/src/main/java/org/apache/storm/daemon/drpc/webapp/ReqContextFilter.java
 
b/storm-webapp/src/main/java/org/apache/storm/daemon/drpc/webapp/ReqContextFilter.java
index 8f9ff9b5a..1727d0577 100644
--- 
a/storm-webapp/src/main/java/org/apache/storm/daemon/drpc/webapp/ReqContextFilter.java
+++ 
b/storm-webapp/src/main/java/org/apache/storm/daemon/drpc/webapp/ReqContextFilter.java
@@ -30,6 +30,7 @@ import jakarta.ws.rs.container.PreMatching;
 import jakarta.ws.rs.ext.Provider;
 
 import java.io.IOException;
+import java.net.InetAddress;
 
 import org.apache.storm.security.auth.IHttpCredentialsPlugin;
 import org.apache.storm.security.auth.ReqContext;
@@ -48,8 +49,23 @@ public class ReqContextFilter implements Filter {
      * @param request the request to populate
      */
     public void populateContext(HttpServletRequest request) {
+        ReqContext context = ReqContext.context();
         if (httpCredsHandler != null) {
-            httpCredsHandler.populateContext(ReqContext.context(), request);
+            httpCredsHandler.populateContext(context, request);
+        }
+        context.setRemoteAddress(getRemoteAddress(request));
+    }
+
+    private static InetAddress getRemoteAddress(HttpServletRequest request) {
+        String remoteAddr = request.getRemoteAddr();
+        if (remoteAddr == null || remoteAddr.isEmpty()) {
+            return null;
+        }
+        try {
+            // The servlet container reports an IP address literal; ofLiteral 
never does a name lookup.
+            return InetAddress.ofLiteral(remoteAddr);
+        } catch (IllegalArgumentException e) {
+            return null;
         }
     }
 
diff --git 
a/storm-webapp/src/main/java/org/apache/storm/daemon/ui/filters/AuthorizedUserFilter.java
 
b/storm-webapp/src/main/java/org/apache/storm/daemon/ui/filters/AuthorizedUserFilter.java
index f26175892..eeb587864 100644
--- 
a/storm-webapp/src/main/java/org/apache/storm/daemon/ui/filters/AuthorizedUserFilter.java
+++ 
b/storm-webapp/src/main/java/org/apache/storm/daemon/ui/filters/AuthorizedUserFilter.java
@@ -125,6 +125,24 @@ public class AuthorizedUserFilter implements 
ContainerRequestFilter {
             return;
         }
 
+        ReqContext reqContext = ReqContext.context();
+
+        //Rejected before the topology conf is fetched, so such a request 
costs no call to Nimbus
+        if (reqContext.isImpersonating()) {
+            String realUserName = reqContext.realPrincipal().getName();
+            if (realUserName == null || realUserName.isEmpty()) {
+                Principal principal = reqContext.principal();
+                containerRequestContext.abortWith(
+                        makeResponse(new AuthorizationException(
+                                "A request without an authenticated user is 
not allowed to act as user '"
+                                        + (principal == null ? "unknown" : 
principal.getName()) + "'. Please "
+                                        + "see SECURITY.MD to learn how to 
configure UI authentication."
+                        ), containerRequestContext, 401)
+                );
+                return;
+            }
+        }
+
         Map topoConf = null;
         if (annotation.needsTopoId()) {
             final String topoId = 
containerRequestContext.getUriInfo().getPathParameters().get("id").get(0);
@@ -144,8 +162,6 @@ public class AuthorizedUserFilter implements 
ContainerRequestFilter {
             }
         }
 
-        ReqContext reqContext = ReqContext.context();
-
         if (reqContext.isImpersonating()) {
             if (uiImpersonationHandler != null) {
                 if (!uiImpersonationHandler.permit(reqContext, op, topoConf)) {
@@ -164,7 +180,7 @@ public class AuthorizedUserFilter implements 
ContainerRequestFilter {
                     containerRequestContext.abortWith(
                             makeResponse(new AuthorizationException(
                                     "user '" + realUser +  "' is not 
authorized to impersonate user '"
-                                            + user + "' from host '" + 
remoteAddress.toString() + "'. Please"
+                                            + user + "' from host '" + 
remoteAddress + "'. Please "
                                             + "see SECURITY.MD to learn how to 
configure impersonation ACL."
                             ), containerRequestContext, 401)
                     );
diff --git 
a/storm-webapp/src/test/java/org/apache/storm/daemon/drpc/DRPCServerTest.java 
b/storm-webapp/src/test/java/org/apache/storm/daemon/drpc/DRPCServerTest.java
index 7e446d91f..385777f6b 100644
--- 
a/storm-webapp/src/test/java/org/apache/storm/daemon/drpc/DRPCServerTest.java
+++ 
b/storm-webapp/src/test/java/org/apache/storm/daemon/drpc/DRPCServerTest.java
@@ -19,20 +19,38 @@
 package org.apache.storm.daemon.drpc;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.fail;
 
 import org.junit.jupiter.api.AfterAll;
 
+import java.io.IOException;
 import java.io.InputStream;
+import java.net.HttpURLConnection;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
 
 import org.apache.storm.Config;
 import org.apache.storm.DaemonConfig;
@@ -40,7 +58,13 @@ import org.apache.storm.drpc.DRPCInvocationsClient;
 import org.apache.storm.generated.DRPCExecutionException;
 import org.apache.storm.generated.DRPCRequest;
 import org.apache.storm.metric.StormMetricsRegistry;
+import org.apache.storm.security.auth.DefaultHttpCredentialsPlugin;
+import org.apache.storm.security.auth.DefaultPrincipalToLocal;
+import org.apache.storm.security.auth.ReqContext;
 import org.apache.storm.security.auth.SimpleTransportPlugin;
+import org.apache.storm.security.auth.SingleUserPrincipal;
+import org.apache.storm.security.auth.authorizer.DRPCSimpleACLAuthorizer;
+import org.apache.storm.security.auth.authorizer.ImpersonationAuthorizer;
 import org.apache.storm.utils.DRPCClient;
 import org.junit.jupiter.api.Test;
 import org.slf4j.Logger;
@@ -191,4 +215,214 @@ public class DRPCServerTest {
             }
         }
     }
+
+    /**
+     * Authenticates every HTTP request as the user given in the "user" init 
parameter.
+     */
+    public static class FixedUserFilter implements Filter {
+        private String user;
+
+        @Override
+        public void init(FilterConfig config) {
+            user = config.getInitParameter("user");
+        }
+
+        @Override
+        public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain)
+            throws IOException, ServletException {
+            chain.doFilter(new HttpServletRequestWrapper((HttpServletRequest) 
request) {
+                @Override
+                public Principal getUserPrincipal() {
+                    return new SingleUserPrincipal(user);
+                }
+
+                @Override
+                public String getRemoteUser() {
+                    return user;
+                }
+            }, response);
+        }
+
+        @Override
+        public void destroy() {
+        }
+    }
+
+    /**
+     * Only "alice" may execute the function "sensitive"; any client may fetch 
and answer requests.
+     */
+    public static class SensitiveFunctionAclAuthorizer extends 
DRPCSimpleACLAuthorizer {
+        @Override
+        protected Map<String, AclFunctionEntry> readAclFromConfig() {
+            Map<String, AclFunctionEntry> acl = new HashMap<>();
+            acl.put("sensitive", new 
AclFunctionEntry(Collections.singletonList("alice"), "sensitive_topo"));
+            return acl;
+        }
+
+        @Override
+        protected boolean permitInvocationRequest(ReqContext context, String 
operation, Map<String, Object> params) {
+            return true;
+        }
+    }
+
+    private Map<String, Object> getImpersonationConf(String httpUser) {
+        return getImpersonationConf(httpUser, new HashMap<>());
+    }
+
+    private Map<String, Object> getImpersonationConf(String httpUser, 
Map<String, Map<String, List<String>>> impersonationAcl) {
+        Map<String, Object> conf = getConf(0, 0, 0);
+        conf.put(DaemonConfig.DRPC_HTTP_FILTER, 
FixedUserFilter.class.getName());
+        conf.put(DaemonConfig.DRPC_HTTP_FILTER_PARAMS, 
Collections.singletonMap("user", httpUser));
+        conf.put(DaemonConfig.DRPC_HTTP_CREDS_PLUGIN, 
DefaultHttpCredentialsPlugin.class.getName());
+        conf.put(DaemonConfig.DRPC_AUTHORIZER, 
SensitiveFunctionAclAuthorizer.class.getName());
+        conf.put(Config.DRPC_AUTHORIZER_ACL_STRICT, true);
+        conf.put(Config.STORM_PRINCIPAL_TO_LOCAL_PLUGIN, 
DefaultPrincipalToLocal.class.getName());
+        conf.put(DaemonConfig.NIMBUS_IMPERSONATION_AUTHORIZER, 
ImpersonationAuthorizer.class.getName());
+        conf.put(Config.NIMBUS_IMPERSONATION_ACL, impersonationAcl);
+        conf.put(DaemonConfig.DRPC_REQUEST_TIMEOUT_SECS, 30);
+        return conf;
+    }
+
+    private static Map<String, Map<String, List<String>>> 
impersonationAclFor(String user, List<String> hosts) {
+        Map<String, List<String>> entry = new HashMap<>();
+        entry.put("groups", Collections.singletonList("*"));
+        entry.put("hosts", hosts);
+        return Collections.singletonMap(user, entry);
+    }
+
+    private static HttpResult doGetWithStatus(int port, String pathAndQuery) {
+        try {
+            URL url = new URL("http://localhost:"; + port + pathAndQuery);
+            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+            try {
+                conn.setReadTimeout(10_000);
+                int status = conn.getResponseCode();
+                String body = "";
+                try (InputStream in = status < 400 ? conn.getInputStream() : 
conn.getErrorStream()) {
+                    if (in != null) {
+                        body = new String(in.readAllBytes(), 
StandardCharsets.UTF_8);
+                    }
+                }
+                return new HttpResult(status, body);
+            } finally {
+                conn.disconnect();
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static final class HttpResult {
+        private final int status;
+        private final String body;
+
+        private HttpResult(int status, String body) {
+            this.status = status;
+            this.body = body;
+        }
+    }
+
+    /**
+     * Sends the GET request and answers any request for "sensitive" that 
reaches the DRPC queue.
+     */
+    private HttpResult runSensitiveRequest(Map<String, Object> conf, String 
query) throws Exception {
+        try (DRPCServer server = new DRPCServer(conf, new 
StormMetricsRegistry())) {
+            server.start();
+            //TODO need a better way to do this
+            Thread.sleep(2000);
+            try (DRPCInvocationsClient invoke = new 
DRPCInvocationsClient(conf, "localhost", server.getDrpcInvokePort())) {
+                Future<HttpResult> found = exec.submit(
+                    () -> doGetWithStatus(server.getHttpServerPort(), 
"/drpc/sensitive/args" + query));
+                long timeout = System.currentTimeMillis() + 5_000;
+                while (!found.isDone() && System.currentTimeMillis() < 
timeout) {
+                    DRPCRequest request = 
invoke.getClient().fetchRequest("sensitive");
+                    if (request != null && request.get_request_id() != null && 
!request.get_request_id().isEmpty()) {
+                        invoke.result(request.get_request_id(), 
"sensitive-result");
+                        break;
+                    }
+                    Thread.sleep(1);
+                }
+                try {
+                    return found.get(5, TimeUnit.SECONDS);
+                } catch (TimeoutException e) {
+                    found.cancel(true);
+                    fail("Test timed out waiting for the response to the 
request on sensitive");
+                    return null;
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testHttpImpersonationDeniedByImpersonationAuthorizer() throws 
Exception {
+        HttpResult result = runSensitiveRequest(getImpersonationConf("bob"), 
"?doAsUser=alice");
+        assertEquals(401, result.status);
+        assertNotEquals("sensitive-result", result.body);
+    }
+
+    @Test
+    public void testHttpImpersonationFromListedHostPermitted() throws 
Exception {
+        Map<String, Map<String, List<String>>> acl =
+            impersonationAclFor("bob", Arrays.asList("localhost", "127.0.0.1", 
"0:0:0:0:0:0:0:1"));
+        HttpResult result = runSensitiveRequest(getImpersonationConf("bob", 
acl), "?doAsUser=alice");
+        assertEquals(200, result.status);
+        assertEquals("sensitive-result", result.body);
+    }
+
+    @Test
+    public void testHttpImpersonationFromUnlistedHostDenied() throws Exception 
{
+        Map<String, Map<String, List<String>>> acl = 
impersonationAclFor("bob", Collections.singletonList("192.0.2.1"));
+        HttpResult result = runSensitiveRequest(getImpersonationConf("bob", 
acl), "?doAsUser=alice");
+        assertEquals(401, result.status);
+        assertNotEquals("sensitive-result", result.body);
+    }
+
+    @Test
+    public void testHttpAllowedUserWithoutImpersonation() throws Exception {
+        HttpResult result = runSensitiveRequest(getImpersonationConf("alice"), 
"");
+        assertEquals(200, result.status);
+        assertEquals("sensitive-result", result.body);
+    }
+
+    @Test
+    public void testHttpOtherUserWithoutImpersonationDenied() throws Exception 
{
+        HttpResult result = runSensitiveRequest(getImpersonationConf("bob"), 
"");
+        assertEquals(401, result.status);
+        assertNotEquals("sensitive-result", result.body);
+    }
+
+    /**
+     * No {@code drpc.http.filter} is configured, so the HTTP requests carry 
no authenticated user.
+     */
+    private Map<String, Object> getUnauthenticatedConf() {
+        Map<String, Object> conf = getConf(0, 0, 0);
+        conf.put(DaemonConfig.DRPC_HTTP_CREDS_PLUGIN, 
DefaultHttpCredentialsPlugin.class.getName());
+        conf.put(Config.STORM_PRINCIPAL_TO_LOCAL_PLUGIN, 
DefaultPrincipalToLocal.class.getName());
+        conf.put(DaemonConfig.DRPC_REQUEST_TIMEOUT_SECS, 30);
+        return conf;
+    }
+
+    @Test
+    public void 
testHttpUnauthenticatedImpersonationWithoutImpersonationAuthorizerDenied() 
throws Exception {
+        HttpResult result = runSensitiveRequest(getUnauthenticatedConf(), 
"?doAsUser=alice");
+        assertEquals(401, result.status);
+        assertNotEquals("sensitive-result", result.body);
+    }
+
+    @Test
+    public void testHttpUnauthenticatedWithoutImpersonation() throws Exception 
{
+        HttpResult result = runSensitiveRequest(getUnauthenticatedConf(), "");
+        assertEquals(200, result.status);
+        assertEquals("sensitive-result", result.body);
+    }
+
+    @Test
+    public void 
testHttpUnauthenticatedImpersonationWithImpersonationAuthorizerDenied() throws 
Exception {
+        Map<String, Object> conf = getUnauthenticatedConf();
+        conf.put(DaemonConfig.NIMBUS_IMPERSONATION_AUTHORIZER, 
ImpersonationAuthorizer.class.getName());
+        conf.put(Config.NIMBUS_IMPERSONATION_ACL, impersonationAclFor("bob", 
Collections.singletonList("*")));
+        HttpResult result = runSensitiveRequest(conf, "?doAsUser=alice");
+        assertEquals(401, result.status);
+        assertNotEquals("sensitive-result", result.body);
+    }
 }
diff --git 
a/storm-webapp/src/test/java/org/apache/storm/daemon/drpc/webapp/ReqContextFilterTest.java
 
b/storm-webapp/src/test/java/org/apache/storm/daemon/drpc/webapp/ReqContextFilterTest.java
new file mode 100644
index 000000000..ad04bbb16
--- /dev/null
+++ 
b/storm-webapp/src/test/java/org/apache/storm/daemon/drpc/webapp/ReqContextFilterTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.storm.daemon.drpc.webapp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import java.net.InetAddress;
+
+import org.apache.storm.security.auth.ReqContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+public class ReqContextFilterTest {
+
+    @AfterEach
+    public void resetContext() {
+        ReqContext.context().setRemoteAddress(null);
+    }
+
+    private static InetAddress remoteAddressFor(String remoteAddr) {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getRemoteAddr()).thenReturn(remoteAddr);
+        new ReqContextFilter(null).populateContext(request);
+        return ReqContext.context().remoteAddress();
+    }
+
+    @Test
+    public void testIpAddressLiteralsAreUsed() {
+        assertEquals(InetAddress.ofLiteral("192.0.2.1"), 
remoteAddressFor("192.0.2.1"));
+        assertEquals(InetAddress.ofLiteral("::1"), remoteAddressFor("::1"));
+    }
+
+    @Test
+    public void testHostNamesAreNotResolved() {
+        //A host name is not looked up, the remote address is unknown instead
+        assertNull(remoteAddressFor("localhost"));
+        assertNull(remoteAddressFor(""));
+        assertNull(remoteAddressFor(null));
+    }
+}
diff --git 
a/storm-webapp/src/test/java/org/apache/storm/daemon/ui/filters/AuthorizedUserFilterTest.java
 
b/storm-webapp/src/test/java/org/apache/storm/daemon/ui/filters/AuthorizedUserFilterTest.java
index 5e9e2c5c1..41c8eb5c3 100644
--- 
a/storm-webapp/src/test/java/org/apache/storm/daemon/ui/filters/AuthorizedUserFilterTest.java
+++ 
b/storm-webapp/src/test/java/org/apache/storm/daemon/ui/filters/AuthorizedUserFilterTest.java
@@ -24,10 +24,13 @@ import jakarta.ws.rs.core.Response;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.Map;
+import javax.security.auth.Subject;
 import org.apache.storm.daemon.ui.resources.AuthNimbusOp;
 import org.apache.storm.daemon.ui.resources.UnauthenticatedNimbusOp;
 import org.apache.storm.security.auth.IAuthorizer;
 import org.apache.storm.security.auth.ReqContext;
+import org.apache.storm.security.auth.SingleUserPrincipal;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 
@@ -50,6 +53,10 @@ public class AuthorizedUserFilterTest {
         public void gated() {
         }
 
+        @AuthNimbusOp(value = "getTopology", needsTopoId = true)
+        public void topologyGated() {
+        }
+
         @UnauthenticatedNimbusOp("filtered by Nimbus using the authenticated 
remote user")
         public void optedOut() {
         }
@@ -79,6 +86,86 @@ public class AuthorizedUserFilterTest {
         return response.getValue().getStatus();
     }
 
+    @AfterEach
+    public void resetContext() {
+        ReqContext.context().setSubject(null);
+        ReqContext.context().setRealPrincipal(null);
+        ReqContext.context().setRemoteAddress(null);
+        AuthorizedUserFilter.uiImpersonationHandler = null;
+    }
+
+    /**
+     * Sets up the current request context for a request by {@code realUser} 
asking to act as {@code user}.
+     */
+    private static void setImpersonation(String realUser, String user) {
+        Subject subject = new Subject();
+        subject.getPrincipals().add(new SingleUserPrincipal(user));
+        ReqContext.context().setSubject(subject);
+        ReqContext.context().setRealPrincipal(new 
SingleUserPrincipal(realUser));
+    }
+
+    @Test
+    public void 
doAsUserWithoutAuthenticatedUserIsDeniedWithoutImpersonationAuthorizer() throws 
Exception {
+        IAuthorizer aclHandler = mock(IAuthorizer.class);
+        when(aclHandler.permit(any(), any(), any())).thenReturn(true);
+        AuthorizedUserFilter filter = filterFor("gated", aclHandler);
+        setImpersonation(null, "alice");
+        ContainerRequestContext request = mock(ContainerRequestContext.class);
+
+        filter.filter(request);
+
+        assertEquals(401, statusOfAbort(request));
+        verify(aclHandler, never()).permit(any(), any(), any());
+    }
+
+    @Test
+    public void 
doAsUserWithoutAuthenticatedUserIsDeniedWithImpersonationAuthorizer() throws 
Exception {
+        IAuthorizer aclHandler = mock(IAuthorizer.class);
+        when(aclHandler.permit(any(), any(), any())).thenReturn(true);
+        AuthorizedUserFilter filter = filterFor("gated", aclHandler);
+        IAuthorizer impersonationHandler = mock(IAuthorizer.class);
+        AuthorizedUserFilter.uiImpersonationHandler = impersonationHandler;
+        setImpersonation(null, "alice");
+        ContainerRequestContext request = mock(ContainerRequestContext.class);
+
+        filter.filter(request);
+
+        assertEquals(401, statusOfAbort(request));
+        verify(aclHandler, never()).permit(any(), any(), any());
+    }
+
+    @Test
+    public void 
doAsUserWithoutAuthenticatedUserIsDeniedBeforeTopologyConfLookup() throws 
Exception {
+        IAuthorizer aclHandler = mock(IAuthorizer.class);
+        AuthorizedUserFilter filter = filterFor("topologyGated", aclHandler);
+        setImpersonation(null, "alice");
+        ContainerRequestContext request = mock(ContainerRequestContext.class);
+
+        filter.filter(request);
+
+        assertEquals(401, statusOfAbort(request));
+        //The topology id is read only to fetch the topology conf from Nimbus, 
so Nimbus is never called
+        verify(request, never()).getUriInfo();
+        verify(aclHandler, never()).permit(any(), any(), any());
+    }
+
+    @Test
+    public void deniedImpersonationWithoutRemoteAddressIsRejected() throws 
Exception {
+        IAuthorizer aclHandler = mock(IAuthorizer.class);
+        when(aclHandler.permit(any(), any(), any())).thenReturn(true);
+        AuthorizedUserFilter filter = filterFor("gated", aclHandler);
+        IAuthorizer impersonationHandler = mock(IAuthorizer.class);
+        when(impersonationHandler.permit(any(), any(), 
any())).thenReturn(false);
+        AuthorizedUserFilter.uiImpersonationHandler = impersonationHandler;
+        setImpersonation("bob", "alice");
+        ContainerRequestContext request = mock(ContainerRequestContext.class);
+
+        filter.filter(request);
+
+        assertEquals(401, statusOfAbort(request));
+        verify(aclHandler, never()).permit(any(), any(), any());
+    }
+
     @Test
     public void unannotatedEndpointIsDenied() throws Exception {
         IAuthorizer aclHandler = mock(IAuthorizer.class);

Reply via email to