aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/auth_credentials.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/s3api/auth_credentials.go')
-rw-r--r--weed/s3api/auth_credentials.go95
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