You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

282 lines
9.1 KiB

package s3api
// This file provides STS (Security Token Service) HTTP endpoints for AWS SDK compatibility.
// It exposes AssumeRoleWithWebIdentity as an HTTP endpoint that can be used with
// AWS SDKs to obtain temporary credentials using OIDC/JWT tokens.
import (
"encoding/xml"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
)
// STS API constants matching AWS STS specification
const (
stsAPIVersion = "2011-06-15"
stsAction = "Action"
stsVersion = "Version"
stsWebIdentityToken = "WebIdentityToken"
stsRoleArn = "RoleArn"
stsRoleSessionName = "RoleSessionName"
stsDurationSeconds = "DurationSeconds"
// STS Action names
actionAssumeRoleWithWebIdentity = "AssumeRoleWithWebIdentity"
)
// STSHandlers provides HTTP handlers for STS operations
type STSHandlers struct {
stsService *sts.STSService
}
// NewSTSHandlers creates a new STSHandlers instance
func NewSTSHandlers(stsService *sts.STSService) *STSHandlers {
return &STSHandlers{
stsService: stsService,
}
}
// HandleSTSRequest is the main entry point for STS requests
// It routes requests based on the Action parameter
func (h *STSHandlers) HandleSTSRequest(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
h.writeSTSErrorResponse(w, r, STSErrInvalidParameterValue, err)
return
}
// Validate API version
version := r.Form.Get(stsVersion)
if version != "" && version != stsAPIVersion {
h.writeSTSErrorResponse(w, r, STSErrInvalidParameterValue,
fmt.Errorf("invalid STS API version %s, expecting %s", version, stsAPIVersion))
return
}
// Route based on action
action := r.Form.Get(stsAction)
switch action {
case actionAssumeRoleWithWebIdentity:
h.handleAssumeRoleWithWebIdentity(w, r)
default:
h.writeSTSErrorResponse(w, r, STSErrInvalidAction,
fmt.Errorf("unsupported action: %s", action))
}
}
// handleAssumeRoleWithWebIdentity handles the AssumeRoleWithWebIdentity API action
func (h *STSHandlers) handleAssumeRoleWithWebIdentity(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Extract parameters from form (supports both query and POST body)
roleArn := r.FormValue("RoleArn")
webIdentityToken := r.FormValue("WebIdentityToken")
roleSessionName := r.FormValue("RoleSessionName")
// Validate required parameters
if webIdentityToken == "" {
h.writeSTSErrorResponse(w, r, STSErrMissingParameter,
fmt.Errorf("WebIdentityToken is required"))
return
}
if roleArn == "" {
h.writeSTSErrorResponse(w, r, STSErrMissingParameter,
fmt.Errorf("RoleArn is required"))
return
}
if roleSessionName == "" {
h.writeSTSErrorResponse(w, r, STSErrMissingParameter,
fmt.Errorf("RoleSessionName is required"))
return
}
// Parse and validate DurationSeconds
var durationSeconds *int64
if dsStr := r.FormValue("DurationSeconds"); dsStr != "" {
ds, err := strconv.ParseInt(dsStr, 10, 64)
if err != nil {
h.writeSTSErrorResponse(w, r, STSErrInvalidParameterValue,
fmt.Errorf("invalid DurationSeconds: %w", err))
return
}
// Enforce AWS STS-compatible duration range for AssumeRoleWithWebIdentity
// AWS allows 900 seconds (15 minutes) to 43200 seconds (12 hours)
const (
minDurationSeconds = int64(900)
maxDurationSeconds = int64(43200)
)
if ds < minDurationSeconds || ds > maxDurationSeconds {
h.writeSTSErrorResponse(w, r, STSErrInvalidParameterValue,
fmt.Errorf("DurationSeconds must be between %d and %d seconds", minDurationSeconds, maxDurationSeconds))
return
}
durationSeconds = &ds
}
// Check if STS service is initialized
if h.stsService == nil || !h.stsService.IsInitialized() {
h.writeSTSErrorResponse(w, r, STSErrSTSNotReady,
fmt.Errorf("STS service not initialized"))
return
}
// Build request for STS service
request := &sts.AssumeRoleWithWebIdentityRequest{
RoleArn: roleArn,
WebIdentityToken: webIdentityToken,
RoleSessionName: roleSessionName,
DurationSeconds: durationSeconds,
}
// Call STS service
response, err := h.stsService.AssumeRoleWithWebIdentity(ctx, request)
if err != nil {
glog.V(2).Infof("AssumeRoleWithWebIdentity failed: %v", err)
// Use typed errors for robust error checking
// This decouples HTTP layer from service implementation details
errCode := STSErrAccessDenied
if errors.Is(err, sts.ErrTypedTokenExpired) {
errCode = STSErrExpiredToken
} else if errors.Is(err, sts.ErrTypedInvalidToken) {
errCode = STSErrInvalidParameterValue
} else if errors.Is(err, sts.ErrTypedInvalidIssuer) {
errCode = STSErrInvalidParameterValue
} else if errors.Is(err, sts.ErrTypedInvalidAudience) {
errCode = STSErrInvalidParameterValue
} else if errors.Is(err, sts.ErrTypedMissingClaims) {
errCode = STSErrInvalidParameterValue
}
h.writeSTSErrorResponse(w, r, errCode, err)
return
}
// Build and return XML response
xmlResponse := &AssumeRoleWithWebIdentityResponse{
Result: WebIdentityResult{
Credentials: STSCredentials{
AccessKeyId: response.Credentials.AccessKeyId,
SecretAccessKey: response.Credentials.SecretAccessKey,
SessionToken: response.Credentials.SessionToken,
Expiration: response.Credentials.Expiration.Format(time.RFC3339),
},
SubjectFromWebIdentityToken: response.AssumedRoleUser.Subject,
},
}
xmlResponse.ResponseMetadata.RequestId = fmt.Sprintf("%d", time.Now().UnixNano())
s3err.WriteXMLResponse(w, r, http.StatusOK, xmlResponse)
}
// STS Response types for XML marshaling
// AssumeRoleWithWebIdentityResponse is the response for AssumeRoleWithWebIdentity
type AssumeRoleWithWebIdentityResponse struct {
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithWebIdentityResponse"`
Result WebIdentityResult `xml:"AssumeRoleWithWebIdentityResult"`
ResponseMetadata struct {
RequestId string `xml:"RequestId,omitempty"`
} `xml:"ResponseMetadata,omitempty"`
}
// WebIdentityResult contains the result of AssumeRoleWithWebIdentity
type WebIdentityResult struct {
Credentials STSCredentials `xml:"Credentials"`
SubjectFromWebIdentityToken string `xml:"SubjectFromWebIdentityToken,omitempty"`
AssumedRoleUser *AssumedRoleUser `xml:"AssumedRoleUser,omitempty"`
}
// STSCredentials represents temporary security credentials
type STSCredentials struct {
AccessKeyId string `xml:"AccessKeyId"`
SecretAccessKey string `xml:"SecretAccessKey"`
SessionToken string `xml:"SessionToken"`
Expiration string `xml:"Expiration"`
}
// AssumedRoleUser contains information about the assumed role
type AssumedRoleUser struct {
AssumedRoleId string `xml:"AssumedRoleId"`
Arn string `xml:"Arn"`
}
// STS Error types
// STSErrorCode represents STS error codes
type STSErrorCode string
const (
STSErrAccessDenied STSErrorCode = "AccessDenied"
STSErrExpiredToken STSErrorCode = "ExpiredTokenException"
STSErrInvalidAction STSErrorCode = "InvalidAction"
STSErrInvalidParameterValue STSErrorCode = "InvalidParameterValue"
STSErrMissingParameter STSErrorCode = "MissingParameter"
STSErrSTSNotReady STSErrorCode = "ServiceUnavailable"
STSErrInternalError STSErrorCode = "InternalError"
)
// stsErrorResponses maps error codes to HTTP status and messages
var stsErrorResponses = map[STSErrorCode]struct {
HTTPStatusCode int
Message string
}{
STSErrAccessDenied: {http.StatusForbidden, "Access Denied"},
STSErrExpiredToken: {http.StatusBadRequest, "Token has expired"},
STSErrInvalidAction: {http.StatusBadRequest, "Invalid action"},
STSErrInvalidParameterValue: {http.StatusBadRequest, "Invalid parameter value"},
STSErrMissingParameter: {http.StatusBadRequest, "Missing required parameter"},
STSErrSTSNotReady: {http.StatusServiceUnavailable, "STS service not ready"},
STSErrInternalError: {http.StatusInternalServerError, "Internal error"},
}
// STSErrorResponse is the XML error response format
type STSErrorResponse struct {
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ ErrorResponse"`
Error struct {
Type string `xml:"Type"`
Code string `xml:"Code"`
Message string `xml:"Message"`
} `xml:"Error"`
RequestId string `xml:"RequestId"`
}
// writeSTSErrorResponse writes an STS error response
func (h *STSHandlers) writeSTSErrorResponse(w http.ResponseWriter, r *http.Request, code STSErrorCode, err error) {
errInfo, ok := stsErrorResponses[code]
if !ok {
errInfo = stsErrorResponses[STSErrInternalError]
}
message := errInfo.Message
if err != nil {
message = err.Error()
}
response := STSErrorResponse{
RequestId: fmt.Sprintf("%d", time.Now().UnixNano()),
}
// Server-side errors use "Receiver" type per AWS spec
if code == STSErrInternalError || code == STSErrSTSNotReady {
response.Error.Type = "Receiver"
} else {
response.Error.Type = "Sender"
}
response.Error.Code = string(code)
response.Error.Message = message
glog.V(1).Infof("STS error response: code=%s, type=%s, message=%s", code, response.Error.Type, message)
s3err.WriteXMLResponse(w, r, errInfo.HTTPStatusCode, response)
}