[ 
https://issues.apache.org/jira/browse/KNOX-3460?focusedWorklogId=1042463&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-1042463
 ]

ASF GitHub Bot logged work on KNOX-3460:
----------------------------------------

                Author: ASF GitHub Bot
            Created on: 17/Sep/26 19:49
            Start Date: 17/Sep/26 19:49
    Worklog Time Spent: 10m 
      Work Description: hsheinblatt commented on code in PR #1409:
URL: https://github.com/apache/knox/pull/1409#discussion_r4040712435


##########
gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandler.java:
##########


Review Comment:
   I think every handleValidationError call needs an audit call first in this 
design, so missing here.



##########
gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandler.java:
##########
@@ -348,6 +232,165 @@ ActionOutcome.SUCCESS, auditMessage(policyDecision, 
actorIdentity, subjectToken,
     }
   }
 
+  private boolean validateExchangeRequestParameters(HttpServletRequest 
request, HttpServletResponse response,
+                                                    HttpServletRequest 
bodyRequest, String subjectTokenValue, boolean hasActorToken) throws 
IOException {
+    final String subjectTokenType = 
bodyRequest.getParameter(SUBJECT_TOKEN_TYPE);
+    final String actorTokenType = bodyRequest.getParameter(ACTOR_TOKEN_TYPE);
+    final boolean hasActorTokenType = actorTokenType != null && 
!actorTokenType.isEmpty();
+
+    // RFC 8693 section 2.1: subject_token and subject_token_type are REQUIRED.
+    if (subjectTokenValue == null || subjectTokenValue.isEmpty()) {
+      filter.handleValidationError(request, response, 
HttpServletResponse.SC_BAD_REQUEST,

Review Comment:
   I think all these need audit messages with them. caller just returns, 
doesn't conditionally audit.



##########
gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandlerTest.java:
##########
@@ -297,6 +320,77 @@ public void 
testSameSubjectExchangeNeverCallsPolicyEvaluation() throws Exception
     assertNull(filter.capturedPolicyCheckRequest);
   }
 
+  @Test
+  public void testSameSubjectExchangeAuditsSuccess() throws Exception {
+    // A plain same-subject exchange previously emitted no TOKEN_EXCHANGE 
record (only the generic
+    // AUTHENTICATION audit). It must now audit SUCCESS with the subject 
acting as its own actor.
+    filter.valid.put("subtok", jwt("alice", "KNOXSSO"));
+    final Capture<String> auditMessage = expectAudit(Action.TOKEN_EXCHANGE, 
"USER/alice",
+        ResourceType.PRINCIPAL, ActionOutcome.SUCCESS);
+    handler.handle(request("subtok", JWT_TYPE, null, null), response, chain);
+
+    assertTrue(filter.continued);
+    EasyMock.verify(auditor);
+    
assertTrue(auditMessage.getValue().contains("event_type=token_exchange_allowed"));
+    assertTrue(auditMessage.getValue().contains("actor_authority=USER"));
+    assertTrue(auditMessage.getValue().contains("actor_id=alice"));
+    assertTrue(auditMessage.getValue().contains("subject_token_iss=KNOXSSO"));
+    assertTrue(auditMessage.getValue().contains("subject_token_sub=alice"));
+  }
+
+  @Test
+  public void 
testSameSubjectExchangeWithAuthorizedRequestedAudienceAuditsSuccess() throws 
Exception {
+    // Honoring on and the requested audience is authorized (carried by the 
subject token): the
+    // exchange succeeds and audits SUCCESS once.
+    filter.tokenExchangeSameSubjectRequestedAudienceEnabled = true;
+    filter.valid.put("subtok", jwtWithAudiences("alice", "KNOXSSO", 
"service-a"));
+    final Capture<String> auditMessage = expectAudit(Action.TOKEN_EXCHANGE, 
"USER/alice",
+        ResourceType.PRINCIPAL, ActionOutcome.SUCCESS);
+    handler.handle(exchangeRequest("subtok", null, new String[] 
{"service-a"}), response, chain);
+
+    assertTrue(filter.continued);
+    EasyMock.verify(auditor);
+    
assertTrue(auditMessage.getValue().contains("event_type=token_exchange_allowed"));
+    
assertTrue(auditMessage.getValue().contains("requested_resources=[service-a]"));
+    assertTrue(auditMessage.getValue().contains("audiences_honored=true"));
+  }
+
+  @Test
+  public void 
testSameSubjectRequestedAudienceDroppedWhenHonoringDisabledAuditsNotHonored() 
throws Exception {
+    // Honoring off but the request carried audiences: they are silently 
dropped. The SUCCESS record
+    // still lists what was requested but must report audiences_honored=false, 
so a reader never
+    // mistakes the listed-but-dropped requested_resources for honored ones.
+    filter.valid.put("subtok", jwtWithAudiences("alice", "KNOXSSO", 
"service-a"));
+    final Capture<String> auditMessage = expectAudit(Action.TOKEN_EXCHANGE, 
"USER/alice",
+        ResourceType.PRINCIPAL, ActionOutcome.SUCCESS);
+    handler.handle(exchangeRequest("subtok", null, new String[] 
{"service-a"}), response, chain);
+
+    assertTrue(filter.continued);
+    EasyMock.verify(auditor);
+    
assertTrue(auditMessage.getValue().contains("event_type=token_exchange_allowed"));
+    
assertTrue(auditMessage.getValue().contains("requested_resources=[service-a]"));
+    assertTrue(auditMessage.getValue().contains("audiences_honored=false"));
+  }
+
+  @Test
+  public void testSameSubjectRequestedAudienceRejectionAuditsFailure() throws 
Exception {
+    // Honoring on but the requested audience is not among the subject token's 
own aud claim: the
+    // exchange is rejected as invalid_target and must audit FAILURE with the 
deny reason.
+    filter.tokenExchangeSameSubjectRequestedAudienceEnabled = true;
+    filter.valid.put("subtok", jwtWithAudiences("alice", "KNOXSSO", 
"service-a"));
+    final Capture<String> auditMessage = expectAudit(Action.TOKEN_EXCHANGE, 
"USER/alice",
+        ResourceType.PRINCIPAL, ActionOutcome.FAILURE);
+    handler.handle(exchangeRequest("subtok", null, new String[] 
{"service-b"}), response, chain);
+
+    assertFalse(filter.continued);
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, filter.errorStatus);
+    assertEquals("invalid_target", filter.error);
+    EasyMock.verify(auditor);
+    
assertTrue(auditMessage.getValue().contains("event_type=token_exchange_denied"));
+    
assertTrue(auditMessage.getValue().contains("deny_reason=requested_audience_not_authorized"));
+    assertTrue(auditMessage.getValue().contains("subject_token_sub=alice"));
+  }
+

Review Comment:
   If we add audits for all the other failure cases, then each needs a test 
too. Possibly we can simplify those to invalid requests with a simpler message, 
but it would be good to have the actor who requested an invalid request and for 
whom.



##########
gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/TokenExchangeHandler.java:
##########
@@ -348,6 +232,165 @@ ActionOutcome.SUCCESS, auditMessage(policyDecision, 
actorIdentity, subjectToken,
     }
   }
 
