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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user