diff options
Diffstat (limited to 'weed/s3api/auth_credentials.go')
| -rw-r--r-- | weed/s3api/auth_credentials.go | 95 |
1 files changed, 82 insertions, 13 deletions
diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index 66b9c7296..7a6a706ff 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -53,6 +53,9 @@ type IdentityAccessManagement struct { // IAM Integration for advanced features iamIntegration *S3IAMIntegration + + // Link to S3ApiServer for bucket policy evaluation + s3ApiServer *S3ApiServer } type Identity struct { @@ -60,7 +63,7 @@ type Identity struct { Account *Account Credentials []*Credential Actions []Action - PrincipalArn string // ARN for IAM authorization (e.g., "arn:seaweed:iam::user/username") + PrincipalArn string // ARN for IAM authorization (e.g., "arn:aws:iam::account-id:user/username") } // Account represents a system user, a system user can @@ -381,11 +384,11 @@ func generatePrincipalArn(identityName string) string { // Handle special cases switch identityName { case AccountAnonymous.Id: - return "arn:seaweed:iam::user/anonymous" + return "arn:aws:iam::user/anonymous" case AccountAdmin.Id: - return "arn:seaweed:iam::user/admin" + return "arn:aws:iam::user/admin" default: - return fmt.Sprintf("arn:seaweed:iam::user/%s", identityName) + return fmt.Sprintf("arn:aws:iam::user/%s", identityName) } } @@ -497,19 +500,57 @@ func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action) // For ListBuckets, authorization is performed in the handler by iterating // through buckets and checking permissions for each. Skip the global check here. + policyAllows := false + if action == s3_constants.ACTION_LIST && bucket == "" { // ListBuckets operation - authorization handled per-bucket in the handler } else { - // Use enhanced IAM authorization if available, otherwise fall back to legacy authorization - if iam.iamIntegration != nil { - // Always use IAM when available for unified authorization - if errCode := iam.authorizeWithIAM(r, identity, action, bucket, object); errCode != s3err.ErrNone { - return identity, errCode - } - } else { - // Fall back to existing authorization when IAM is not configured - if !identity.canDo(action, bucket, object) { + // First check bucket policy if one exists + // Bucket policies can grant or deny access to specific users/principals + // Following AWS semantics: + // - Explicit DENY in bucket policy → immediate rejection + // - Explicit ALLOW in bucket policy → grant access (bypass IAM checks) + // - No policy or indeterminate → fall through to IAM checks + if iam.s3ApiServer != nil && iam.s3ApiServer.policyEngine != nil && bucket != "" { + principal := buildPrincipalARN(identity) + allowed, evaluated, err := iam.s3ApiServer.policyEngine.EvaluatePolicy(bucket, object, string(action), principal) + + if err != nil { + // SECURITY: Fail-close on policy evaluation errors + // If we can't evaluate the policy, deny access rather than falling through to IAM + glog.Errorf("Error evaluating bucket policy for %s/%s: %v - denying access", bucket, object, err) return identity, s3err.ErrAccessDenied + } else if evaluated { + // A bucket policy exists and was evaluated with a matching statement + if allowed { + // Policy explicitly allows this action - grant access immediately + // This bypasses IAM checks to support cross-account access and policy-only principals + glog.V(3).Infof("Bucket policy allows %s to %s on %s/%s (bypassing IAM)", identity.Name, action, bucket, object) + policyAllows = true + } else { + // Policy explicitly denies this action - deny access immediately + // Note: Explicit Deny in bucket policy overrides all other permissions + glog.V(3).Infof("Bucket policy explicitly denies %s to %s on %s/%s", identity.Name, action, bucket, object) + return identity, s3err.ErrAccessDenied + } + } + // If not evaluated (no policy or no matching statements), fall through to IAM/identity checks + } + + // Only check IAM if bucket policy didn't explicitly allow + // This ensures bucket policies can independently grant access (AWS semantics) + if !policyAllows { + // Use enhanced IAM authorization if available, otherwise fall back to legacy authorization + if iam.iamIntegration != nil { + // Always use IAM when available for unified authorization + if errCode := iam.authorizeWithIAM(r, identity, action, bucket, object); errCode != s3err.ErrNone { + return identity, errCode + } + } else { + // Fall back to existing authorization when IAM is not configured + if !identity.canDo(action, bucket, object) { + return identity, s3err.ErrAccessDenied + } } } } @@ -570,6 +611,34 @@ func (identity *Identity) isAdmin() bool { return slices.Contains(identity.Actions, s3_constants.ACTION_ADMIN) } +// buildPrincipalARN builds an ARN for an identity to use in bucket policy evaluation +func buildPrincipalARN(identity *Identity) string { + if identity == nil { + return "*" // Anonymous + } + + // Check if this is the anonymous user identity (authenticated as anonymous) + // S3 policies expect Principal: "*" for anonymous access + if identity.Name == s3_constants.AccountAnonymousId || + (identity.Account != nil && identity.Account.Id == s3_constants.AccountAnonymousId) { + return "*" // Anonymous user + } + + // Build an AWS-compatible principal ARN + // Format: arn:aws:iam::account-id:user/user-name + accountId := identity.Account.Id + if accountId == "" { + accountId = "000000000000" // Default account ID + } + + userName := identity.Name + if userName == "" { + userName = "unknown" + } + + return fmt.Sprintf("arn:aws:iam::%s:user/%s", accountId, userName) +} + // GetCredentialManager returns the credential manager instance func (iam *IdentityAccessManagement) GetCredentialManager() *credential.CredentialManager { return iam.credentialManager |
