* request_id: add shared request middleware
* s3err: preserve request ids in responses and logs
* iam: reuse request ids in XML responses
* sts: reuse request ids in XML responses
* request_id: drop legacy header fallback
* request_id: use AWS-style request id format
* iam: fix AWS-compatible XML format for ErrorResponse and field ordering
- ErrorResponse uses bare <RequestId> at root level instead of
<ResponseMetadata> wrapper, matching the AWS IAM error response spec
- Move CommonResponse to last field in success response structs so
<ResponseMetadata> serializes after result elements
- Add randomness to request ID generation to avoid collisions
- Add tests for XML ordering and ErrorResponse format
* iam: remove duplicate error_response_test.go
Test is already covered by responses_test.go.
* address PR review comments
- Guard against typed nil pointers in SetResponseRequestID before
interface assertion (CodeRabbit)
- Use regexp instead of strings.Index in test helpers for extracting
request IDs (Gemini)
* request_id: prevent spoofing, fix nil-error branch, thread reqID to error writers
- Ensure() now always generates a server-side ID, ignoring client-sent
x-amz-request-id headers to prevent request ID spoofing. Uses a
private context key (contextKey{}) instead of the header string.
- writeIamErrorResponse in both iamapi and embedded IAM now accepts
reqID as a parameter instead of calling Ensure() internally, ensuring
a single request ID per request lifecycle.
- The nil-iamError branch in writeIamErrorResponse now writes a 500
Internal Server Error response instead of returning silently.
- Updated tests to set request IDs via context (not headers) and added
tests for spoofing prevention and context reuse.
* sts: add request-id consistency assertions to ActionInBody tests
* test: update admin test to expect server-generated request IDs
The test previously sent a client x-amz-request-id header and expected
it echoed back. Since Ensure() now ignores client headers to prevent
spoofing, update the test to verify the server returns a non-empty
server-generated request ID instead.
* iam: add generic WithRequestID helper alongside reflection-based fallback
Add WithRequestID[T] that uses generics to take the address of a value
type, satisfying the pointer receiver on SetRequestId without reflection.
The existing SetResponseRequestID is kept for the two call sites that
operate on interface{} (from large action switches where the concrete
type varies at runtime). Generics cannot replace reflection there since
Go cannot infer type parameters from interface{}.
* Remove reflection and generics from request ID setting
Call SetRequestId directly on concrete response types in each switch
branch before boxing into interface{}, eliminating the need for
WithRequestID (generics) and SetResponseRequestID (reflection).
* iam: return pointer responses in action dispatch
* Fix IAM error handling consistency and ensure request IDs on all responses
- UpdateUser/CreatePolicy error branches: use writeIamErrorResponse instead
of s3err.WriteErrorResponse to preserve IAM formatting and request ID
- ExecuteAction: accept reqID parameter and generate one if empty, ensuring
every response carries a RequestId regardless of caller
* Clean up inline policies on DeleteUser and UpdateUser rename
DeleteUser: remove InlinePolicies[userName] from policy storage before
removing the identity, so policies are not orphaned.
UpdateUser: move InlinePolicies[userName] to InlinePolicies[newUserName]
when renaming, so GetUserPolicy/DeleteUserPolicy work under the new name.
Both operations persist the updated policies and return an error if
the storage write fails, preventing partial state.
752 lines
26 KiB
Go
752 lines
26 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"
|
|
"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"
|
|
|
|
// 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
|
|
)
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
|
|
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)
|
|
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: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)
|
|
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:AssumeRole"), "", ""); 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)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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"
|
|
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)
|
|
}
|