validationErrors = new HashSet<>();
+ try {
+ S3STSUtils.validateDuration(durationSeconds);
+ } catch (OMException e) {
+ validationErrors.add(e.getMessage());
+ }
+
+ try {
+ AwsRoleArnValidator.validateAndExtractRoleNameFromArn(roleArn);
+ } catch (OMException e) {
+ validationErrors.add(e.getMessage());
+ }
+
+ try {
+ S3STSUtils.validateRoleSessionName(roleSessionName);
+ } catch (OMException e) {
+ validationErrors.add(e.getMessage());
+ }
+
+ if (StringUtils.isBlank(webIdentityToken)) {
+ validationErrors.add("Value null at 'webIdentityToken' failed to "
+ + "satisfy constraint: Member must not be null");
+ }
+
+ if (!validationErrors.isEmpty()) {
+ final String validationMessage = validationErrors.size()
+ + " validation "
+ + (validationErrors.size() > 1 ? "errors detected: "
+ : "error detected: ")
+ + String.join(";", validationErrors);
+ throw new OSTSException(
+ VALIDATION_ERROR, validationMessage, BAD_REQUEST.getStatusCode());
+ }
+
+ final int duration = durationSeconds == null
+ ? S3STSUtils.DEFAULT_DURATION_SECONDS : durationSeconds;
+ final String assumedRoleUserArn =
+ S3STSUtils.toAssumedRoleUserArn(roleArn, roleSessionName);
+ try {
+ final AssumeRoleWithWebIdentityResponseInfo responseInfo = getClient()
+ .getObjectStore()
+ .assumeRoleWithWebIdentity(roleArn, roleSessionName, duration,
+ webIdentityToken, providerId, requestId);
+ final String responseXml = generateAssumeRoleWithWebIdentityResponse(
+ assumedRoleUserArn, responseInfo, requestId);
+ return Response.ok(responseXml)
+ .header("Content-Type", "text/xml")
+ .build();
+ } catch (IOException e) {
+ LOG.error("Error during AssumeRoleWithWebIdentity processing", e);
+ if (e instanceof OMException) {
+ final OMException omException = (OMException) e;
+ if (omException.getResult() == OMException.ResultCodes.ACCESS_DENIED ||
+ omException.getResult() == OMException.ResultCodes.PERMISSION_DENIED ||
+ omException.getResult() == OMException.ResultCodes.TOKEN_EXPIRED) {
+ throw new OSTSException(
+ ACCESS_DENIED,
+ "User is not authorized to perform: "
+ + "sts:AssumeRoleWithWebIdentity on resource: " + roleArn,
+ FORBIDDEN.getStatusCode());
+ }
+ if (omException.getResult() == OMException.ResultCodes.INVALID_TOKEN) {
+ throw new OSTSException(
+ INVALID_CLIENT_TOKEN_ID,
+ "The web identity token included in the request is invalid.",
+ FORBIDDEN.getStatusCode());
+ }
+ if (omException.getResult() == OMException.ResultCodes.NOT_SUPPORTED_OPERATION ||
+ omException.getResult() == OMException.ResultCodes.FEATURE_NOT_ENABLED) {
+ throw new OSTSException(UNSUPPORTED_OPERATION,
+ omException.getMessage(), NOT_IMPLEMENTED.getStatusCode());
+ }
+ if (omException.getResult() == OMException.ResultCodes.INVALID_REQUEST) {
+ throw new OSTSException(VALIDATION_ERROR, omException.getMessage(),
+ BAD_REQUEST.getStatusCode());
+ }
+ }
+ throw new OSTSException(
+ INTERNAL_FAILURE, "An internal error has occurred.",
+ INTERNAL_SERVER_ERROR.getStatusCode(), "Receiver");
+ }
+ }
+
+ private boolean isWebIdentityEnabled() {
+ return OzoneConfigurationHolder.configuration() != null
+ && OzoneConfigurationHolder.configuration().getBoolean(
+ OZONE_STS_WEB_IDENTITY_ENABLED,
+ OZONE_STS_WEB_IDENTITY_ENABLED_DEFAULT);
+ }
+
private Response handleAssumeRole(String roleArn, String roleSessionName, Integer durationSeconds,
String awsIamSessionPolicy, String version, String requestId) throws OSTSException {
final String action = "AssumeRole";
@@ -350,5 +492,54 @@ private String generateAssumeRoleResponse(String assumedRoleUserArn, AssumeRoleR
throw new IOException("Failed to marshal AssumeRole response", e);
}
}
-}
+ private String generateAssumeRoleWithWebIdentityResponse(
+ String assumedRoleUserArn,
+ AssumeRoleWithWebIdentityResponseInfo responseInfo, String requestId)
+ throws IOException {
+ final String expiration = DateTimeFormatter.ISO_INSTANT.format(
+ Instant.ofEpochSecond(responseInfo.getExpirationEpochSeconds())
+ .atOffset(ZoneOffset.UTC).toInstant());
+
+ try {
+ final S3AssumeRoleWithWebIdentityResponseXml response =
+ new S3AssumeRoleWithWebIdentityResponseXml();
+ final S3AssumeRoleWithWebIdentityResponseXml
+ .AssumeRoleWithWebIdentityResult result =
+ new S3AssumeRoleWithWebIdentityResponseXml
+ .AssumeRoleWithWebIdentityResult();
+ final S3AssumeRoleWithWebIdentityResponseXml.Credentials credentials =
+ new S3AssumeRoleWithWebIdentityResponseXml.Credentials();
+ credentials.setAccessKeyId(responseInfo.getAccessKeyId());
+ credentials.setSecretAccessKey(responseInfo.getSecretAccessKey());
+ credentials.setSessionToken(responseInfo.getSessionToken());
+ credentials.setExpiration(expiration);
+ result.setCredentials(credentials);
+ result.setSubjectFromWebIdentityToken(
+ responseInfo.getSubjectFromWebIdentityToken());
+ result.setAudience(responseInfo.getAudience());
+ if (StringUtils.isNotBlank(responseInfo.getProvider())) {
+ result.setProvider(responseInfo.getProvider());
+ }
+ final S3AssumeRoleWithWebIdentityResponseXml.AssumedRoleUser user =
+ new S3AssumeRoleWithWebIdentityResponseXml.AssumedRoleUser();
+ user.setAssumedRoleId(responseInfo.getAssumedRoleId());
+ user.setArn(assumedRoleUserArn);
+ result.setAssumedRoleUser(user);
+ response.setResult(result);
+ final S3AssumeRoleWithWebIdentityResponseXml.ResponseMetadata meta =
+ new S3AssumeRoleWithWebIdentityResponseXml.ResponseMetadata();
+ meta.setRequestId(requestId);
+ response.setResponseMetadata(meta);
+
+ final Marshaller marshaller = JAXB_CONTEXT.createMarshaller();
+ marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
+ final StringWriter stringWriter = new StringWriter();
+ marshaller.marshal(response, stringWriter);
+ return stringWriter.toString();
+ } catch (JAXBException e) {
+ throw new IOException(
+ "Failed to marshal AssumeRoleWithWebIdentity response", e);
+ }
+ }
+}
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..af3caa877085 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
@@ -32,6 +32,7 @@
import org.apache.hadoop.ozone.client.OzoneClient;
import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
import org.apache.hadoop.ozone.om.protocol.S3Auth;
+import org.apache.hadoop.ozone.s3.AuthorizationFilter;
import org.apache.hadoop.ozone.s3.signature.SignatureInfo;
import org.apache.hadoop.ozone.s3.util.AuditUtils;
@@ -63,6 +64,11 @@ public void setAuditLogger(AuditLogger auditLogger) {
@PostConstruct
public void initialization() {
+ if (context != null && Boolean.TRUE.equals(context.getProperty(
+ AuthorizationFilter.SKIP_AWS_AUTH_PROPERTY))) {
+ return;
+ }
+
S3Auth s3Auth = new S3Auth(signatureInfo.getStringToSign(),
signatureInfo.getSignature(),
signatureInfo.getAwsAccessId(), signatureInfo.getAwsAccessId());
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSWebIdentityAuthBypassFilter.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSWebIdentityAuthBypassFilter.java
new file mode 100644
index 000000000000..8096542c92b2
--- /dev/null
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSWebIdentityAuthBypassFilter.java
@@ -0,0 +1,82 @@
+/*
+ * 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.hadoop.ozone.s3sts;
+
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_STS_WEB_IDENTITY_ENABLED;
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_STS_WEB_IDENTITY_ENABLED_DEFAULT;
+
+import java.io.IOException;
+import javax.annotation.Priority;
+import javax.inject.Inject;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.container.PreMatching;
+import javax.ws.rs.ext.Provider;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.s3.AuthorizationFilter;
+import org.apache.hadoop.ozone.s3.OzoneConfigurationHolder;
+
+/**
+ * Allows unauthenticated bootstrap only for STS WebIdentity requests.
+ *
+ * This filter is registered only in the STS JAX-RS application. It does not
+ * validate or trust the JWT; OM remains the authoritative WebIdentity
+ * validator and temporary credential issuer.
+ */
+@Provider
+@PreMatching
+@Priority(AuthorizationFilter.PRIORITY - 1)
+public class S3STSWebIdentityAuthBypassFilter
+ implements ContainerRequestFilter {
+
+ private final OzoneConfiguration ozoneConfiguration;
+
+ /**
+ * No-arg constructor for Jersey provider instantiation without injection.
+ * Falls back to OzoneConfigurationHolder and fails closed when no
+ * configuration is available.
+ */
+ public S3STSWebIdentityAuthBypassFilter() {
+ this(null);
+ }
+
+ @Inject
+ public S3STSWebIdentityAuthBypassFilter(OzoneConfiguration conf) {
+ this.ozoneConfiguration = conf;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext context) throws IOException {
+ if (isWebIdentityEnabled()
+ && S3STSWebIdentityRequestParser.isAssumeRoleWithWebIdentity(
+ context)) {
+ context.setProperty(AuthorizationFilter.SKIP_AWS_AUTH_PROPERTY,
+ Boolean.TRUE);
+ }
+ }
+
+ private boolean isWebIdentityEnabled() {
+ OzoneConfiguration conf = ozoneConfiguration;
+ if (conf == null) {
+ conf = OzoneConfigurationHolder.configuration();
+ }
+ return conf != null && conf.getBoolean(OZONE_STS_WEB_IDENTITY_ENABLED,
+ OZONE_STS_WEB_IDENTITY_ENABLED_DEFAULT);
+ }
+
+}
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSWebIdentityRequestParser.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSWebIdentityRequestParser.java
new file mode 100644
index 000000000000..2765b308c62e
--- /dev/null
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSWebIdentityRequestParser.java
@@ -0,0 +1,123 @@
+/*
+ * 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.hadoop.ozone.s3sts;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.core.Response;
+
+/**
+ * Parses the STS action without logging request parameters.
+ */
+final class S3STSWebIdentityRequestParser {
+
+ static final String ACTION = "Action";
+ static final String ASSUME_ROLE_WITH_WEB_IDENTITY =
+ "AssumeRoleWithWebIdentity";
+ static final int MAX_FORM_BODY_BYTES = 64 * 1024;
+
+ private static final int BUFFER_SIZE = 8192;
+ private static final int HTTP_PAYLOAD_TOO_LARGE = 413;
+
+ private S3STSWebIdentityRequestParser() {
+ }
+
+ static boolean isAssumeRoleWithWebIdentity(ContainerRequestContext context)
+ throws IOException {
+ String action = getAction(context);
+ return ASSUME_ROLE_WITH_WEB_IDENTITY.equals(action);
+ }
+
+ private static String getAction(ContainerRequestContext context)
+ throws IOException {
+ String method = context.getMethod();
+ if (HttpMethod.GET.equalsIgnoreCase(method)) {
+ return singleAction(context.getUriInfo().getQueryParameters()
+ .get(ACTION));
+ }
+ if (HttpMethod.POST.equalsIgnoreCase(method)) {
+ return getFormAction(context);
+ }
+ return null;
+ }
+
+ private static String getFormAction(ContainerRequestContext context)
+ throws IOException {
+ InputStream stream = context.getEntityStream();
+ if (stream == null) {
+ return null;
+ }
+
+ byte[] body = readBoundedFormBody(stream);
+ context.setEntityStream(new ByteArrayInputStream(body));
+ String form = new String(body, StandardCharsets.UTF_8);
+ String action = null;
+ for (String pair : form.split("&")) {
+ int equals = pair.indexOf('=');
+ if (equals < 0) {
+ continue;
+ }
+ String name = decode(pair.substring(0, equals));
+ if (ACTION.equals(name)) {
+ if (action != null) {
+ return null;
+ }
+ action = decode(pair.substring(equals + 1));
+ }
+ }
+ return action;
+ }
+
+ private static String decode(String value) throws IOException {
+ return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
+ }
+
+ private static byte[] readBoundedFormBody(InputStream stream)
+ throws IOException {
+ ByteArrayOutputStream body =
+ new ByteArrayOutputStream(Math.min(MAX_FORM_BODY_BYTES, BUFFER_SIZE));
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int remaining = MAX_FORM_BODY_BYTES;
+ while (remaining > 0) {
+ int read = stream.read(buffer, 0, Math.min(buffer.length, remaining));
+ if (read == -1) {
+ return body.toByteArray();
+ }
+ body.write(buffer, 0, read);
+ remaining -= read;
+ }
+ if (stream.read() != -1) {
+ throw new WebApplicationException(
+ "STS WebIdentity request body is too large",
+ Response.status(HTTP_PAYLOAD_TOO_LARGE).build());
+ }
+ return body.toByteArray();
+ }
+
+ private static String singleAction(List values) {
+ return values != null && values.size() == 1 ? values.get(0) : null;
+ }
+}
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
index 5159f6214128..b4c41f51faba 100644
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
@@ -39,6 +39,7 @@
import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
import org.apache.hadoop.ozone.client.protocol.ListStatusLightOptions;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
+import org.apache.hadoop.ozone.om.helpers.AssumeRoleWithWebIdentityResponseInfo;
import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
import org.apache.hadoop.ozone.om.helpers.ErrorInfo;
import org.apache.hadoop.ozone.om.helpers.LeaseKeyInfo;
@@ -809,6 +810,14 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
return null;
}
+ @Override
+ public AssumeRoleWithWebIdentityResponseInfo assumeRoleWithWebIdentity(
+ String roleArn, String roleSessionName, int durationSeconds,
+ String webIdentityToken, String providerId, String requestId)
+ throws IOException {
+ return null;
+ }
+
@Override
public void revokeSTSToken(String sessionToken) throws IOException {
}
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/TestAuthorizationFilter.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/TestAuthorizationFilter.java
index 5171138710e0..7ea3a3516a58 100644
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/TestAuthorizationFilter.java
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/TestAuthorizationFilter.java
@@ -35,6 +35,7 @@
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
@@ -53,8 +54,10 @@
import javax.ws.rs.core.UriInfo;
import org.apache.hadoop.ozone.s3.signature.AWSSignatureProcessor;
import org.apache.hadoop.ozone.s3.signature.SignatureInfo;
+import org.apache.hadoop.ozone.s3.signature.SignatureProcessor;
import org.apache.hadoop.ozone.s3.signature.StringToSignProducer;
import org.apache.kerby.util.Hex;
+import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@@ -71,6 +74,21 @@ public class TestAuthorizationFilter {
private static final String CURDATE = DATE_FORMATTER.format(LocalDate.now());
+ @Test
+ void skipsAwsAuthWhenExplicitlyRequested() throws Exception {
+ ContainerRequestContext context = mock(ContainerRequestContext.class);
+ SignatureProcessor signatureProcessor = mock(SignatureProcessor.class);
+ SignatureInfo signatureInfo = mock(SignatureInfo.class);
+ when(context.getProperty(AuthorizationFilter.SKIP_AWS_AUTH_PROPERTY))
+ .thenReturn(Boolean.TRUE);
+
+ authorizationFilter.setSignatureParser(signatureProcessor);
+ authorizationFilter.setSignatureInfo(signatureInfo);
+ authorizationFilter.filter(context);
+
+ verifyNoInteractions(signatureProcessor, signatureInfo);
+ }
+
private static StreamtestAuthFilterFailuresInput() {
return Stream.of(
arguments(
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..bd92c3669c41 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
@@ -18,6 +18,7 @@
package org.apache.hadoop.ozone.s3sts;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_S3_ADMINISTRATORS;
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_STS_WEB_IDENTITY_ENABLED;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -50,6 +51,7 @@
import org.apache.hadoop.ozone.client.OzoneClientStub;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
+import org.apache.hadoop.ozone.om.helpers.AssumeRoleWithWebIdentityResponseInfo;
import org.apache.hadoop.ozone.s3.OzoneConfigurationHolder;
import org.apache.hadoop.ozone.s3.RequestIdentifier;
import org.apache.hadoop.ozone.s3.exception.OSTSException;
@@ -77,6 +79,7 @@ public class TestS3STSEndpoint {
@BeforeEach
public void setup() throws Exception {
+ OzoneConfigurationHolder.resetConfiguration();
OzoneConfiguration config = new OzoneConfiguration();
config.set(OZONE_S3_ADMINISTRATORS, "test-user");
OzoneConfigurationHolder.setConfiguration(config);
@@ -98,6 +101,17 @@ public void setup() throws Exception {
"session-token",
Instant.now().plusSeconds(3600).getEpochSecond(),
"AROA1234567890123456:test-session"));
+ when(objectStore.assumeRoleWithWebIdentity(anyString(), anyString(),
+ anyInt(), anyString(), any(), anyString()))
+ .thenReturn(new AssumeRoleWithWebIdentityResponseInfo(
+ "ASIAWEBIDENTITY123456",
+ "webIdentitySecretAccessKey",
+ "web-identity-session-token",
+ Instant.now().plusSeconds(3600).getEpochSecond(),
+ "AROA1234567890123456:test-session",
+ "subject-123",
+ "ozone",
+ "keycloak"));
when(clientStub.getObjectStore()).thenReturn(objectStore);
endpoint = new S3STSEndpoint();
@@ -360,6 +374,84 @@ public void testStsWhenActionNotImplemented() throws Exception {
"Operation GetSessionToken is not supported yet.");
}
+ @Test
+ public void testStsAssumeRoleWithWebIdentityDisabled() throws Exception {
+ final OSTSException ex = assertThrows(OSTSException.class, () ->
+ endpoint.get("AssumeRoleWithWebIdentity", ROLE_ARN,
+ ROLE_SESSION_NAME, 3600, "2011-06-15", null,
+ "sensitive-token-material", "keycloak"));
+
+ assertEquals(501, ex.getHttpCode());
+ verifyNoInteractions(auditLogger);
+ verify(objectStore, never()).assumeRole(anyString(), anyString(),
+ anyInt(), any(), anyString());
+ verify(objectStore, never()).assumeRoleWithWebIdentity(
+ anyString(), anyString(), anyInt(), anyString(), any(), anyString());
+
+ ex.setRequestId(REQUEST_ID);
+ assertStsErrorXml(ex.toXml(), AWS_FAULT_NS, "Sender", "InvalidAction",
+ "Operation AssumeRoleWithWebIdentity is not supported yet.");
+ }
+
+ @Test
+ public void testStsAssumeRoleWithWebIdentityMissingToken()
+ throws Exception {
+ enableWebIdentity();
+
+ final OSTSException ex = assertThrows(OSTSException.class, () ->
+ endpoint.get("AssumeRoleWithWebIdentity", ROLE_ARN,
+ ROLE_SESSION_NAME, 3600, "2011-06-15", null, null,
+ "keycloak"));
+
+ assertEquals(400, ex.getHttpCode());
+ verifyNoInteractions(auditLogger);
+ verify(objectStore, never()).assumeRole(anyString(), anyString(),
+ anyInt(), any(), anyString());
+ verify(objectStore, never()).assumeRoleWithWebIdentity(
+ anyString(), anyString(), anyInt(), anyString(), any(), anyString());
+
+ ex.setRequestId(REQUEST_ID);
+ assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError",
+ "webIdentityToken");
+ }
+
+ @Test
+ public void testStsAssumeRoleWithWebIdentityRoutesWhenEnabled()
+ throws Exception {
+ enableWebIdentity();
+
+ final Response response = endpoint.get("AssumeRoleWithWebIdentity",
+ ROLE_ARN, ROLE_SESSION_NAME, 3600, "2011-06-15", null,
+ "sensitive-token-material", "keycloak");
+
+ assertEquals(200, response.getStatus());
+ verifyNoInteractions(auditLogger);
+ verify(objectStore).assumeRoleWithWebIdentity(ROLE_ARN,
+ ROLE_SESSION_NAME, 3600, "sensitive-token-material", "keycloak",
+ REQUEST_ID);
+
+ final Document doc = parseXml((String) response.getEntity());
+ final Element root = doc.getDocumentElement();
+ assertEquals("AssumeRoleWithWebIdentityResponse", root.getLocalName());
+ assertEquals(STS_NS, root.getNamespaceURI());
+ assertNotNull(doc.getElementsByTagNameNS(
+ STS_NS, "AssumeRoleWithWebIdentityResult").item(0));
+ assertEquals("ASIAWEBIDENTITY123456",
+ doc.getElementsByTagName("AccessKeyId").item(0).getTextContent());
+ assertEquals("webIdentitySecretAccessKey",
+ doc.getElementsByTagName("SecretAccessKey").item(0)
+ .getTextContent());
+ assertEquals("web-identity-session-token",
+ doc.getElementsByTagName("SessionToken").item(0).getTextContent());
+ assertEquals("subject-123",
+ doc.getElementsByTagName("SubjectFromWebIdentityToken").item(0)
+ .getTextContent());
+ assertEquals("ozone",
+ doc.getElementsByTagName("Audience").item(0).getTextContent());
+ assertEquals("keycloak",
+ doc.getElementsByTagName("Provider").item(0).getTextContent());
+ }
+
@Test
public void testStsMissingRoleSessionName() throws Exception {
final OSTSException ex = assertThrows(OSTSException.class, () ->
@@ -570,6 +662,13 @@ private static Document parseXml(String xml) throws Exception {
return documentBuilder.parse(new InputSource(new StringReader(xml)));
}
+ private static void enableWebIdentity() {
+ OzoneConfigurationHolder.resetConfiguration();
+ OzoneConfiguration config = new OzoneConfiguration();
+ config.setBoolean(OZONE_STS_WEB_IDENTITY_ENABLED, true);
+ OzoneConfigurationHolder.setConfiguration(config);
+ }
+
private static void assertStsErrorXml(String xml, String expectedNamespace, String expectedType, String expectedCode,
String expectedMessageContains) throws Exception {
final Document doc = parseXml(xml);
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSWebIdentityAuthBypassFilter.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSWebIdentityAuthBypassFilter.java
new file mode 100644
index 000000000000..bcc22b26e6ab
--- /dev/null
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSWebIdentityAuthBypassFilter.java
@@ -0,0 +1,220 @@
+/*
+ * 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.hadoop.ozone.s3sts;
+
+import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_STS_WEB_IDENTITY_ENABLED;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+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.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.UriInfo;
+import org.apache.commons.io.IOUtils;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.s3.AuthorizationFilter;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Tests for the STS-only WebIdentity auth bypass filter.
+ */
+public class TestS3STSWebIdentityAuthBypassFilter {
+
+ @Test
+ public void enabledGetWebIdentityRequestSkipsAwsAuth() throws Exception {
+ S3STSWebIdentityAuthBypassFilter filter = filter(true);
+ ContainerRequestContext context = context(HttpMethod.GET,
+ "AssumeRoleWithWebIdentity", null);
+
+ filter.filter(context);
+
+ verify(context).setProperty(AuthorizationFilter.SKIP_AWS_AUTH_PROPERTY,
+ Boolean.TRUE);
+ }
+
+ @Test
+ public void disabledWebIdentityRequestDoesNotSkipAwsAuth()
+ throws Exception {
+ S3STSWebIdentityAuthBypassFilter filter = filter(false);
+ ContainerRequestContext context = context(HttpMethod.GET,
+ "AssumeRoleWithWebIdentity", null);
+
+ filter.filter(context);
+
+ verify(context, never()).setProperty(anyString(), any());
+ }
+
+ @Test
+ public void otherStsActionDoesNotSkipAwsAuth() throws Exception {
+ S3STSWebIdentityAuthBypassFilter filter = filter(true);
+ ContainerRequestContext context = context(HttpMethod.GET, "AssumeRole",
+ null);
+
+ filter.filter(context);
+
+ verify(context, never()).setProperty(anyString(), any());
+ }
+
+ @Test
+ public void postWebIdentityRequestSkipsAwsAuthAndRestoresBody()
+ throws Exception {
+ S3STSWebIdentityAuthBypassFilter filter = filter(true);
+ String body = "Action=AssumeRoleWithWebIdentity"
+ + "&WebIdentityToken=sensitive-token-material";
+ ContainerRequestContext context = context(HttpMethod.POST, null, body);
+
+ filter.filter(context);
+
+ verify(context).setProperty(AuthorizationFilter.SKIP_AWS_AUTH_PROPERTY,
+ Boolean.TRUE);
+ ArgumentCaptor streamCaptor =
+ ArgumentCaptor.forClass(InputStream.class);
+ verify(context).setEntityStream(streamCaptor.capture());
+ assertEquals(body, read(streamCaptor.getValue()));
+ }
+
+ @Test
+ public void postWebIdentityRequestAtBodyLimitSkipsAwsAuthAndRestoresBody()
+ throws Exception {
+ S3STSWebIdentityAuthBypassFilter filter = filter(true);
+ String body = formBody(S3STSWebIdentityRequestParser.MAX_FORM_BODY_BYTES);
+ ContainerRequestContext context = context(HttpMethod.POST, null, body);
+
+ filter.filter(context);
+
+ verify(context).setProperty(AuthorizationFilter.SKIP_AWS_AUTH_PROPERTY,
+ Boolean.TRUE);
+ ArgumentCaptor streamCaptor =
+ ArgumentCaptor.forClass(InputStream.class);
+ verify(context).setEntityStream(streamCaptor.capture());
+ assertEquals(body, read(streamCaptor.getValue()));
+ }
+
+ @Test
+ public void oversizedPostWebIdentityRequestFailsBeforeBypass()
+ throws Exception {
+ S3STSWebIdentityAuthBypassFilter filter = filter(true);
+ String body = formBody(
+ S3STSWebIdentityRequestParser.MAX_FORM_BODY_BYTES + 1);
+ ContainerRequestContext context = context(HttpMethod.POST, null, body);
+
+ WebApplicationException ex = assertThrows(WebApplicationException.class,
+ () -> filter.filter(context));
+
+ assertEquals(413, ex.getResponse().getStatus());
+ verify(context, never()).setProperty(anyString(), any());
+ verify(context, never()).setEntityStream(any());
+ }
+
+ @Test
+ public void adversarialActionValuesDoNotSkipAwsAuth() throws Exception {
+ assertDoesNotSkipPost("Action=AssumeRoleWithWebIdentity%20");
+ assertDoesNotSkipPost("action=assumerolewithwebidentity");
+ assertDoesNotSkipPost("Action=AssumeRoleWithWebIdentity%00");
+ assertDoesNotSkipPost("Action=AssumeRoleWithWebIdentity"
+ + "&Action=AssumeRole");
+ assertDoesNotSkipPost("{\"Action\":\"AssumeRoleWithWebIdentity\"}");
+ assertDoesNotSkipPost("Version=2011-06-15");
+ assertDoesNotSkipPost("Action=");
+ }
+
+ @Test
+ public void duplicateQueryActionDoesNotSkipAwsAuth() throws Exception {
+ S3STSWebIdentityAuthBypassFilter filter = filter(true);
+ ContainerRequestContext context = contextWithQueryActions(
+ "AssumeRoleWithWebIdentity", "AssumeRole");
+
+ filter.filter(context);
+
+ verify(context, never()).setProperty(anyString(), any());
+ }
+
+ private static S3STSWebIdentityAuthBypassFilter filter(boolean enabled) {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.setBoolean(OZONE_STS_WEB_IDENTITY_ENABLED, enabled);
+ return new S3STSWebIdentityAuthBypassFilter(conf);
+ }
+
+ private static void assertDoesNotSkipPost(String body) throws Exception {
+ S3STSWebIdentityAuthBypassFilter filter = filter(true);
+ ContainerRequestContext context = context(HttpMethod.POST, null, body);
+
+ filter.filter(context);
+
+ verify(context, never()).setProperty(anyString(), any());
+ }
+
+ private static ContainerRequestContext context(String method,
+ String queryAction, String body) {
+ ContainerRequestContext context = mock(ContainerRequestContext.class);
+ UriInfo uriInfo = mock(UriInfo.class);
+ MultivaluedHashMap queryParams =
+ new MultivaluedHashMap<>();
+ if (queryAction != null) {
+ queryParams.putSingle("Action", queryAction);
+ }
+ when(context.getMethod()).thenReturn(method);
+ when(context.getUriInfo()).thenReturn(uriInfo);
+ when(uriInfo.getQueryParameters()).thenReturn(queryParams);
+ if (body != null) {
+ when(context.getEntityStream()).thenReturn(new ByteArrayInputStream(
+ body.getBytes(StandardCharsets.UTF_8)));
+ }
+ return context;
+ }
+
+ private static ContainerRequestContext contextWithQueryActions(
+ String... queryActions) {
+ ContainerRequestContext context = mock(ContainerRequestContext.class);
+ UriInfo uriInfo = mock(UriInfo.class);
+ MultivaluedHashMap queryParams =
+ new MultivaluedHashMap<>();
+ queryParams.put("Action", Arrays.asList(queryActions));
+ when(context.getMethod()).thenReturn(HttpMethod.GET);
+ when(context.getUriInfo()).thenReturn(uriInfo);
+ when(uriInfo.getQueryParameters()).thenReturn(queryParams);
+ return context;
+ }
+
+ private static String read(InputStream stream) throws Exception {
+ return new String(IOUtils.toByteArray(stream), StandardCharsets.UTF_8);
+ }
+
+ private static String formBody(int size) {
+ String prefix = "Action=AssumeRoleWithWebIdentity&WebIdentityToken=";
+ return prefix + repeat('a', size - prefix.length());
+ }
+
+ private static String repeat(char ch, int count) {
+ char[] chars = new char[count];
+ Arrays.fill(chars, ch);
+ return new String(chars);
+ }
+}
diff --git a/pom.xml b/pom.xml
index 696eb499952b..2de60387e3a5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -224,6 +224,7 @@
1.5.4
${test.build.dir}
${project.build.directory}/test-dir
+ 2.0.5
4
@@ -1570,6 +1571,11 @@