s3api: fix static IAM policy enforcement after reload (#8532)

* s3api: honor attached IAM policies over legacy actions

* s3api: hydrate IAM policy docs during config reload

* s3api: use policy-aware auth when listing buckets

* credential: propagate context through filer_etc policy reads

* credential: make legacy policy deletes durable

* s3api: exercise managed policy runtime loader

* s3api: allow static IAM users without session tokens

* iam: deny unmatched attached policies under default allow

* iam: load embedded policy files from filer store

* s3api: require session tokens for IAM presigning

* s3api: sync runtime policies into zero-config IAM

* credential: respect context in policy file loads

* credential: serialize legacy policy deletes

* iam: align filer policy store naming

* s3api: use authenticated principals for presigning

* iam: deep copy policy conditions

* s3api: require request creation in policy tests

* filer: keep ReadInsideFiler as the context-aware API

* iam: harden filer policy store writes

* credential: strengthen legacy policy serialization test

* credential: forward runtime policy loaders through wrapper

* s3api: harden runtime policy merging

* iam: require typed already-exists errors
This commit is contained in:
Chris Lu
2026-03-06 12:35:08 -08:00
committed by GitHub
parent 338be16254
commit f9311a3422
30 changed files with 1903 additions and 168 deletions

View File

@@ -9,6 +9,7 @@ import (
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
)
@@ -18,13 +19,113 @@ const (
)
type PoliciesCollection struct {
Policies map[string]policy_engine.PolicyDocument `json:"policies"`
Policies map[string]policy_engine.PolicyDocument `json:"policies"`
InlinePolicies map[string]map[string]policy_engine.PolicyDocument `json:"inlinePolicies"`
}
func validatePolicyName(name string) error {
return credential.ValidatePolicyName(name)
}
func newPoliciesCollection() *PoliciesCollection {
return &PoliciesCollection{
Policies: make(map[string]policy_engine.PolicyDocument),
InlinePolicies: make(map[string]map[string]policy_engine.PolicyDocument),
}
}
func (store *FilerEtcStore) loadLegacyPoliciesCollection(ctx context.Context) (*PoliciesCollection, bool, error) {
policiesCollection := newPoliciesCollection()
content, foundLegacy, err := store.readInsideFiler(ctx, filer.IamConfigDirectory, filer.IamPoliciesFile)
if err != nil {
return nil, false, err
}
if !foundLegacy || len(content) == 0 {
return policiesCollection, foundLegacy, nil
}
if err := json.Unmarshal(content, policiesCollection); err != nil {
return nil, false, err
}
if policiesCollection.Policies == nil {
policiesCollection.Policies = make(map[string]policy_engine.PolicyDocument)
}
if policiesCollection.InlinePolicies == nil {
policiesCollection.InlinePolicies = make(map[string]map[string]policy_engine.PolicyDocument)
}
return policiesCollection, true, nil
}
func (store *FilerEtcStore) saveLegacyPoliciesCollection(ctx context.Context, policiesCollection *PoliciesCollection) error {
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
content, err := json.MarshalIndent(policiesCollection, "", " ")
if err != nil {
return err
}
return filer.SaveInsideFiler(client, filer.IamConfigDirectory, filer.IamPoliciesFile, content)
})
}
func policyDocumentToPbPolicy(name string, policy policy_engine.PolicyDocument) (*iam_pb.Policy, error) {
content, err := json.Marshal(policy)
if err != nil {
return nil, err
}
return &iam_pb.Policy{Name: name, Content: string(content)}, nil
}
// LoadManagedPolicies loads managed policies for the S3 runtime without
// triggering legacy-to-multifile migration. This lets the runtime hydrate
// policies while preserving any legacy inline policy data stored alongside
// managed policies.
func (store *FilerEtcStore) LoadManagedPolicies(ctx context.Context) ([]*iam_pb.Policy, error) {
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
if err != nil {
return nil, err
}
policies := make(map[string]policy_engine.PolicyDocument, len(policiesCollection.Policies))
for name, policy := range policiesCollection.Policies {
policies[name] = policy
}
if err := store.loadPoliciesFromMultiFile(ctx, policies); err != nil {
return nil, err
}
managedPolicies := make([]*iam_pb.Policy, 0, len(policies))
for name, policy := range policies {
pbPolicy, err := policyDocumentToPbPolicy(name, policy)
if err != nil {
return nil, err
}
managedPolicies = append(managedPolicies, pbPolicy)
}
return managedPolicies, nil
}
// LoadInlinePolicies loads legacy inline policies keyed by user name. Inline
// policies are still stored in the legacy shared policies file.
func (store *FilerEtcStore) LoadInlinePolicies(ctx context.Context) (map[string]map[string]policy_engine.PolicyDocument, error) {
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
if err != nil {
return nil, err
}
inlinePolicies := make(map[string]map[string]policy_engine.PolicyDocument, len(policiesCollection.InlinePolicies))
for userName, userPolicies := range policiesCollection.InlinePolicies {
inlinePolicies[userName] = make(map[string]policy_engine.PolicyDocument, len(userPolicies))
for policyName, policy := range userPolicies {
inlinePolicies[userName][policyName] = policy
}
}
return inlinePolicies, nil
}
// GetPolicies retrieves all IAM policies from the filer
func (store *FilerEtcStore) GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error) {
policies := make(map[string]policy_engine.PolicyDocument)
@@ -43,23 +144,12 @@ func (store *FilerEtcStore) GetPolicies(ctx context.Context) (map[string]policy_
filer.IamConfigDirectory, filer.IamPoliciesFile)
// 1. Load from legacy single file (low priority)
content, foundLegacy, err := store.readInsideFiler(filer.IamConfigDirectory, filer.IamPoliciesFile)
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
if err != nil {
return nil, err
}
if foundLegacy && len(content) > 0 {
policiesCollection := &PoliciesCollection{
Policies: make(map[string]policy_engine.PolicyDocument),
}
if err := json.Unmarshal(content, policiesCollection); err != nil {
glog.Errorf("Failed to parse legacy IAM policies from %s/%s: %v",
filer.IamConfigDirectory, filer.IamPoliciesFile, err)
} else {
for name, policy := range policiesCollection.Policies {
policies[name] = policy
}
}
for name, policy := range policiesCollection.Policies {
policies[name] = policy
}
// 2. Load from multi-file structure (high priority, overrides legacy)
@@ -67,14 +157,6 @@ func (store *FilerEtcStore) GetPolicies(ctx context.Context) (map[string]policy_
return nil, err
}
// 3. Perform migration if we loaded legacy config
if foundLegacy {
if err := store.migratePoliciesToMultiFile(ctx, policies); err != nil {
glog.Errorf("Failed to migrate IAM policies to multi-file layout: %v", err)
return policies, err
}
}
return policies, nil
}
@@ -98,7 +180,7 @@ func (store *FilerEtcStore) loadPoliciesFromMultiFile(ctx context.Context, polic
if len(entry.Content) > 0 {
content = entry.Content
} else {
c, err := filer.ReadInsideFiler(client, dir, entry.Name)
c, err := filer.ReadInsideFiler(ctx, client, dir, entry.Name)
if err != nil {
glog.Warningf("Failed to read policy file %s: %v", entry.Name, err)
continue
@@ -115,7 +197,7 @@ func (store *FilerEtcStore) loadPoliciesFromMultiFile(ctx context.Context, polic
// The file name is "policyName.json"
policyName := entry.Name
if len(policyName) > 5 && policyName[len(policyName)-5:] == ".json" {
if strings.HasSuffix(policyName, ".json") {
policyName = policyName[:len(policyName)-5]
policies[policyName] = policy
}
@@ -184,7 +266,23 @@ func (store *FilerEtcStore) DeletePolicy(ctx context.Context, name string) error
if err := validatePolicyName(name); err != nil {
return err
}
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
store.policyMu.Lock()
defer store.policyMu.Unlock()
policiesCollection, foundLegacy, err := store.loadLegacyPoliciesCollection(ctx)
if err != nil {
return err
}
deleteLegacyPolicy := false
if foundLegacy {
if _, exists := policiesCollection.Policies[name]; exists {
delete(policiesCollection.Policies, name)
deleteLegacyPolicy = true
}
}
if err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
_, err := client.DeleteEntry(ctx, &filer_pb.DeleteEntryRequest{
Directory: filer.IamConfigDirectory + "/" + IamPoliciesDirectory,
Name: name + ".json",
@@ -193,7 +291,15 @@ func (store *FilerEtcStore) DeletePolicy(ctx context.Context, name string) error
return err
}
return nil
})
}); err != nil {
return err
}
if deleteLegacyPolicy {
return store.saveLegacyPoliciesCollection(ctx, policiesCollection)
}
return nil
}
// GetPolicy retrieves a specific IAM policy by name from the filer
@@ -204,7 +310,7 @@ func (store *FilerEtcStore) GetPolicy(ctx context.Context, name string) (*policy
var policy *policy_engine.PolicyDocument
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
data, err := filer.ReadInsideFiler(client, filer.IamConfigDirectory+"/"+IamPoliciesDirectory, name+".json")
data, err := filer.ReadInsideFiler(ctx, client, filer.IamConfigDirectory+"/"+IamPoliciesDirectory, name+".json")
if err != nil {
if err == filer_pb.ErrNotFound {
return nil
@@ -239,6 +345,7 @@ func (store *FilerEtcStore) GetPolicy(ctx context.Context, name string) (*policy
// ListPolicyNames returns all managed policy names stored in the filer.
func (store *FilerEtcStore) ListPolicyNames(ctx context.Context) ([]string, error) {
names := make([]string, 0)
seenNames := make(map[string]struct{})
store.mu.RLock()
configured := store.filerAddressFunc != nil
@@ -248,7 +355,19 @@ func (store *FilerEtcStore) ListPolicyNames(ctx context.Context) ([]string, erro
return names, nil
}
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
if err != nil {
return nil, err
}
for name := range policiesCollection.Policies {
if _, found := seenNames[name]; found {
continue
}
names = append(names, name)
seenNames[name] = struct{}{}
}
err = store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
dir := filer.IamConfigDirectory + "/" + IamPoliciesDirectory
entries, err := listEntries(ctx, client, dir)
if err != nil {
@@ -266,7 +385,11 @@ func (store *FilerEtcStore) ListPolicyNames(ctx context.Context) ([]string, erro
if strings.HasSuffix(name, ".json") {
name = name[:len(name)-5]
}
if _, found := seenNames[name]; found {
continue
}
names = append(names, name)
seenNames[name] = struct{}{}
}
return nil