mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-31 09:20:50 +01:00
Generated with [Devin](https://devin.ai) Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
541 lines
20 KiB
Go
541 lines
20 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
|
)
|
|
|
|
func TestMatchesActionPattern(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pattern string
|
|
action string
|
|
expected bool
|
|
}{
|
|
// Exact matches
|
|
{"exact match", "GetTable", "GetTable", true},
|
|
{"no match", "GetTable", "DeleteTable", false},
|
|
|
|
// Universal wildcard
|
|
{"universal wildcard", "*", "anything", true},
|
|
|
|
// Suffix wildcards
|
|
{"suffix wildcard match", "s3tables:*", "s3tables:GetTable", true},
|
|
{"suffix wildcard no match", "s3tables:*", "iam:GetUser", false},
|
|
|
|
// Middle wildcards (new capability from policy_engine)
|
|
{"middle wildcard Get*Table", "s3tables:Get*Table", "s3tables:GetTable", true},
|
|
{"middle wildcard Get*Table no match GetTableBucket", "s3tables:Get*Table", "s3tables:GetTableBucket", false},
|
|
{"middle wildcard Get*Table no match DeleteTable", "s3tables:Get*Table", "s3tables:DeleteTable", false},
|
|
{"middle wildcard *Table*", "s3tables:*Table*", "s3tables:GetTableBucket", true},
|
|
{"middle wildcard *Table* match CreateTable", "s3tables:*Table*", "s3tables:CreateTable", true},
|
|
|
|
// Question mark wildcards
|
|
{"question mark single char", "GetTable?", "GetTableX", true},
|
|
{"question mark no match", "GetTable?", "GetTableXY", false},
|
|
|
|
// Combined wildcards
|
|
{"combined * and ? singular", "s3tables:Get?able*", "s3tables:GetTable", true},
|
|
{"combined * and ? plural", "s3tables:Get?able*", "s3tables:GetTables", true},
|
|
{"combined no match - ? needs 1 char", "s3tables:Get?able*", "s3tables:Getable", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := matchesActionPattern(tt.pattern, tt.action)
|
|
if result != tt.expected {
|
|
t.Errorf("matchesActionPattern(%q, %q) = %v, want %v", tt.pattern, tt.action, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMatchesPrincipal(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
principalSpec interface{}
|
|
principal string
|
|
expected bool
|
|
}{
|
|
// String principals
|
|
{"exact match", "user123", "user123", true},
|
|
{"no match", "user123", "user456", false},
|
|
{"universal wildcard", "*", "anyone", true},
|
|
|
|
// Wildcard principals
|
|
{"prefix wildcard", "arn:aws:iam::123456789012:user/*", "arn:aws:iam::123456789012:user/admin", true},
|
|
{"prefix wildcard no match", "arn:aws:iam::123456789012:user/*", "arn:aws:iam::987654321098:user/admin", false},
|
|
{"middle wildcard", "arn:aws:iam::*:user/admin", "arn:aws:iam::123456789012:user/admin", true},
|
|
|
|
// Array of principals
|
|
{"array match first", []interface{}{"user1", "user2"}, "user1", true},
|
|
{"array match second", []interface{}{"user1", "user2"}, "user2", true},
|
|
{"array no match", []interface{}{"user1", "user2"}, "user3", false},
|
|
{"array wildcard", []interface{}{"user1", "arn:aws:iam::*:user/admin"}, "arn:aws:iam::123:user/admin", true},
|
|
|
|
// Map-style AWS principals
|
|
{"AWS map exact", map[string]interface{}{"AWS": "user123"}, "user123", true},
|
|
{"AWS map wildcard", map[string]interface{}{"AWS": "arn:aws:iam::*:user/admin"}, "arn:aws:iam::123:user/admin", true},
|
|
{"AWS map array", map[string]interface{}{"AWS": []interface{}{"user1", "user2"}}, "user1", true},
|
|
|
|
// ARN statements do not reach normalized-name callers on the allow
|
|
// path: normalization drops the ARN's account and path, so the grant
|
|
// could land on a same-named caller the statement never verified.
|
|
{"arn spec exact caller", "arn:aws:iam::123456789012:user/bob", "bob", false},
|
|
{"arn spec wildcard caller", "arn:aws:iam::*:user/alice", "alice", false},
|
|
{"arn spec no match", "arn:aws:iam::123456789012:user/carol", "dan", false},
|
|
|
|
// Nil/empty cases
|
|
{"nil principal", nil, "user123", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := matchesPrincipal(tt.principalSpec, tt.principal)
|
|
if result != tt.expected {
|
|
t.Errorf("matchesPrincipal(%v, %q) = %v, want %v", tt.principalSpec, tt.principal, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvaluatePolicyWithConditions(t *testing.T) {
|
|
policy := &PolicyDocument{
|
|
Statement: []Statement{
|
|
{
|
|
Effect: "Allow",
|
|
Principal: "*",
|
|
Action: "s3tables:GetTable",
|
|
Condition: map[string]map[string]interface{}{
|
|
"StringEquals": {
|
|
"s3tables:namespace": "default",
|
|
},
|
|
"StringLike": {
|
|
"s3tables:tableName": "test_*",
|
|
},
|
|
"NumericGreaterThan": {
|
|
"aws:RequestTag/priority": "10",
|
|
},
|
|
"Bool": {
|
|
"aws:ResourceTag/is_public": "true",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
policyBytes, _ := json.Marshal(policy)
|
|
policyStr := string(policyBytes)
|
|
|
|
tests := []struct {
|
|
name string
|
|
ctx *PolicyContext
|
|
expected bool
|
|
}{
|
|
{
|
|
"all conditions match",
|
|
&PolicyContext{
|
|
Namespace: "default",
|
|
TableName: "test_table",
|
|
RequestTags: map[string]string{
|
|
"priority": "15",
|
|
},
|
|
ResourceTags: map[string]string{
|
|
"is_public": "true",
|
|
},
|
|
},
|
|
true,
|
|
},
|
|
{
|
|
"namespace mismatch",
|
|
&PolicyContext{
|
|
Namespace: "other",
|
|
TableName: "test_table",
|
|
RequestTags: map[string]string{
|
|
"priority": "15",
|
|
},
|
|
ResourceTags: map[string]string{
|
|
"is_public": "true",
|
|
},
|
|
},
|
|
false,
|
|
},
|
|
{
|
|
"table name mismatch",
|
|
&PolicyContext{
|
|
Namespace: "default",
|
|
TableName: "other_table",
|
|
RequestTags: map[string]string{
|
|
"priority": "15",
|
|
},
|
|
ResourceTags: map[string]string{
|
|
"is_public": "true",
|
|
},
|
|
},
|
|
false,
|
|
},
|
|
{
|
|
"numeric condition failure",
|
|
&PolicyContext{
|
|
Namespace: "default",
|
|
TableName: "test_table",
|
|
RequestTags: map[string]string{
|
|
"priority": "5",
|
|
},
|
|
ResourceTags: map[string]string{
|
|
"is_public": "true",
|
|
},
|
|
},
|
|
false,
|
|
},
|
|
{
|
|
"bool condition failure",
|
|
&PolicyContext{
|
|
Namespace: "default",
|
|
TableName: "test_table",
|
|
RequestTags: map[string]string{
|
|
"priority": "15",
|
|
},
|
|
ResourceTags: map[string]string{
|
|
"is_public": "false",
|
|
},
|
|
},
|
|
false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// principal="user123", owner="owner123"
|
|
result := CheckPermissionWithContext("s3tables:GetTable", "user123", "owner123", policyStr, "", tt.ctx)
|
|
if result != tt.expected {
|
|
t.Errorf("CheckPermissionWithContext() = %v, want %v", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestCheckPermissionWithDefaultAllow(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
defaultAllow bool
|
|
policy string
|
|
expected bool
|
|
}{
|
|
{
|
|
"default deny (no policy, DefaultAllow=false)",
|
|
false,
|
|
"",
|
|
false,
|
|
},
|
|
{
|
|
"default allow (no policy, DefaultAllow=true)",
|
|
true,
|
|
"",
|
|
true,
|
|
},
|
|
{
|
|
"explicit deny overrides DefaultAllow=true",
|
|
true,
|
|
`{"Statement":[{"Effect":"Deny","Principal":"*","Action":"s3tables:GetTable"}]}`,
|
|
false,
|
|
},
|
|
{
|
|
"explicit allow works with DefaultAllow=false",
|
|
false,
|
|
`{"Statement":[{"Effect":"Allow","Principal":"*","Action":"s3tables:GetTable"}]}`,
|
|
true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := &PolicyContext{
|
|
DefaultAllow: tt.defaultAllow,
|
|
}
|
|
result := CheckPermissionWithContext("s3tables:GetTable", "user123", "owner123", tt.policy, "", ctx)
|
|
if result != tt.expected {
|
|
t.Errorf("CheckPermissionWithContext() = %v, want %v (DefaultAllow=%v, Policy=%s)", result, tt.expected, tt.defaultAllow, tt.policy)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckTableOrBucketPermissionDenyVeto(t *testing.T) {
|
|
tablePolicy := `{
|
|
"Version": "2012-10-17",
|
|
"Statement": [{
|
|
"Effect": "Deny",
|
|
"Principal": "*",
|
|
"Action": "s3tables:DeleteTable",
|
|
"Resource": "*"
|
|
}]
|
|
}`
|
|
bucketAllow := `{
|
|
"Version": "2012-10-17",
|
|
"Statement": [{
|
|
"Effect": "Allow",
|
|
"Principal": "*",
|
|
"Action": "s3tables:*",
|
|
"Resource": "*"
|
|
}]
|
|
}`
|
|
tableARN := "arn:aws:s3tables:us-east-1:acct:bucket/b/table/ns/t"
|
|
bucketARN := "arn:aws:s3tables:us-east-1:acct:bucket/b"
|
|
ctx := &PolicyContext{TableBucketName: "b", Namespace: "ns", TableName: "t"}
|
|
|
|
// A Deny on the table policy vetoes the bucket Allow.
|
|
if CheckTableOrBucketPermission("DeleteTable", "user123", "owner", tablePolicy, tableARN, "owner", bucketAllow, bucketARN, ctx) {
|
|
t.Error("table Deny should veto bucket Allow")
|
|
}
|
|
|
|
// The same Deny in the bucket policy vetoes a table Allow.
|
|
if CheckTableOrBucketPermission("DeleteTable", "user123", "owner", bucketAllow, tableARN, "owner", tablePolicy, bucketARN, ctx) {
|
|
t.Error("bucket Deny should veto table Allow")
|
|
}
|
|
|
|
// A table Deny also vetoes ownership of the table.
|
|
if CheckTableOrBucketPermission("DeleteTable", "owner", "owner", tablePolicy, tableARN, "owner", "", bucketARN, ctx) {
|
|
t.Error("table Deny should veto the owner grant")
|
|
}
|
|
|
|
// No Deny anywhere: either policy may allow.
|
|
if !CheckTableOrBucketPermission("DeleteTable", "user123", "other-owner", "", tableARN, "owner", bucketAllow, bucketARN, ctx) {
|
|
t.Error("bucket Allow should grant when no policy denies")
|
|
}
|
|
}
|
|
|
|
// A principal that merely normalizes to the admin id must not bypass policy
|
|
// denies: when the caller's actions are known, the admin bypass requires an
|
|
// admin action; transports carrying no action list keep it.
|
|
func TestCheckPermissionAdminRequiresAdminAction(t *testing.T) {
|
|
denyAll := `{"Statement":[{"Effect":"Deny","Principal":"*","Action":"s3tables:DeleteTable","Resource":"*"}]}`
|
|
arn := "arn:aws:s3tables:us-east-1:acct:bucket/b/table/ns/t"
|
|
|
|
reader := &PolicyContext{IdentityActions: []string{"Read"}}
|
|
if CheckPermissionWithContext("s3tables:DeleteTable", "admin", "admin", denyAll, arn, reader) {
|
|
t.Error("a name-only admin must not bypass an explicit deny")
|
|
}
|
|
admin := &PolicyContext{IdentityActions: []string{"Admin"}}
|
|
if !CheckPermissionWithContext("s3tables:DeleteTable", "admin", "admin", denyAll, arn, admin) {
|
|
t.Error("an identity holding the admin action keeps the bypass")
|
|
}
|
|
if !CheckPermissionWithContext("s3tables:DeleteTable", "admin", "admin", denyAll, arn, nil) {
|
|
t.Error("a caller with no action list keeps the trusted bypass")
|
|
}
|
|
empty := &PolicyContext{IdentityActions: []string{}}
|
|
if CheckPermissionWithContext("s3tables:DeleteTable", "admin", "admin", denyAll, arn, empty) {
|
|
t.Error("an identified admin with an empty action list must not bypass a deny")
|
|
}
|
|
}
|
|
|
|
func TestCheckTableOrBucketPermissionOwnerlessFailsClosed(t *testing.T) {
|
|
ctx := &PolicyContext{
|
|
IdentityActions: []string{"s3tables:*"},
|
|
DefaultAllow: true,
|
|
}
|
|
allowPolicy := `{"Statement":[{"Effect":"Allow","Principal":"*","Action":"s3tables:*","Resource":"*"}]}`
|
|
|
|
if CheckTableOrBucketPermission("GetTable", "user1",
|
|
"", "", "arn:aws:s3tables:t",
|
|
"", "", "arn:aws:s3tables:b", ctx) {
|
|
t.Error("identity actions and default allow must not reach an ownerless resource")
|
|
}
|
|
|
|
// With no identity or default grants in play, an ownerless resource
|
|
// side contributes nothing even when its own policy allows.
|
|
plainCtx := &PolicyContext{}
|
|
if CheckTableOrBucketPermission("GetTable", "user1",
|
|
"", allowPolicy, "arn:aws:s3tables:t",
|
|
"owner", "", "arn:aws:s3tables:b", plainCtx) {
|
|
t.Error("an ownerless resource side must not grant even via an explicit allow")
|
|
}
|
|
|
|
if !CheckTableOrBucketPermission("GetTable", "user1",
|
|
"", "", "arn:aws:s3tables:t",
|
|
"owner", allowPolicy, "arn:aws:s3tables:b", plainCtx) {
|
|
t.Error("the bucket side can still grant when its owner is set")
|
|
}
|
|
}
|
|
|
|
// Owners written before issuer scoping hold a bare subject. Re-anchoring it
|
|
// is only unambiguous when exactly one OIDC issuer is configured; under two
|
|
// or more issuers a bare subject could belong to either, so it must not
|
|
// match anyone.
|
|
func TestCheckPermissionLegacyOwnerScopedToIssuer(t *testing.T) {
|
|
owner := "alice-sub"
|
|
principal := sts.ComputeParentUser(owner, "corp-idp")
|
|
noLocal := &PolicyContext{
|
|
TrustedIssuers: []string{"corp-idp"},
|
|
IsLocalAccountID: func(string) bool { return false },
|
|
}
|
|
|
|
if !CheckPermissionWithContext("GetTable", principal, owner, "", "arn", noLocal) {
|
|
t.Error("single-issuer deployments must keep recognizing the legacy owner")
|
|
}
|
|
|
|
if CheckPermissionWithContext("GetTable", principal, owner, "", "arn", &PolicyContext{
|
|
TrustedIssuers: []string{"corp-idp", "evil-idp"},
|
|
IsLocalAccountID: func(string) bool { return false },
|
|
}) {
|
|
t.Error("a bare subject is ambiguous across multiple issuers and must not match")
|
|
}
|
|
|
|
if CheckPermissionWithContext("GetTable", sts.ComputeParentUser(owner, "evil-idp"), owner, "", "arn", &PolicyContext{
|
|
TrustedIssuers: []string{"corp-idp", "evil-idp"},
|
|
IsLocalAccountID: func(string) bool { return false },
|
|
}) {
|
|
t.Error("a same-sub caller from another issuer must not claim the legacy owner")
|
|
}
|
|
|
|
if CheckPermissionWithContext("GetTable", principal, owner, "", "arn", &PolicyContext{
|
|
IsLocalAccountID: func(string) bool { return false },
|
|
}) {
|
|
t.Error("legacy owner matching must not engage without a trusted issuer")
|
|
}
|
|
|
|
// With no local-account predicate only the unconditional exclusions apply;
|
|
// a plain legacy subject still re-anchors.
|
|
if !CheckPermissionWithContext("GetTable", principal, owner, "", "arn", &PolicyContext{
|
|
TrustedIssuers: []string{"corp-idp"},
|
|
}) {
|
|
t.Error("a missing local-account check keeps plain legacy subjects working")
|
|
}
|
|
}
|
|
|
|
// A stored owner that names a reserved or configured local account was never
|
|
// an OIDC subject, and an already issuer-scoped id is not a legacy subject
|
|
// either; re-anchoring them would let a caller pick sub=<stored owner> and
|
|
// claim the resource.
|
|
func TestCheckPermissionLegacyOwnerExcludesLocalAndScoped(t *testing.T) {
|
|
ctx := &PolicyContext{
|
|
TrustedIssuers: []string{"corp-idp"},
|
|
IsLocalAccountID: func(id string) bool { return id == "admin" || id == "owner2" },
|
|
}
|
|
|
|
for _, owner := range []string{"admin", "owner2"} {
|
|
principal := sts.ComputeParentUser(owner, "corp-idp")
|
|
if CheckPermissionWithContext("GetTable", principal, owner, "", "arn", ctx) {
|
|
t.Errorf("sub=%q must not re-anchor onto the local account owner", owner)
|
|
}
|
|
}
|
|
|
|
scoped := sts.ComputeParentUser("victim-sub", "corp-idp")
|
|
if CheckPermissionWithContext("GetTable", sts.ComputeParentUser(scoped, "corp-idp"), scoped, "", "arn", ctx) {
|
|
t.Error("sub equal to a stored scoped id must not re-anchor")
|
|
}
|
|
|
|
// An ARN-shaped subject claims nothing; ARNs name IAM principals, not OIDC
|
|
// subjects, and this shape is what let a forged sub reach the admin account.
|
|
arn := "arn:aws:iam::123456789012:user/admin"
|
|
if CheckPermissionWithContext("GetTable", sts.ComputeParentUser(arn, "corp-idp"), arn, "", "arn", ctx) {
|
|
t.Error("an ARN-shaped stored owner must not re-anchor")
|
|
}
|
|
}
|
|
|
|
// Legacy policies can carry the bare subject in the AWS map or array forms,
|
|
// not only as a top-level string; re-anchoring must reach all of them or a
|
|
// pre-scoping Deny silently stops matching the caller.
|
|
func TestCheckPermissionLegacyPolicyPrincipalShapes(t *testing.T) {
|
|
ctx := &PolicyContext{
|
|
TrustedIssuers: []string{"corp-idp"},
|
|
IsLocalAccountID: func(string) bool { return false },
|
|
}
|
|
principal := sts.ComputeParentUser("alice-sub", "corp-idp")
|
|
other := "other-owner"
|
|
|
|
// The caller owns the resource, so only a matched Deny can veto it.
|
|
for _, policy := range []string{
|
|
`{"Statement":[{"Effect":"Deny","Principal":{"AWS":"alice-sub"},"Action":"s3tables:GetTable"}]}`,
|
|
`{"Statement":[{"Effect":"Deny","Principal":["alice-sub"],"Action":"s3tables:GetTable"}]}`,
|
|
} {
|
|
if CheckPermissionWithContext("GetTable", principal, principal, policy, "arn", ctx) {
|
|
t.Errorf("legacy Deny must veto the scoped caller's owner grant: %s", policy)
|
|
}
|
|
}
|
|
|
|
allow := `{"Statement":[{"Effect":"Allow","Principal":["alice-sub"],"Action":"s3tables:GetTable"}]}`
|
|
if !CheckPermissionWithContext("GetTable", principal, other, allow, "arn", ctx) {
|
|
t.Error("legacy Allow with array principal must still match the scoped caller")
|
|
}
|
|
}
|
|
|
|
func TestValidatePolicyDocument(t *testing.T) {
|
|
valid := `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":"s3tables:GetTable","Resource":"*"}]}`
|
|
if err := validatePolicyDocument(valid); err != nil {
|
|
t.Fatalf("valid policy rejected: %v", err)
|
|
}
|
|
if err := validatePolicyDocument("not-json"); err == nil {
|
|
t.Fatal("malformed policy should be rejected")
|
|
}
|
|
dead := `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":"s3vectors:PutVectors","Resource":"*"}]}`
|
|
if err := validatePolicyDocument(dead); err == nil {
|
|
t.Fatal("s3vectors action should be rejected")
|
|
}
|
|
deadList := `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":["s3tables:*","s3vectors:*"],"Resource":"*"}]}`
|
|
if err := validatePolicyDocument(deadList); err == nil {
|
|
t.Fatal("action list containing s3vectors should be rejected")
|
|
}
|
|
deadResource := `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":"s3tables:*","Resource":"arn:aws:s3vectors:r:a:bucket/b"}]}`
|
|
if err := validatePolicyDocument(deadResource); err == nil {
|
|
t.Fatal("s3vectors resource should be rejected")
|
|
}
|
|
deadPartition := `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":"s3tables:*","Resource":"arn:aws-cn:s3vectors:cn-1:a:bucket/b"}]}`
|
|
if err := validatePolicyDocument(deadPartition); err == nil {
|
|
t.Fatal("s3vectors resource in another partition should be rejected")
|
|
}
|
|
deadWildcard := `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":"s3tables:*","Resource":"arn:*:s3vectors:r:a:bucket/b"}]}`
|
|
if err := validatePolicyDocument(deadWildcard); err == nil {
|
|
t.Fatal("s3vectors resource in a wildcard partition should be rejected")
|
|
}
|
|
deadMixed := `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":"s3tables:*","Resource":["arn:aws:s3tables:r:a:bucket/b","arn:aws:s3vectors:r:a:bucket/b"]}]}`
|
|
if err := validatePolicyDocument(deadMixed); err == nil {
|
|
t.Fatal("resource list containing s3vectors should be rejected")
|
|
}
|
|
}
|
|
|
|
// An ARN-scoped statement reaches a caller recorded under the ARN's trailing
|
|
// name only for denies: normalization drops the account and path, so an allow
|
|
// matched that way could grant a same-named caller the statement never
|
|
// verified, while a deny that missed would let the caller duck the rule.
|
|
func TestArnScopedPrincipalMatchesOnlyForDeny(t *testing.T) {
|
|
arn := "arn:aws:s3vectors:us-east-1:acct:bucket/b"
|
|
denyPolicy := `{"Statement":[{"Effect":"Deny","Principal":"arn:aws:iam::111:user/bob","Action":"s3vectors:DeleteVectors","Resource":"*"}]}`
|
|
allowPolicy := `{"Statement":[{"Effect":"Allow","Principal":"arn:aws:iam::111:user/bob","Action":"s3vectors:DeleteVectors","Resource":"*"}]}`
|
|
|
|
if CheckPermissionWithContext("s3vectors:DeleteVectors", "bob", "owner", denyPolicy, arn, &PolicyContext{}) {
|
|
t.Error("an ARN-scoped deny must still reach the normalized caller")
|
|
}
|
|
if CheckPermissionWithContext("s3vectors:DeleteVectors", "bob", "owner", allowPolicy, arn, &PolicyContext{}) {
|
|
t.Error("an ARN-scoped allow must not grant on the normalized name alone")
|
|
}
|
|
}
|
|
|
|
// A service wildcard administers only its own service: s3tables:* satisfies
|
|
// the admin and identity grants for table operations but must not reach an
|
|
// s3vectors operation.
|
|
func TestServiceWildcardScopesToOwnService(t *testing.T) {
|
|
vectorOp := "s3vectors:DeleteVectors"
|
|
vectorArn := "arn:aws:s3vectors:us-east-1:acct:bucket/b"
|
|
tableOnly := &PolicyContext{IdentityActions: []string{"s3tables:*"}}
|
|
|
|
if CheckPermissionWithContext(vectorOp, "bob", "owner", "", vectorArn, tableOnly) {
|
|
t.Error("s3tables:* must not grant a vector operation")
|
|
}
|
|
if CheckPermissionWithContext(vectorOp, "admin", "owner", "", vectorArn, tableOnly) {
|
|
t.Error("s3tables:* must not fire the admin bypass for a vector operation")
|
|
}
|
|
if !CheckPermissionWithContext("GetTable", "bob", "owner", "", "arn:aws:s3tables:b", tableOnly) {
|
|
t.Error("s3tables:* keeps administering table operations")
|
|
}
|
|
|
|
vectorAdmin := &PolicyContext{IdentityActions: []string{"s3vectors:*"}}
|
|
if !CheckPermissionWithContext(vectorOp, "bob", "owner", "", vectorArn, vectorAdmin) {
|
|
t.Error("s3vectors:* administers vector operations")
|
|
}
|
|
global := &PolicyContext{IdentityActions: []string{"Admin"}}
|
|
if !CheckPermissionWithContext(vectorOp, "admin", "owner", "", vectorArn, global) {
|
|
t.Error("the global admin action keeps the bypass on vector operations")
|
|
}
|
|
}
|