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/credential"
	"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+=,.@-]+$`)

// roleSessionNameRegex validates RoleSessionName per AWS spec.
// Same character class as federation Name, but the length bounds differ
// (RoleSessionName is 2..64).
var roleSessionNameRegex = regexp.MustCompile(`^[\w+=,.@-]+$`)

const (
	minRoleSessionNameLen = 2
	maxRoleSessionNameLen = 64
)

// validateRoleSessionName enforces the AWS RoleSessionName contract:
// length 2..64, characters [\w+=,.@-]+. Returns the STS error code and a
// descriptive error suitable for callers to surface to the caller.
func validateRoleSessionName(name string) (STSErrorCode, error) {
	if name == "" {
		return STSErrMissingParameter, fmt.Errorf("RoleSessionName is required")
	}
	if len(name) < minRoleSessionNameLen || len(name) > maxRoleSessionNameLen {
		return STSErrInvalidParameterValue,
			fmt.Errorf("RoleSessionName must be between %d and %d characters", minRoleSessionNameLen, maxRoleSessionNameLen)
	}
	if !roleSessionNameRegex.MatchString(name) {
		return STSErrInvalidParameterValue,
			fmt.Errorf(`RoleSessionName contains invalid characters; allowed: [\w+=,.@-]`)
	}
	return "", nil
}

// 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)
)

// AWS limits inline session policies to 2048 characters for AssumeRole,
// AssumeRoleWithWebIdentity, and AssumeRoleWithSAML. PackedPolicySize is
// returned as a percentage of that budget so callers can detect how close
// they are to the limit.
const sessionPolicyBudgetBytes = 2048

// computePackedPolicySize returns the inline session policy size as a
// percentage of the per-action budget, or nil when no session policy was
// provided. Output is bounded to [0, 100] for AWS-compat reporting; the
// actual policy size validation happens upstream in NormalizeSessionPolicy.
func computePackedPolicySize(policyJSON string) *int64 {
	if policyJSON == "" {
		return nil
	}
	pct := int64(len(policyJSON)) * 100 / sessionPolicyBudgetBytes
	if pct > 100 {
		pct = 100
	}
	return &pct
}

// 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
}

// callerPrincipalArn resolves the identity's principal ARN, synthesizing the
// canonical user ARN when one was not set (e.g. legacy static identities) so
// trust policies that name a concrete principal still match.
func (h *STSHandlers) callerPrincipalArn(identity *Identity) string {
	if identity.PrincipalArn != "" {
		return identity.PrincipalArn
	}
	return fmt.Sprintf("arn:aws:iam::%s:user/%s", h.getAccountID(), identity.Name)
}

// assumeRoleWithWebIdentity dispatches the request through the IAMManager
// wrapper when one is wired so its cross-account provider scope check and
// per-role MaxSessionDuration clamp run for the public AWS-SDK path. Without
// this dispatch, both checks are silently skipped because they live on the
// IAMManager, not on the bare STS service.
func (h *STSHandlers) assumeRoleWithWebIdentity(ctx context.Context, request *sts.AssumeRoleWithWebIdentityRequest) (*sts.AssumeRoleResponse, error) {
	if h.iam != nil && h.iam.iamIntegration != nil {
		if provider, ok := h.iam.iamIntegration.(IAMManagerProvider); ok {
			if mgr := provider.GetIAMManager(); mgr != nil {
				return mgr.AssumeRoleWithWebIdentity(ctx, request)
			}
		}
	}
	return h.stsService.AssumeRoleWithWebIdentity(ctx, request)
}

