fix: apply PathPrefix, Scope and OnlyAttached in IAM ListPolicies

This commit is contained in:
Julian Sommer
2026-09-28 15:34:52 +10:00
parent 377c573bf6
commit 0fd0e2ba15
3 changed files with 111 additions and 1 deletions
+25 -1
View File
@@ -1303,7 +1303,10 @@ func (s *IAMServiceImpl) ListPolicies(accountID string, input *iam.ListPoliciesI
}
keyPrefix := accountID + "."
var policies []*iam.Policy
pathPrefix := aws.StringValue(input.PathPrefix)
scope := aws.StringValue(input.Scope)
onlyAttached := aws.BoolValue(input.OnlyAttached)
policies := []*iam.Policy{}
for _, key := range keys {
if key == utils.VersionKey {
continue
@@ -1328,6 +1331,13 @@ func (s *IAMServiceImpl) ListPolicies(accountID string, input *iam.ListPoliciesI
continue
}
if !strings.HasPrefix(policy.Path, pathPrefix) || !policyInScope(policy.ARN, scope) {
continue
}
if onlyAttached && attachCounts[policy.ARN] == 0 {
continue
}
createdAt := parseCreatedAt(policy.CreatedAt)
policies = append(policies, &iam.Policy{
PolicyName: aws.String(policy.PolicyName),
@@ -1348,6 +1358,20 @@ func (s *IAMServiceImpl) ListPolicies(accountID string, input *iam.ListPoliciesI
}, nil
}
// policyInScope applies ListPolicies' Scope: Local is customer-managed, AWS is
// arn:aws:iam::aws:policy/..., and All (or unset) keeps both.
func policyInScope(policyARN, scope string) bool {
awsManaged := isAWSManagedPolicyARN(policyARN)
switch scope {
case iam.PolicyScopeTypeLocal:
return !awsManaged
case iam.PolicyScopeTypeAws:
return awsManaged
default:
return true
}
}
func (s *IAMServiceImpl) DeletePolicy(accountID string, input *iam.DeletePolicyInput) (*iam.DeletePolicyOutput, error) {
ctx := context.Background()
policyARN := *input.PolicyArn
@@ -1479,6 +1479,73 @@ func TestListPolicies_Empty(t *testing.T) {
assert.Empty(t, out.Policies)
}
func TestListPolicies_Filters(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)
createTestPolicy(t, svc, "RootPolicy")
_, err := svc.CreatePolicy(testAccountID, &iam.CreatePolicyInput{
PolicyName: aws.String("TeamPolicy"),
Path: aws.String("/team/"),
PolicyDocument: aws.String(validPolicyDocument()),
})
require.NoError(t, err)
attached := createTestPolicy(t, svc, "AttachedPolicy")
createTestUser(t, svc, "filterlistuser")
_, err = svc.AttachUserPolicy(testAccountID, &iam.AttachUserPolicyInput{
UserName: aws.String("filterlistuser"),
PolicyArn: attached.Arn,
})
require.NoError(t, err)
tests := []struct {
name string
input *iam.ListPoliciesInput
want []string
}{
{"no filters", &iam.ListPoliciesInput{}, []string{"AttachedPolicy", "RootPolicy", "TeamPolicy"}},
{"path prefix", &iam.ListPoliciesInput{PathPrefix: aws.String("/team/")}, []string{"TeamPolicy"}},
{"partial path prefix", &iam.ListPoliciesInput{PathPrefix: aws.String("/te")}, []string{"TeamPolicy"}},
{"root path prefix", &iam.ListPoliciesInput{PathPrefix: aws.String("/")}, []string{"AttachedPolicy", "RootPolicy", "TeamPolicy"}},
{"scope local", &iam.ListPoliciesInput{Scope: aws.String("Local")}, []string{"AttachedPolicy", "RootPolicy", "TeamPolicy"}},
{"scope all", &iam.ListPoliciesInput{Scope: aws.String("All")}, []string{"AttachedPolicy", "RootPolicy", "TeamPolicy"}},
{"scope aws", &iam.ListPoliciesInput{Scope: aws.String("AWS")}, []string{}},
{"only attached", &iam.ListPoliciesInput{OnlyAttached: aws.Bool(true)}, []string{"AttachedPolicy"}},
{"only attached false", &iam.ListPoliciesInput{OnlyAttached: aws.Bool(false)}, []string{"AttachedPolicy", "RootPolicy", "TeamPolicy"}},
{"only attached under path", &iam.ListPoliciesInput{PathPrefix: aws.String("/team/"), OnlyAttached: aws.Bool(true)}, []string{}},
{"local under path", &iam.ListPoliciesInput{Scope: aws.String("Local"), PathPrefix: aws.String("/team/")}, []string{"TeamPolicy"}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
out, err := svc.ListPolicies(testAccountID, tc.input)
require.NoError(t, err)
// A nil slice would drop the Policies element from the XML response.
require.NotNil(t, out.Policies)
names := []string{}
for _, p := range out.Policies {
names = append(names, *p.PolicyName)
}
assert.ElementsMatch(t, tc.want, names)
})
}
}
func TestPolicyInScope(t *testing.T) {
t.Parallel()
local := "arn:aws:iam::000000000000:policy/AdministratorAccess"
awsManaged := "arn:aws:iam::aws:policy/AdministratorAccess"
assert.True(t, policyInScope(local, ""))
assert.True(t, policyInScope(awsManaged, ""))
assert.True(t, policyInScope(local, "All"))
assert.True(t, policyInScope(awsManaged, "All"))
assert.True(t, policyInScope(local, "Local"))
assert.False(t, policyInScope(awsManaged, "Local"))
assert.False(t, policyInScope(local, "AWS"))
assert.True(t, policyInScope(awsManaged, "AWS"))
}
func TestDeletePolicy(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)
+19
View File
@@ -370,6 +370,25 @@ func TestIAMPolicyCRUD(t *testing.T) {
all, err := iamCli.ListPolicies(&iam.ListPoliciesInput{})
require.NoError(t, err, "list-policies")
require.GreaterOrEqual(t, len(all.Policies), 5, "expected >=5 policies, got %d", len(all.Policies))
pathed, err := iamCli.ListPolicies(&iam.ListPoliciesInput{
Scope: aws.String("Local"),
PathPrefix: aws.String(iamPolicyFullAdminPath),
})
require.NoError(t, err, "list-policies scope Local path %s", iamPolicyFullAdminPath)
require.Len(t, pathed.Policies, 1)
require.Equal(t, iamPolicyFullAdmin, aws.StringValue(pathed.Policies[0].PolicyName))
attached, err := iamCli.ListPolicies(&iam.ListPoliciesInput{
PathPrefix: aws.String(iamPolicyFullAdminPath),
OnlyAttached: aws.Bool(true),
})
require.NoError(t, err, "list-policies only attached")
require.Empty(t, attached.Policies)
awsScope, err := iamCli.ListPolicies(&iam.ListPoliciesInput{Scope: aws.String("AWS")})
require.NoError(t, err, "list-policies scope AWS")
require.Empty(t, awsScope.Policies)
}
// TestIAMPolicyAttachmentEnforcement proves AttachUserPolicy idempotency,