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

rzo1 pushed a commit to branch fix/drpc-impersonation-check
in repository https://gitbox.apache.org/repos/asf/storm.git

commit ebdefd9d57d9c82b23fc22c5ceb330054f10e98e
Author: Richard Zowalla <[email protected]>
AuthorDate: Mon Oct 19 16:37:46 2026 +0200

    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.
---
 SECURITY.md                                        |   2 +
 docs/SECURITY.md                                   |   2 +
 .../auth/authorizer/ImpersonationAuthorizer.java   |   9 +-
 .../java/org/apache/storm/daemon/drpc/DRPC.java    |  41 ++++-
 .../org/apache/storm/daemon/drpc/DRPCTest.java     | 130 ++++++++++++++
 .../storm/daemon/drpc/webapp/ReqContextFilter.java |  19 +-
 .../apache/storm/daemon/drpc/DRPCServerTest.java   | 199 +++++++++++++++++++++
 7 files changed, 392 insertions(+), 10 deletions(-)

diff --git a/SECURITY.md b/SECURITY.md
index 366c10271..3cd40f895 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -451,6 +451,8 @@ 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`).
+
 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..ebf233d46 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -620,6 +620,8 @@ 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`).
+
 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/authorizer/ImpersonationAuthorizer.java
 
b/storm-client/src/jvm/org/apache/storm/security/auth/authorizer/ImpersonationAuthorizer.java
index 22aa4e166..63d0294e1 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
@@ -125,8 +125,13 @@ public class ImpersonationAuthorizer implements 
IAuthorizer {
     }
 
     private boolean isAllowedToImpersonateFromHost(Set<String> 
authorizedHosts, InetAddress remoteAddress) {
-        return authorizedHosts.contains(WILD_CARD)
-                || 
authorizedHosts.contains(remoteAddress.getCanonicalHostName())
+        if (authorizedHosts.contains(WILD_CARD)) {
+            return true;
+        }
+        if (remoteAddress == null) {
+            return false;
+        }
+        return authorizedHosts.contains(remoteAddress.getCanonicalHostName())
                 || authorizedHosts.contains(remoteAddress.getHostName())
                 || authorizedHosts.contains(remoteAddress.getHostAddress());
     }
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..6b58e9c1b 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,36 @@ 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()) {
+            if (log) {
+                LOG.info("principal: {} is trying to impersonate principal: 
{}", reqContext.realPrincipal(), reqContext.principal());
+            } else {
+                LOG.debug("principal: {} is trying to impersonate principal: 
{}", reqContext.realPrincipal(), reqContext.principal());
+            }
+            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 + "'."
+                                                        + " 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 +164,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..4c6057f6b 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,9 +45,11 @@ 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;
@@ -72,6 +76,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 +368,127 @@ 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());
+        }
+    }
+
     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..0137bef4c 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,8 @@ import jakarta.ws.rs.container.PreMatching;
 import jakarta.ws.rs.ext.Provider;
 
 import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
 
 import org.apache.storm.security.auth.IHttpCredentialsPlugin;
 import org.apache.storm.security.auth.ReqContext;
@@ -48,8 +50,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, so no name 
lookup happens here.
+            return InetAddress.getByName(remoteAddr);
+        } catch (UnknownHostException e) {
+            return null;
         }
     }
 
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..8ed64e596 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,179 @@ 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);
+    }
 }

Reply via email to