+  private boolean validateExchangeRequestParameters(HttpServletRequest 
request, HttpServletResponse response,
+                                                    HttpServletRequest 
bodyRequest, String subjectTokenValue, boolean hasActorToken) throws 
IOException {
+    final String subjectTokenType = 
bodyRequest.getParameter(SUBJECT_TOKEN_TYPE);
+    final String actorTokenType = bodyRequest.getParameter(ACTOR_TOKEN_TYPE);
+    final boolean hasActorTokenType = actorTokenType != null && 
!actorTokenType.isEmpty();
+
+    // RFC 8693 section 2.1: subject_token and subject_token_type are REQUIRED.
+    if (subjectTokenValue == null || subjectTokenValue.isEmpty()) {
+      filter.handleValidationError(request, response, 
HttpServletResponse.SC_BAD_REQUEST,
+              "invalid_request", "the subject_token parameter is required");
+      return false;
+    }
+    if (subjectTokenType == null || subjectTokenType.isEmpty()) {
+      filter.handleValidationError(request, response, 
HttpServletResponse.SC_BAD_REQUEST,
+              "invalid_request", "the subject_token_type parameter is 
required");
+      return false;
+    }
+    // RFC 8693 section 2.1: actor_token_type is REQUIRED when actor_token is 
present and MUST NOT
+    // be present otherwise.
+    if (hasActorToken && !hasActorTokenType) {
+      filter.handleValidationError(request, response, 
HttpServletResponse.SC_BAD_REQUEST,
+              "invalid_request", "actor_token_type is required when 
actor_token is present");
+      return false;
+    }
+    if (!hasActorToken && hasActorTokenType) {
+      filter.handleValidationError(request, response, 
HttpServletResponse.SC_BAD_REQUEST,
+              "invalid_request", "actor_token_type must not be present without 
actor_token");
+      return false;
+    }
+    // Only JWT-family token types are supported.
+    if (isNotSupportedTokenType(subjectTokenType)) {
+      filter.handleValidationError(request, response, 
HttpServletResponse.SC_BAD_REQUEST,
+              "invalid_request", "unsupported subject_token_type " + 
subjectTokenType);
+      return false;
+    }
+    if (hasActorToken && isNotSupportedTokenType(actorTokenType)) {
+      filter.handleValidationError(request, response, 
HttpServletResponse.SC_BAD_REQUEST,
+              "invalid_request", "unsupported actor_token_type " + 
actorTokenType);
+      return false;
+    }
+    return true;
+  }
+
+  private DelegationTokenExchangeOutcome 
handleDelegationExchange(HttpServletRequest request,
+      HttpServletResponse response, FilterChain chain, JWT subjectToken, 
boolean hasActorToken,
+      String actorTokenValue, String requestedSubjectValue, boolean 
requestedSubjectDiffersFromSubject,
+      List<String> requestedAudiences)
+      throws IOException, ServletException, ParseException, 
UnknownTokenException {
+    final Set<String> uniqueRequestedAudiences = 
distinctNonBlankValues(requestedAudiences);
+    if (!validateDelegationExchangeRequest(request, response, hasActorToken,
+        requestedSubjectDiffersFromSubject, requestedSubjectValue, 
uniqueRequestedAudiences)) {
+      // Rejected; error response already sent.
+      return DelegationTokenExchangeOutcome.rejected();
+    }
+
+    // The actor for this exchange is the actor_token's identity when an 
actor_token is
+    // present, or the subject_token's identity when this is a headless 
delegation
+    // exchange. Either way it must be parsed/validated before the policy 
check below; when
+    // an actor_token is present it is reused for Subject construction further 
down.
+    JWT actorToken = null;
+    if (hasActorToken) {
+      actorToken = filter.parseAndValidateJWT(request, response, chain, 
actorTokenValue);
+      if (actorToken == null) {
+        // Validation failed, error response already sent
+        return DelegationTokenExchangeOutcome.rejected();
+      }
+    }
+    final int actChainDepth = hasActorToken ? 
TokenUtils.extractActorChain(subjectToken).size() : 0;
+    final JWT actorIdentitySource = hasActorToken ? actorToken : subjectToken;
+    final ActorIdentity actorIdentity = 
ActorIdentity.fromJwt(actorIdentitySource);
+
+    // A single policy-evaluation call per exchange, carrying the full 
validated requested-
+    // resource set and an always-empty requestedScopes set (scope enforcement 
is deferred).
+    final PolicyCheckRequest policyCheckRequest = new PolicyCheckRequest(
+        actorIdentity.actorAuthority, actorIdentity.actorId,
+        requestedSubjectDiffersFromSubject ? requestedSubjectValue : 
subjectToken.getSubject(),
+        uniqueRequestedAudiences, Collections.emptySet(), 
requestedSubjectDiffersFromSubject);
+
+    // Policy evaluation resolves canActFor.users and canActFor.groups (the 
latter via an LDAP
+    // group lookup on the impersonated subject) and returns a decision; a 
subject that matches
+    // neither is reported as a denial below, not as an error.
+    final PolicyDecision policyDecision;
+    try {
+      policyDecision = filter.evaluateDelegationPolicy(policyCheckRequest);
+    } catch (DelegationGroupLookupUnavailableException e) {
+      // The policy is group-based but its canActFor.groups rule could not be 
evaluated: LDAP is
+      // either disabled/absent or the group lookup itself failed. This is a 
server-side
+      // condition, not a policy denial, so surface a server_error directing 
the operator to LDAP
+      // rather than a misleading rejection. The underlying cause is logged by 
the policy service.
+      // 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);
+      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 "
+              + "and reachable to evaluate group-based delegation policies");
+      return DelegationTokenExchangeOutcome.rejected();
+    }
+
+    if (policyDecision.getDenyReason() != null) {
+      auditing.denied(actorIdentity, subjectToken, requestedSubjectValue,
+          policyDecision.getDenyReason(), uniqueRequestedAudiences, 
actChainDepth);
+      // 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);
+
+    request.setAttribute(CommonTokenConstants.REQUESTED_TTL_REQUEST_ATTR, 
policyDecision.getEffectiveTtlSec());
+
+    return DelegationTokenExchangeOutcome.authorized(actorToken, 
!requestedAudiences.isEmpty());
+  }
+
+  private boolean validateDelegationExchangeRequest(HttpServletRequest 
request, HttpServletResponse response,
+      boolean hasActorToken, boolean requestedSubjectDiffersFromSubject, 
String requestedSubjectValue,
+      Set<String> uniqueRequestedAudiences) throws IOException {
+    // Delegation exchanges are default denied unless 
DELEGATION_SERVER_ENABLED is set to true. When
+    // true, only authorized token exchanges will be permitted. Otherwise, any 
actor could impersonate
+    // any subject without authorization.
+    if (!filter.isDelegationServerEnabled()) {
+      filter.handleValidationError(request, response, 
HttpServletResponse.SC_BAD_REQUEST,

Review Comment:
   I think all these calls to handleValidationError need audit calls before 
them. The caller doesn't do it.





Issue Time Tracking
-------------------

    Worklog Id:     (was: 1042463)
    Time Spent: 40m  (was: 0.5h)

> Audit all token-exchange flows including minted token fields
> ------------------------------------------------------------
>
>                 Key: KNOX-3460
>                 URL: https://issues.apache.org/jira/browse/KNOX-3460
>             Project: Apache Knox
>          Issue Type: Sub-task
>          Components: Server
>    Affects Versions: 3.1.0
>            Reporter: Sandor Molnar
>            Assignee: Sandor Molnar
>            Priority: Major
>             Fix For: 3.1.0
>
>          Time Spent: 40m
>  Remaining Estimate: 0h
>
> Only the delegation policy decision is audited today (TokenExchangeHandler 
> success/failure inside the delegation branch). The same-subject exchange path 
> emits no TOKEN_EXCHANGE audit record at all (only the generic AUTHENTICATION 
> audit from the shared {{{}continueWithEstablishedSecurityContext{}}}). 
> Minted-token fields — jti, expiry, issued_subject — are unauditable from the 
> filter because they only exist after TokenResource mints the token, and 
> TokenResource has no auditing whatsoever today.
> *Scope:*
>  # Audit all three exchange paths in the handler with the fields known 
> pre-mint (incl. same-subject).
>  # Add a TOKEN_EXCHANGE mint-time audit in TokenResource.getJWT() for 
> exchange-originated mints, emitting jti/expiry/issued_subject
> *Explicitly out of scope:* auditing ordinary (non-exchange) token issuance.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to