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);