S3: Enforce bucket policy (#7471)

* evaluate policies during authorization

* cache bucket policy

* refactor

* matching with regex special characters

* Case Sensitivity, pattern cache, Dead Code Removal

* Fixed Typo, Restored []string Case, Added Cache Size Limit

* hook up with policy engine

* remove old implementation

* action mapping

* validate

* if not specified, fall through to IAM checks

* fmt

* Fail-close on policy evaluation errors

* Explicit `Allow` bypasses IAM checks

* fix error message

* arn:seaweed => arn:aws

* remove legacy support

* fix tests

* Clean up bucket policy after this test

* fix for tests

* address comments

* security fixes

* fix tests

* temp comment out
This commit is contained in:
Chris Lu
2025-11-12 22:14:50 -08:00
committed by GitHub
parent 50f067bcfd
commit 508d06d9a5
47 changed files with 1104 additions and 749 deletions

View File

@@ -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