This is an automated email from the ASF dual-hosted git repository.

sammichen pushed a commit to branch HDDS-13323-sts
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/HDDS-13323-sts by this push:
     new bb2b540bd28 HDDS-14514. [STS] Revamp error handling in endpoint to 
conform to AWS XML (#9674)
bb2b540bd28 is described below

commit bb2b540bd28a6f95de2ede4e8f8c2c208112755c
Author: fmorg-git <[email protected]>
AuthorDate: Wed Jan 28 06:17:27 2026 -0800

    HDDS-14514. [STS] Revamp error handling in endpoint to conform to AWS XML 
(#9674)
---
 .../hadoop/ozone/s3/exception/OSTSException.java   | 160 ++++++++++
 .../ozone/s3/exception/OSTSExceptionMapper.java    |  49 +++
 .../org/apache/hadoop/ozone/s3sts/Application.java |   2 +
 .../apache/hadoop/ozone/s3sts/S3STSEndpoint.java   | 151 +++++----
 .../apache/hadoop/ozone/s3sts/package-info.java    |  11 +
 .../ozone/s3/exception/TestOSTSExceptions.java     | 102 ++++++
 .../hadoop/ozone/s3sts/TestS3STSEndpoint.java      | 346 +++++++++++++++++++--
 7 files changed, 725 insertions(+), 96 deletions(-)

diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/exception/OSTSException.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/exception/OSTSException.java
new file mode 100644
index 00000000000..57c870a7eb0
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/exception/OSTSException.java
@@ -0,0 +1,160 @@
+/*
+ * 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.s3.exception;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class represents exceptions raised from Ozone STS service.
+ */
+public class OSTSException extends OS3Exception {
+  private static final Logger LOG = 
LoggerFactory.getLogger(OSTSException.class);
+  private static final ObjectMapper MAPPER;
+  private static final String AWS_FAULT_NS = 
"http://webservices.amazon.com/AWSFault/2005-15-09";;
+  private static final String STS_NS = 
"https://sts.amazonaws.com/doc/2011-06-15/";;
+  private static final String INVALID_ACTION = "InvalidAction";
+
+  static {
+    MAPPER = new XmlMapper();
+    MAPPER.registerModule(new JaxbAnnotationModule());
+    MAPPER.enable(SerializationFeature.INDENT_OUTPUT);
+  }
+
+  private String type = "Sender";
+
+  public OSTSException(String codeVal, String messageVal, int httpCode) {
+    super(codeVal, messageVal, httpCode);
+  }
+
+  public OSTSException(String codeVal, String messageVal, int httpCode, String 
typeVal) {
+    this(codeVal, messageVal, httpCode);
+    this.type = typeVal;
+  }
+
+  public String getType() {
+    return type;
+  }
+
+  public void setType(String type) {
+    this.type = type;
+  }
+
+  @Override
+  public String toXml() {
+    try {
+      final ErrorResponse response = new ErrorResponse(this);
+      final String val = MAPPER.writeValueAsString(response);
+      LOG.debug("toXml val is {}", val);
+      return val;  // STS error responses don't have prolog <?xml 
version="1.0" encoding="UTF-8"?>
+    } catch (Exception ex) {
+      LOG.error("Exception occurred", ex);
+      // Fallback
+      final String namespace = INVALID_ACTION.equals(getCode()) ? AWS_FAULT_NS 
: STS_NS;
+
+      // STS error responses don't have prolog <?xml version="1.0" 
encoding="UTF-8"?>
+      final StringBuilder builder = new StringBuilder();
+      builder.append("<ErrorResponse 
xmlns=\"").append(namespace).append("\">\n")
+          .append("  <Error>\n")
+          .append("    <Type>").append(getType()).append("</Type>\n")
+          .append("    <Code>").append(getCode()).append("</Code>\n")
+          .append("    
<Message>").append(getErrorMessage()).append("</Message>\n")
+          .append("  </Error>\n")
+          .append("  
<RequestId>").append(getRequestId()).append("</RequestId>\n")
+          .append("</ErrorResponse>");
+      return builder.toString();
+    }
+  }
+
+  @XmlAccessorType(XmlAccessType.FIELD)
+  @XmlRootElement(name = "ErrorResponse")
+  private static class ErrorResponse {
+    
+    @XmlAttribute
+    private String xmlns;
+
+    @XmlElement(name = "Error")
+    private ErrorDetails error;
+
+    @XmlElement(name = "RequestId")
+    private String requestId;
+
+    ErrorResponse() {
+    }
+
+    ErrorResponse(OSTSException ex) {
+      this.xmlns = INVALID_ACTION.equals(ex.getCode()) ? AWS_FAULT_NS : STS_NS;
+      this.error = new ErrorDetails(ex.getType(), ex.getCode(), 
ex.getErrorMessage());
+      this.requestId = ex.getRequestId();
+    }
+
+    public String getXmlns() {
+      return xmlns;
+    }
+
+    public ErrorDetails getError() {
+      return error;
+    }
+
+    public String getRequestId() {
+      return requestId;
+    }
+  }
+
+  @XmlAccessorType(XmlAccessType.FIELD)
+  private static class ErrorDetails {
+    @XmlElement(name = "Type")
+    private String type;
+    
+    @XmlElement(name = "Code")
+    private String code;
+    
+    @XmlElement(name = "Message")
+    private String message;
+
+    ErrorDetails() {
+    }
+
+    ErrorDetails(String type, String code, String message) {
+      this.type = type;
+      this.code = code;
+      this.message = message;
+    }
+
+    public String getType() {
+      return type;
+    }
+
+    public String getCode() {
+      return code;
+    }
+
+    public String getMessage() {
+      return message;
+    }
+  }
+}
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/exception/OSTSExceptionMapper.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/exception/OSTSExceptionMapper.java
new file mode 100644
index 00000000000..bb564e0e061
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/exception/OSTSExceptionMapper.java
@@ -0,0 +1,49 @@
+/*
+ * 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.s3.exception;
+
+import javax.inject.Inject;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import org.apache.hadoop.ozone.s3.RequestIdentifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class that represents various errors returned by the Ozone STS service.
+ */
+@Provider
+public class OSTSExceptionMapper implements ExceptionMapper<OSTSException> {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(OSTSExceptionMapper.class);
+
+  @Inject
+  private RequestIdentifier requestIdentifier;
+
+  @Override
+  public Response toResponse(OSTSException exception) {
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("Returning exception. ex: {}", exception.toString());
+    }
+    exception.setRequestId(requestIdentifier.getRequestId());
+    return Response.status(exception.getHttpCode())
+        .entity(exception.toXml()).build();
+  }
+}
+
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/Application.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/Application.java
index 65081d5d47f..1605532db1c 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/Application.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/Application.java
@@ -17,6 +17,7 @@
 
 package org.apache.hadoop.ozone.s3sts;
 
+import org.apache.hadoop.ozone.s3.exception.OSTSExceptionMapper;
 import org.glassfish.jersey.server.ResourceConfig;
 
 /**
@@ -26,5 +27,6 @@ public class Application extends ResourceConfig {
   public Application() {
     packages("org.apache.hadoop.ozone.s3sts");
     register(org.apache.hadoop.ozone.s3.AuthorizationFilter.class);
+    register(OSTSExceptionMapper.class);
   }
 }
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 e33e9f80552..9d6b1b8d77f 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
@@ -17,6 +17,11 @@
 
 package org.apache.hadoop.ozone.s3sts;
 
+import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
+import static javax.ws.rs.core.Response.Status.FORBIDDEN;
+import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
+import static javax.ws.rs.core.Response.Status.NOT_IMPLEMENTED;
+
 import com.google.common.base.Strings;
 import java.io.IOException;
 import java.io.StringWriter;
@@ -35,8 +40,10 @@
 import javax.xml.bind.JAXBContext;
 import javax.xml.bind.JAXBException;
 import javax.xml.bind.Marshaller;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
 import org.apache.hadoop.ozone.s3.exception.OS3Exception;
+import org.apache.hadoop.ozone.s3.exception.OSTSException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -58,9 +65,7 @@ public class S3STSEndpoint extends S3STSEndpointBase {
   private static final Logger LOG = 
LoggerFactory.getLogger(S3STSEndpoint.class);
 
   // STS API constants
-  private static final String STS_ACTION_PARAM = "Action";
   private static final String ASSUME_ROLE_ACTION = "AssumeRole";
-  private static final String ROLE_ARN_PARAM = "RoleArn";
   private static final String ROLE_DURATION_SECONDS_PARAM = "DurationSeconds";
   private static final String GET_SESSION_TOKEN_ACTION = "GetSessionToken";
   private static final String ASSUME_ROLE_WITH_SAML_ACTION = 
"AssumeRoleWithSAML";
@@ -69,6 +74,8 @@ public class S3STSEndpoint extends S3STSEndpointBase {
   private static final String DECODE_AUTHORIZATION_MESSAGE_ACTION = 
"DecodeAuthorizationMessage";
   private static final String GET_ACCESS_KEY_INFO_ACTION = "GetAccessKeyInfo";
 
+  private static final String EXPECTED_VERSION = "2011-06-15";
+
   // Default token duration (in seconds) - AWS default is 3600 (1 hour)
   // TODO - add these constants and also validations in a common place that 
both endpoint and backend can use
   private static final int DEFAULT_DURATION_SECONDS = 3600;
@@ -126,30 +133,19 @@ public Response post(
 
   private Response handleSTSRequest(String action, String roleArn, String 
roleSessionName,
       Integer durationSeconds, String version, String awsIamSessionPolicy) 
throws OS3Exception {
+    final String requestId = UUID.randomUUID().toString();
     try {
       if (action == null) {
-        return Response.status(Response.Status.BAD_REQUEST)
-            .entity("Missing required parameter: " + STS_ACTION_PARAM)
-            .build();
-      }
-      int duration;
-      try {
-        duration = validateDuration(durationSeconds);
-      } catch (IllegalArgumentException e) {
-        return Response.status(Response.Status.BAD_REQUEST)
-            .entity(e.getMessage())
-            .build();
-      }
-
-      if (version == null || !version.equals("2011-06-15")) {
-        return Response.status(Response.Status.BAD_REQUEST)
-            .entity("Invalid or missing Version parameter. Supported version 
is 2011-06-15.")
+        // Amazon STS has a different structure for the XML error response 
when the action is missing
+        return Response.status(BAD_REQUEST)
+            .entity("<UnknownOperationException/>")
+            .type(MediaType.APPLICATION_XML)
             .build();
       }
 
       switch (action) {
       case ASSUME_ROLE_ACTION:
-        return handleAssumeRole(roleArn, roleSessionName, duration, 
awsIamSessionPolicy);
+        return handleAssumeRole(roleArn, roleSessionName, durationSeconds, 
awsIamSessionPolicy, version, requestId);
       // These operations are not supported yet
       case GET_SESSION_TOKEN_ACTION:
       case ASSUME_ROLE_WITH_SAML_ACTION:
@@ -157,25 +153,24 @@ private Response handleSTSRequest(String action, String 
roleArn, String roleSess
       case GET_CALLER_IDENTITY_ACTION:
       case DECODE_AUTHORIZATION_MESSAGE_ACTION:
       case GET_ACCESS_KEY_INFO_ACTION:
-        return Response.status(Response.Status.NOT_IMPLEMENTED)
-            .entity("Operation " + action + " is not supported yet.")
-            .build();
+        throw new OSTSException(
+            "InvalidAction", "Operation " + action + " is not supported yet.", 
NOT_IMPLEMENTED.getStatusCode());
       default:
-        return Response.status(Response.Status.BAD_REQUEST)
-            .entity("Unsupported Action: " + action)
-            .build();
+        throw new OSTSException(
+            "InvalidAction", "Could not find operation " + action + " for 
version " +
+            (version == null ? "NO_VERSION_SPECIFIED.  Expected version is: " 
+ EXPECTED_VERSION : version),
+            BAD_REQUEST.getStatusCode());
       }
-    } catch (OS3Exception s3e) {
-      // Handle known S3 exceptions
-      LOG.error("S3 Error during STS request: {}", s3e.toXml());
-      throw s3e;
+    } catch (OSTSException e) {
+      throw e;
     } catch (Exception ex) {
       LOG.error("Unexpected error during STS request", ex);
-      return Response.serverError().build();
+      throw new OSTSException(
+          "InternalFailure", "An internal error has occurred.", 
INTERNAL_SERVER_ERROR.getStatusCode(), "Receiver");
     }
   }
 
-  private int validateDuration(Integer durationSeconds) throws 
IllegalArgumentException, OS3Exception {
+  private int validateDuration(Integer durationSeconds) throws 
IllegalArgumentException {
     if (durationSeconds == null) {
       return DEFAULT_DURATION_SECONDS;
     }
@@ -189,53 +184,87 @@ private int validateDuration(Integer durationSeconds) 
throws IllegalArgumentExce
     return durationSeconds;
   }
 
-  private Response handleAssumeRole(String roleArn, String roleSessionName, 
int duration, String awsIamSessionPolicy)
-      throws IOException, OS3Exception {
-    // Validate required parameters for AssumeRole. RoleArn is required
+  private Response handleAssumeRole(String roleArn, String roleSessionName, 
Integer durationSeconds,
+      String awsIamSessionPolicy, String version, String requestId) throws 
OSTSException {
+    // Validate parameters
+    final String action = "AssumeRole";
+    int duration;
+    try {
+      duration = validateDuration(durationSeconds);
+    } catch (IllegalArgumentException e) {
+      throw new OSTSException("ValidationError", e.getMessage(), 
BAD_REQUEST.getStatusCode());
+    }
+
+    if (version == null || !version.equals(EXPECTED_VERSION)) {
+      throw new OSTSException(
+          "InvalidAction", "Could not find operation " + action + " for 
version " +
+          (version == null ? "NO_VERSION_SPECIFIED.  Expected version is: " + 
EXPECTED_VERSION : version),
+          BAD_REQUEST.getStatusCode());
+    }
+
     if (roleArn == null || roleArn.isEmpty()) {
-      return Response.status(Response.Status.BAD_REQUEST)
-          .entity("Missing required parameter: " + ROLE_ARN_PARAM)
-          .build();
+      throw new OSTSException(
+          "ValidationError", "Value null at 'roleArn' failed to satisfy 
constraint: Member must not be null",
+          BAD_REQUEST.getStatusCode());
     }
 
     if (roleSessionName == null || roleSessionName.isEmpty()) {
-      return Response.status(Response.Status.BAD_REQUEST)
-          .entity("Missing required parameter: RoleSessionName")
-          .build();
+      throw new OSTSException(
+          "ValidationError", "Value null at 'roleSessionName' failed to 
satisfy constraint: Member must not be null",
+          BAD_REQUEST.getStatusCode());
     }
 
     // Validate role session name format (AWS requirements)
     if (!isValidRoleSessionName(roleSessionName)) {
-      return Response.status(Response.Status.BAD_REQUEST)
-          .entity("Invalid RoleSessionName: must be 2-64 characters long and " 
+
-              "contain only alphanumeric characters, +, =, ,, ., @, -")
-          .build();
+      throw new OSTSException(
+          "ValidationError", "Invalid RoleSessionName: must be 2-64 characters 
long and " +
+          "contain only alphanumeric characters, +, =, ,, ., @, -",
+          BAD_REQUEST.getStatusCode());
     }
 
     // Check Policy size if available
     if (awsIamSessionPolicy != null && awsIamSessionPolicy.length() > 
MAX_SESSION_POLICY_SIZE) {
-      return Response.status(Response.Status.BAD_REQUEST)
-          .entity("Policy length exceeded maximum allowed length of " + 
MAX_SESSION_POLICY_SIZE)
-          .build();
+      throw new OSTSException(
+          "ValidationError", "Value '" + awsIamSessionPolicy + "' at 'policy' 
failed to satisfy constraint: Member " +
+          "must have length less than or equal to 2048", 
BAD_REQUEST.getStatusCode());
     }
 
     final String assumedRoleUserArn;
     try {
       assumedRoleUserArn = toAssumedRoleUserArn(roleArn, roleSessionName);
     } catch (IllegalArgumentException e) {
-      return Response.status(Response.Status.BAD_REQUEST)
-          .entity(e.getMessage())
-          .build();
+      throw new OSTSException("ValidationError", e.getMessage(), 
BAD_REQUEST.getStatusCode());
     }
 
-    final AssumeRoleResponseInfo responseInfo = getClient()
-        .getObjectStore()
-        .assumeRole(roleArn, roleSessionName, duration, awsIamSessionPolicy);
-    // Generate AssumeRole response
-    final String responseXml = generateAssumeRoleResponse(assumedRoleUserArn, 
responseInfo);
-    return Response.ok(responseXml)
-        .header("Content-Type", "text/xml")
-        .build();
+    try {
+      final AssumeRoleResponseInfo responseInfo = getClient()
+          .getObjectStore()
+          .assumeRole(roleArn, roleSessionName, duration, awsIamSessionPolicy);
+      // Generate AssumeRole response
+      final String responseXml = 
generateAssumeRoleResponse(assumedRoleUserArn, responseInfo, requestId);
+      return Response.ok(responseXml)
+          .header("Content-Type", "text/xml")
+          .build();
+    } catch (IOException e) {
+      LOG.error("Error during AssumeRole 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(
+              "AccessDenied", "User is not authorized to perform: 
sts:AssumeRole on resource: " + roleArn,
+              FORBIDDEN.getStatusCode());
+        }
+        if (omException.getResult() == OMException.ResultCodes.INVALID_TOKEN) {
+          throw new OSTSException(
+              "InvalidClientTokenId", "The security token included in the 
request is invalid.",
+              FORBIDDEN.getStatusCode());
+        }
+      }
+      throw new OSTSException("InternalFailure", "An internal error has 
occurred.",
+          INTERNAL_SERVER_ERROR.getStatusCode(), "Receiver");
+    }
   }
 
   private boolean isValidRoleSessionName(String roleSessionName) {
@@ -247,8 +276,8 @@ private boolean isValidRoleSessionName(String 
roleSessionName) {
     return roleSessionName.matches("[a-zA-Z0-9+=,.@\\-]+");
   }
 
-  private String generateAssumeRoleResponse(String assumedRoleUserArn, 
AssumeRoleResponseInfo responseInfo)
-      throws IOException {
+  private String generateAssumeRoleResponse(String assumedRoleUserArn, 
AssumeRoleResponseInfo responseInfo,
+      String requestId) throws IOException {
     final String accessKeyId = responseInfo.getAccessKeyId();
     final String secretAccessKey = responseInfo.getSecretAccessKey();
     final String sessionToken = responseInfo.getSessionToken();
@@ -257,8 +286,6 @@ private String generateAssumeRoleResponse(String 
assumedRoleUserArn, AssumeRoleR
     final String expiration = DateTimeFormatter.ISO_INSTANT.format(
         
Instant.ofEpochSecond(responseInfo.getExpirationEpochSeconds()).atOffset(ZoneOffset.UTC).toInstant());
 
-    final String requestId = UUID.randomUUID().toString();
-
     try {
       final S3AssumeRoleResponseXml response = new S3AssumeRoleResponseXml();
       final S3AssumeRoleResponseXml.AssumeRoleResult result = new 
S3AssumeRoleResponseXml.AssumeRoleResult();
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/package-info.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/package-info.java
index 76f77800182..3383580a5eb 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/package-info.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/package-info.java
@@ -18,4 +18,15 @@
 /**
  * This package contains the AWS STS (Security Token Service) compatible API 
for S3 Gateway.
  */
+@XmlSchema(
+    namespace = "https://sts.amazonaws.com/doc/2011-06-15/";,
+    elementFormDefault = XmlNsForm.QUALIFIED,
+    xmlns = {
+        @XmlNs(prefix = "", namespaceURI = 
"https://sts.amazonaws.com/doc/2011-06-15/";)
+    }
+)
 package org.apache.hadoop.ozone.s3sts;
+
+import javax.xml.bind.annotation.XmlNs;
+import javax.xml.bind.annotation.XmlNsForm;
+import javax.xml.bind.annotation.XmlSchema;
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/exception/TestOSTSExceptions.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/exception/TestOSTSExceptions.java
new file mode 100644
index 00000000000..cd77979ec20
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/exception/TestOSTSExceptions.java
@@ -0,0 +1,102 @@
+/*
+ * 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.s3.exception;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.io.StringReader;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.apache.hadoop.ozone.web.utils.OzoneUtils;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+
+/**
+ * This class tests OSTSException class.  It is named TestOSTSExceptions 
instead of
+ * TestOSTSException to avoid findbugs rule about classes ending in *Exception 
must
+ * extend Exception or Throwable.
+ */
+public class TestOSTSExceptions {
+
+  @Test
+  public void testOSTSException() throws Exception {
+    final OSTSException ex = new OSTSException("ValidationError", "1 
validation error detected", 400);
+    final String requestId = OzoneUtils.getRequestID();
+    ex.setRequestId(requestId);
+    final String val = ex.toXml();
+
+    final Document doc = parseXml(val);
+    final Element root = doc.getDocumentElement();
+    assertEquals("ErrorResponse", root.getLocalName());
+    // Ensure the response uses the default namespace (no prefix like "ns2:")
+    assertEquals("ErrorResponse", root.getNodeName());
+    assertEquals("https://sts.amazonaws.com/doc/2011-06-15/";, 
root.getNamespaceURI());
+
+    assertEquals("Sender", 
doc.getElementsByTagName("Type").item(0).getTextContent());
+    assertEquals("ValidationError", 
doc.getElementsByTagName("Code").item(0).getTextContent());
+    assertEquals("1 validation error detected", 
doc.getElementsByTagName("Message").item(0).getTextContent());
+    assertEquals(requestId, 
doc.getElementsByTagName("RequestId").item(0).getTextContent());
+  }
+
+  @Test
+  public void testOSTSExceptionInvalidAction() throws Exception {
+    final OSTSException ex = new OSTSException("InvalidAction", "Could not 
find operation", 400);
+    final String requestId = OzoneUtils.getRequestID();
+    ex.setRequestId(requestId);
+    final String val = ex.toXml();
+
+    final Document doc = parseXml(val);
+    final Element root = doc.getDocumentElement();
+    assertEquals("ErrorResponse", root.getLocalName());
+    assertEquals("http://webservices.amazon.com/AWSFault/2005-15-09";, 
root.getNamespaceURI());
+
+    assertEquals("Sender", 
doc.getElementsByTagName("Type").item(0).getTextContent());
+    assertEquals("InvalidAction", 
doc.getElementsByTagName("Code").item(0).getTextContent());
+    assertEquals("Could not find operation", 
doc.getElementsByTagName("Message").item(0).getTextContent());
+    assertEquals(requestId, 
doc.getElementsByTagName("RequestId").item(0).getTextContent());
+  }
+
+  @Test
+  public void testOSTSExceptionWithCustomType() throws Exception {
+    final OSTSException ex = new OSTSException("InternalFailure", "An internal 
error has occurred.", 500, "Receiver");
+    final String requestId = OzoneUtils.getRequestID();
+    ex.setRequestId(requestId);
+    final String val = ex.toXml();
+
+    final Document doc = parseXml(val);
+    final Element root = doc.getDocumentElement();
+    assertEquals("ErrorResponse", root.getLocalName());
+    assertEquals("https://sts.amazonaws.com/doc/2011-06-15/";, 
root.getNamespaceURI());
+
+    assertEquals("Receiver", 
doc.getElementsByTagName("Type").item(0).getTextContent());
+    assertEquals("InternalFailure", 
doc.getElementsByTagName("Code").item(0).getTextContent());
+    assertEquals("An internal error has occurred.", 
doc.getElementsByTagName("Message").item(0).getTextContent());
+    assertEquals(requestId, 
doc.getElementsByTagName("RequestId").item(0).getTextContent());
+  }
+
+  private static Document parseXml(String xml) throws Exception {
+    assertNotNull(xml);
+    final DocumentBuilderFactory documentBuilderFactory = 
DocumentBuilderFactory.newInstance();
+    documentBuilderFactory.setNamespaceAware(true);
+    final DocumentBuilder documentBuilder = 
documentBuilderFactory.newDocumentBuilder();
+    return documentBuilder.parse(new InputSource(new StringReader(xml)));
+  }
+}
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 a78c2c394e5..aefb525448b 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
@@ -20,14 +20,17 @@
 import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_S3_ADMINISTRATORS;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import java.io.IOException;
 import java.io.StringReader;
 import java.time.Instant;
 import javax.ws.rs.container.ContainerRequestContext;
@@ -39,11 +42,14 @@
 import org.apache.hadoop.ozone.client.ObjectStore;
 import org.apache.hadoop.ozone.client.OzoneClient;
 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.s3.OzoneConfigurationHolder;
+import org.apache.hadoop.ozone.s3.exception.OSTSException;
 import org.apache.hadoop.ozone.s3.signature.SignatureInfo;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
@@ -54,9 +60,12 @@
  */
 public class TestS3STSEndpoint {
   private S3STSEndpoint endpoint;
+  private ObjectStore objectStore;
   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;
+  private static final String STS_NS = 
"https://sts.amazonaws.com/doc/2011-06-15/";;
+  private static final String AWS_FAULT_NS = 
"http://webservices.amazon.com/AWSFault/2005-15-09";;
 
   @Mock
   private ContainerRequestContext context;
@@ -69,7 +78,7 @@ public void setup() throws Exception {
     OzoneClient clientStub = spy(new OzoneClientStub());
 
     // Stub assumeRole to return deterministic credentials.
-    ObjectStore objectStore = mock(ObjectStore.class);
+    objectStore = mock(ObjectStore.class);
     when(objectStore.assumeRole(anyString(), anyString(), anyInt(), any()))
         .thenReturn(new AssumeRoleResponseInfo(
             "ASIA1234567890123456",
@@ -91,7 +100,7 @@ public void setup() throws Exception {
   }
 
   @Test
-  public void testStsAssumeRole() throws Exception {
+  public void testStsAssumeRoleValidForGetMethod() throws Exception {
     Response response = endpoint.get(
         "AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, "2011-06-15", null);
 
@@ -101,13 +110,18 @@ public void testStsAssumeRole() throws Exception {
     assertNotNull(responseXml);
 
     // Parse response XML and verify values
-    final DocumentBuilderFactory documentBuilderFactory = 
DocumentBuilderFactory.newInstance();
-    documentBuilderFactory.setNamespaceAware(true);
-    final DocumentBuilder documentBuilder = 
documentBuilderFactory.newDocumentBuilder();
-    final Document doc = documentBuilder.parse(new InputSource(new 
StringReader(responseXml)));
+    final Document doc = parseXml(responseXml);
 
     final Element root = doc.getDocumentElement();
     assertEquals("AssumeRoleResponse", root.getLocalName());
+    assertEquals(STS_NS, root.getNamespaceURI());
+    // Ensure the response uses the default namespace (no prefix like "ns2:")
+    assertEquals("AssumeRoleResponse", root.getNodeName());
+
+    // Verify some key elements are present in the STS namespace
+    assertNotNull(doc.getElementsByTagNameNS(STS_NS, 
"AssumeRoleResult").item(0));
+    assertNotNull(doc.getElementsByTagNameNS(STS_NS, "Credentials").item(0));
+    assertNotNull(doc.getElementsByTagNameNS(STS_NS, "AccessKeyId").item(0));
 
     final String accessKeyId = 
doc.getElementsByTagName("AccessKeyId").item(0).getTextContent();
     assertEquals("ASIA1234567890123456", accessKeyId);
@@ -123,56 +137,320 @@ public void testStsAssumeRole() throws Exception {
   }
 
   @Test
-  public void testStsInvalidDuration() throws Exception {
-    Response response = endpoint.get(
-        "AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, -1, "2011-06-15", null);
+  public void testStsAssumeRoleValidForPostMethod() throws Exception {
+    final Response response = endpoint.post("AssumeRole", ROLE_ARN, 
ROLE_SESSION_NAME, 3600, "2011-06-15", null);
 
-    assertEquals(400, response.getStatus());
-    String errorMessage = (String) response.getEntity();
-    assertTrue(errorMessage.contains("Invalid Value: DurationSeconds"));
+    assertEquals(200, response.getStatus());
+    final String responseXml = (String) response.getEntity();
+    assertNotNull(responseXml);
+
+    final Document doc = parseXml(responseXml);
+    final Element root = doc.getDocumentElement();
+    assertEquals("AssumeRoleResponse", root.getLocalName());
+    assertEquals(STS_NS, root.getNamespaceURI());
+    // Ensure the response uses the default namespace (no prefix like "ns2:")
+    assertEquals("AssumeRoleResponse", root.getNodeName());
+
+    // Verify some key elements are present in the STS namespace
+    assertNotNull(doc.getElementsByTagNameNS(STS_NS, 
"AssumeRoleResult").item(0));
+    assertNotNull(doc.getElementsByTagNameNS(STS_NS, "Credentials").item(0));
+    assertNotNull(doc.getElementsByTagNameNS(STS_NS, "AccessKeyId").item(0));
+
+    final String accessKeyId = 
doc.getElementsByTagName("AccessKeyId").item(0).getTextContent();
+    assertEquals("ASIA1234567890123456", accessKeyId);
+
+    final String secretAccessKey = 
doc.getElementsByTagName("SecretAccessKey").item(0).getTextContent();
+    assertEquals("mySecretAccessKey", secretAccessKey);
+
+    final String sessionToken = 
doc.getElementsByTagName("SessionToken").item(0).getTextContent();
+    assertEquals("session-token", sessionToken);
+
+    final String arn = 
doc.getElementsByTagName("Arn").item(0).getTextContent();
+    assertEquals(ROLE_USER_ARN, arn);
   }
 
   @Test
-  public void testStsUnsupportedAction() throws Exception {
-    Response response = endpoint.get(
-        "UnsupportedAction", ROLE_ARN, ROLE_SESSION_NAME, 3600, "2011-06-15", 
null);
+  public void testStsNullAction() throws Exception {
+    final Response response = endpoint.get(null, ROLE_ARN, ROLE_SESSION_NAME, 
3600, "2011-06-15", null);
 
     assertEquals(400, response.getStatus());
-    String errorMessage = (String) response.getEntity();
-    assertTrue(errorMessage.contains("Unsupported Action"));
+    final String errorMessage = (String) response.getEntity();
+    assertEquals("<UnknownOperationException/>", errorMessage);
+
+    final Document doc = parseXml(errorMessage);
+    final Element root = doc.getDocumentElement();
+    assertEquals("UnknownOperationException", root.getLocalName());
   }
 
   @Test
-  public void testStsInvalidVersion() throws Exception {
-    Response response = endpoint.get(
-        "AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, "2000-01-01", null);
+  public void testStsUnsupportedActionWithVersionSupplied() throws Exception {
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("UnsupportedAction", ROLE_ARN, ROLE_SESSION_NAME, 3600, 
"2011-06-15", null));
 
-    assertEquals(400, response.getStatus());
-    String errorMessage = (String) response.getEntity();
-    assertTrue(errorMessage.contains("Invalid or missing Version parameter. 
Supported version is 2011-06-15."));
+    assertEquals(400, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), AWS_FAULT_NS, "Sender", "InvalidAction",
+        "Could not find operation UnsupportedAction for version 2011-06-15");
+  }
+
+  @Test
+  public void testStsUnsupportedActionWithVersionNotSupplied() throws 
Exception {
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("UnsupportedAction", ROLE_ARN, ROLE_SESSION_NAME, 3600, 
null, null));
+
+    assertEquals(400, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), AWS_FAULT_NS, "Sender", "InvalidAction",
+        "Could not find operation UnsupportedAction for version 
NO_VERSION_SPECIFIED");
+  }
+
+  @Test
+  public void testStsAssumeRoleWithInvalidVersion() throws Exception {
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, 
"2000-01-01", null));
+
+    assertEquals(400, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), AWS_FAULT_NS, "Sender", "InvalidAction",
+        "Could not find operation AssumeRole for version 2000-01-01");
+  }
+
+  @Test
+  public void testStsInvalidDuration() throws Exception {
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, -1, 
"2011-06-15", null));
+
+    assertEquals(400, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid Value: DurationSeconds");
+  }
+
+  @Test
+  public void testStsNullDurationUsesDefault3600() throws Exception {
+    final Response response = endpoint.get(
+        "AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, null, "2011-06-15", null);
+    assertEquals(200, response.getStatus());
+
+    final ArgumentCaptor<Integer> durationCaptor = 
ArgumentCaptor.forClass(Integer.class);
+    verify(objectStore).assumeRole(anyString(), anyString(), 
durationCaptor.capture(), any());
+    assertEquals(3600, durationCaptor.getValue());
   }
 
   @Test
   public void testStsPolicyTooLarge() throws Exception {
     final String tooLargePolicy = 
RandomStringUtils.insecure().nextAlphanumeric(2049);
 
-    final Response response = endpoint.get(
-        "AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, "2011-06-15", 
tooLargePolicy);
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, 
"2011-06-15", tooLargePolicy));
 
-    assertEquals(400, response.getStatus());
-    final String errorMessage = (String) response.getEntity();
-    assertTrue(errorMessage.contains("Policy length exceeded maximum allowed 
length of 2048"));
+    assertEquals(400, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError",
+        "Value '" + tooLargePolicy + "' at 'policy' failed to satisfy 
constraint: Member " +
+        "must have length less than or equal to 2048");
   }
 
   @Test
   public void testStsInvalidRoleArn() throws Exception {
     final String invalidRoleArn = 
"arn:awsNotValid::123456789012:role/test-role";
-    final Response response = endpoint.get(
-        "AssumeRole", invalidRoleArn, ROLE_SESSION_NAME, 3600, "2011-06-15", 
null);
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", invalidRoleArn, ROLE_SESSION_NAME, 3600, 
"2011-06-15", null));
 
-    assertEquals(400, response.getStatus());
-    final String errorMessage = (String) response.getEntity();
-    assertTrue(
-        errorMessage.contains("Invalid RoleArn: must be in the format 
arn:aws:iam::<account-id>:role/<role-name>"));
+    assertEquals(400, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError",
+        "Invalid RoleArn: must be in the format 
arn:aws:iam::<account-id>:role/<role-name>");
+  }
+
+  @Test
+  public void testStsMissingRoleArn() throws Exception {
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", null, ROLE_SESSION_NAME, 3600, 
"2011-06-15", null));
+
+    assertEquals(400, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", "Value 
null at 'roleArn'");
+  }
+
+  @Test
+  public void testStsInvalidRoleArnMissingRoleName() throws Exception {
+    final String invalidRoleArn = "arn:aws:iam::123456789012:role/";
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", invalidRoleArn, ROLE_SESSION_NAME, 3600, 
"2011-06-15", null));
+
+    assertEquals(400, ex.getHttpCode());
+    assertEquals("ValidationError", ex.getCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid RoleArn: must be in the format");
+  }
+
+  @Test
+  public void testStsInvalidRoleArnMissingAccountId() throws Exception {
+    final String invalidRoleArn = "arn:aws:iam:::role/test-role";
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", invalidRoleArn, ROLE_SESSION_NAME, 3600, 
"2011-06-15", null));
+
+    assertEquals(400, ex.getHttpCode());
+    assertEquals("ValidationError", ex.getCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid RoleArn: must be in the format"
+    );
+  }
+
+  @Test
+  public void testStsWhenActionNotImplemented() throws Exception {
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("GetSessionToken", ROLE_ARN, ROLE_SESSION_NAME, 3600, 
"2011-06-15", null));
+
+    assertEquals(501, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), AWS_FAULT_NS, "Sender", "InvalidAction",
+        "Operation GetSessionToken is not supported yet.");
+  }
+
+  @Test
+  public void testStsMissingRoleSessionName() throws Exception {
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", ROLE_ARN, null, 3600, "2011-06-15", null));
+
+    assertEquals(400, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", "Value 
null at 'roleSessionName'");
+  }
+
+  @Test
+  public void testStsInvalidRoleSessionNameWithInvalidCharacter() throws 
Exception {
+    final String invalidSession = "test/session";
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", ROLE_ARN, invalidSession, 3600, 
"2011-06-15", null));
+
+    assertEquals(400, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid RoleSessionName");
+  }
+
+  @Test
+  public void testStsInvalidRoleSessionNameTooShort() throws Exception {
+    final String invalidSession = "a";
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", ROLE_ARN, invalidSession, 3600, 
"2011-06-15", null));
+
+    assertEquals(400, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid RoleSessionName");
+  }
+
+  @Test
+  public void testStsInvalidRoleArnResourceType() throws Exception {
+    // Resource type must be role, not user
+    final String invalidRoleArn = "arn:aws:iam::123456789012:user/test-user";
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", invalidRoleArn, ROLE_SESSION_NAME, 3600, 
"2011-06-15", null));
+
+    assertEquals(400, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid RoleArn: must be in the format");
+  }
+
+  @Test
+  public void testStsInternalFailureWhenBackendThrows() throws Exception {
+    when(objectStore.assumeRole(anyString(), anyString(), anyInt(), any()))
+        .thenThrow(new RuntimeException("some unexpected error"));
+
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, 
"2011-06-15", null));
+
+    assertEquals(500, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), STS_NS, "Receiver", "InternalFailure", "An 
internal error has occurred.");
+  }
+
+  @Test
+  public void testStsAccessDenied() throws Exception {
+    when(objectStore.assumeRole(anyString(), anyString(), anyInt(), any()))
+        .thenThrow(new OMException("Permission denied", 
OMException.ResultCodes.ACCESS_DENIED));
+
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, 
"2011-06-15", null));
+
+    assertEquals(403, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "AccessDenied",
+        "User is not authorized to perform: sts:AssumeRole on resource: " + 
ROLE_ARN);
+  }
+
+  @Test
+  public void testStsIOExceptionWrappedAsInternalFailure() throws Exception {
+    when(objectStore.assumeRole(anyString(), anyString(), anyInt(), any()))
+        .thenThrow(new IOException("An IO error occurred"));
+
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, 
"2011-06-15", null));
+
+    assertEquals(500, ex.getHttpCode());
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+    assertStsErrorXml(ex.toXml(), STS_NS, "Receiver", "InternalFailure", "An 
internal error has occurred.");
+  }
+
+  private static Document parseXml(String xml) throws Exception {
+    final DocumentBuilderFactory documentBuilderFactory = 
DocumentBuilderFactory.newInstance();
+    documentBuilderFactory.setNamespaceAware(true);
+    final DocumentBuilder documentBuilder = 
documentBuilderFactory.newDocumentBuilder();
+    return documentBuilder.parse(new InputSource(new StringReader(xml)));
+  }
+
+  private static void assertStsErrorXml(String xml, String expectedNamespace, 
String expectedType, String expectedCode,
+      String expectedMessageContains) throws Exception {
+    final Document doc = parseXml(xml);
+    final Element root = doc.getDocumentElement();
+    assertEquals("ErrorResponse", root.getLocalName());
+    assertEquals(expectedNamespace, root.getNamespaceURI());
+
+    final String type = 
doc.getElementsByTagName("Type").item(0).getTextContent();
+    assertEquals(expectedType, type);
+
+    final String code = 
doc.getElementsByTagName("Code").item(0).getTextContent();
+    assertEquals(expectedCode, code);
+
+    final String message = 
doc.getElementsByTagName("Message").item(0).getTextContent();
+    assertNotNull(message);
+    assertTrue(message.contains(expectedMessageContains), "Expected message to 
contain: " + expectedMessageContains);
+
+    final String requestId = 
doc.getElementsByTagName("RequestId").item(0).getTextContent();
+    assertEquals("test-request-id", requestId);
   }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to