You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by ha...@apache.org on 2026/09/21 11:39:57 UTC
(knox) branch master updated: KNOX-3477: Capture the RFC 8693 delegation actor-chain in gateway-audit.log (#1419)
This is an automated email from the ASF dual-hosted git repository.
hanicz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new 02428a7d5 KNOX-3477: Capture the RFC 8693 delegation actor-chain in gateway-audit.log (#1419)
02428a7d5 is described below
commit 02428a7d5457dabe36af182a0f877e6120b0432d
Author: hanicz <ha...@users.noreply.github.com>
AuthorDate: Mon Sep 21 13:39:52 2026 +0200
KNOX-3477: Capture the RFC 8693 delegation actor-chain in gateway-audit.log (#1419)
---
.../federation/jwt/filter/AbstractJWTFilter.java | 7 +-
.../jwt/filter/TokenExchangeAuditing.java | 30 ++--
.../jwt/filter/TokenExchangeHandler.java | 14 +-
.../AuthenticationActorChainAuditTest.java | 173 +++++++++++++++++++++
.../jwt/filter/TokenExchangeHandlerTest.java | 2 +
.../apache/knox/gateway/security/SubjectUtils.java | 33 ++++
.../knox/gateway/security/SubjectUtilsTest.java | 75 +++++++++
7 files changed, 315 insertions(+), 19 deletions(-)
diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
index 27035fa46..bc4a34e11 100644
--- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
+++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
@@ -370,7 +370,12 @@ public abstract class AbstractJWTFilter implements Filter {
auditService.attachContext(context);
String sourceUri = (String)request.getAttribute( AbstractGatewayFilter.SOURCE_REQUEST_CONTEXT_URL_ATTRIBUTE_NAME );
if (sourceUri != null) {
- auditor.audit( Action.AUTHENTICATION , sourceUri, ResourceType.URI, ActionOutcome.SUCCESS );
+ final String actorChain = SubjectUtils.renderActorChain(SubjectUtils.getActorChainPrincipal(subject));
+ if (actorChain.isEmpty()) {
+ auditor.audit( Action.AUTHENTICATION , sourceUri, ResourceType.URI, ActionOutcome.SUCCESS );
+ } else {
+ auditor.audit( Action.AUTHENTICATION , sourceUri, ResourceType.URI, ActionOutcome.SUCCESS, "act_chain=" + actorChain );
+ }
}
}
diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeAuditing.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeAuditing.java
index bc4f30ca8..b45435be5 100644
--- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeAuditing.java
+++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeAuditing.java
@@ -22,9 +22,12 @@ import org.apache.knox.gateway.audit.api.AuditServiceFactory;
import org.apache.knox.gateway.audit.api.Auditor;
import org.apache.knox.gateway.audit.api.ResourceType;
import org.apache.knox.gateway.audit.log4j.audit.AuditConstants;
+import org.apache.knox.gateway.security.SubjectUtils;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import java.util.Collections;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -56,18 +59,18 @@ final class TokenExchangeAuditing {
* mistakes a listed-but-dropped {@code requested_resources} for honored)
*/
void allowed(ActorIdentity actor, JWT subjectToken, String requestedSubject,
- Set<String> requestedResources, boolean audiencesHonored, int actChainDepth) {
+ Set<String> requestedResources, boolean audiencesHonored, List<Map<String, Object>> actChain) {
auditor.audit(Action.TOKEN_EXCHANGE, actor.resourceName(), ResourceType.PRINCIPAL,
ActionOutcome.SUCCESS, message("token_exchange_allowed", null, actor, subjectToken,
- requestedSubject, requestedResources, audiencesHonored, actChainDepth));
+ requestedSubject, requestedResources, audiencesHonored, actChain));
}
/** Audit a policy/validation denial. Nothing is minted, so no audiences are honored. */
void denied(ActorIdentity actor, JWT subjectToken, String requestedSubject, String denyReason,
- Set<String> requestedResources, int actChainDepth) {
+ Set<String> requestedResources, List<Map<String, Object>> actChain) {
auditor.audit(Action.TOKEN_EXCHANGE, actor.resourceName(), ResourceType.PRINCIPAL,
ActionOutcome.FAILURE, message("token_exchange_denied", "deny_reason=" + denyReason, actor,
- subjectToken, requestedSubject, requestedResources, false, actChainDepth));
+ subjectToken, requestedSubject, requestedResources, false, actChain));
}
/**
@@ -75,10 +78,10 @@ final class TokenExchangeAuditing {
* delegation policy needs is unavailable). Nothing is minted, so no audiences are honored.
*/
void unavailable(ActorIdentity actor, JWT subjectToken, String requestedSubject, String reason,
- Set<String> requestedResources, int actChainDepth) {
+ Set<String> requestedResources, List<Map<String, Object>> actChain) {
auditor.audit(Action.TOKEN_EXCHANGE, actor.resourceName(), ResourceType.PRINCIPAL,
ActionOutcome.UNAVAILABLE, message("token_exchange_unavailable", "reason=" + reason, actor,
- subjectToken, requestedSubject, requestedResources, false, actChainDepth));
+ subjectToken, requestedSubject, requestedResources, false, actChain));
}
/**
@@ -103,7 +106,7 @@ final class TokenExchangeAuditing {
final String resourceName = actor != null ? actor.resourceName() : "unknown";
auditor.audit(Action.TOKEN_EXCHANGE, resourceName, ResourceType.PRINCIPAL, ActionOutcome.FAILURE,
message("token_exchange_rejected", "reason=" + reason, actor, subjectToken, requestedSubject,
- Collections.emptySet(), false, 0));
+ Collections.emptySet(), false, Collections.emptyList()));
}
/**
@@ -111,8 +114,9 @@ final class TokenExchangeAuditing {
* value (token_exchange_allowed / token_exchange_denied / token_exchange_unavailable /
* token_exchange_rejected) and {@code reasonField} is a fully-formed, pre-labeled reason token
* (e.g. {@code deny_reason=...} or {@code reason=...}) appended verbatim, or null when there is
- * none. {@code actChainDepth} is the depth of the delegation history arriving on the subject_token
- * (0 for a headless exchange, whose incoming chain is not propagated).
+ * none. {@code actChain} is the delegation history arriving on the subject_token, from which both
+ * {@code act_chain_depth} and the rendered {@code act_chain} are derived (empty for a headless
+ * exchange, whose incoming chain is not propagated).
*
* <p>{@code actor} and {@code subjectToken} may be null for a rejection audited before the
* request's identity could be established; their fields are then reported empty.</p>
@@ -120,7 +124,7 @@ final class TokenExchangeAuditing {
private static String message(String eventType, String reasonField, ActorIdentity actor,
JWT subjectToken, String requestedSubject,
Set<String> requestedResources, boolean audiencesHonored,
- int actChainDepth) {
+ List<Map<String, Object>> actChain) {
final StringBuilder message = new StringBuilder();
message.append("event_type=").append(eventType);
if (reasonField != null) {
@@ -133,7 +137,11 @@ final class TokenExchangeAuditing {
message.append(" requested_subject=").append(label(requestedSubject));
message.append(" requested_resources=").append(requestedResources);
message.append(" audiences_honored=").append(audiencesHonored);
- message.append(" act_chain_depth=").append(actChainDepth);
+ message.append(" act_chain_depth=").append(actChain == null ? 0 : actChain.size());
+ final String renderedChain = SubjectUtils.renderActorChain(actChain);
+ if (!renderedChain.isEmpty()) {
+ message.append(" act_chain=").append(renderedChain);
+ }
return message.toString();
}
diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandler.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandler.java
index f4f1b1aaf..7308a506f 100644
--- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandler.java
+++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandler.java
@@ -182,7 +182,7 @@ class TokenExchangeHandler {
// carries (it is exchanged as-is; TokenResource preserves any existing chain).
final ActorIdentity subjectAsActor = ActorIdentity.fromJwt(subjectToken);
final Set<String> uniqueRequestedAudiences = distinctNonBlankValues(requestedAudiences);
- final int actChainDepth = TokenUtils.extractActorChain(subjectToken).size();
+ final List<Map<String, Object>> actChain = TokenUtils.extractActorChain(subjectToken);
if (!requestedAudiences.isEmpty() && filter.isTokenExchangeSameSubjectRequestedAudienceEnabled()) {
// Honoring enabled: authorize each requested audience against the subject token's own aud
// claim; a value the subject token does not already carry is rejected. This is what allows a
@@ -191,7 +191,7 @@ class TokenExchangeHandler {
final Set<String> subjectAudiences = audienceClaimSet(subjectToken);
if (!subjectAudiences.containsAll(uniqueRequestedAudiences)) {
auditing.denied(subjectAsActor, subjectToken, null,
- "requested_audience_not_authorized", uniqueRequestedAudiences, actChainDepth);
+ "requested_audience_not_authorized", uniqueRequestedAudiences, actChain);
filter.handleValidationError(request, response, HttpServletResponse.SC_BAD_REQUEST,
"invalid_target",
"The requested audience is not authorized for this subject");
@@ -205,7 +205,7 @@ class TokenExchangeHandler {
// SUCCESS record still lists the requested resources but reports audiences_honored=false, so a
// dropped request is never mistaken for an honored one.
auditing.allowed(subjectAsActor, subjectToken, null, uniqueRequestedAudiences,
- conveyRequestedAudiences, actChainDepth);
+ conveyRequestedAudiences, actChain);
}
final Subject subject;
@@ -316,7 +316,7 @@ class TokenExchangeHandler {
return DelegationTokenExchangeOutcome.rejected();
}
}
- final int actChainDepth = hasActorToken ? TokenUtils.extractActorChain(subjectToken).size() : 0;
+ final List<Map<String, Object>> actChain = hasActorToken ? TokenUtils.extractActorChain(subjectToken) : Collections.emptyList();
final JWT actorIdentitySource = hasActorToken ? actorToken : subjectToken;
final ActorIdentity actorIdentity = ActorIdentity.fromJwt(actorIdentitySource);
@@ -341,7 +341,7 @@ class TokenExchangeHandler {
// Audit it as an UNAVAILABLE outcome so every delegation exchange (allow/deny/unavailable)
// leaves a TOKEN_EXCHANGE record.
auditing.unavailable(actorIdentity, subjectToken, requestedSubjectValue,
- "delegation_group_lookup_unavailable", uniqueRequestedAudiences, actChainDepth);
+ "delegation_group_lookup_unavailable", uniqueRequestedAudiences, actChain);
filter.handleValidationError(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"server_error", "This delegation policy restricts canActFor by group, which requires "
+ "the LDAP service to resolve group membership; ensure the LDAP service is enabled "
@@ -351,14 +351,14 @@ class TokenExchangeHandler {
if (policyDecision.getDenyReason() != null) {
auditing.denied(actorIdentity, subjectToken, requestedSubjectValue,
- policyDecision.getDenyReason(), uniqueRequestedAudiences, actChainDepth);
+ policyDecision.getDenyReason(), uniqueRequestedAudiences, actChain);
// A single, generic denial that does not identify which requested value failed.
filter.handleValidationError(request, response, HttpServletResponse.SC_BAD_REQUEST,
"invalid_request", "The token exchange request is rejected by policy");
return DelegationTokenExchangeOutcome.rejected();
}
auditing.allowed(actorIdentity, subjectToken, requestedSubjectValue, uniqueRequestedAudiences,
- !requestedAudiences.isEmpty(), actChainDepth);
+ !requestedAudiences.isEmpty(), actChain);
request.setAttribute(CommonTokenConstants.REQUESTED_TTL_REQUEST_ATTR, policyDecision.getEffectiveTtlSec());
diff --git a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/AuthenticationActorChainAuditTest.java b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/AuthenticationActorChainAuditTest.java
new file mode 100644
index 000000000..932ad81bc
--- /dev/null
+++ b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/AuthenticationActorChainAuditTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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.knox.gateway.provider.federation;
+
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.junit.Assert.assertEquals;
+
+import org.apache.knox.gateway.audit.api.Action;
+import org.apache.knox.gateway.audit.api.ActionOutcome;
+import org.apache.knox.gateway.audit.api.AuditContext;
+import org.apache.knox.gateway.audit.api.AuditService;
+import org.apache.knox.gateway.audit.api.Auditor;
+import org.apache.knox.gateway.audit.api.ResourceType;
+import org.apache.knox.gateway.filter.AbstractGatewayFilter;
+import org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter;
+import org.apache.knox.gateway.security.ActorChainPrincipalImpl;
+import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.security.auth.Subject;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class AuthenticationActorChainAuditTest {
+
+ private static final String SOURCE_URI = "https://localhost:8443/gateway/sandbox/service";
+
+ private Auditor originalAuditor;
+ private AuditService originalAuditService;
+
+ @Before
+ public void setUp() throws Exception {
+ originalAuditor = (Auditor) auditorField().get(null);
+ originalAuditService = (AuditService) auditServiceField().get(null);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ auditorField().set(null, originalAuditor);
+ auditServiceField().set(null, originalAuditService);
+ }
+
+ @Test
+ public void testDelegatedTokenEmitsActChainInAuthenticationMessage() throws Exception {
+ final Capture<String> message = EasyMock.newCapture();
+ final Auditor auditor = EasyMock.createMock(Auditor.class);
+ auditor.audit(eq(Action.AUTHENTICATION), eq(SOURCE_URI), eq(ResourceType.URI),
+ eq(ActionOutcome.SUCCESS), EasyMock.capture(message));
+ expectLastCall().once();
+ EasyMock.replay(auditor);
+
+ invokeContinue(auditor, subjectWithChain(Arrays.asList(
+ actor("idp1", "svc-c"), actor("idp1", "svc-b"), actor("idp2", "svc-a"))));
+
+ EasyMock.verify(auditor);
+ assertEquals("act_chain=idp1/svc-c<-idp1/svc-b<-idp2/svc-a", message.getValue());
+ }
+
+ @Test
+ public void testNonDelegatedTokenEmitsNoMessage() throws Exception {
+ final Auditor auditor = EasyMock.createMock(Auditor.class);
+ auditor.audit(eq(Action.AUTHENTICATION), eq(SOURCE_URI), eq(ResourceType.URI),
+ eq(ActionOutcome.SUCCESS));
+ expectLastCall().once();
+ EasyMock.replay(auditor);
+
+ invokeContinue(auditor, subjectWithChain(null));
+
+ EasyMock.verify(auditor);
+ }
+
+ private void invokeContinue(Auditor auditor, Subject subject) throws Exception {
+ final AuditContext context = EasyMock.createNiceMock(AuditContext.class);
+ final AuditService auditService = EasyMock.createNiceMock(AuditService.class);
+ EasyMock.expect(auditService.getContext()).andReturn(context).anyTimes();
+ EasyMock.replay(context, auditService);
+
+ auditorField().set(null, auditor);
+ auditServiceField().set(null, auditService);
+
+ final HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class);
+ EasyMock.expect(request.getAttribute(AbstractGatewayFilter.SOURCE_REQUEST_CONTEXT_URL_ATTRIBUTE_NAME))
+ .andReturn(SOURCE_URI).anyTimes();
+ final HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class);
+ EasyMock.replay(request, response);
+
+ new TestHandler().invokeContinueWithEstablishedSecurityContext(subject, request, response,
+ new NoOpFilterChain());
+ }
+
+ private static Subject subjectWithChain(List<Map<String, Object>> chain) {
+ final Subject subject = new Subject();
+ subject.getPrincipals().add(new PrimaryPrincipal("alice"));
+ if (chain != null) {
+ subject.getPrincipals().add(new ActorChainPrincipalImpl(chain));
+ }
+ return subject;
+ }
+
+ private static Map<String, Object> actor(String iss, String sub) {
+ final Map<String, Object> actor = new LinkedHashMap<>();
+ actor.put("iss", iss);
+ actor.put("sub", sub);
+ return actor;
+ }
+
+ private static Field auditorField() throws Exception {
+ final Field field = AbstractJWTFilter.class.getDeclaredField("auditor");
+ field.setAccessible(true);
+ return field;
+ }
+
+ private static Field auditServiceField() throws Exception {
+ final Field field = AbstractJWTFilter.class.getDeclaredField("auditService");
+ field.setAccessible(true);
+ return field;
+ }
+
+ static final class TestHandler extends AbstractJWTFilter {
+ void invokeContinueWithEstablishedSecurityContext(Subject subject, HttpServletRequest request,
+ HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
+ continueWithEstablishedSecurityContext(subject, request, response, chain);
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
+ }
+
+ @Override
+ protected void handleValidationError(HttpServletRequest request, HttpServletResponse response,
+ int status, String error) {
+ }
+
+ @Override
+ public void destroy() {
+ }
+ }
+
+ static final class NoOpFilterChain implements FilterChain {
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response) {
+ }
+ }
+}
diff --git a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandlerTest.java b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandlerTest.java
index 588f199aa..2f693b198 100644
--- a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandlerTest.java
+++ b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandlerTest.java
@@ -501,6 +501,7 @@ public class TokenExchangeHandlerTest {
assertTrue(auditMessage.getValue().contains("actor_id=svc-dataservice"));
// subject_token carries no prior 'act' chain, so the audited incoming depth is 0
assertTrue(auditMessage.getValue().contains("act_chain_depth=0"));
+ assertFalse(auditMessage.getValue().contains("act_chain="));
}
@Test
@@ -516,6 +517,7 @@ public class TokenExchangeHandlerTest {
EasyMock.verify(auditor);
assertTrue(auditMessage.getValue().contains("act_chain_depth=2"));
+ assertTrue(auditMessage.getValue().contains("act_chain=actor-1<-actor-2"));
}
@Test
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java b/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java
index 9b6b50a62..b2f3632fc 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java
@@ -24,6 +24,8 @@ import javax.security.auth.Subject;
import java.security.AccessController;
import java.security.Principal;
import java.util.Collections;
+import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -106,6 +108,37 @@ public class SubjectUtils {
return currentSubject.getPrincipals(ActorChainPrincipal.class);
}
+ public static ActorChainPrincipal getActorChainPrincipal(Subject subject) {
+ if (subject == null) {
+ return null;
+ }
+ final Set<ActorChainPrincipal> principals = subject.getPrincipals(ActorChainPrincipal.class);
+ return principals.isEmpty() ? null : principals.iterator().next();
+ }
+
+ public static String renderActorChain(List<Map<String, Object>> actorChain) {
+ if (actorChain == null || actorChain.isEmpty()) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder();
+ for (Map<String, Object> actor : actorChain) {
+ if (sb.length() > 0) {
+ sb.append("<-");
+ }
+ final Object iss = actor.get("iss");
+ if (iss != null) {
+ sb.append(iss).append('/');
+ }
+ final Object sub = actor.get("sub");
+ sb.append(sub == null ? "" : sub.toString());
+ }
+ return sb.toString();
+ }
+
+ public static String renderActorChain(ActorChainPrincipal principal) {
+ return principal == null ? "" : renderActorChain(principal.getActorChain());
+ }
+
/**
* Get the TokenExchangePrincipal from the subject if present.
*
diff --git a/gateway-spi/src/test/java/org/apache/knox/gateway/security/SubjectUtilsTest.java b/gateway-spi/src/test/java/org/apache/knox/gateway/security/SubjectUtilsTest.java
new file mode 100644
index 000000000..3b9f1cdb5
--- /dev/null
+++ b/gateway-spi/src/test/java/org/apache/knox/gateway/security/SubjectUtilsTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.knox.gateway.security;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+public class SubjectUtilsTest {
+
+ private static Map<String, Object> actor(String iss, String sub) {
+ final Map<String, Object> actor = new LinkedHashMap<>();
+ if (iss != null) {
+ actor.put("iss", iss);
+ }
+ if (sub != null) {
+ actor.put("sub", sub);
+ }
+ return actor;
+ }
+
+ @Test
+ public void testRenderActorChainWithIssuerSubjectPairs() {
+ final List<Map<String, Object>> chain = Arrays.asList(
+ actor("idp1", "svc-c"), actor("idp1", "svc-b"), actor("idp2", "svc-a"));
+ assertEquals("idp1/svc-c<-idp1/svc-b<-idp2/svc-a", SubjectUtils.renderActorChain(chain));
+ }
+
+ @Test
+ public void testRenderActorChainFallsBackToSubjectWhenNoIssuer() {
+ final List<Map<String, Object>> chain = Arrays.asList(actor(null, "actor-1"), actor(null, "actor-2"));
+ assertEquals("actor-1<-actor-2", SubjectUtils.renderActorChain(chain));
+ }
+
+ @Test
+ public void testRenderActorChainMixesPresentAndAbsentIssuers() {
+ final List<Map<String, Object>> chain = Arrays.asList(actor("idp1", "svc-c"), actor(null, "svc-a"));
+ assertEquals("idp1/svc-c<-svc-a", SubjectUtils.renderActorChain(chain));
+ }
+
+ @Test
+ public void testRenderActorChainEmptyAndNull() {
+ assertEquals("", SubjectUtils.renderActorChain((List<Map<String, Object>>) null));
+ assertEquals("", SubjectUtils.renderActorChain(Collections.emptyList()));
+ assertEquals("", SubjectUtils.renderActorChain((ActorChainPrincipal) null));
+ }
+
+ @Test
+ public void testRenderActorChainFromPrincipal() {
+ final List<Map<String, Object>> chain = Arrays.asList(actor("idp1", "svc-c"), actor("idp2", "svc-a"));
+ assertEquals("idp1/svc-c<-idp2/svc-a",
+ SubjectUtils.renderActorChain(new ActorChainPrincipalImpl(chain)));
+ }
+}