mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-20 06:52:24 +00:00
9b9fdb5b76
* fix(s3): sync IAM policies to advanced IAM Manager policy engine * test(s3): add unit tests for PutPolicy/DeletePolicy IAM Manager sync * fix(s3): flush loaded policies in SetIAMIntegration, drop extra reload Sync the policies already loaded from the credential store into the IAM Manager's engine from SetIAMIntegration itself, instead of re-running a full LoadS3ApiConfigurationFromCredentialManager after setup. This covers both startup orderings without a second filer round-trip or racing the async loader goroutine: if the load won, the policies are in memory to push; if SetIAMIntegration won, the load's own sync runs afterward. Move the runtime PutPolicy/DeletePolicy sync out of the iam.m write lock so the per-request auth RLock path isn't blocked by the policy recompile. * fix(s3): serialize IAM manager policy resync to avoid stale snapshots SyncRuntimePolicies replaces the manager's full policy set, so applying a policy view captured before a later mutation can resurrect a deleted policy or drop a new one. Funnel every path (PutPolicy, DeletePolicy, SetIAMIntegration, and the credential-manager load) through a single resyncIAMManagerPolicies that serializes on a dedicated mutex and reads iam.policies fresh at apply time, so the live map always wins regardless of interleaving. The load now installs the config into iam.policies before resyncing, closing the window where the manager held policies the map didn't yet have. --------- Co-authored-by: Chris Lu <chris.lu@gmail.com>
159 lines
5.8 KiB
Go
159 lines
5.8 KiB
Go
package s3api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/integration"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/policy"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newTestIAMManager(t *testing.T) *integration.IAMManager {
|
|
t.Helper()
|
|
mgr := integration.NewIAMManager()
|
|
require.NoError(t, mgr.Initialize(&integration.IAMConfig{
|
|
STS: &sts.STSConfig{
|
|
TokenDuration: sts.FlexibleDuration{Duration: time.Hour},
|
|
MaxSessionLength: sts.FlexibleDuration{Duration: 12 * time.Hour},
|
|
Issuer: "test",
|
|
SigningKey: []byte("test-signing-key-32-characters!!"),
|
|
AccountId: "111122223333",
|
|
},
|
|
Policy: &policy.PolicyEngineConfig{DefaultEffect: "Allow", StoreType: "memory"},
|
|
Roles: &integration.RoleStoreConfig{StoreType: "memory"},
|
|
}, func() string { return "localhost:8888" }))
|
|
return mgr
|
|
}
|
|
|
|
func TestPutPolicy_SyncsToIAMManager(t *testing.T) {
|
|
mgr := newTestIAMManager(t)
|
|
iam := &IdentityAccessManagement{}
|
|
iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))
|
|
|
|
policyDoc, _ := json.Marshal(map[string]interface{}{
|
|
"Version": "2012-10-17",
|
|
"Statement": []map[string]interface{}{
|
|
{"Effect": "Allow", "Action": "s3:*", "Resource": "arn:aws:s3:::backup/*"},
|
|
},
|
|
})
|
|
|
|
require.NoError(t, iam.PutPolicy("BackupAll", string(policyDoc)))
|
|
|
|
allowed, err := mgr.IsActionAllowed(context.Background(), &integration.ActionRequest{
|
|
Principal: "arn:aws:iam::111122223333:user/test",
|
|
Action: "s3:PutObject",
|
|
Resource: "arn:aws:s3:::backup/file.txt",
|
|
PolicyNames: []string{"BackupAll"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, allowed, "PutPolicy should sync to IAM Manager policy engine")
|
|
}
|
|
|
|
func TestDeletePolicy_SyncsToIAMManager(t *testing.T) {
|
|
mgr := newTestIAMManager(t)
|
|
iam := &IdentityAccessManagement{}
|
|
iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))
|
|
|
|
policyDoc, _ := json.Marshal(map[string]interface{}{
|
|
"Version": "2012-10-17",
|
|
"Statement": []map[string]interface{}{
|
|
{"Effect": "Allow", "Action": "s3:*", "Resource": "arn:aws:s3:::backup/*"},
|
|
},
|
|
})
|
|
|
|
require.NoError(t, iam.PutPolicy("BackupAll", string(policyDoc)))
|
|
require.NoError(t, iam.DeletePolicy("BackupAll"))
|
|
|
|
allowed, err := mgr.IsActionAllowed(context.Background(), &integration.ActionRequest{
|
|
Principal: "arn:aws:iam::111122223333:user/test",
|
|
Action: "s3:PutObject",
|
|
Resource: "arn:aws:s3:::backup/file.txt",
|
|
PolicyNames: []string{"BackupAll"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, allowed, "DeletePolicy should remove from IAM Manager policy engine")
|
|
}
|
|
|
|
// TestSetIAMIntegration_FlushesLoadedPolicies reproduces the startup race: a
|
|
// policy is loaded into the IAM cache before the integration is attached (so the
|
|
// sync was skipped), then SetIAMIntegration must flush it into the manager's
|
|
// engine. Without the flush, policy_names identities get AccessDenied until an
|
|
// external IAM change triggers a reload.
|
|
func TestSetIAMIntegration_FlushesLoadedPolicies(t *testing.T) {
|
|
iam := &IdentityAccessManagement{}
|
|
|
|
policyDoc, _ := json.Marshal(map[string]interface{}{
|
|
"Version": "2012-10-17",
|
|
"Statement": []map[string]interface{}{
|
|
{"Effect": "Allow", "Action": "s3:*", "Resource": "arn:aws:s3:::backup/*"},
|
|
},
|
|
})
|
|
|
|
// Integration not attached yet, so this only lands in the legacy engine.
|
|
require.NoError(t, iam.PutPolicy("BackupAll", string(policyDoc)))
|
|
|
|
mgr := newTestIAMManager(t)
|
|
iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))
|
|
|
|
allowed, err := mgr.IsActionAllowed(context.Background(), &integration.ActionRequest{
|
|
Principal: "arn:aws:iam::111122223333:user/test",
|
|
Action: "s3:PutObject",
|
|
Resource: "arn:aws:s3:::backup/file.txt",
|
|
PolicyNames: []string{"BackupAll"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, allowed, "SetIAMIntegration should flush already-loaded policies into the IAM Manager")
|
|
}
|
|
|
|
// TestResyncIAMManager_ReflectsCurrentPolicies pins the property that prevents
|
|
// the stale-snapshot race: SyncRuntimePolicies replaces the full desired set, so
|
|
// the resync must derive that set from the live iam.policies map at apply time,
|
|
// not from a view captured earlier. Here the map is mutated out from under an
|
|
// already-synced policy, and the next resync must converge the manager onto the
|
|
// new map rather than reinstating the old state.
|
|
func TestResyncIAMManager_ReflectsCurrentPolicies(t *testing.T) {
|
|
mgr := newTestIAMManager(t)
|
|
iam := &IdentityAccessManagement{}
|
|
iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))
|
|
|
|
doc := func(name string) string {
|
|
b, _ := json.Marshal(map[string]interface{}{
|
|
"Version": "2012-10-17",
|
|
"Statement": []map[string]interface{}{
|
|
{"Effect": "Allow", "Action": "s3:*", "Resource": "arn:aws:s3:::" + name + "/*"},
|
|
},
|
|
})
|
|
return string(b)
|
|
}
|
|
allows := func(name string) bool {
|
|
allowed, err := mgr.IsActionAllowed(context.Background(), &integration.ActionRequest{
|
|
Principal: "arn:aws:iam::111122223333:user/test",
|
|
Action: "s3:PutObject",
|
|
Resource: "arn:aws:s3:::" + name + "/file.txt",
|
|
PolicyNames: []string{name},
|
|
})
|
|
require.NoError(t, err)
|
|
return allowed
|
|
}
|
|
|
|
require.NoError(t, iam.PutPolicy("alpha", doc("alpha")))
|
|
require.True(t, allows("alpha"))
|
|
|
|
// Swap the map contents without going through PutPolicy/DeletePolicy, then
|
|
// resync. A snapshot-based sync would push a pre-swap view; the fresh-read
|
|
// resync must mirror the current map: alpha gone, beta present.
|
|
iam.m.Lock()
|
|
delete(iam.policies, "alpha")
|
|
iam.policies["beta"] = &iam_pb.Policy{Name: "beta", Content: doc("beta")}
|
|
iam.m.Unlock()
|
|
iam.resyncIAMManagerPolicies()
|
|
|
|
require.False(t, allows("alpha"), "resync should drop a policy no longer in the map")
|
|
require.True(t, allows("beta"), "resync should add a policy newly in the map")
|
|
}
|