mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-12 23:09:33 +01:00
* s3api: record the caller on AssumeRole sessions The session name is the caller's choice; the session now carries the caller itself as the user it was issued to, as the web identity, LDAP and credential paths already do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3api: take a role session's aws:username from its user, not its name An assumed-role ARN ends in the session name the caller picked, so naming a session after another user resolved ${aws:username} and ${aws:userid} to that user in bucket and attached policies. Resolve them from the user the session was issued to instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * iam: take a role session's aws:username from its user, not its name IsActionAllowed set aws:username to the last segment of an assumed-role ARN, the caller-chosen session name, for role and bucket policies. Use the user the session was issued to, and drop a forwarded value when the session names none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3api: pass a role session caller's user on to the session it assumes A role session that calls AssumeRole is named by its session ID, so the new session recorded that ID as its user. An STS session caller now passes on its own recorded user, or none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3api: size the session claims map from the request context Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
208 lines
7.7 KiB
Go
208 lines
7.7 KiB
Go
package s3api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
|
|
)
|
|
|
|
// BucketPolicyEngine wraps the policy_engine to provide bucket policy evaluation
|
|
type BucketPolicyEngine struct {
|
|
engine *policy_engine.PolicyEngine
|
|
// MultipartSSELookup retrieves the canonical SSE algorithm ("AES256" or
|
|
// "aws:kms") that was stored when a multipart upload was initiated.
|
|
// Returns "" when the upload had no SSE or when the entry cannot be found.
|
|
// Set by S3ApiServer after construction to give the engine filer access.
|
|
MultipartSSELookup func(bucket, uploadID string) string
|
|
}
|
|
|
|
// NewBucketPolicyEngine creates a new bucket policy engine
|
|
func NewBucketPolicyEngine() *BucketPolicyEngine {
|
|
return &BucketPolicyEngine{
|
|
engine: policy_engine.NewPolicyEngine(),
|
|
}
|
|
}
|
|
|
|
// LoadBucketPolicy loads a bucket policy into the engine from the filer entry
|
|
func (bpe *BucketPolicyEngine) LoadBucketPolicy(bucket string, entry *filer_pb.Entry) error {
|
|
if entry == nil || entry.Extended == nil {
|
|
return nil
|
|
}
|
|
|
|
policyJSON, exists := entry.Extended[BUCKET_POLICY_METADATA_KEY]
|
|
if !exists || len(policyJSON) == 0 {
|
|
// No policy for this bucket - remove it if it exists
|
|
bpe.engine.DeleteBucketPolicy(bucket)
|
|
return nil
|
|
}
|
|
|
|
// Set the policy in the engine
|
|
if err := bpe.engine.SetBucketPolicy(bucket, string(policyJSON)); err != nil {
|
|
glog.Errorf("Failed to load bucket policy for %s: %v", bucket, err)
|
|
return err
|
|
}
|
|
|
|
glog.V(3).Infof("Loaded bucket policy for %s into policy engine", bucket)
|
|
return nil
|
|
}
|
|
|
|
// LoadBucketPolicyFromCache loads a bucket policy from a cached BucketConfig
|
|
//
|
|
// This function loads the policy directly into the engine
|
|
func (bpe *BucketPolicyEngine) LoadBucketPolicyFromCache(bucket string, policyDoc *policy_engine.PolicyDocument) error {
|
|
if policyDoc == nil {
|
|
// No policy for this bucket - remove it if it exists
|
|
bpe.engine.DeleteBucketPolicy(bucket)
|
|
return nil
|
|
}
|
|
|
|
// Policy is already in correct format, just load it
|
|
// We need to re-marshal to string because SetBucketPolicy expects JSON string
|
|
policyJSON, err := json.Marshal(policyDoc)
|
|
if err != nil {
|
|
glog.Errorf("Failed to marshal bucket policy for %s: %v", bucket, err)
|
|
return err
|
|
}
|
|
|
|
// Set the policy in the engine
|
|
if err := bpe.engine.SetBucketPolicy(bucket, string(policyJSON)); err != nil {
|
|
glog.Errorf("Failed to load bucket policy for %s: %v", bucket, err)
|
|
return err
|
|
}
|
|
|
|
glog.V(4).Infof("Loaded bucket policy for %s into policy engine from cache", bucket)
|
|
return nil
|
|
}
|
|
|
|
// DeleteBucketPolicy removes a bucket policy from the engine
|
|
func (bpe *BucketPolicyEngine) DeleteBucketPolicy(bucket string) error {
|
|
return bpe.engine.DeleteBucketPolicy(bucket)
|
|
}
|
|
|
|
// HasPolicyForBucket checks if a bucket has a policy configured
|
|
func (bpe *BucketPolicyEngine) HasPolicyForBucket(bucket string) bool {
|
|
return bpe.engine.HasPolicyForBucket(bucket)
|
|
}
|
|
|
|
// GetBucketPolicy gets the policy for a bucket
|
|
func (bpe *BucketPolicyEngine) GetBucketPolicy(bucket string) (*policy_engine.PolicyDocument, error) {
|
|
return bpe.engine.GetBucketPolicy(bucket)
|
|
}
|
|
|
|
// ListBucketPolicies returns all buckets that have policies
|
|
func (bpe *BucketPolicyEngine) ListBucketPolicies() []string {
|
|
return bpe.engine.GetAllBucketsWithPolicies()
|
|
}
|
|
|
|
// EvaluatePolicy evaluates whether an action is allowed by bucket policy
|
|
//
|
|
// Parameters:
|
|
// - bucket: the bucket name
|
|
// - object: the object key (can be empty for bucket-level operations)
|
|
// - action: the action being performed (e.g., "Read", "Write")
|
|
// - principal: the principal ARN or identifier
|
|
// - r: the HTTP request (optional, used for condition evaluation and action resolution)
|
|
// - objectEntry: the object's metadata from entry.Extended (can be nil at auth time,
|
|
// should be passed when available for tag-based conditions like s3:ExistingObjectTag)
|
|
//
|
|
// Returns:
|
|
// - allowed: whether the policy allows the action
|
|
// - evaluated: whether a policy was found and evaluated (false = no policy exists)
|
|
// - error: any error during evaluation
|
|
func (bpe *BucketPolicyEngine) EvaluatePolicy(bucket, object, action, principal string, r *http.Request, claims map[string]interface{}, objectEntry map[string][]byte) (allowed bool, evaluated bool, err error) {
|
|
// Validate required parameters
|
|
if bucket == "" {
|
|
return false, false, fmt.Errorf("bucket cannot be empty")
|
|
}
|
|
if action == "" {
|
|
return false, false, fmt.Errorf("action cannot be empty")
|
|
}
|
|
|
|
// Convert action to S3 action format
|
|
// ResolveS3Action handles nil request internally (falls back to mapBaseActionToS3Format)
|
|
s3Action := ResolveS3Action(r, action, bucket, object)
|
|
|
|
// Build resource ARN
|
|
resource := buildResourceARN(bucket, object)
|
|
|
|
glog.V(4).Infof("EvaluatePolicy: bucket=%s, resource=%s, action=%s, principal=%s",
|
|
bucket, resource, s3Action, principal)
|
|
|
|
// Evaluate using the policy engine
|
|
args := &policy_engine.PolicyEvaluationArgs{
|
|
Action: s3Action,
|
|
Resource: resource,
|
|
Principal: principal,
|
|
ObjectEntry: objectEntry,
|
|
}
|
|
|
|
// glog.V(4).Infof("EvaluatePolicy [Wrapper]: bucket=%s, resource=%s, action=%s, principal=%s",
|
|
// bucket, resource, s3Action, principal)
|
|
|
|
// Extract conditions and claims from request if available
|
|
if r != nil {
|
|
args.OriginalGrantConditions = policy_engine.OriginalGrantConditionsFromRequest(r)
|
|
args.Conditions = bpe.engine.ExtractConditionValuesFromRequest(r)
|
|
|
|
// Extract principal-related variables (aws:username, etc.) from principal ARN
|
|
principalVars := principalVariables(principal, claims)
|
|
for k, v := range principalVars {
|
|
args.Conditions[k] = v
|
|
}
|
|
|
|
// Extract JWT claims if authenticated via JWT or STS
|
|
if claims != nil {
|
|
args.Claims = claims
|
|
} else {
|
|
// If claims were not provided directly, try to get them from context Identity?
|
|
// But the caller is responsible for passing them.
|
|
// Falling back to empty claims if not provided.
|
|
}
|
|
|
|
// For multipart continuation actions look up the SSE algorithm that was
|
|
// set at CreateMultipartUpload time. UploadPart/UploadPartCopy do not
|
|
// re-send the SSE header, so we inject the stored value so that bucket
|
|
// policy conditions on s3:x-amz-server-side-encryption evaluate correctly.
|
|
if policy_engine.IsMultipartContinuationAction(s3Action) && bpe.MultipartSSELookup != nil {
|
|
if uploadID := r.URL.Query().Get("uploadId"); uploadID != "" {
|
|
args.InheritedSSEAlgorithm = bpe.MultipartSSELookup(bucket, uploadID)
|
|
}
|
|
}
|
|
}
|
|
|
|
result := bpe.engine.EvaluatePolicy(bucket, args)
|
|
|
|
switch result {
|
|
case policy_engine.PolicyResultAllow:
|
|
// glog.V(4).Infof("EvaluatePolicy [Wrapper]: ALLOW - bucket=%s, action=%s, principal=%s", bucket, s3Action, principal)
|
|
return true, true, nil
|
|
case policy_engine.PolicyResultDeny:
|
|
// glog.V(4).Infof("EvaluatePolicy [Wrapper]: DENY - bucket=%s, action=%s, principal=%s", bucket, s3Action, principal)
|
|
return false, true, nil
|
|
case policy_engine.PolicyResultIndeterminate:
|
|
// No policy exists for this bucket
|
|
// glog.V(4).Infof("EvaluatePolicy [Wrapper]: INDETERMINATE (no policy) - bucket=%s", bucket)
|
|
return false, false, nil
|
|
default:
|
|
return false, false, fmt.Errorf("unknown policy result: %v", result)
|
|
}
|
|
}
|
|
|
|
// principalVariables resolves the policy variables of a principal. A role
|
|
// session's ARN ends in the session name the caller chose, so its user comes
|
|
// from the session itself: the user the session was issued to.
|
|
func principalVariables(principal string, claims map[string]interface{}) map[string][]string {
|
|
vars := policy_engine.ExtractPrincipalVariables(principal)
|
|
if types := vars["aws:principaltype"]; len(types) == 1 && types[0] == "AssumedRole" {
|
|
if user, ok := claims["aws:username"].(string); ok && user != "" {
|
|
vars["aws:username"] = []string{user}
|
|
vars["aws:userid"] = []string{user}
|
|
}
|
|
}
|
|
return vars
|
|
}
|