// 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
	}

	// RoleArn is intentionally optional here: claim-based policy mode
	// (Phase 3b) advertises that callers MAY omit RoleArn so the STS
	// service derives the assumed-role ARN from the configured policy
	// claim. The bare-STS path validates this — when the IDP isn't
	// configured for claim-based mode (or fails to emit policies) it
	// returns a precise error that this handler maps to the right STS
	// error code below.

	if errCode, err := validateRoleSessionName(roleSessionName); err != nil {
		h.writeSTSErrorResponse(w, r, errCode, err)
		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,
	}

	// Prefer the IAMManager wrapper so the cross-account provider scope
	// (enforceProviderAccountScope) and per-role MaxSessionDuration clamp
	// run for SDK callers too. Falling back to the bare STS service keeps
	// embedded test setups (no IAM integration wired) working.
	response, err := h.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,
			PackedPolicySize:            computePackedPolicySize(sessionPolicyJSON),
		},
	}
	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 errCode, err := validateRoleSessionName(roleSessionName); err != nil {
		h.writeSTSErrorResponse(w, r, errCode, err)
		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
	}

	// Record the caller so the audit entry for the AssumeRole call itself names
	// who asked for the session, not just the session it minted.
	r = r.WithContext(recordIdentityInContext(r, identity))

	glog.V(2).Infof("AssumeRole: caller identity=%s, roleArn=%s, sessionName=%s",
		identity.Name, roleArn, roleSessionName)

	assumesSelf := roleArn == ""

	// A named role is authorized by its trust policy, which declares which
	// principals may assume it, so no separate identity-side sts:AssumeRole allow
	// is required. An explicit identity-side deny still wins (deny-always-wins).
	// Without a RoleArn the caller assumes a session for itself.
	if roleArn != "" {
		// An ARN that names something other than a role can never resolve to one,
		// and reporting that as "not authorized" sends the caller looking for a
		// permission problem they do not have.
		if utils.ExtractRoleNameFromArn(roleArn) == "" {
			h.writeSTSErrorResponse(w, r, STSErrInvalidParameterValue,
				fmt.Errorf("RoleArn %q is not an IAM role ARN, expected arn:aws:iam::<account>:role/<name>", roleArn))
			return
		}
		callerArn := h.callerPrincipalArn(identity)
		if err := h.iam.ValidateTrustPolicyForPrincipal(r.Context(), roleArn, callerArn); err != nil {
			glog.V(2).Infof("AssumeRole: %s not authorized to assume %s: %v", identity.Name, roleArn, err)
			h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
				fmt.Errorf("user %s is not authorized to assume role %s", identity.Name, roleArn))
			return
		}
		if h.iam.isActionExplicitlyDeniedByIAM(r, identity, callerArn, sts.ActionAssumeRole, roleArn) {
			glog.V(2).Infof("AssumeRole: identity policy explicitly denies %s assuming %s", identity.Name, roleArn)
			h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
				fmt.Errorf("user %s is not authorized to assume role %s", identity.Name, roleArn))
			return
		}
	} else {
		// Synthesize the caller ARN when the identity carries none, else the
		// session ends up with an empty role name in its assumed-role ARN.
		roleArn = h.callerPrincipalArn(identity)
		glog.V(2).Infof("AssumeRole: no RoleArn provided, defaulting to caller identity: %s", roleArn)
		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
	}

	// is_admin lets the session bypass base policy evaluation, so it may only
	// travel into a session the caller assumed for itself — a legacy static admin
	// carries no IAM policies for such a session to inherit. Assuming a named role
	// scopes the session to that role's policies, admin caller or not.
	var modifyClaims func(claims *sts.STSSessionClaims)
	if assumesSelf && 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,
			PackedPolicySize: computePackedPolicySize(sessionPolicyJSON),
		},
	}
	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 errCode, err := validateRoleSessionName(roleSessionName); err != nil {
		h.writeSTSErrorResponse(w, r, errCode, err)
		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,
			PackedPolicySize: computePackedPolicySize(sessionPolicyJSON),
		},
	}
	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
	}

	r = r.WithContext(recordIdentityInContext(r, identity))

	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)
		switch {
		case err == nil:
			for _, p := range userPolicies {
				policySet[p] = struct{}{}
			}
		case errors.Is(err, credential.ErrUserNotFound):
			// Legacy-config IAM users authenticated via SigV4 are not
			// present in the IAM user store. Fall back to
			// identity.PolicyNames — the caller's SigV4 identity is
			// authoritative for them.
			glog.V(2).Infof("GetFederationToken: %s not in IAM user store, using SigV4 identity policies only", identity.Name)
		default:
			// Any other failure (store unreachable, misconfigured, etc.)
			// means we cannot compute the caller's effective policies.
			// Fail closed rather than mint a token with an incomplete set.
			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
		}
	}

	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 := h.callerPrincipalArn(identity)
	userId := identity.Name

	r = r.WithContext(recordIdentityInContext(r, identity))

	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"`
	PackedPolicySize            *int64           `xml:"PackedPolicySize,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"`
	PackedPolicySize *int64           `xml:"PackedPolicySize,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"`
	PackedPolicySize *int64           `xml:"PackedPolicySize,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)
}
