* feat(s3): add STS GetFederationToken support Implement the AWS STS GetFederationToken API, which allows long-term IAM users to obtain temporary credentials scoped down by an optional inline session policy. This is useful for server-side applications that mint per-user temporary credentials. Key behaviors: - Requires SigV4 authentication from a long-term IAM user - Rejects calls from temporary credentials (session tokens) - Name parameter (2-64 chars) identifies the federated user - DurationSeconds supports 900-129600 (15 min to 36 hours, default 12h) - Optional inline session policy for permission scoping - Caller's attached policies are embedded in the JWT token - Returns federated user ARN: arn:aws:sts::<account>:federated-user/<Name> No performance impact on the S3 hot path — credential vending is a separate control-plane operation, and all policy data is embedded in the stateless JWT token. * fix(s3): address GetFederationToken PR review feedback - Fix Name validation: max 32 chars (not 64) per AWS spec, add regex validation for [\w+=,.@-]+ character whitelist - Refactor parseDurationSeconds into parseDurationSecondsWithBounds to eliminate duplicated duration parsing logic - Add sts:GetFederationToken permission check via VerifyActionPermission mirroring the AssumeRole authorization pattern - Change GetPoliciesForUser to return ([]string, error) so callers fail closed on policy-resolution failures instead of silently returning nil - Move temporary-credentials rejection before SigV4 verification for early rejection and proper test coverage - Update tests: verify specific error message for temp cred rejection, add regex validation test cases (spaces, slashes rejected) * refactor(s3): use sts.Action* constants instead of hard-coded strings Replace hard-coded "sts:AssumeRole" and "sts:GetFederationToken" strings in VerifyActionPermission calls with sts.ActionAssumeRole and sts.ActionGetFederationToken package constants. * fix(s3): pass through sts: prefix in action resolver and merge policies Two fixes: 1. mapBaseActionToS3Format now passes through "sts:" prefix alongside "s3:" and "iam:", preventing sts:GetFederationToken from being rewritten to s3:sts:GetFederationToken in VerifyActionPermission. This also fixes the existing sts:AssumeRole permission checks. 2. GetFederationToken policy embedding now merges identity.PolicyNames (from SigV4 identity) with policies from the IAM manager (which may include group-attached policies), deduplicated via a map. Previously the IAM manager lookup was skipped when identity.PolicyNames was non-empty, causing group policies to be omitted from the token. * test(s3): add integration tests for sts: action passthrough and policy merge Action resolver tests: - TestMapBaseActionToS3Format_ServicePrefixPassthrough: verifies s3:, iam:, and sts: prefixed actions pass through unchanged while coarse actions (Read, Write) are mapped to S3 format - TestResolveS3Action_STSActionsPassthrough: verifies sts:AssumeRole, sts:GetFederationToken, sts:GetCallerIdentity pass through ResolveS3Action unchanged with both nil and real HTTP requests Policy merge tests: - TestGetFederationToken_GetPoliciesForUser: tests IAMManager.GetPoliciesForUser with no user store (error), missing user, user with policies, user without - TestGetFederationToken_PolicyMergeAndDedup: tests that identity.PolicyNames and IAM-manager-resolved policies are merged and deduplicated (SharedPolicy appears in both sources, result has 3 unique policies) - TestGetFederationToken_PolicyMergeNoManager: tests that when IAM manager is unavailable, identity.PolicyNames alone are embedded * test(s3): add end-to-end integration tests for GetFederationToken Add integration tests that call GetFederationToken using real AWS SigV4 signed HTTP requests against a running SeaweedFS instance, following the existing pattern in test/s3/iam/s3_sts_assume_role_test.go. Tests: - TestSTSGetFederationTokenValidation: missing name, name too short/long, invalid characters, duration too short/long, malformed policy, anonymous rejection (7 subtests) - TestSTSGetFederationTokenRejectTemporaryCredentials: obtains temp creds via AssumeRole then verifies GetFederationToken rejects them - TestSTSGetFederationTokenSuccess: basic success, custom 1h duration, 36h max duration with expiration time verification - TestSTSGetFederationTokenWithSessionPolicy: creates a bucket, obtains federated creds with GetObject-only session policy, verifies GetObject succeeds and PutObject is denied using the AWS SDK S3 client
1051 lines
36 KiB
Go
1051 lines
36 KiB
Go
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 (
|
|
"context"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/integration"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/ldap"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/utils"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/request_id"
|
|
)
|
|
|
|
// 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
|
|
actionAssumeRole = "AssumeRole"
|
|
actionAssumeRoleWithWebIdentity = "AssumeRoleWithWebIdentity"
|
|
actionAssumeRoleWithLDAPIdentity = "AssumeRoleWithLDAPIdentity"
|
|
actionGetCallerIdentity = "GetCallerIdentity"
|
|
actionGetFederationToken = "GetFederationToken"
|
|
|
|
// GetFederationToken-specific parameters
|
|
stsFederationName = "Name"
|
|
|
|
// LDAP parameter names
|
|
stsLDAPUsername = "LDAPUsername"
|
|
stsLDAPPassword = "LDAPPassword"
|
|
stsLDAPProviderName = "LDAPProviderName"
|
|
)
|
|
|
|
// federationNameRegex validates the Name parameter for GetFederationToken per AWS spec
|
|
var federationNameRegex = regexp.MustCompile(`^[\w+=,.@-]+$`)
|
|
|
|
// STS duration constants (AWS specification)
|
|
const (
|
|
minDurationSeconds = int64(900) // 15 minutes
|
|
maxDurationSeconds = int64(43200) // 12 hours (AssumeRole)
|
|
defaultFederationDurationSeconds = int64(43200) // 12 hours (GetFederationToken default)
|
|
maxFederationDurationSeconds = int64(129600) // 36 hours (GetFederationToken max)
|
|
)
|
|
|
|
// parseDurationSecondsWithBounds parses and validates the DurationSeconds parameter
|
|
// against the given min and max bounds. Returns nil if the parameter is not provided.
|
|
func parseDurationSecondsWithBounds(r *http.Request, minSec, maxSec int64) (*int64, STSErrorCode, error) {
|
|
dsStr := r.FormValue("DurationSeconds")
|
|
if dsStr == "" {
|
|
return nil, "", nil
|
|
}
|
|
|
|
ds, err := strconv.ParseInt(dsStr, 10, 64)
|
|
if err != nil {
|
|
return nil, STSErrInvalidParameterValue, fmt.Errorf("invalid DurationSeconds: %w", err)
|
|
}
|
|
|
|
if ds < minSec || ds > maxSec {
|
|
return nil, STSErrInvalidParameterValue,
|
|
fmt.Errorf("DurationSeconds must be between %d and %d seconds", minSec, maxSec)
|
|
}
|
|
|
|
return &ds, "", nil
|
|
}
|
|
|
|
// parseDurationSeconds parses DurationSeconds for AssumeRole (15 min to 12 hours)
|
|
func parseDurationSeconds(r *http.Request) (*int64, STSErrorCode, error) {
|
|
return parseDurationSecondsWithBounds(r, minDurationSeconds, maxDurationSeconds)
|
|
}
|
|
|
|
// Removed generateSecureCredentials - now using STS service's JWT token generation
|
|
// The STS service generates proper JWT tokens with embedded claims that can be validated
|
|
// across distributed instances without shared state.
|
|
|
|
// STSHandlers provides HTTP handlers for STS operations
|
|
type STSHandlers struct {
|
|
stsService *sts.STSService
|
|
iam *IdentityAccessManagement
|
|
}
|
|
|
|
// NewSTSHandlers creates a new STSHandlers instance
|
|
func NewSTSHandlers(stsService *sts.STSService, iam *IdentityAccessManagement) *STSHandlers {
|
|
return &STSHandlers{
|
|
stsService: stsService,
|
|
iam: iam,
|
|
}
|
|
}
|
|
|
|
func (h *STSHandlers) getAccountID() string {
|
|
if h.stsService != nil && h.stsService.Config != nil && h.stsService.Config.AccountId != "" {
|
|
return h.stsService.Config.AccountId
|
|
}
|
|
return defaultAccountID
|
|
}
|
|
|
|
// 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) {
|
|
r, _ = request_id.Ensure(r)
|
|
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 actionAssumeRole:
|
|
h.handleAssumeRole(w, r)
|
|
case actionAssumeRoleWithWebIdentity:
|
|
h.handleAssumeRoleWithWebIdentity(w, r)
|
|
case actionAssumeRoleWithLDAPIdentity:
|
|
h.handleAssumeRoleWithLDAPIdentity(w, r)
|
|
case actionGetCallerIdentity:
|
|
h.handleGetCallerIdentity(w, r)
|
|
case actionGetFederationToken:
|
|
h.handleGetFederationToken(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 using helper
|
|
durationSeconds, errCode, err := parseDurationSeconds(r)
|
|
if err != nil {
|
|
h.writeSTSErrorResponse(w, r, errCode, err)
|
|
return
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
sessionPolicyJSON, err := sts.NormalizeSessionPolicy(r.FormValue("Policy"))
|
|
if err != nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrMalformedPolicyDocument,
|
|
fmt.Errorf("invalid Policy document: %w", err))
|
|
return
|
|
}
|
|
|
|
var sessionPolicyPtr *string
|
|
if sessionPolicyJSON != "" {
|
|
sessionPolicyPtr = &sessionPolicyJSON
|
|
}
|
|
|
|
// Build request for STS service
|
|
request := &sts.AssumeRoleWithWebIdentityRequest{
|
|
RoleArn: roleArn,
|
|
WebIdentityToken: webIdentityToken,
|
|
RoleSessionName: roleSessionName,
|
|
DurationSeconds: durationSeconds,
|
|
Policy: sessionPolicyPtr,
|
|
}
|
|
|
|
// 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 = request_id.GetFromRequest(r)
|
|
|
|
s3err.WriteXMLResponse(w, r, http.StatusOK, xmlResponse)
|
|
}
|
|
|
|
// handleAssumeRole handles the AssumeRole API action
|
|
// This requires AWS Signature V4 authentication
|
|
// Inline session policies (Policy parameter) are supported for AssumeRole,
|
|
// AssumeRoleWithWebIdentity, and AssumeRoleWithLDAPIdentity.
|
|
func (h *STSHandlers) handleAssumeRole(w http.ResponseWriter, r *http.Request) {
|
|
// Extract parameters from form
|
|
roleArn := r.FormValue("RoleArn")
|
|
roleSessionName := r.FormValue("RoleSessionName")
|
|
|
|
// Validate required parameters
|
|
// RoleArn is optional to support S3-compatible clients that omit it
|
|
|
|
if roleSessionName == "" {
|
|
h.writeSTSErrorResponse(w, r, STSErrMissingParameter,
|
|
fmt.Errorf("RoleSessionName is required"))
|
|
return
|
|
}
|
|
|
|
// Parse and validate DurationSeconds using helper
|
|
durationSeconds, errCode, err := parseDurationSeconds(r)
|
|
if err != nil {
|
|
h.writeSTSErrorResponse(w, r, errCode, err)
|
|
return
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Check if IAM is available for SigV4 verification
|
|
if h.iam == nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrSTSNotReady,
|
|
fmt.Errorf("IAM not configured for STS"))
|
|
return
|
|
}
|
|
|
|
// Validate AWS SigV4 authentication
|
|
identity, _, _, _, sigErrCode := h.iam.verifyV4Signature(r, false)
|
|
if sigErrCode != s3err.ErrNone {
|
|
glog.V(2).Infof("AssumeRole SigV4 verification failed: %v", sigErrCode)
|
|
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
|
|
fmt.Errorf("invalid AWS signature: %v", sigErrCode))
|
|
return
|
|
}
|
|
|
|
if identity == nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
|
|
fmt.Errorf("unable to identify caller"))
|
|
return
|
|
}
|
|
|
|
glog.V(2).Infof("AssumeRole: caller identity=%s, roleArn=%s, sessionName=%s",
|
|
identity.Name, roleArn, roleSessionName)
|
|
|
|
// Check if the caller is authorized to assume the role (sts:AssumeRole permission)
|
|
// This validates that the caller has a policy allowing sts:AssumeRole on the target role
|
|
// Check authorizations
|
|
if roleArn != "" {
|
|
// Check if the caller is authorized to assume the role (sts:AssumeRole permission)
|
|
if authErr := h.iam.VerifyActionPermission(r, identity, Action(sts.ActionAssumeRole), "", roleArn); authErr != s3err.ErrNone {
|
|
glog.V(2).Infof("AssumeRole: caller %s is not authorized to assume role %s", identity.Name, roleArn)
|
|
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
|
|
fmt.Errorf("user %s is not authorized to assume role %s", identity.Name, roleArn))
|
|
return
|
|
}
|
|
|
|
// Validate that the target role trusts the caller (Trust Policy)
|
|
if err := h.iam.ValidateTrustPolicyForPrincipal(r.Context(), roleArn, identity.PrincipalArn); err != nil {
|
|
glog.V(2).Infof("AssumeRole: trust policy validation failed for %s to assume %s: %v", identity.Name, roleArn, err)
|
|
h.writeSTSErrorResponse(w, r, STSErrAccessDenied, fmt.Errorf("trust policy denies access"))
|
|
return
|
|
}
|
|
} else {
|
|
// If RoleArn is missing, default to the caller's identity (User Context)
|
|
// This allows the user to "assume" a session for themselves, inheriting their own permissions.
|
|
roleArn = identity.PrincipalArn
|
|
glog.V(2).Infof("AssumeRole: no RoleArn provided, defaulting to caller identity: %s", roleArn)
|
|
|
|
// We still enforce a global "sts:AssumeRole" check, similar to how we'd check if they can assume *any* role.
|
|
// However, for self-assumption, this might be implicit.
|
|
// For safety/consistency with previous logic, we keep the check but strictly it might not be required by AWS for GetSessionToken.
|
|
// But since this IS AssumeRole, let's keep it.
|
|
// Admin/Global check when no specific role is requested
|
|
if authErr := h.iam.VerifyActionPermission(r, identity, Action(sts.ActionAssumeRole), "", ""); authErr != s3err.ErrNone {
|
|
glog.Warningf("AssumeRole: caller %s attempted to assume role without RoleArn and lacks global sts:AssumeRole permission", identity.Name)
|
|
h.writeSTSErrorResponse(w, r, STSErrAccessDenied, fmt.Errorf("access denied"))
|
|
return
|
|
}
|
|
}
|
|
|
|
sessionPolicyJSON, err := sts.NormalizeSessionPolicy(r.FormValue("Policy"))
|
|
if err != nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrMalformedPolicyDocument,
|
|
fmt.Errorf("invalid Policy document: %w", err))
|
|
return
|
|
}
|
|
|
|
// Prepare custom claims for the session
|
|
var modifyClaims func(claims *sts.STSSessionClaims)
|
|
if identity.isAdmin() {
|
|
modifyClaims = func(claims *sts.STSSessionClaims) {
|
|
if claims.RequestContext == nil {
|
|
claims.RequestContext = make(map[string]interface{})
|
|
}
|
|
claims.RequestContext["is_admin"] = true
|
|
}
|
|
}
|
|
|
|
// Generate common STS components
|
|
stsCreds, assumedUser, err := h.prepareSTSCredentials(r.Context(), roleArn, roleSessionName, durationSeconds, sessionPolicyJSON, modifyClaims)
|
|
if err != nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrInternalError, err)
|
|
return
|
|
}
|
|
|
|
// Build and return response
|
|
xmlResponse := &AssumeRoleResponse{
|
|
Result: AssumeRoleResult{
|
|
Credentials: stsCreds,
|
|
AssumedRoleUser: assumedUser,
|
|
},
|
|
}
|
|
xmlResponse.ResponseMetadata.RequestId = request_id.GetFromRequest(r)
|
|
|
|
s3err.WriteXMLResponse(w, r, http.StatusOK, xmlResponse)
|
|
}
|
|
|
|
// handleAssumeRoleWithLDAPIdentity handles the AssumeRoleWithLDAPIdentity API action
|
|
func (h *STSHandlers) handleAssumeRoleWithLDAPIdentity(w http.ResponseWriter, r *http.Request) {
|
|
// Extract parameters from form
|
|
roleArn := r.FormValue("RoleArn")
|
|
roleSessionName := r.FormValue("RoleSessionName")
|
|
ldapUsername := r.FormValue(stsLDAPUsername)
|
|
ldapPassword := r.FormValue(stsLDAPPassword)
|
|
|
|
// Validate required parameters
|
|
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
|
|
}
|
|
|
|
if ldapUsername == "" {
|
|
h.writeSTSErrorResponse(w, r, STSErrMissingParameter,
|
|
fmt.Errorf("LDAPUsername is required"))
|
|
return
|
|
}
|
|
|
|
if ldapPassword == "" {
|
|
h.writeSTSErrorResponse(w, r, STSErrMissingParameter,
|
|
fmt.Errorf("LDAPPassword is required"))
|
|
return
|
|
}
|
|
|
|
// Parse and validate DurationSeconds using helper
|
|
durationSeconds, errCode, err := parseDurationSeconds(r)
|
|
if err != nil {
|
|
h.writeSTSErrorResponse(w, r, errCode, err)
|
|
return
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Optional: specific LDAP provider name
|
|
ldapProviderName := r.FormValue(stsLDAPProviderName)
|
|
|
|
// Find an LDAP provider from the registered providers
|
|
var ldapProvider *ldap.LDAPProvider
|
|
ldapProvidersFound := 0
|
|
for _, provider := range h.stsService.GetProviders() {
|
|
// Check if this is an LDAP provider by type assertion
|
|
if p, ok := provider.(*ldap.LDAPProvider); ok {
|
|
if ldapProviderName != "" && p.Name() == ldapProviderName {
|
|
ldapProvider = p
|
|
break
|
|
} else if ldapProviderName == "" && ldapProvider == nil {
|
|
ldapProvider = p
|
|
}
|
|
ldapProvidersFound++
|
|
}
|
|
}
|
|
|
|
if ldapProvidersFound > 1 && ldapProviderName == "" {
|
|
glog.Warningf("Multiple LDAP providers found (%d). Using the first one found (non-deterministic). Consider specifying LDAPProviderName.", ldapProvidersFound)
|
|
}
|
|
|
|
if ldapProvider == nil {
|
|
glog.V(2).Infof("AssumeRoleWithLDAPIdentity: no LDAP provider configured")
|
|
h.writeSTSErrorResponse(w, r, STSErrSTSNotReady,
|
|
fmt.Errorf("no LDAP provider configured - please add an LDAP provider to IAM configuration"))
|
|
return
|
|
}
|
|
|
|
// Authenticate with LDAP provider
|
|
// The provider expects credentials in "username:password" format
|
|
credentials := ldapUsername + ":" + ldapPassword
|
|
identity, err := ldapProvider.Authenticate(r.Context(), credentials)
|
|
if err != nil {
|
|
glog.V(2).Infof("AssumeRoleWithLDAPIdentity: LDAP authentication failed for user %s: %v", ldapUsername, err)
|
|
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
|
|
fmt.Errorf("authentication failed"))
|
|
return
|
|
}
|
|
|
|
glog.V(2).Infof("AssumeRoleWithLDAPIdentity: user %s authenticated successfully, groups=%v",
|
|
ldapUsername, identity.Groups)
|
|
|
|
accountID := h.getAccountID()
|
|
|
|
ldapUserIdentity := &Identity{
|
|
Name: identity.UserID,
|
|
Account: &Account{
|
|
DisplayName: identity.DisplayName,
|
|
EmailAddress: identity.Email,
|
|
Id: identity.UserID,
|
|
},
|
|
PrincipalArn: fmt.Sprintf("arn:aws:iam::%s:user/%s", accountID, identity.UserID),
|
|
}
|
|
|
|
// Verify that the identity is allowed to assume the role by checking the Trust Policy
|
|
// The LDAP user doesn't have identity policies, so we strictly check if the Role trusts this principal.
|
|
if err := h.iam.ValidateTrustPolicyForPrincipal(r.Context(), roleArn, ldapUserIdentity.PrincipalArn); err != nil {
|
|
glog.V(2).Infof("AssumeRoleWithLDAPIdentity: trust policy validation failed for %s to assume %s: %v", ldapUsername, roleArn, err)
|
|
h.writeSTSErrorResponse(w, r, STSErrAccessDenied, fmt.Errorf("trust policy denies access"))
|
|
return
|
|
}
|
|
|
|
sessionPolicyJSON, err := sts.NormalizeSessionPolicy(r.FormValue("Policy"))
|
|
if err != nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrMalformedPolicyDocument,
|
|
fmt.Errorf("invalid Policy document: %w", err))
|
|
return
|
|
}
|
|
|
|
// Generate common STS components with LDAP-specific claims
|
|
modifyClaims := func(claims *sts.STSSessionClaims) {
|
|
claims.WithIdentityProvider("ldap", identity.UserID, identity.Provider)
|
|
}
|
|
|
|
stsCreds, assumedUser, err := h.prepareSTSCredentials(r.Context(), roleArn, roleSessionName, durationSeconds, sessionPolicyJSON, modifyClaims)
|
|
if err != nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrInternalError, err)
|
|
return
|
|
}
|
|
|
|
// Build and return response
|
|
xmlResponse := &AssumeRoleWithLDAPIdentityResponse{
|
|
Result: LDAPIdentityResult{
|
|
Credentials: stsCreds,
|
|
AssumedRoleUser: assumedUser,
|
|
},
|
|
}
|
|
xmlResponse.ResponseMetadata.RequestId = request_id.GetFromRequest(r)
|
|
|
|
s3err.WriteXMLResponse(w, r, http.StatusOK, xmlResponse)
|
|
}
|
|
|
|
// handleGetFederationToken handles the GetFederationToken API action.
|
|
// This allows long-term IAM users to obtain temporary credentials scoped down
|
|
// by an optional inline session policy. Temporary credentials cannot call this action.
|
|
func (h *STSHandlers) handleGetFederationToken(w http.ResponseWriter, r *http.Request) {
|
|
// Extract parameters
|
|
name := r.FormValue(stsFederationName)
|
|
|
|
// Validate required parameters
|
|
if name == "" {
|
|
h.writeSTSErrorResponse(w, r, STSErrMissingParameter,
|
|
fmt.Errorf("Name is required"))
|
|
return
|
|
}
|
|
|
|
// AWS requires Name to be 2-32 characters matching [\w+=,.@-]+
|
|
if len(name) < 2 || len(name) > 32 {
|
|
h.writeSTSErrorResponse(w, r, STSErrInvalidParameterValue,
|
|
fmt.Errorf("Name must be between 2 and 32 characters"))
|
|
return
|
|
}
|
|
if !federationNameRegex.MatchString(name) {
|
|
h.writeSTSErrorResponse(w, r, STSErrInvalidParameterValue,
|
|
fmt.Errorf("Name contains invalid characters, must match [\\w+=,.@-]+"))
|
|
return
|
|
}
|
|
|
|
// Parse and validate DurationSeconds (GetFederationToken allows up to 36 hours)
|
|
durationSeconds, errCode, err := parseDurationSecondsWithBounds(r, minDurationSeconds, maxFederationDurationSeconds)
|
|
if err != nil {
|
|
h.writeSTSErrorResponse(w, r, errCode, err)
|
|
return
|
|
}
|
|
|
|
// Reject calls from temporary credentials (session tokens) early,
|
|
// before SigV4 verification — no need to authenticate first.
|
|
// GetFederationToken can only be called by long-term IAM users.
|
|
securityToken := r.Header.Get("X-Amz-Security-Token")
|
|
if securityToken == "" {
|
|
securityToken = r.URL.Query().Get("X-Amz-Security-Token")
|
|
}
|
|
if securityToken != "" {
|
|
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
|
|
fmt.Errorf("GetFederationToken cannot be called with temporary credentials"))
|
|
return
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Check if IAM is available for SigV4 verification
|
|
if h.iam == nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrSTSNotReady,
|
|
fmt.Errorf("IAM not configured for STS"))
|
|
return
|
|
}
|
|
|
|
// Validate AWS SigV4 authentication
|
|
identity, _, _, _, sigErrCode := h.iam.verifyV4Signature(r, false)
|
|
if sigErrCode != s3err.ErrNone {
|
|
glog.V(2).Infof("GetFederationToken SigV4 verification failed: %v", sigErrCode)
|
|
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
|
|
fmt.Errorf("invalid AWS signature: %v", sigErrCode))
|
|
return
|
|
}
|
|
|
|
if identity == nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
|
|
fmt.Errorf("unable to identify caller"))
|
|
return
|
|
}
|
|
|
|
glog.V(2).Infof("GetFederationToken: caller identity=%s, name=%s", identity.Name, name)
|
|
|
|
// Check if the caller is authorized to call GetFederationToken
|
|
if authErr := h.iam.VerifyActionPermission(r, identity, Action(sts.ActionGetFederationToken), "", ""); authErr != s3err.ErrNone {
|
|
glog.V(2).Infof("GetFederationToken: caller %s is not authorized to call GetFederationToken", identity.Name)
|
|
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
|
|
fmt.Errorf("user %s is not authorized to call GetFederationToken", identity.Name))
|
|
return
|
|
}
|
|
|
|
// Validate session policy if provided
|
|
sessionPolicyJSON, err := sts.NormalizeSessionPolicy(r.FormValue("Policy"))
|
|
if err != nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrMalformedPolicyDocument,
|
|
fmt.Errorf("invalid Policy document: %w", err))
|
|
return
|
|
}
|
|
|
|
// Calculate duration (default 12 hours for GetFederationToken)
|
|
duration := time.Duration(defaultFederationDurationSeconds) * time.Second
|
|
if durationSeconds != nil {
|
|
duration = time.Duration(*durationSeconds) * time.Second
|
|
}
|
|
|
|
// Generate session ID
|
|
sessionId, err := sts.GenerateSessionId()
|
|
if err != nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrInternalError,
|
|
fmt.Errorf("failed to generate session ID: %w", err))
|
|
return
|
|
}
|
|
|
|
expiration := time.Now().Add(duration)
|
|
accountID := h.getAccountID()
|
|
|
|
// Build federated user ARN: arn:aws:sts::<account>:federated-user/<Name>
|
|
federatedUserArn := fmt.Sprintf("arn:aws:sts::%s:federated-user/%s", accountID, name)
|
|
federatedUserId := fmt.Sprintf("%s:%s", accountID, name)
|
|
|
|
// Create session claims — use the caller's principal ARN as the RoleArn
|
|
// so that policy evaluation resolves the caller's attached policies
|
|
claims := sts.NewSTSSessionClaims(sessionId, h.stsService.Config.Issuer, expiration).
|
|
WithSessionName(name).
|
|
WithRoleInfo(identity.PrincipalArn, federatedUserId, federatedUserArn)
|
|
|
|
// Embed the caller's effective policies into the token.
|
|
// Merge identity.PolicyNames (from SigV4 identity) with policies resolved
|
|
// from the IAM manager (which may include group-attached policies).
|
|
policySet := make(map[string]struct{})
|
|
for _, p := range identity.PolicyNames {
|
|
policySet[p] = struct{}{}
|
|
}
|
|
|
|
var policyManager *integration.IAMManager
|
|
if h.iam.iamIntegration != nil {
|
|
if provider, ok := h.iam.iamIntegration.(IAMManagerProvider); ok {
|
|
policyManager = provider.GetIAMManager()
|
|
}
|
|
}
|
|
if policyManager != nil {
|
|
userPolicies, err := policyManager.GetPoliciesForUser(r.Context(), identity.Name)
|
|
if err != nil {
|
|
glog.V(2).Infof("GetFederationToken: failed to resolve policies for %s: %v", identity.Name, err)
|
|
h.writeSTSErrorResponse(w, r, STSErrInternalError,
|
|
fmt.Errorf("failed to resolve caller policies"))
|
|
return
|
|
}
|
|
for _, p := range userPolicies {
|
|
policySet[p] = struct{}{}
|
|
}
|
|
}
|
|
|
|
if len(policySet) > 0 {
|
|
merged := make([]string, 0, len(policySet))
|
|
for p := range policySet {
|
|
merged = append(merged, p)
|
|
}
|
|
claims.WithPolicies(merged)
|
|
}
|
|
|
|
if sessionPolicyJSON != "" {
|
|
claims.WithSessionPolicy(sessionPolicyJSON)
|
|
}
|
|
|
|
// Generate JWT session token
|
|
sessionToken, err := h.stsService.GetTokenGenerator().GenerateJWTWithClaims(claims)
|
|
if err != nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrInternalError,
|
|
fmt.Errorf("failed to generate session token: %w", err))
|
|
return
|
|
}
|
|
|
|
// Generate temporary credentials
|
|
stsCredGen := sts.NewCredentialGenerator()
|
|
stsCredsDet, err := stsCredGen.GenerateTemporaryCredentials(sessionId, expiration)
|
|
if err != nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrInternalError,
|
|
fmt.Errorf("failed to generate temporary credentials: %w", err))
|
|
return
|
|
}
|
|
|
|
// Build and return response
|
|
xmlResponse := &GetFederationTokenResponse{
|
|
Result: GetFederationTokenResult{
|
|
Credentials: STSCredentials{
|
|
AccessKeyId: stsCredsDet.AccessKeyId,
|
|
SecretAccessKey: stsCredsDet.SecretAccessKey,
|
|
SessionToken: sessionToken,
|
|
Expiration: expiration.Format(time.RFC3339),
|
|
},
|
|
FederatedUser: FederatedUser{
|
|
FederatedUserId: federatedUserId,
|
|
Arn: federatedUserArn,
|
|
},
|
|
},
|
|
}
|
|
xmlResponse.ResponseMetadata.RequestId = request_id.GetFromRequest(r)
|
|
|
|
s3err.WriteXMLResponse(w, r, http.StatusOK, xmlResponse)
|
|
}
|
|
|
|
// prepareSTSCredentials extracts common shared logic for credential generation
|
|
func (h *STSHandlers) prepareSTSCredentials(ctx context.Context, roleArn, roleSessionName string,
|
|
durationSeconds *int64, sessionPolicy string, modifyClaims func(*sts.STSSessionClaims)) (STSCredentials, *AssumedRoleUser, error) {
|
|
|
|
// Calculate duration
|
|
duration := time.Hour // Default 1 hour
|
|
if durationSeconds != nil {
|
|
duration = time.Duration(*durationSeconds) * time.Second
|
|
}
|
|
|
|
// Generate session ID
|
|
sessionId, err := sts.GenerateSessionId()
|
|
if err != nil {
|
|
return STSCredentials{}, nil, fmt.Errorf("failed to generate session ID: %w", err)
|
|
}
|
|
|
|
expiration := time.Now().Add(duration)
|
|
|
|
// Extract role name from ARN for proper response formatting
|
|
roleName := utils.ExtractRoleNameFromPrincipal(roleArn)
|
|
if roleName == "" {
|
|
// Try to extract user name if it's a user ARN (for "User Context" assumption)
|
|
roleName = utils.ExtractUserNameFromPrincipal(roleArn)
|
|
}
|
|
|
|
if roleName == "" {
|
|
roleName = roleArn // Fallback to full ARN if extraction fails
|
|
}
|
|
|
|
accountID := h.getAccountID()
|
|
|
|
// Construct AssumedRoleUser ARN - this will be used as the principal for the vended token
|
|
assumedRoleArn := fmt.Sprintf("arn:aws:sts::%s:assumed-role/%s/%s", accountID, roleName, roleSessionName)
|
|
|
|
// Use assumedRoleArn as RoleArn in claims if original RoleArn is empty
|
|
// This ensures STSSessionClaims.IsValid() passes (it requires non-empty RoleArn)
|
|
effectiveRoleArn := roleArn
|
|
if effectiveRoleArn == "" {
|
|
effectiveRoleArn = assumedRoleArn
|
|
}
|
|
|
|
// Create session claims with role information
|
|
// SECURITY: Use the assumedRoleArn as the principal in the token.
|
|
// This ensures that subsequent requests using this token are correctly identified as the assumed role.
|
|
claims := sts.NewSTSSessionClaims(sessionId, h.stsService.Config.Issuer, expiration).
|
|
WithSessionName(roleSessionName).
|
|
WithRoleInfo(effectiveRoleArn, fmt.Sprintf("%s:%s", roleName, roleSessionName), assumedRoleArn)
|
|
|
|
// If IAM integration is available, embed the role's attached policies into the session token.
|
|
// This makes the token self-sufficient for authorization even when role lookup is unavailable.
|
|
var policyManager *integration.IAMManager
|
|
if h.iam != nil && h.iam.iamIntegration != nil {
|
|
if provider, ok := h.iam.iamIntegration.(IAMManagerProvider); ok {
|
|
policyManager = provider.GetIAMManager()
|
|
}
|
|
}
|
|
|
|
if policyManager != nil {
|
|
roleNameForPolicies := utils.ExtractRoleNameFromArn(roleArn)
|
|
if roleNameForPolicies == "" {
|
|
roleNameForPolicies = utils.ExtractRoleNameFromPrincipal(roleArn)
|
|
}
|
|
|
|
if roleNameForPolicies != "" && len(claims.Policies) == 0 {
|
|
roleDef, err := policyManager.GetRole(ctx, roleNameForPolicies)
|
|
if err != nil {
|
|
glog.V(2).Infof("Failed to load role %q for policy embedding: %v", roleNameForPolicies, err)
|
|
} else if roleDef == nil {
|
|
glog.V(2).Infof("Role definition %q was missing for policy embedding", roleNameForPolicies)
|
|
} else if len(roleDef.AttachedPolicies) > 0 {
|
|
claims.WithPolicies(roleDef.AttachedPolicies)
|
|
}
|
|
}
|
|
}
|
|
|
|
if sessionPolicy != "" {
|
|
claims.WithSessionPolicy(sessionPolicy)
|
|
}
|
|
|
|
// Apply custom claims if provided (e.g., LDAP identity)
|
|
if modifyClaims != nil {
|
|
modifyClaims(claims)
|
|
}
|
|
|
|
// Generate JWT session token
|
|
sessionToken, err := h.stsService.GetTokenGenerator().GenerateJWTWithClaims(claims)
|
|
if err != nil {
|
|
return STSCredentials{}, nil, fmt.Errorf("failed to generate session token: %w", err)
|
|
}
|
|
|
|
// Generate temporary credentials (deterministic based on sessionId)
|
|
stsCredGen := sts.NewCredentialGenerator()
|
|
stsCredsDet, err := stsCredGen.GenerateTemporaryCredentials(sessionId, expiration)
|
|
if err != nil {
|
|
return STSCredentials{}, nil, fmt.Errorf("failed to generate temporary credentials: %w", err)
|
|
}
|
|
accessKeyId := stsCredsDet.AccessKeyId
|
|
secretAccessKey := stsCredsDet.SecretAccessKey
|
|
|
|
stsCreds := STSCredentials{
|
|
AccessKeyId: accessKeyId,
|
|
SecretAccessKey: secretAccessKey,
|
|
SessionToken: sessionToken,
|
|
Expiration: expiration.Format(time.RFC3339),
|
|
}
|
|
|
|
assumedUser := &AssumedRoleUser{
|
|
AssumedRoleId: fmt.Sprintf("%s:%s", roleName, roleSessionName),
|
|
Arn: assumedRoleArn,
|
|
}
|
|
|
|
return stsCreds, assumedUser, nil
|
|
}
|
|
|
|
// handleGetCallerIdentity handles the GetCallerIdentity API action.
|
|
// It returns the identity (ARN, account, user ID) of the caller based on SigV4 authentication.
|
|
func (h *STSHandlers) handleGetCallerIdentity(w http.ResponseWriter, r *http.Request) {
|
|
if h.iam == nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrSTSNotReady,
|
|
fmt.Errorf("IAM not configured for STS"))
|
|
return
|
|
}
|
|
|
|
identity, _, _, _, sigErrCode := h.iam.verifyV4Signature(r, false)
|
|
if sigErrCode != s3err.ErrNone {
|
|
glog.V(2).Infof("GetCallerIdentity SigV4 verification failed: %v", sigErrCode)
|
|
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
|
|
fmt.Errorf("invalid AWS signature: %v", sigErrCode))
|
|
return
|
|
}
|
|
|
|
if identity == nil {
|
|
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
|
|
fmt.Errorf("unable to identify caller"))
|
|
return
|
|
}
|
|
|
|
accountID := h.getAccountID()
|
|
|
|
arn := identity.PrincipalArn
|
|
if arn == "" {
|
|
arn = fmt.Sprintf("arn:aws:iam::%s:user/%s", accountID, identity.Name)
|
|
}
|
|
|
|
userId := identity.Name
|
|
|
|
glog.V(2).Infof("GetCallerIdentity: identity=%s, arn=%s, account=%s", identity.Name, arn, accountID)
|
|
|
|
xmlResponse := &GetCallerIdentityResponse{
|
|
Result: GetCallerIdentityResult{
|
|
Arn: arn,
|
|
UserId: userId,
|
|
Account: accountID,
|
|
},
|
|
}
|
|
xmlResponse.ResponseMetadata.RequestId = request_id.GetFromRequest(r)
|
|
|
|
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"`
|
|
}
|
|
|
|
// AssumeRoleResponse is the response for AssumeRole
|
|
type AssumeRoleResponse struct {
|
|
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleResponse"`
|
|
Result AssumeRoleResult `xml:"AssumeRoleResult"`
|
|
ResponseMetadata struct {
|
|
RequestId string `xml:"RequestId,omitempty"`
|
|
} `xml:"ResponseMetadata,omitempty"`
|
|
}
|
|
|
|
// AssumeRoleResult contains the result of AssumeRole
|
|
type AssumeRoleResult struct {
|
|
Credentials STSCredentials `xml:"Credentials"`
|
|
AssumedRoleUser *AssumedRoleUser `xml:"AssumedRoleUser,omitempty"`
|
|
}
|
|
|
|
// AssumeRoleWithLDAPIdentityResponse is the response for AssumeRoleWithLDAPIdentity
|
|
type AssumeRoleWithLDAPIdentityResponse struct {
|
|
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithLDAPIdentityResponse"`
|
|
Result LDAPIdentityResult `xml:"AssumeRoleWithLDAPIdentityResult"`
|
|
ResponseMetadata struct {
|
|
RequestId string `xml:"RequestId,omitempty"`
|
|
} `xml:"ResponseMetadata,omitempty"`
|
|
}
|
|
|
|
// LDAPIdentityResult contains the result of AssumeRoleWithLDAPIdentity
|
|
type LDAPIdentityResult struct {
|
|
Credentials STSCredentials `xml:"Credentials"`
|
|
AssumedRoleUser *AssumedRoleUser `xml:"AssumedRoleUser,omitempty"`
|
|
}
|
|
|
|
// GetCallerIdentityResponse is the response for GetCallerIdentity
|
|
type GetCallerIdentityResponse struct {
|
|
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ GetCallerIdentityResponse"`
|
|
Result GetCallerIdentityResult `xml:"GetCallerIdentityResult"`
|
|
ResponseMetadata struct {
|
|
RequestId string `xml:"RequestId,omitempty"`
|
|
} `xml:"ResponseMetadata,omitempty"`
|
|
}
|
|
|
|
// GetCallerIdentityResult contains the result of GetCallerIdentity
|
|
type GetCallerIdentityResult struct {
|
|
Arn string `xml:"Arn"`
|
|
UserId string `xml:"UserId"`
|
|
Account string `xml:"Account"`
|
|
}
|
|
|
|
// GetFederationTokenResponse is the response for GetFederationToken
|
|
type GetFederationTokenResponse struct {
|
|
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ GetFederationTokenResponse"`
|
|
Result GetFederationTokenResult `xml:"GetFederationTokenResult"`
|
|
ResponseMetadata struct {
|
|
RequestId string `xml:"RequestId,omitempty"`
|
|
} `xml:"ResponseMetadata,omitempty"`
|
|
}
|
|
|
|
// GetFederationTokenResult contains the result of GetFederationToken
|
|
type GetFederationTokenResult struct {
|
|
Credentials STSCredentials `xml:"Credentials"`
|
|
FederatedUser FederatedUser `xml:"FederatedUser"`
|
|
}
|
|
|
|
// FederatedUser contains information about the federated user
|
|
type FederatedUser struct {
|
|
FederatedUserId string `xml:"FederatedUserId"`
|
|
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"
|
|
STSErrMalformedPolicyDocument STSErrorCode = "MalformedPolicyDocument"
|
|
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"},
|
|
STSErrMalformedPolicyDocument: {http.StatusBadRequest, "Malformed policy document"},
|
|
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: request_id.GetFromRequest(r),
|
|
}
|
|
|
|
// 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)
|
|
}
|