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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user