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.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to