Files
seaweedfs/weed/s3api/auth_credentials_policy_sync_test.go
T
Chris LuandGitHub 0002e5cc7f s3api: load document-style policies from the advanced IAM config (#10481)
* s3api: load document-style policies from the advanced IAM config

The advanced IAM file doubles as the S3 identity config when only
-s3.iam.config is given. protojson drops its "document" field, so every
policy landed with empty content and warned "skipping invalid policy" on
each reload. Worse, if the same file also declares identities the empty
content sticks in the policy map and fails the whole runtime policy sync
into the IAM manager, so policies created later never reach it.

* iam: skip an unparsable policy instead of failing the whole runtime sync

One policy the engine cannot parse aborted SyncRuntimePolicies before it
touched anything, so every other policy stayed unsynced and the engine
kept serving whatever it last held.

* s3api: reject a non-role RoleArn in AssumeRole as a bad request

arn:aws:iam:::user/name can never resolve to a role, but the handler ran
it through the trust-policy check and answered "not authorized to assume
role", pointing the caller at a permission problem they do not have.

* s3api: build the policy content before touching the entry

Deleting "document" up front meant a marshal failure left the policy with
neither field, so a later rewrite would emit it with no definition at all.

* iam: pin the fail-closed handling of an unparsable policy

Say in the comment that dropping it from the desired set deletes it from
the engine on purpose, and cover it with a test.

* s3api: widen the non-role RoleArn test to canonical ARN shapes

The reported ARN omits the account id; a user ARN that carries one, and a
non-principal ARN, must be rejected the same way.
2026-07-29 10:30:30 -07:00

224 lines
7.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")
}
func TestResync_SkipsUnparsablePolicy(t *testing.T) {
mgr := newTestIAMManager(t)
iam := &IdentityAccessManagement{}
iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))
doc, _ := json.Marshal(map[string]interface{}{
"Version": "2012-10-17",
"Statement": []map[string]interface{}{
{"Effect": "Allow", "Action": "s3:*", "Resource": "arn:aws:s3:::good/*"},
},
})
iam.m.Lock()
iam.policies = map[string]*iam_pb.Policy{
"broken": {Name: "broken"},
"good": {Name: "good", Content: string(doc)},
}
iam.m.Unlock()
iam.resyncIAMManagerPolicies()
allowed, err := mgr.IsActionAllowed(context.Background(), &integration.ActionRequest{
Principal: "arn:aws:iam::111122223333:user/test",
Action: "s3:PutObject",
Resource: "arn:aws:s3:::good/file.txt",
PolicyNames: []string{"good"},
})
require.NoError(t, err)
require.True(t, allowed, "an unparsable policy must not block the rest of the sync")
}
// A policy whose stored definition stops parsing must stop granting access:
// enforcing a document the operator can no longer see is worse than denying.
func TestResync_UnparsablePolicyStopsGranting(t *testing.T) {
mgr := newTestIAMManager(t)
iam := &IdentityAccessManagement{}
iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))
doc, _ := json.Marshal(map[string]interface{}{
"Version": "2012-10-17",
"Statement": []map[string]interface{}{
{"Effect": "Allow", "Action": "s3:*", "Resource": "arn:aws:s3:::good/*"},
},
})
allows := func() bool {
allowed, err := mgr.IsActionAllowed(context.Background(), &integration.ActionRequest{
Principal: "arn:aws:iam::111122223333:user/test",
Action: "s3:PutObject",
Resource: "arn:aws:s3:::good/file.txt",
PolicyNames: []string{"good"},
})
require.NoError(t, err)
return allowed
}
require.NoError(t, iam.PutPolicy("good", string(doc)))
require.True(t, allows())
iam.m.Lock()
iam.policies["good"] = &iam_pb.Policy{Name: "good", Content: "{not json"}
iam.m.Unlock()
iam.resyncIAMManagerPolicies()
require.False(t, allows(), "an unparsable policy must not keep granting its old permissions")
}