STS: Fallback to Caller Identity when RoleArn is missing in AssumeRole (#8345)

* s3api: make RoleArn optional in AssumeRole

* s3api: address PR feedback for optional RoleArn

* iam: add configurable default role for AssumeRole

* S3 STS: Use caller identity when RoleArn is missing

- Fallback to PrincipalArn/Context in AssumeRole if RoleArn is empty

- Handle User ARNs in prepareSTSCredentials

- Fix PrincipalArn generation for env var credentials

* Test: Add unit test for AssumeRole caller identity fallback

* fix(s3api): propagate admin permissions to assumed role session when using caller identity fallback

* STS: Fix is_admin propagation and optimize IAM policy evaluation for assumed roles

- Restore is_admin propagation via JWT req_ctx
- Optimize IsActionAllowed to skip role lookups for admin sessions
- Ensure session policies are still applied for downscoping
- Remove debug logging
- Fix syntax errors in cleanup

* fix(iam): resolve STS policy bypass for admin sessions

- Fixed IsActionAllowed in iam_manager.go to correctly identify and validate internal STS tokens, ensuring session policies are enforced.
- Refactored VerifyActionPermission in auth_credentials.go to properly handle session tokens and avoid legacy authorization short-circuits.
- Added debug logging for better tracing of policy evaluation and session validation.
This commit is contained in:
Chris Lu
2026-02-14 22:00:59 -08:00
committed by GitHub
parent f49f6c6876
commit cf8e383e1e
8 changed files with 323 additions and 71 deletions

View File

@@ -186,6 +186,8 @@ func (h *STSHandlers) handleAssumeRoleWithWebIdentity(w http.ResponseWriter, r *
Policy: sessionPolicyPtr,
}
glog.V(0).Infof("DEBUG: AssumeRoleWithWebIdentity: RoleArn=%s SessionPolicyLen=%d", roleArn, len(sessionPolicyJSON))
// Call STS service
response, err := h.stsService.AssumeRoleWithWebIdentity(ctx, request)
if err != nil {
@@ -237,11 +239,7 @@ func (h *STSHandlers) handleAssumeRole(w http.ResponseWriter, r *http.Request) {
roleSessionName := r.FormValue("RoleSessionName")
// Validate required parameters
if roleArn == "" {
h.writeSTSErrorResponse(w, r, STSErrMissingParameter,
fmt.Errorf("RoleArn is required"))
return
}
// RoleArn is optional to support S3-compatible clients that omit it
if roleSessionName == "" {
h.writeSTSErrorResponse(w, r, STSErrMissingParameter,
@@ -290,22 +288,40 @@ func (h *STSHandlers) handleAssumeRole(w http.ResponseWriter, r *http.Request) {
// 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
// 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
}
}
// 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
}
// Parse optional inline session policy for downscoping
sessionPolicyJSON, err := sts.NormalizeSessionPolicy(r.FormValue("Policy"))
if err != nil {
h.writeSTSErrorResponse(w, r, STSErrMalformedPolicyDocument,
@@ -313,8 +329,19 @@ func (h *STSHandlers) handleAssumeRole(w http.ResponseWriter, r *http.Request) {
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(roleArn, roleSessionName, durationSeconds, sessionPolicyJSON, nil)
stsCreds, assumedUser, err := h.prepareSTSCredentials(roleArn, roleSessionName, durationSeconds, sessionPolicyJSON, modifyClaims)
if err != nil {
h.writeSTSErrorResponse(w, r, STSErrInternalError, err)
return
@@ -492,7 +519,12 @@ func (h *STSHandlers) prepareSTSCredentials(roleArn, roleSessionName string,
expiration := time.Now().Add(duration)
// Extract role name from ARN for proper response formatting
roleName := utils.ExtractRoleNameFromArn(roleArn)
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
}
@@ -502,12 +534,19 @@ func (h *STSHandlers) prepareSTSCredentials(roleArn, roleSessionName string,
// 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(roleArn, fmt.Sprintf("%s:%s", roleName, roleSessionName), assumedRoleArn)
WithRoleInfo(effectiveRoleArn, fmt.Sprintf("%s:%s", roleName, roleSessionName), assumedRoleArn)
if sessionPolicy != "" {
claims.WithSessionPolicy(sessionPolicy)