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.
 
 
 
 
 
 

661 lines
23 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 (
"crypto/rand"
"encoding/base64"
"encoding/xml"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"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"
)
// 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"
// LDAP parameter names
stsLDAPUsername = "LDAPUsername"
stsLDAPPassword = "LDAPPassword"
stsLDAPProviderName = "LDAPProviderName"
)
// STS duration constants (AWS specification)
const (
minDurationSeconds = int64(900) // 15 minutes
maxDurationSeconds = int64(43200) // 12 hours
// Default account ID for federated users
defaultAccountId = "111122223333"
)
// parseDurationSeconds parses and validates the DurationSeconds parameter
// Returns nil if the parameter is not provided, or a pointer to the parsed value
func parseDurationSeconds(r *http.Request) (*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 < minDurationSeconds || ds > maxDurationSeconds {
return nil, STSErrInvalidParameterValue,
fmt.Errorf("DurationSeconds must be between %d and %d seconds", minDurationSeconds, maxDurationSeconds)
}
return &ds, "", nil
}
// 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,
}
}
// 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 actionAssumeRole:
h.handleAssumeRole(w, r)
case actionAssumeRoleWithWebIdentity:
h.handleAssumeRoleWithWebIdentity(w, r)
case actionAssumeRoleWithLDAPIdentity:
h.handleAssumeRoleWithLDAPIdentity(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
}
// 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)
}
// handleAssumeRole handles the AssumeRole API action
// This requires AWS Signature V4 authentication
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
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
}
// 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
if authErr := h.iam.VerifyActionPermission(r, identity, Action("sts:AssumeRole"), "", 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)
// This ensures the role's trust policy explicitly allows the principal to assume it
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
}
// Generate common STS components
stsCreds, assumedUser, err := h.prepareSTSCredentials(roleArn, roleSessionName, identity.PrincipalArn, durationSeconds, nil)
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 = fmt.Sprintf("%d", time.Now().UnixNano())
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)
// Verify that the identity is allowed to assume the role
// We create a temporary identity to represent the LDAP user for permission checking
// The checking logic will verify if the role's trust policy allows this principal
// Use configured account ID or default to "111122223333" for federated users
accountId := defaultAccountId
if h.stsService != nil && h.stsService.Config != nil && h.stsService.Config.AccountId != "" {
accountId = h.stsService.Config.AccountId
}
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
}
// 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(roleArn, roleSessionName, ldapUserIdentity.PrincipalArn, durationSeconds, 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 = fmt.Sprintf("%d", time.Now().UnixNano())
s3err.WriteXMLResponse(w, r, http.StatusOK, xmlResponse)
}
// prepareSTSCredentials extracts common shared logic for credential generation
func (h *STSHandlers) prepareSTSCredentials(roleArn, roleSessionName, principalArn string,
durationSeconds *int64, 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.ExtractRoleNameFromArn(roleArn)
if roleName == "" {
roleName = roleArn // Fallback to full ARN if extraction fails
}
// Create session claims with role information
claims := sts.NewSTSSessionClaims(sessionId, h.stsService.Config.Issuer, expiration).
WithSessionName(roleSessionName).
WithRoleInfo(roleArn, fmt.Sprintf("%s:%s", roleName, roleSessionName), principalArn)
// 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 (cryptographically secure)
// AccessKeyId: ASIA + 16 chars hex
// SecretAccessKey: 40 chars base64
randBytes := make([]byte, 30) // Sufficient for both
if _, err := rand.Read(randBytes); err != nil {
return STSCredentials{}, nil, fmt.Errorf("failed to generate random bytes: %w", err)
}
// Generate AccessKeyId (ASIA + 16 upper-hex chars)
// We use 8 bytes (16 hex chars)
accessKeyId := "ASIA" + fmt.Sprintf("%X", randBytes[:8])
// Generate SecretAccessKey: 30 random bytes, base64-encoded to a 40-character string
secretBytes := make([]byte, 30)
if _, err := rand.Read(secretBytes); err != nil {
return STSCredentials{}, nil, fmt.Errorf("failed to generate secret bytes: %w", err)
}
secretAccessKey := base64.StdEncoding.EncodeToString(secretBytes)
// Get account ID from STS config or use default
accountId := defaultAccountId
if h.stsService != nil && h.stsService.Config != nil && h.stsService.Config.AccountId != "" {
accountId = h.stsService.Config.AccountId
}
stsCreds := STSCredentials{
AccessKeyId: accessKeyId,
SecretAccessKey: secretAccessKey,
SessionToken: sessionToken,
Expiration: expiration.Format(time.RFC3339),
}
assumedUser := &AssumedRoleUser{
AssumedRoleId: fmt.Sprintf("%s:%s", roleName, roleSessionName),
Arn: fmt.Sprintf("arn:aws:sts::%s:assumed-role/%s/%s", accountId, roleName, roleSessionName),
}
return stsCreds, assumedUser, nil
}
// 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"`
}
// 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)
}