From 7e560f017a0b2981835ddb4b036d25f798148042 Mon Sep 17 00:00:00 2001 From: Fabian Morgan Date: Sun, 26 Jul 2026 11:53:09 -0700 Subject: [PATCH 1/2] improve s3:prefix Condition handling --- .../acl/iam/IamSessionPolicyResolver.java | 7 ++++- .../acl/iam/TestIamSessionPolicyResolver.java | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java index 4e591d14b5f0..415ae5e8af75 100644 --- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java +++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java @@ -309,7 +309,12 @@ private static Condition parsePrefixesFromConditions(JsonNode stmt) throws OMExc ERROR_PREFIX + "Invalid Condition operator value structure - " + operatorValue, MALFORMED_POLICY_DOCUMENT); } - final String keyName = operatorValue.fieldNames().hasNext() ? operatorValue.fieldNames().next() : null; + if (operatorValue.size() != 1) { + throw new OMException( + ERROR_PREFIX + "Only one Condition key is supported per operator", NOT_SUPPORTED_OPERATION); + } + + final String keyName = operatorValue.fieldNames().next(); if (!"s3:prefix".equalsIgnoreCase(keyName)) { throw new OMException(ERROR_PREFIX + "Unsupported Condition key name - " + keyName, NOT_SUPPORTED_OPERATION); } diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java index 2ac9d195cf53..2902aa4fba09 100644 --- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java +++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java @@ -138,6 +138,36 @@ public void testUnsupportedConditionAttributeThrows() { json, "IAM session policy: Unsupported Condition key name - aws:SourceArn", NOT_SUPPORTED_OPERATION); } + @Test + public void testMultipleConditionKeysWithPrefixFirstThrows() { + final String json = "{\n" + + " \"Statement\": [{\n" + + " \"Effect\": \"Allow\",\n" + + " \"Action\": \"s3:ListBucket\",\n" + + " \"Resource\": \"arn:aws:s3:::b\",\n" + + " \"Condition\": { \"StringEquals\": { \"s3:prefix\": \"x\", \"aws:SourceIp\": \"1.2.3.4\" } }\n" + + " }]\n" + + "}"; + + expectResolveThrowsForBothAuthorizers( + json, "IAM session policy: Only one Condition key is supported per operator", NOT_SUPPORTED_OPERATION); + } + + @Test + public void testMultipleConditionKeysWithUnsupportedKeyFirstThrows() { + final String json = "{\n" + + " \"Statement\": [{\n" + + " \"Effect\": \"Allow\",\n" + + " \"Action\": \"s3:ListBucket\",\n" + + " \"Resource\": \"arn:aws:s3:::b\",\n" + + " \"Condition\": { \"StringEquals\": { \"aws:SourceIp\": \"1.2.3.4\", \"s3:prefix\": \"x\" } }\n" + + " }]\n" + + "}"; + + expectResolveThrowsForBothAuthorizers( + json, "IAM session policy: Only one Condition key is supported per operator", NOT_SUPPORTED_OPERATION); + } + @Test public void testUnsupportedEffectThrows() { final String json = "{\n" + From dea43c12b769954ef7eb4e503d66324992693698 Mon Sep 17 00:00:00 2001 From: Fabian Morgan Date: Sun, 26 Jul 2026 12:17:40 -0700 Subject: [PATCH 2/2] reject unsupported AssumeRole parameters --- .../hadoop/ozone/s3sts/S3STSEndpoint.java | 161 ++++++++++++++-- .../hadoop/ozone/s3sts/S3STSEndpointBase.java | 10 + .../hadoop/ozone/s3sts/TestS3STSEndpoint.java | 179 +++++++++++++++++- 3 files changed, 327 insertions(+), 23 deletions(-) diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpoint.java index 1cc922301498..2f7f24285bef 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpoint.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpoint.java @@ -28,27 +28,34 @@ import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.STS_VALIDATION_ERROR; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.io.StringWriter; import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; -import javax.ws.rs.FormParam; +import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Form; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.apache.hadoop.ozone.audit.S3GAction; import org.apache.hadoop.ozone.om.exceptions.OMException; import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo; @@ -88,6 +95,19 @@ public class S3STSEndpoint extends S3STSEndpointBase { private static final String EXPECTED_VERSION = "2011-06-15"; + private static final String SIGV4_PARAM_PREFIX = "X-Amz-"; + + private static final Set ASSUME_ROLE_ALLOWED_PARAMS = ImmutableSet.of( + "Action", "RoleArn", "RoleSessionName", "DurationSeconds", "Version", "Policy"); + + private static final String POLICY_ARNS_MEMBER_PREFIX = "PolicyArns.member."; + private static final String PROVIDED_CONTEXTS_MEMBER_PREFIX = "ProvidedContexts.member."; + private static final String TAGS_MEMBER_PREFIX = "Tags.member."; + private static final String TRANSITIVE_TAG_KEYS_MEMBER_PREFIX = "TransitiveTagKeys.member."; + + private static final Set AWS_VALID_ASSUME_ROLE_OPTIONAL_PARAMS = ImmutableSet.of( + "ExternalId", "SerialNumber", "SourceIdentity", "TokenCode"); + // JAXBContext is relatively expensive to create and is threadsafe, so cache and reuse private static final JAXBContext JAXB_CONTEXT; @@ -128,35 +148,37 @@ public Response get( @QueryParam("Version") String version, @QueryParam("Policy") String awsIamSessionPolicy) throws OS3Exception { - return handleSTSRequest(action, roleArn, roleSessionName, durationSeconds, version, awsIamSessionPolicy); + return handleSTSRequest( + getQueryParameters().keySet(), action, roleArn, roleSessionName, durationSeconds, version, awsIamSessionPolicy); } /** * STS endpoint that handles POST requests with form data. * AWS STS typically uses POST requests with form-encoded parameters. * - * @param action The STS action to perform - * @param roleArn The ARN of the role to assume - * @param roleSessionName Session name for the role - * @param durationSeconds Duration of the token validity - * @param version AWS STS API version + * @param form form-encoded request parameters * @return Response containing STS response XML or error */ @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.APPLICATION_XML) - public Response post( - @FormParam("Action") String action, - @FormParam("RoleArn") String roleArn, - @FormParam("RoleSessionName") String roleSessionName, - @FormParam("DurationSeconds") Integer durationSeconds, - @FormParam("Version") String version, - @FormParam("Policy") String awsIamSessionPolicy) throws OS3Exception { - - return handleSTSRequest(action, roleArn, roleSessionName, durationSeconds, version, awsIamSessionPolicy); + public Response post(Form form) throws OS3Exception { + final MultivaluedMap formParams = form == null ? null : form.asMap(); + final String action = formParams == null ? null : formParams.getFirst("Action"); + final String roleArn = formParams == null ? null : formParams.getFirst("RoleArn"); + final String roleSessionName = formParams == null ? null : formParams.getFirst("RoleSessionName"); + final Integer durationSeconds = + parseIntegerOrNull(formParams == null ? null : formParams.getFirst("DurationSeconds")); + final String version = formParams == null ? null : formParams.getFirst("Version"); + final String awsIamSessionPolicy = formParams == null ? null : formParams.getFirst("Policy"); + + final Set formParamNames = formParams == null ? Collections.emptySet() : formParams.keySet(); + return handleSTSRequest( + formParamNames, action, roleArn, roleSessionName, durationSeconds, version, awsIamSessionPolicy); } - private Response handleSTSRequest(String action, String roleArn, String roleSessionName, - Integer durationSeconds, String version, String awsIamSessionPolicy) throws OS3Exception { + private Response handleSTSRequest(Set paramNamesToValidate, String action, String roleArn, + String roleSessionName, Integer durationSeconds, String version, String awsIamSessionPolicy) throws OS3Exception { final String requestId = requestIdentifier.getRequestId(); // NOTE: invalid, missing or unsupported actions are not added to the audit log try { @@ -170,7 +192,8 @@ private Response handleSTSRequest(String action, String roleArn, String roleSess switch (action) { case ASSUME_ROLE_ACTION: - return handleAssumeRole(roleArn, roleSessionName, durationSeconds, awsIamSessionPolicy, version, requestId); + return handleAssumeRole( + paramNamesToValidate, roleArn, roleSessionName, durationSeconds, awsIamSessionPolicy, version, requestId); // These operations are not supported yet case GET_SESSION_TOKEN_ACTION: case ASSUME_ROLE_WITH_SAML_ACTION: @@ -193,8 +216,8 @@ private Response handleSTSRequest(String action, String roleArn, String roleSess } } - private Response handleAssumeRole(String roleArn, String roleSessionName, Integer durationSeconds, - String awsIamSessionPolicy, String version, String requestId) throws OSTSException { + private Response handleAssumeRole(Set paramNamesToValidate, String roleArn, String roleSessionName, + Integer durationSeconds, String awsIamSessionPolicy, String version, String requestId) throws OSTSException { final String action = "AssumeRole"; final Map auditParams = getAuditParameters(); S3STSUtils.addAssumeRoleAuditParams( @@ -211,6 +234,16 @@ private Response handleAssumeRole(String roleArn, String roleSessionName, Intege throw exception; } + final AssumeRoleParamValidationResult assumeRoleParamValidationResult = validateAssumeRoleParameters( + paramNamesToValidate); + if (!assumeRoleParamValidationResult.getNotImplementedOptionalParams().isEmpty()) { + final OSTSException exception = new OSTSException(STS_UNSUPPORTED_OPERATION).withMessage( + "AssumeRole optional parameter(s) not implemented: " + + String.join(", ", assumeRoleParamValidationResult.getNotImplementedOptionalParams())); + getAuditLogger().logWriteFailure(buildAuditMessageForFailure(S3GAction.ASSUME_ROLE, auditParams, exception)); + throw exception; + } + final Set validationErrors = new HashSet<>(); int duration = durationSeconds == null ? S3STSUtils.DEFAULT_DURATION_SECONDS : durationSeconds; try { @@ -242,6 +275,11 @@ private Response handleAssumeRole(String roleArn, String roleSessionName, Intege validationErrors.add(e.getMessage()); } + if (!assumeRoleParamValidationResult.getUnsupportedParams().isEmpty()) { + validationErrors.add("Unsupported AssumeRole parameter(s): " + String.join(", ", + assumeRoleParamValidationResult.getUnsupportedParams())); + } + final int numValidationErrors = validationErrors.size(); if (numValidationErrors > 0) { //noinspection StringBufferReplaceableByString @@ -303,6 +341,87 @@ private Response handleAssumeRole(String roleArn, String roleSessionName, Intege } } + private AssumeRoleParamValidationResult validateAssumeRoleParameters(Set paramNamesToValidate) { + if (paramNamesToValidate == null || paramNamesToValidate.isEmpty()) { + return AssumeRoleParamValidationResult.empty(); + } + + final List notImplementedOptionalParams = new ArrayList<>(); + final List unsupportedParams = new ArrayList<>(); + for (String paramName : paramNamesToValidate) { + if (isAllowedAssumeRoleParameter(paramName)) { + continue; + } + + if (isAwsValidButNotImplementedAssumeRoleParameter(paramName)) { + notImplementedOptionalParams.add(paramName); + } else { + unsupportedParams.add(paramName); + } + } + + Collections.sort(notImplementedOptionalParams); + Collections.sort(unsupportedParams); + return new AssumeRoleParamValidationResult(notImplementedOptionalParams, unsupportedParams); + } + + private static boolean isAllowedAssumeRoleParameter(String paramName) { + return StringUtils.isBlank(paramName) + || ASSUME_ROLE_ALLOWED_PARAMS.contains(paramName) + || Strings.CI.startsWith(paramName, SIGV4_PARAM_PREFIX); + } + + private static boolean isAwsValidButNotImplementedAssumeRoleParameter(String paramName) { + if (StringUtils.isBlank(paramName)) { + return false; + } + if (AWS_VALID_ASSUME_ROLE_OPTIONAL_PARAMS.contains(paramName)) { + return true; + } + + return Strings.CI.startsWith(paramName, POLICY_ARNS_MEMBER_PREFIX) + || Strings.CI.startsWith(paramName, PROVIDED_CONTEXTS_MEMBER_PREFIX) + || Strings.CI.startsWith(paramName, TAGS_MEMBER_PREFIX) + || Strings.CI.startsWith(paramName, TRANSITIVE_TAG_KEYS_MEMBER_PREFIX); + } + + private static Integer parseIntegerOrNull(String value) throws OSTSException { + if (StringUtils.isBlank(value)) { + return null; + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new OSTSException(STS_VALIDATION_ERROR) + .withMessage("1 validation error detected: Invalid Value: DurationSeconds must be a number"); + } + } + + private static final class AssumeRoleParamValidationResult { + private static final AssumeRoleParamValidationResult EMPTY = new AssumeRoleParamValidationResult( + Collections.emptyList(), Collections.emptyList()); + + private final List notImplementedOptionalParams; + private final List unsupportedParams; + + private AssumeRoleParamValidationResult(List notImplementedOptionalParams, List unsupportedParams) { + this.notImplementedOptionalParams = notImplementedOptionalParams; + this.unsupportedParams = unsupportedParams; + } + + private static AssumeRoleParamValidationResult empty() { + return EMPTY; + } + + private List getNotImplementedOptionalParams() { + return notImplementedOptionalParams; + } + + private List getUnsupportedParams() { + return unsupportedParams; + } + } + private String generateAssumeRoleResponse(String assumedRoleUserArn, AssumeRoleResponseInfo responseInfo, String requestId) throws IOException { final String accessKeyId = responseInfo.getAccessKeyId(); diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpointBase.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpointBase.java index 027784b0edc9..de52a29d6af2 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpointBase.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpointBase.java @@ -23,6 +23,8 @@ import javax.inject.Inject; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.core.Context; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; import org.apache.hadoop.ozone.audit.AuditAction; import org.apache.hadoop.ozone.audit.AuditEventStatus; import org.apache.hadoop.ozone.audit.AuditLogger; @@ -133,4 +135,12 @@ public void setSignatureInfo(SignatureInfo signatureInfo) { protected Map getAuditParameters() { return AuditUtils.getAuditParameters(context); } + + protected MultivaluedMap getQueryParameters() { + if (context == null || context.getUriInfo() == null) { + return new MultivaluedHashMap<>(); + } + final MultivaluedMap params = context.getUriInfo().getQueryParameters(); + return params == null ? new MultivaluedHashMap<>() : params; + } } diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSEndpoint.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSEndpoint.java index 059f54e0993c..826a26fb3d23 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSEndpoint.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSEndpoint.java @@ -36,6 +36,7 @@ import java.io.StringReader; import java.time.Instant; import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Form; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -68,6 +69,8 @@ public class TestS3STSEndpoint { private S3STSEndpoint endpoint; private ObjectStore objectStore; private AuditLogger auditLogger; + private MultivaluedHashMap queryParameters; + private Form formParameters; private static final String ROLE_ARN = "arn:aws:iam::123456789012:role/test-role"; private static final String ROLE_SESSION_NAME = "test-session"; private static final String ROLE_USER_ARN = "arn:aws:sts::123456789012:assumed-role/test-role/" + ROLE_SESSION_NAME; @@ -87,7 +90,9 @@ public void setup() throws Exception { final UriInfo uriInfo = mock(UriInfo.class); when(context.getUriInfo()).thenReturn(uriInfo); when(uriInfo.getPathParameters()).thenReturn(new MultivaluedHashMap<>()); - when(uriInfo.getQueryParameters()).thenReturn(new MultivaluedHashMap<>()); + queryParameters = new MultivaluedHashMap<>(); + when(uriInfo.getQueryParameters()).thenReturn(queryParameters); + formParameters = new Form(); // Stub assumeRole to return deterministic credentials. objectStore = mock(ObjectStore.class); @@ -117,6 +122,152 @@ public void setup() throws Exception { endpoint.setSignatureInfo(signatureInfo); } + @Test + public void testStsAssumeRoleRejectsUnsupportedParameterForGetMethod() throws Exception { + setAssumeRoleQueryParameters("PolicyArns.member.1", "arn:aws:iam::123456789012:policy/test-policy"); + + final OSTSException ex = assertThrows( + OSTSException.class, () -> endpoint.get("AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, "2011-06-15", null)); + + assertEquals(501, ex.getHttpCode()); + verify(auditLogger).logWriteFailure(any(AuditMessage.class)); + verify(auditLogger, never()).logWriteSuccess(any(AuditMessage.class)); + verify(objectStore, never()).assumeRole(anyString(), anyString(), anyInt(), any(), anyString()); + + ex.setRequestId(REQUEST_ID); + assertStsErrorXml( + ex.toXml(), STS_NS, "Sender", "UnsupportedOperation", + "AssumeRole optional parameter(s) not implemented: PolicyArns.member.1"); + } + + @Test + public void testStsAssumeRoleRejectsUnsupportedParameterForPostMethod() throws Exception { + setBaseAssumeRoleFormParameters(); + formParameters.param("ExternalId", "external-id"); + + final OSTSException ex = assertThrows( + OSTSException.class, () -> endpoint.post(formParameters)); + + assertEquals(501, ex.getHttpCode()); + verify(auditLogger).logWriteFailure(any(AuditMessage.class)); + verify(auditLogger, never()).logWriteSuccess(any(AuditMessage.class)); + verify(objectStore, never()).assumeRole(anyString(), anyString(), anyInt(), any(), anyString()); + + ex.setRequestId(REQUEST_ID); + assertStsErrorXml( + ex.toXml(), STS_NS, "Sender", "UnsupportedOperation", + "AssumeRole optional parameter(s) not implemented: ExternalId"); + } + + @Test + public void testStsAssumeRoleAllowsSupportedParametersForGetMethod() { + setAssumeRoleQueryParameters( + "Action", "AssumeRole", + "RoleArn", ROLE_ARN, + "RoleSessionName", ROLE_SESSION_NAME, + "DurationSeconds", "3600", + "Version", "2011-06-15"); + + final Response response = endpoint.get("AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, "2011-06-15", null); + + assertEquals(200, response.getStatus()); + verify(auditLogger).logWriteSuccess(any(AuditMessage.class)); + verify(auditLogger, never()).logWriteFailure(any(AuditMessage.class)); + } + + @Test + public void testStsAssumeRoleAllowsSignatureParametersForGetMethod() { + setAssumeRoleQueryParameters( + "Action", "AssumeRole", + "RoleArn", ROLE_ARN, + "RoleSessionName", ROLE_SESSION_NAME, + "DurationSeconds", "3600", + "Version", "2011-06-15", + "X-Amz-Algorithm", "AWS4-HMAC-SHA256", + "X-Amz-Credential", "test-user/20260101/us-east-1/sts/aws4_request", + "X-Amz-Date", "20260101T000000Z", + "X-Amz-Expires", "3600", + "X-Amz-SignedHeaders", "host", + "X-Amz-Signature", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + + final Response response = endpoint.get("AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, "2011-06-15", null); + + assertEquals(200, response.getStatus()); + verify(auditLogger).logWriteSuccess(any(AuditMessage.class)); + verify(auditLogger, never()).logWriteFailure(any(AuditMessage.class)); + } + + @Test + public void testStsAssumeRoleRejectsUnknownParameterForGetMethod() throws Exception { + setAssumeRoleQueryParameters("TotallyUnknownParam", "x"); + + final OSTSException ex = assertThrows( + OSTSException.class, () -> endpoint.get("AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, "2011-06-15", null)); + + assertEquals(400, ex.getHttpCode()); + verify(auditLogger).logWriteFailure(any(AuditMessage.class)); + verify(auditLogger, never()).logWriteSuccess(any(AuditMessage.class)); + verify(objectStore, never()).assumeRole(anyString(), anyString(), anyInt(), any(), anyString()); + + ex.setRequestId(REQUEST_ID); + assertStsErrorXml( + ex.toXml(), STS_NS, "Sender", "ValidationError", "Unsupported AssumeRole parameter(s): TotallyUnknownParam"); + } + + @Test + public void testStsAssumeRoleRejectsUnknownParameterForPostMethod() throws Exception { + setBaseAssumeRoleFormParameters(); + formParameters.param("TotallyUnknownParam", "y"); + + final OSTSException ex = assertThrows( + OSTSException.class, () -> endpoint.post(formParameters)); + + assertEquals(400, ex.getHttpCode()); + verify(auditLogger).logWriteFailure(any(AuditMessage.class)); + verify(auditLogger, never()).logWriteSuccess(any(AuditMessage.class)); + verify(objectStore, never()).assumeRole(anyString(), anyString(), anyInt(), any(), anyString()); + + ex.setRequestId(REQUEST_ID); + assertStsErrorXml( + ex.toXml(), STS_NS, "Sender", "ValidationError", + "Unsupported AssumeRole parameter(s): TotallyUnknownParam"); + } + + @Test + public void testStsAssumeRoleIgnoresUnknownQueryStringParameterForPostMethod() { + queryParameters.add("foo", "bar"); + + // For POST requests, only body (form) parameters should be validated. + // Query string parameters should not affect validation results. + setBaseAssumeRoleFormParameters(); + final Response response = endpoint.post(formParameters); + + assertEquals(200, response.getStatus()); + verify(auditLogger).logWriteSuccess(any(AuditMessage.class)); + verify(auditLogger, never()).logWriteFailure(any(AuditMessage.class)); + } + + @Test + public void testStsAssumeRoleRejectsUnsupportedSigningParametersForPostMethod() throws Exception { + setBaseAssumeRoleFormParameters(); + formParameters.param("AWSAccessKeyId", "test-user"); + formParameters.param("Signature", "signature"); + formParameters.param("Expires", "3600"); + + final OSTSException ex = assertThrows( + OSTSException.class, () -> endpoint.post(formParameters)); + + assertEquals(400, ex.getHttpCode()); + verify(auditLogger).logWriteFailure(any(AuditMessage.class)); + verify(auditLogger, never()).logWriteSuccess(any(AuditMessage.class)); + verify(objectStore, never()).assumeRole(anyString(), anyString(), anyInt(), any(), anyString()); + + ex.setRequestId(REQUEST_ID); + assertStsErrorXml( + ex.toXml(), STS_NS, "Sender", "ValidationError", + "Unsupported AssumeRole parameter(s): AWSAccessKeyId, Expires, Signature"); + } + @Test public void testStsAssumeRoleValidForGetMethod() throws Exception { final Response response = endpoint.get("AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, "2011-06-15", null); @@ -157,8 +308,9 @@ public void testStsAssumeRoleValidForGetMethod() throws Exception { @Test public void testStsAssumeRoleValidForPostMethod() throws Exception { + setBaseAssumeRoleFormParameters(); //noinspection resource - final Response response = endpoint.post("AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, "2011-06-15", null); + final Response response = endpoint.post(formParameters); assertEquals(200, response.getStatus()); verify(auditLogger).logWriteSuccess(any(AuditMessage.class)); @@ -563,6 +715,29 @@ public void testStsMultipleValidationErrors() throws Exception { "'policy' failed to satisfy constraint: Member must have length less than or equal to 2048")); } + private void setAssumeRoleQueryParameters(String... nameValuePairs) { + queryParameters.clear(); + for (int i = 0; i < nameValuePairs.length; i += 2) { + queryParameters.add(nameValuePairs[i], nameValuePairs[i + 1]); + } + } + + private void setBaseAssumeRoleFormParameters() { + setAssumeRoleFormParameters( + "Action", "AssumeRole", + "RoleArn", ROLE_ARN, + "RoleSessionName", ROLE_SESSION_NAME, + "DurationSeconds", "3600", + "Version", "2011-06-15"); + } + + private void setAssumeRoleFormParameters(String... nameValuePairs) { + formParameters = new Form(); + for (int i = 0; i < nameValuePairs.length; i += 2) { + formParameters.param(nameValuePairs[i], nameValuePairs[i + 1]); + } + } + private static Document parseXml(String xml) throws Exception { final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true);