-
Notifications
You must be signed in to change notification settings - Fork 616
HDDS-13345. Security Token Service (STS) - S3 compatible API for temporary S3 credentials #8794
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
dc250d0
Skelton STS API endpoint implemented by Claude
len548 731a3e0
Move STS endpoint service to webadmin by Claude Code. s3g is not work…
len548 ff3a1df
Combined /secret and /sts endpoints into one servlet, but still failing.
len548 2952260
Renamed package name (s3sts -> sts)
len548 f44a2d3
Add skeleton STS API on 9878 S3 Gateway
len548 f61e8cb
Add authentication to only allow admins to call assume-role API
len548 bfd10a4
unit test, improve error handlings, and add TODOs
len548 1738429
removed unused lines and metrics
len548 edff7ce
Added a desgin doc
len548 6065b94
Corrected misinformation on the design doc
len548 b7038fa
Move a design doc to HDDS-13323 branch
len548 ad8260f
WIP: refactoring STS into webadmin port 19878 [skip ci]
len548 4475770
WIP: new third port in S3 Gateway for STS [skip ci]
len548 593fc4c
WIP: fixed to establish a third port [skip ci] with sts endpoint not …
len548 05a9b7a
Refactored webadmin port to house both s3secret and sts endpoints.
len548 19dcdb3
Address CI tests
len548 47f232a
removed unused sts config keys and fixed TestSTS
len548 ccc683f
Added 19878 port number to ozonesecure and modified web.xml
len548 f09633d
Fixed checkstyle
len548 bf98d1b
Address intergration test failure
len548 720876e
Merge branch 'master' of github.com:apache/ozone into HDDS-13345-sts-…
len548 7e64053
Fixed checkstyle
len548 75a5b2e
Add filter to disable STS API
len548 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
321 changes: 321 additions & 0 deletions
321
hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/STSEndpoint.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,321 @@ | ||
| /* | ||
| * 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.endpoint; | ||
|
|
||
| import java.io.IOException; | ||
| import java.time.Instant; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.ArrayList; | ||
| import java.util.Base64; | ||
| import java.util.List; | ||
| import java.util.UUID; | ||
| import javax.ws.rs.FormParam; | ||
| 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.MediaType; | ||
| import javax.ws.rs.core.Response; | ||
| import org.apache.hadoop.ozone.OzoneAcl; | ||
| import org.apache.hadoop.ozone.s3.exception.OS3Exception; | ||
| import org.apache.hadoop.ozone.s3.exception.S3ErrorTable; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.getInternalError; | ||
| import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.newError; | ||
|
|
||
| /** | ||
| * AWS STS (Security Token Service) compatible endpoint for Ozone S3 Gateway. | ||
| * | ||
| * This endpoint provides temporary security credentials compatible with | ||
| * AWS STS API, exposed on the webadmin port (19878) at /sts endpoint. | ||
| * | ||
| * Currently supports only AssumeRole operation. Other STS operations will | ||
| * return appropriate error responses. | ||
| * | ||
| * @see <a href="https://docs.aws.amazon.com/STS/latest/APIReference/">AWS STS API Reference</a> | ||
| */ | ||
| @Path("/") | ||
| public class STSEndpoint extends EndpointBase { | ||
|
|
||
| private static final Logger LOG = LoggerFactory.getLogger(STSEndpoint.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"; | ||
| private static final String ASSUME_ROLE_WITH_WEB_IDENTITY_ACTION = "AssumeRoleWithWebIdentity"; | ||
| private static final String GET_CALLER_IDENTITY_ACTION = "GetCallerIdentity"; | ||
| private static final String DECODE_AUTHORIZATION_MESSAGE_ACTION = "DecodeAuthorizationMessage"; | ||
| private static final String GET_ACCESS_KEY_INFO_ACTION = "GetAccessKeyInfo"; | ||
|
|
||
| // Default token duration (in seconds) - AWS default is 3600 (1 hour) | ||
| private static final int DEFAULT_DURATION_SECONDS = 3600; | ||
| private static final int MAX_DURATION_SECONDS = 43200; // 12 hours | ||
| private static final int MIN_DURATION_SECONDS = 900; // 15 minutes | ||
|
|
||
| /** | ||
| * STS endpoint that handles GET requests with query parameters. | ||
| * AWS STS supports both GET and POST requests. | ||
| * | ||
| * @param action The STS action to perform (AssumeRole, GetSessionToken, etc.) | ||
| * @param roleArn The ARN of the role to assume (for AssumeRole) | ||
| * @param roleSessionName Session name for the role (for AssumeRole) | ||
| * @param durationSeconds Duration of the token validity in seconds | ||
| * @param version AWS STS API version (should be "2011-06-15") | ||
| * @return Response containing STS response XML or error | ||
| */ | ||
| @GET | ||
| @Produces(MediaType.APPLICATION_XML) | ||
| public Response handleSTSGet( | ||
| @QueryParam("Action") String action, | ||
| @QueryParam("RoleArn") String roleArn, | ||
| @QueryParam("RoleSessionName") String roleSessionName, | ||
| @QueryParam("DurationSeconds") Integer durationSeconds, | ||
| @QueryParam("Version") String version) throws OS3Exception { | ||
|
|
||
| return handleSTSRequest(action, roleArn, roleSessionName, durationSeconds, version); | ||
| } | ||
|
|
||
| /** | ||
| * 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 | ||
| * @return Response containing STS response XML or error | ||
| */ | ||
| @POST | ||
| @Produces(MediaType.APPLICATION_XML) | ||
| public Response handleSTSPost( | ||
| @FormParam("Action") String action, | ||
| @FormParam("RoleArn") String roleArn, | ||
| @FormParam("RoleSessionName") String roleSessionName, | ||
| @FormParam("DurationSeconds") Integer durationSeconds, | ||
| @FormParam("Version") String version) throws OS3Exception { | ||
|
|
||
| return handleSTSRequest(action, roleArn, roleSessionName, durationSeconds, version); | ||
| } | ||
|
|
||
| private Response handleSTSRequest(String action, String roleArn, String roleSessionName, | ||
| Integer durationSeconds, String version) throws OS3Exception { | ||
|
|
||
| try { | ||
|
|
||
| // Default action if not specified (following AWS behavior) | ||
| if (action == null) { | ||
| throw newError(S3ErrorTable.MISSING_PARAMETER, STS_ACTION_PARAM); | ||
| } | ||
|
|
||
| // Validate and normalize duration | ||
| int duration; | ||
| try { | ||
| duration = validateDuration(durationSeconds); | ||
| } catch (IllegalArgumentException e) { | ||
| throw newError(S3ErrorTable.INVALID_PARAMETER_VALUE, | ||
| ROLE_DURATION_SECONDS_PARAM, e); | ||
| } | ||
|
|
||
| // Handle different STS actions | ||
| switch (action) { | ||
| case ASSUME_ROLE_ACTION: | ||
| return handleAssumeRole(roleArn, roleSessionName, duration); | ||
|
|
||
| // All other STS operations are not supported yet | ||
| case GET_SESSION_TOKEN_ACTION: | ||
| case ASSUME_ROLE_WITH_SAML_ACTION: | ||
| case ASSUME_ROLE_WITH_WEB_IDENTITY_ACTION: | ||
| case GET_CALLER_IDENTITY_ACTION: | ||
| case DECODE_AUTHORIZATION_MESSAGE_ACTION: | ||
| case GET_ACCESS_KEY_INFO_ACTION: | ||
| throw newError(S3ErrorTable.NOT_IMPLEMENTED, action); | ||
| default: | ||
| throw newError(S3ErrorTable.INVALID_ACTION, action); | ||
| } | ||
|
|
||
| } catch (Exception ex) { | ||
| OS3Exception os3Exception = getInternalError(ex); | ||
| os3Exception.setResource("STS Request: " + action); | ||
| throw os3Exception; | ||
| } | ||
| } | ||
|
|
||
| private int validateDuration(Integer durationSeconds) throws IllegalArgumentException { | ||
| if (durationSeconds == null) { | ||
| return DEFAULT_DURATION_SECONDS; | ||
| } | ||
|
|
||
| if (durationSeconds < MIN_DURATION_SECONDS || durationSeconds > MAX_DURATION_SECONDS) { | ||
| throw new IllegalArgumentException( | ||
| "DurationSeconds must be between " + MIN_DURATION_SECONDS + | ||
| " and " + MAX_DURATION_SECONDS + " seconds"); | ||
| } | ||
|
|
||
| return durationSeconds; | ||
| } | ||
|
|
||
| private Response handleAssumeRole(String roleArn, String roleSessionName, int duration) | ||
| throws IOException, OS3Exception { | ||
| // Validate required parameters for AssumeRole. RoleArn is required to pass the | ||
| if (roleArn == null || roleArn.trim().isEmpty()) { | ||
| throw newError(S3ErrorTable.MISSING_PARAMETER, ROLE_ARN_PARAM); | ||
| } | ||
|
|
||
| if (roleSessionName == null || roleSessionName.trim().isEmpty()) { | ||
| OS3Exception os3Exception = newError(S3ErrorTable.MISSING_PARAMETER, roleSessionName); | ||
| os3Exception.setErrorMessage("RoleSessionName is required for AssumeRole operation"); | ||
| throw os3Exception; | ||
| } | ||
|
|
||
| // Validate role session name format (AWS requirements) | ||
| if (!isValidRoleSessionName(roleSessionName)) { | ||
| OS3Exception os3Exception = newError(S3ErrorTable.INVALID_PARAMETER_VALUE, roleSessionName); | ||
| os3Exception.setErrorMessage("RoleSessionName must be 2-64 characters long and contain only alphanumeric" + | ||
| "characters, plus signs (+), equal signs (=), commas (,), periods (.), at symbols (@), and hyphens (-)"); | ||
| throw os3Exception; | ||
| } | ||
| // TODO: Add a validation if a user is not an admin but still allowed to call AssumeRole | ||
|
|
||
| // TODO: Convert roleArn to a valid Ozone ACL | ||
| // TODO: Validate requested ACLs | ||
| // TODO: Create a new S3 credentials for this role session | ||
| // TODO: Add validated ACLs for the new credentials | ||
| // TODO: How do we handle expired credentials? We don't support renewal? | ||
|
|
||
| // Generate AssumeRole response | ||
| String responseXml = generateAssumeRoleResponse(roleArn, roleSessionName, duration); | ||
|
|
||
| return Response.ok(responseXml) | ||
| .header("Content-Type", "text/xml") | ||
| .build(); | ||
| } | ||
|
|
||
|
|
||
| private List<OzoneAcl> toOzoneAcls(String roleArn) { | ||
| // TODO: Implement logic to convert roleArn (String) to Ozone ACLs | ||
| // TODO: Throw an exception if roleArn is invalid or not found | ||
| List<OzoneAcl> acls = new ArrayList<>(); | ||
| return acls; | ||
| } | ||
|
|
||
|
|
||
|
|
||
| private List<OzoneAcl> checkStsAclSubset(List<OzoneAcl> requestedAcls) throws IOException { | ||
| List<OzoneAcl> validAcls = new ArrayList<>(); | ||
| // TODO: Implement logic to check if requested ACLs are a valid subset of the user's ACLs | ||
| return validAcls; | ||
| } | ||
|
|
||
| // Helper methods to implement: | ||
| // OzoneObject getOzoneObjectFromAcl(OzoneAcl acl); // Parses the ACL to identify the target object | ||
| // List<OzoneAcl> getAclsForObject(OzoneObject object); // Efficiently fetches ACLs for a single object | ||
| // boolean isUserOrGroupMatch(OzoneAcl objectAcl, String userName, Set<String> groups); | ||
|
|
||
| private boolean isValidRoleSessionName(String roleSessionName) { | ||
| if (roleSessionName.length() < 2 || roleSessionName.length() > 64) { | ||
| return false; | ||
| } | ||
|
|
||
| // AWS allows: alphanumeric, +, =, ,, ., @, - | ||
| return roleSessionName.matches("[a-zA-Z0-9+=,.@-]+"); | ||
| } | ||
|
|
||
| // TODO: replace mock implementation with actual logic to generate new credentials | ||
| private String generateAssumeRoleResponse(String roleArn, String roleSessionName, int duration) { | ||
| // Generate realistic-looking temporary credentials | ||
| String accessKeyId = "ASIA" + generateRandomAlphanumeric(16); // AWS temp keys start with ASIA | ||
| String secretAccessKey = generateRandomBase64(40); | ||
| String sessionToken = generateSessionToken(); | ||
| String expiration = getExpirationTime(duration); | ||
|
|
||
| // Generate AssumedRoleId (format: AROLEID:RoleSessionName) | ||
| String roleId = "AROA" + generateRandomAlphanumeric(16); | ||
| String assumedRoleId = roleId + ":" + roleSessionName; | ||
|
|
||
| String requestId = UUID.randomUUID().toString(); | ||
|
|
||
| return String.format( | ||
| "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + | ||
| "<AssumeRoleResponse xmlns=\"https://sts.amazonaws.com/doc/2011-06-15/\">\n" + | ||
| " <AssumeRoleResult>\n" + | ||
| " <Credentials>\n" + | ||
| " <AccessKeyId>%s</AccessKeyId>\n" + | ||
| " <SecretAccessKey>%s</SecretAccessKey>\n" + | ||
| " <SessionToken>%s</SessionToken>\n" + | ||
| " <Expiration>%s</Expiration>\n" + | ||
| " </Credentials>\n" + | ||
| " <AssumedRoleUser>\n" + | ||
| " <AssumedRoleId>%s</AssumedRoleId>\n" + | ||
| " <Arn>%s</Arn>\n" + | ||
| " </AssumedRoleUser>\n" + | ||
| " </AssumeRoleResult>\n" + | ||
| " <ResponseMetadata>\n" + | ||
| " <RequestId>%s</RequestId>\n" + | ||
| " </ResponseMetadata>\n" + | ||
| "</AssumeRoleResponse>", | ||
| accessKeyId, secretAccessKey, sessionToken, expiration, | ||
| assumedRoleId, roleArn, requestId); | ||
| } | ||
|
|
||
| // Helper methods to generate random alphanumeric and base64 strings for mock credentials. | ||
| // TODO: these should be replaced with actual credential generation logic. | ||
| private String generateRandomAlphanumeric(int length) { | ||
| String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; | ||
| StringBuilder sb = new StringBuilder(); | ||
| for (int i = 0; i < length; i++) { | ||
| sb.append(chars.charAt((int) (Math.random() * chars.length()))); | ||
| } | ||
| return sb.toString(); | ||
| } | ||
|
|
||
| private String generateRandomBase64(int length) { | ||
| String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | ||
| StringBuilder sb = new StringBuilder(); | ||
| for (int i = 0; i < length; i++) { | ||
| sb.append(chars.charAt((int) (Math.random() * chars.length()))); | ||
| } | ||
| return sb.toString(); | ||
| } | ||
|
|
||
| private String generateSessionToken() { | ||
| // Generate a realistic-looking session token (base64 encoded) | ||
| byte[] tokenBytes = new byte[128]; // Longer session tokens like AWS | ||
| for (int i = 0; i < tokenBytes.length; i++) { | ||
| tokenBytes[i] = (byte) (Math.random() * 256); | ||
| } | ||
| return Base64.getEncoder().encodeToString(tokenBytes); | ||
| } | ||
|
|
||
| private String getExpirationTime(int durationSeconds) { | ||
| Instant expiration = Instant.now().plusSeconds(durationSeconds); | ||
| return DateTimeFormatter.ISO_INSTANT.format(expiration); | ||
| } | ||
|
|
||
|
|
||
| public void init () { | ||
|
|
||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this would collide with RootEndpoint? I guess you want it at /sts