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:
@@ -323,14 +323,30 @@ func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest
|
||||
return false, fmt.Errorf("IAM manager not initialized")
|
||||
}
|
||||
|
||||
// Validate session token if present (skip for OIDC tokens which are already validated,
|
||||
// and skip for empty tokens which represent static access keys)
|
||||
// Validate session token if present
|
||||
// We always try to validate with the internal STS service first if it's a SeaweedFS token.
|
||||
// This ensures that session policies embedded in the token are correctly extracted and enforced.
|
||||
var sessionInfo *sts.SessionInfo
|
||||
if request.SessionToken != "" && !isOIDCToken(request.SessionToken) {
|
||||
var err error
|
||||
sessionInfo, err = m.stsService.ValidateSessionToken(ctx, request.SessionToken)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid session: %w", err)
|
||||
if request.SessionToken != "" {
|
||||
// Parse unverified to check issuer
|
||||
parsed, _, err := new(jwt.Parser).ParseUnverified(request.SessionToken, jwt.MapClaims{})
|
||||
isInternal := false
|
||||
if err == nil {
|
||||
if claims, ok := parsed.Claims.(jwt.MapClaims); ok {
|
||||
if issuer, ok := claims["iss"].(string); ok && m.stsService != nil && m.stsService.Config != nil {
|
||||
if issuer == m.stsService.Config.Issuer {
|
||||
isInternal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isInternal || !isOIDCToken(request.SessionToken) {
|
||||
var err error
|
||||
sessionInfo, err = m.stsService.ValidateSessionToken(ctx, request.SessionToken)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid session: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +365,17 @@ func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest
|
||||
// Add principal to context for policy matching
|
||||
// The PolicyEngine checks RequestContext["principal"] or RequestContext["aws:PrincipalArn"]
|
||||
evalCtx.RequestContext["principal"] = request.Principal
|
||||
evalCtx.RequestContext["aws:PrincipalArn"] = request.Principal
|
||||
evalCtx.RequestContext["aws:PrincipalArn"] = request.Principal // AWS standard key
|
||||
|
||||
// Check if this is an admin request - bypass policy evaluation if so
|
||||
// This mirrors the logic in auth_signature_v4.go but applies it at authorization time
|
||||
isAdmin := false
|
||||
if request.RequestContext != nil {
|
||||
if val, ok := request.RequestContext["is_admin"].(bool); ok && val {
|
||||
isAdmin = true
|
||||
}
|
||||
// Print full request context for debugging
|
||||
}
|
||||
|
||||
// Parse principal ARN to extract details for context variables (e.g. ${aws:username})
|
||||
arnInfo := utils.ParsePrincipalARN(request.Principal)
|
||||
@@ -382,48 +408,56 @@ func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest
|
||||
}
|
||||
}
|
||||
|
||||
policies := request.PolicyNames
|
||||
if len(policies) == 0 {
|
||||
// Extract role name from principal ARN
|
||||
roleName := utils.ExtractRoleNameFromPrincipal(request.Principal)
|
||||
if roleName == "" {
|
||||
userName := utils.ExtractUserNameFromPrincipal(request.Principal)
|
||||
if userName == "" {
|
||||
return false, fmt.Errorf("could not extract role from principal: %s", request.Principal)
|
||||
}
|
||||
if m.userStore == nil {
|
||||
return false, fmt.Errorf("user store unavailable for principal: %s", request.Principal)
|
||||
}
|
||||
user, err := m.userStore.GetUser(ctx, userName)
|
||||
if err != nil || user == nil {
|
||||
return false, fmt.Errorf("user not found for principal: %s (user=%s)", request.Principal, userName)
|
||||
}
|
||||
policies = user.GetPolicyNames()
|
||||
} else {
|
||||
// Get role definition
|
||||
roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("role not found: %s", roleName)
|
||||
}
|
||||
var baseResult *policy.EvaluationResult
|
||||
var err error
|
||||
|
||||
policies = roleDef.AttachedPolicies
|
||||
if isAdmin {
|
||||
// Admin always has base access allowed
|
||||
baseResult = &policy.EvaluationResult{Effect: policy.EffectAllow}
|
||||
} else {
|
||||
policies := request.PolicyNames
|
||||
if len(policies) == 0 {
|
||||
// Extract role name from principal ARN
|
||||
roleName := utils.ExtractRoleNameFromPrincipal(request.Principal)
|
||||
if roleName == "" {
|
||||
userName := utils.ExtractUserNameFromPrincipal(request.Principal)
|
||||
if userName == "" {
|
||||
return false, fmt.Errorf("could not extract role from principal: %s", request.Principal)
|
||||
}
|
||||
if m.userStore == nil {
|
||||
return false, fmt.Errorf("user store unavailable for principal: %s", request.Principal)
|
||||
}
|
||||
user, err := m.userStore.GetUser(ctx, userName)
|
||||
if err != nil || user == nil {
|
||||
return false, fmt.Errorf("user not found for principal: %s (user=%s)", request.Principal, userName)
|
||||
}
|
||||
policies = user.GetPolicyNames()
|
||||
} else {
|
||||
// Get role definition
|
||||
roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("role not found: %s", roleName)
|
||||
}
|
||||
|
||||
policies = roleDef.AttachedPolicies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bucketPolicyName != "" {
|
||||
// Enforce an upper bound on the number of policies to avoid excessive allocations
|
||||
if len(policies) >= maxPoliciesForEvaluation {
|
||||
return false, fmt.Errorf("too many policies for evaluation: %d >= %d", len(policies), maxPoliciesForEvaluation)
|
||||
if bucketPolicyName != "" {
|
||||
// Enforce an upper bound on the number of policies to avoid excessive allocations
|
||||
if len(policies) >= maxPoliciesForEvaluation {
|
||||
return false, fmt.Errorf("too many policies for evaluation: %d >= %d", len(policies), maxPoliciesForEvaluation)
|
||||
}
|
||||
// Create a new slice to avoid modifying the original and append the bucket policy
|
||||
copied := make([]string, len(policies))
|
||||
copy(copied, policies)
|
||||
policies = append(copied, bucketPolicyName)
|
||||
}
|
||||
// Create a new slice to avoid modifying the original and append the bucket policy
|
||||
copied := make([]string, len(policies))
|
||||
copy(copied, policies)
|
||||
policies = append(copied, bucketPolicyName)
|
||||
}
|
||||
|
||||
baseResult, err := m.policyEngine.Evaluate(ctx, "", evalCtx, policies)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("policy evaluation failed: %w", err)
|
||||
baseResult, err = m.policyEngine.Evaluate(ctx, "", evalCtx, policies)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("policy evaluation failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Base policy must allow; if it doesn't, deny immediately (session policy can only further restrict)
|
||||
|
||||
@@ -44,6 +44,8 @@ func (t *TokenGenerator) GenerateJWTWithClaims(claims *STSSessionClaims) (string
|
||||
claims.Issuer = t.issuer
|
||||
}
|
||||
|
||||
// SECURITY: Use deterministic signing results for troubleshooting if needed,
|
||||
// but standard HS256 with common secret is usually sufficient.
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(t.signingKey)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user