Files
seaweedFS/weed/s3api/s3api_bucket_policy_engine.go
Chris Lu 551a31e156 Implement IAM propagation to S3 servers (#8130)
* Implement IAM propagation to S3 servers

- Add PropagatingCredentialStore to propagate IAM changes to S3 servers via gRPC
- Add Policy management RPCs to S3 proto and S3ApiServer
- Update CredentialManager to use PropagatingCredentialStore when MasterClient is available
- Wire FilerServer to enable propagation

* Implement parallel IAM propagation and fix S3 cluster registration

- Parallelized IAM change propagation with 10s timeout.
- Refined context usage in PropagatingCredentialStore.
- Added S3Type support to cluster node management.
- Enabled S3 servers to register with gRPC address to the master.
- Ensured IAM configuration reload after policy updates via gRPC.

* Optimize IAM propagation with direct in-memory cache updates

* Secure IAM propagation: Use metadata to skip persistence only on propagation

* pb: refactor IAM and S3 services for unidirectional IAM propagation

- Move SeaweedS3IamCache service from iam.proto to s3.proto.
- Remove legacy IAM management RPCs and empty SeaweedS3 service from s3.proto.
- Enforce that S3 servers only use the synchronization interface.

* pb: regenerate Go code for IAM and S3 services

Updated generated code following the proto refactoring of IAM synchronization services.

* s3api: implement read-only mode for Embedded IAM API

- Add readOnly flag to EmbeddedIamApi to reject write operations via HTTP.
- Enable read-only mode by default in S3ApiServer.
- Handle AccessDenied error in writeIamErrorResponse.
- Embed SeaweedS3IamCacheServer in S3ApiServer.

* credential: refactor PropagatingCredentialStore for unidirectional IAM flow

- Update to use s3_pb.SeaweedS3IamCacheClient for propagation to S3 servers.
- Propagate full Identity object via PutIdentity for consistency.
- Remove redundant propagation of specific user/account/policy management RPCs.
- Add timeout context for propagation calls.

* s3api: implement SeaweedS3IamCacheServer for unidirectional sync

- Update S3ApiServer to implement the cache synchronization gRPC interface.
- Methods (PutIdentity, RemoveIdentity, etc.) now perform direct in-memory cache updates.
- Register SeaweedS3IamCacheServer in command/s3.go.
- Remove registration for the legacy and now empty SeaweedS3 service.

* s3api: update tests for read-only IAM and propagation

- Added TestEmbeddedIamReadOnly to verify rejection of write operations in read-only mode.
- Update test setup to pass readOnly=false to NewEmbeddedIamApi in routing tests.
- Updated EmbeddedIamApiForTest helper with read-only checks matching production behavior.

* s3api: add back temporary debug logs for IAM updates

Log IAM updates received via:
- gRPC propagation (PutIdentity, PutPolicy, etc.)
- Metadata configuration reloads (LoadS3ApiConfigurationFromCredentialManager)
- Core identity management (UpsertIdentity, RemoveIdentity)

* IAM: finalize propagation fix with reduced logging and clarified architecture

* Allow configuring IAM read-only mode for S3 server integration tests

* s3api: add defensive validation to UpsertIdentity

* s3api: fix log message to reference correct IAM read-only flag

* test/s3/iam: ensure WaitForS3Service checks for IAM write permissions

* test: enable writable IAM in Makefile for integration tests

* IAM: add GetPolicy/ListPolicies RPCs to s3.proto

* S3: add GetBucketPolicy and ListBucketPolicies helpers

* S3: support storing generic IAM policies in IdentityAccessManagement

* S3: implement IAM policy RPCs using IdentityAccessManagement

* IAM: fix stale user identity on rename propagation
2026-01-26 22:59:43 -08:00

178 lines
6.2 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
}
// 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.Conditions = policy_engine.ExtractConditionValuesFromRequest(r)
// Extract principal-related variables (aws:username, etc.) from principal ARN
principalVars := policy_engine.ExtractPrincipalVariables(principal)
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.
}
}
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)
}
}