mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-21 14:46:58 +00:00
* s3: require a bucket-policy action to write a bucket policy PutBucketPolicy and DeleteBucketPolicy were gated on ACTION_WRITE, the same action that grants object writes. An explicit Allow in a bucket policy short-circuits IAM entirely -- authRequestWithAuthType sets policyAllows and skips VerifyActionPermission -- so anyone who could write an object could author a policy granting itself, or anonymous, anything on the bucket. That is what separates a bucket policy from the sibling bucket controls also gated on ACTION_WRITE: rewriting cors or lifecycle can destroy data, but only a policy hands out access. Give the two verbs their own actions, mapped to the AWS names that were already defined but unrouted. ACTION_ADMIN would also have closed it, but it resolves to s3:* for IAM identities, forcing a blanket grant on a user holding a precise s3:PutBucketPolicy. Admins are unaffected, since isAdmin short-circuits CanDo, and an operator can delegate with PutBucketPolicy:bucket. The route binding is asserted from the router source: checking the action constants alone still passes when the route says ACTION_WRITE. * s3: also read the action from a direct iam.Auth call in the route test Routes read iam.Auth(cb.Limit(handler, ACTION)), a multi-value pass-through: Limit returns (http.HandlerFunc, Action) and those become Auth's parameters, so the action Auth authorizes on is Limit's second argument and the two cannot disagree -- Auth(Limit(h, X), Y) does not compile. A route that skipped Limit and called Auth with its own action would compile, though, and the test reported that as a missing route rather than as the wrong action. Recognise the two-argument Auth form so it names the action instead. * s3: make the bucket-policy actions grantable through an IAM policy The new actions close the escalation only if an operator can grant them, and they were not reachable: MapToStatementAction had no entry for PutBucketPolicy, so an IAM policy naming s3:PutBucketPolicy was rejected outright with "not a valid action". GetBucketPolicy was unmapped the same way. DeleteBucketPolicy was mapped, but to ACTION_ADMIN -- granting an identity permission to delete a bucket policy handed it full administrative access. Map all three to the actions the router now uses, and add the reverse direction so an identity holding them renders back as a policy statement instead of a bare "s3:". * admin: offer the bucket-policy permissions in the user editor The two new actions are otherwise only grantable by hand-editing identity JSON or by calling the IAM API, so an operator using the UI cannot delegate bucket policy management without granting Admin. Regenerating this file also picks up codegen the repo has not taken yet: the checked-in _templ.go files were produced by templ v0.3.1001 while go.mod pins v0.3.1020, so the generator rewrites the attribute-value calls. That churn is confined to this one file; running `make generate` in weed/admin reproduces it across all 36.
202 lines
7.7 KiB
Go
202 lines
7.7 KiB
Go
package iam
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"fmt"
|
|
"math/big"
|
|
"sort"
|
|
|
|
"github.com/aws/aws-sdk-go/service/iam"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
)
|
|
|
|
// Hash computes a SHA1 hash of the input string.
|
|
func Hash(s *string) string {
|
|
h := sha1.New()
|
|
h.Write([]byte(*s))
|
|
return fmt.Sprintf("%x", h.Sum(nil))
|
|
}
|
|
|
|
// UserArn builds an AWS-compatible IAM user ARN.
|
|
func UserArn(userName string) string {
|
|
return fmt.Sprintf("arn:aws:iam::%s:user/%s", DefaultAccountID, userName)
|
|
}
|
|
|
|
// NewUser builds an iam.User for IAM API responses. The Arn must be a real ARN:
|
|
// the terraform aws provider (>= 6.41) reads a user back after creating it and
|
|
// blocks until GetUser returns a value that passes arn.IsARN, so an empty Arn
|
|
// leaves apply hanging until it times out.
|
|
func NewUser(userName string) iam.User {
|
|
arn := UserArn(userName)
|
|
path := "/"
|
|
return iam.User{UserName: &userName, Arn: &arn, Path: &path}
|
|
}
|
|
|
|
// GenerateRandomString generates a cryptographically secure random string.
|
|
// Uses crypto/rand for security-sensitive credential generation.
|
|
func GenerateRandomString(length int, charset string) (string, error) {
|
|
if length <= 0 {
|
|
return "", fmt.Errorf("length must be positive, got %d", length)
|
|
}
|
|
if charset == "" {
|
|
return "", fmt.Errorf("charset must not be empty")
|
|
}
|
|
b := make([]byte, length)
|
|
for i := range b {
|
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate random index: %w", err)
|
|
}
|
|
b[i] = charset[n.Int64()]
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
// GenerateSecretAccessKey generates a new secret access key.
|
|
func GenerateSecretAccessKey() (string, error) {
|
|
return GenerateRandomString(SecretAccessKeyLength, Charset)
|
|
}
|
|
|
|
// StringSlicesEqual compares two string slices for equality, ignoring order.
|
|
// This is used instead of reflect.DeepEqual to avoid order-dependent comparisons.
|
|
func StringSlicesEqual(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
// Make copies to avoid modifying the originals
|
|
aCopy := make([]string, len(a))
|
|
bCopy := make([]string, len(b))
|
|
copy(aCopy, a)
|
|
copy(bCopy, b)
|
|
sort.Strings(aCopy)
|
|
sort.Strings(bCopy)
|
|
for i := range aCopy {
|
|
if aCopy[i] != bCopy[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// fineGrainedActionMap maps S3 IAM action names to internal S3 action constants.
|
|
// Supports both prefixed (e.g., "s3:DeleteObject") and unprefixed (e.g., "DeleteObject") formats.
|
|
// Populated in init() to avoid duplication of prefixed/unprefixed variants.
|
|
var fineGrainedActionMap = map[string]string{
|
|
// Coarse-grained actions (populated statically)
|
|
StatementActionAdmin: s3_constants.ACTION_ADMIN,
|
|
StatementActionWrite: s3_constants.ACTION_WRITE,
|
|
StatementActionWriteAcp: s3_constants.ACTION_WRITE_ACP,
|
|
StatementActionRead: s3_constants.ACTION_READ,
|
|
StatementActionReadAcp: s3_constants.ACTION_READ_ACP,
|
|
StatementActionList: s3_constants.ACTION_LIST,
|
|
StatementActionTagging: s3_constants.ACTION_TAGGING,
|
|
StatementActionDelete: s3_constants.ACTION_DELETE_BUCKET,
|
|
}
|
|
|
|
// baseS3ActionMap defines the base S3 actions that will be populated with both
|
|
// prefixed (s3:Action) and unprefixed (Action) variants in init().
|
|
var baseS3ActionMap = map[string]string{
|
|
// Object operations
|
|
"DeleteObject": s3_constants.ACTION_WRITE,
|
|
"PutObject": s3_constants.ACTION_WRITE,
|
|
"GetObject": s3_constants.ACTION_READ,
|
|
"DeleteObjectVersion": s3_constants.ACTION_WRITE,
|
|
"GetObjectVersion": s3_constants.ACTION_READ,
|
|
// Tagging operations
|
|
"GetObjectTagging": s3_constants.ACTION_TAGGING,
|
|
"GetObjectVersionTagging": s3_constants.ACTION_TAGGING,
|
|
"PutObjectTagging": s3_constants.ACTION_TAGGING,
|
|
"DeleteObjectTagging": s3_constants.ACTION_TAGGING,
|
|
"GetBucketTagging": s3_constants.ACTION_TAGGING,
|
|
"PutBucketTagging": s3_constants.ACTION_TAGGING,
|
|
"DeleteBucketTagging": s3_constants.ACTION_TAGGING,
|
|
// ACL operations
|
|
"PutObjectAcl": s3_constants.ACTION_WRITE_ACP,
|
|
"GetObjectAcl": s3_constants.ACTION_READ_ACP,
|
|
"GetObjectVersionAcl": s3_constants.ACTION_READ_ACP,
|
|
"PutBucketAcl": s3_constants.ACTION_WRITE_ACP,
|
|
"GetBucketAcl": s3_constants.ACTION_READ_ACP,
|
|
// Bucket operations
|
|
"DeleteBucket": s3_constants.ACTION_DELETE_BUCKET,
|
|
// Bucket policy is permissions management: an explicit Allow in one skips
|
|
// the IAM check, so it gets its own actions rather than folding into Write
|
|
// (which any object writer holds) or Admin (which grants everything).
|
|
"GetBucketPolicy": s3_constants.ACTION_READ,
|
|
"PutBucketPolicy": s3_constants.ACTION_PUT_BUCKET_POLICY,
|
|
"DeleteBucketPolicy": s3_constants.ACTION_DELETE_BUCKET_POLICY,
|
|
"ListBucket": s3_constants.ACTION_LIST,
|
|
"ListBucketVersions": s3_constants.ACTION_LIST,
|
|
"ListAllMyBuckets": s3_constants.ACTION_LIST,
|
|
"GetBucketLocation": s3_constants.ACTION_READ,
|
|
"GetBucketVersioning": s3_constants.ACTION_READ,
|
|
"PutBucketVersioning": s3_constants.ACTION_WRITE,
|
|
"GetBucketCors": s3_constants.ACTION_READ,
|
|
"PutBucketCors": s3_constants.ACTION_WRITE,
|
|
"DeleteBucketCors": s3_constants.ACTION_WRITE,
|
|
"GetBucketNotification": s3_constants.ACTION_READ,
|
|
"PutBucketNotification": s3_constants.ACTION_WRITE,
|
|
"GetBucketObjectLockConfiguration": s3_constants.ACTION_READ,
|
|
"PutBucketObjectLockConfiguration": s3_constants.ACTION_WRITE,
|
|
// Multipart upload operations
|
|
"CreateMultipartUpload": s3_constants.ACTION_WRITE,
|
|
"UploadPart": s3_constants.ACTION_WRITE,
|
|
"CompleteMultipartUpload": s3_constants.ACTION_WRITE,
|
|
"AbortMultipartUpload": s3_constants.ACTION_WRITE,
|
|
"ListMultipartUploads": s3_constants.ACTION_WRITE,
|
|
"ListParts": s3_constants.ACTION_WRITE,
|
|
// Retention and legal hold operations
|
|
"GetObjectRetention": s3_constants.ACTION_READ,
|
|
"PutObjectRetention": s3_constants.ACTION_WRITE,
|
|
"GetObjectLegalHold": s3_constants.ACTION_READ,
|
|
"PutObjectLegalHold": s3_constants.ACTION_WRITE,
|
|
"BypassGovernanceRetention": s3_constants.ACTION_WRITE,
|
|
}
|
|
|
|
func init() {
|
|
// Populate both prefixed and unprefixed variants for all base S3 actions.
|
|
// This avoids duplication and makes it easy to add new actions in one place.
|
|
for action, constant := range baseS3ActionMap {
|
|
fineGrainedActionMap[action] = constant // unprefixed: "DeleteObject"
|
|
fineGrainedActionMap["s3:"+action] = constant // prefixed: "s3:DeleteObject"
|
|
}
|
|
}
|
|
|
|
// MapToStatementAction converts a policy statement action to an S3 action constant.
|
|
// It handles both coarse-grained statement actions (e.g., "Put*", "Get*") and
|
|
// fine-grained S3 actions (e.g., "s3:DeleteObject", "s3:PutObject") via exact lookup.
|
|
func MapToStatementAction(action string) string {
|
|
if val, ok := fineGrainedActionMap[action]; ok {
|
|
return val
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// MapToIdentitiesAction converts an S3 action constant to a policy statement action.
|
|
func MapToIdentitiesAction(action string) string {
|
|
switch action {
|
|
case s3_constants.ACTION_ADMIN:
|
|
return StatementActionAdmin
|
|
case s3_constants.ACTION_WRITE:
|
|
return StatementActionWrite
|
|
case s3_constants.ACTION_WRITE_ACP:
|
|
return StatementActionWriteAcp
|
|
case s3_constants.ACTION_READ:
|
|
return StatementActionRead
|
|
case s3_constants.ACTION_READ_ACP:
|
|
return StatementActionReadAcp
|
|
case s3_constants.ACTION_LIST:
|
|
return StatementActionList
|
|
case s3_constants.ACTION_TAGGING:
|
|
return StatementActionTagging
|
|
case s3_constants.ACTION_DELETE_BUCKET:
|
|
return StatementActionDelete
|
|
case s3_constants.ACTION_PUT_BUCKET_POLICY:
|
|
return "PutBucketPolicy"
|
|
case s3_constants.ACTION_DELETE_BUCKET_POLICY:
|
|
return "DeleteBucketPolicy"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|