feat(iam): enforce per-type policy size quotas and accept Version 2008-10-17 (#1150)

This commit is contained in:
Julian Sommer
2026-10-01 14:19:16 +10:00
committed by GitHub
parent 10247a958c
commit 6a873d3c3c
11 changed files with 355 additions and 98 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ require (
github.com/klauspost/cpuid/v2 v2.4.0
github.com/miekg/dns v1.1.73
github.com/minio/highwayhash v1.0.4
github.com/mulgadc/bluebottle v1.21.1-0.20261001033338-1f3267ed0373
github.com/mulgadc/bluebottle v1.21.1-0.20261001041718-8a7bd5d4f2e3
github.com/mulgadc/northstar v1.21.0
github.com/mulgadc/predastore v1.21.0
github.com/mulgadc/viperblock v1.21.0
+2 -2
View File
@@ -312,8 +312,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/mulgadc/bluebottle v1.21.1-0.20261001033338-1f3267ed0373 h1:QootF30+HJfaZ1ZAwBsgGZYb8R/XbA8366WdvHMgeZM=
github.com/mulgadc/bluebottle v1.21.1-0.20261001033338-1f3267ed0373/go.mod h1:LH0MuQcFxet0dDEISlZsuULIYj592eCN2Fv39A6EH5o=
github.com/mulgadc/bluebottle v1.21.1-0.20261001041718-8a7bd5d4f2e3 h1:ICj61S//Rs6Unc+H03kQZ3/kHyEE8CPLljPjLSFjcgc=
github.com/mulgadc/bluebottle v1.21.1-0.20261001041718-8a7bd5d4f2e3/go.mod h1:LH0MuQcFxet0dDEISlZsuULIYj592eCN2Fv39A6EH5o=
github.com/mulgadc/northstar v1.21.0 h1:LE/5kiT3kBelCg02cXa3b7awhaYkGHsOXeJ4nHRLNaQ=
github.com/mulgadc/northstar v1.21.0/go.mod h1:O446cApNMB6qebqieBhDZfCIGVWX++v3QdOz+wIGhUk=
github.com/mulgadc/predastore v1.21.0 h1:pAm4fFWf0vnlwRgnoL2oJ+KSCw8mtjT2rQoBHFmnjzk=
+6
View File
@@ -404,6 +404,9 @@ func (s *IAMServiceImpl) PutGroupPolicy(accountID string, input *iam.PutGroupPol
if err := validateIAMName("policyName", policyName, 128); err != nil {
return nil, err
}
if err := checkPolicyDocumentLength(policyDoc); err != nil {
return nil, err
}
if _, err := ValidatePolicyDocument(policyDoc); err != nil {
return nil, awserrors.Errorf(awserrors.ErrorIAMMalformedPolicyDocument,
"policy %q on group %q: %w", policyName, groupName, err)
@@ -413,6 +416,9 @@ func (s *IAMServiceImpl) PutGroupPolicy(accountID string, input *iam.PutGroupPol
if err != nil {
return nil, err
}
if err := checkInlinePolicySize("group", groupName, groupPolicySizeQuota, group.InlinePolicies, policyName, policyDoc); err != nil {
return nil, err
}
if group.InlinePolicies == nil {
group.InlinePolicies = map[string]string{}
-15
View File
@@ -790,21 +790,6 @@ func TestPutGroupPolicy_MalformedDocument(t *testing.T) {
assert.Contains(t, err.Error(), awserrors.ErrorIAMMalformedPolicyDocument)
}
func TestPutGroupPolicy_OversizedDocument(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)
createTestGroup(t, svc, "oversized-group")
huge := strings.Repeat("a", maxPolicyDocumentSize+1)
_, err := svc.PutGroupPolicy(testAccountID, &iam.PutGroupPolicyInput{
GroupName: aws.String("oversized-group"),
PolicyName: aws.String("Huge"),
PolicyDocument: aws.String(huge),
})
assert.Error(t, err)
assert.Contains(t, err.Error(), awserrors.ErrorIAMMalformedPolicyDocument)
}
func TestPutGroupPolicy_GroupNotFound(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)
+228
View File
@@ -0,0 +1,228 @@
package handlers_iam
import (
"fmt"
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/mulgadc/spinifex/spinifex/awserrors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// sizedPolicy returns a valid identity policy whose size, as AWS counts it, is
// exactly size characters, padded through its resource ARN.
func sizedPolicy(t *testing.T, size int) string {
t.Helper()
const format = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::%s"}]}`
base := policySize(fmt.Sprintf(format, ""))
require.GreaterOrEqual(t, size, base)
doc := fmt.Sprintf(format, strings.Repeat("a", size-base))
require.Equal(t, size, policySize(doc))
return doc
}
func TestPolicySize_IgnoresWhiteSpace(t *testing.T) {
t.Parallel()
compact := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}`
pretty := "{\n \"Version\": \"2012-10-17\",\n\t\"Statement\": [ {\"Effect\": \"Allow\", \"Action\": \"s3:*\", \"Resource\": \"*\"} ]\r\n}"
assert.Equal(t, len(compact), policySize(compact))
assert.Equal(t, policySize(compact), policySize(pretty))
assert.Equal(t, 3, policySize("aéb"), "size counts characters, not bytes")
}
// inlinePutter puts an inline policy on one entity type, for running the same
// quota cases against users, groups and roles.
type inlinePutter struct {
entityType string
quota int
create func(t *testing.T, svc *IAMServiceImpl, name string)
put func(svc *IAMServiceImpl, name, policyName, doc string) error
}
var inlinePutters = []inlinePutter{
{
entityType: "user",
quota: 2048,
create: func(t *testing.T, svc *IAMServiceImpl, name string) { createTestUser(t, svc, name) },
put: func(svc *IAMServiceImpl, name, policyName, doc string) error {
_, err := svc.PutUserPolicy(testAccountID, &iam.PutUserPolicyInput{
UserName: aws.String(name), PolicyName: aws.String(policyName), PolicyDocument: aws.String(doc),
})
return err
},
},
{
entityType: "group",
quota: 5120,
create: func(t *testing.T, svc *IAMServiceImpl, name string) { createTestGroup(t, svc, name) },
put: func(svc *IAMServiceImpl, name, policyName, doc string) error {
_, err := svc.PutGroupPolicy(testAccountID, &iam.PutGroupPolicyInput{
GroupName: aws.String(name), PolicyName: aws.String(policyName), PolicyDocument: aws.String(doc),
})
return err
},
},
{
entityType: "role",
quota: 10240,
create: func(t *testing.T, svc *IAMServiceImpl, name string) { createTestRole(t, svc, name) },
put: func(svc *IAMServiceImpl, name, policyName, doc string) error {
_, err := svc.PutRolePolicy(testAccountID, &iam.PutRolePolicyInput{
RoleName: aws.String(name), PolicyName: aws.String(policyName), PolicyDocument: aws.String(doc),
})
return err
},
},
}
func TestPutInlinePolicy_SingleDocumentQuota(t *testing.T) {
t.Parallel()
for _, p := range inlinePutters {
t.Run(p.entityType, func(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)
p.create(t, svc, "sized")
err := p.put(svc, "sized", "Over", sizedPolicy(t, p.quota+1))
require.Error(t, err)
assert.Contains(t, err.Error(), awserrors.ErrorIAMLimitExceeded)
assert.Contains(t, err.Error(), fmt.Sprintf("Maximum policy size of %d bytes exceeded for %s sized", p.quota, p.entityType))
require.NoError(t, p.put(svc, "sized", "AtQuota", sizedPolicy(t, p.quota)))
})
}
}
// The quota applies to the sum of an entity's inline policies.
func TestPutInlinePolicy_AggregateQuota(t *testing.T) {
t.Parallel()
for _, p := range inlinePutters {
t.Run(p.entityType, func(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)
p.create(t, svc, "aggregate")
first := p.quota / 2
require.NoError(t, p.put(svc, "aggregate", "First", sizedPolicy(t, first)))
err := p.put(svc, "aggregate", "Second", sizedPolicy(t, p.quota-first+1))
require.Error(t, err)
assert.Contains(t, err.Error(), awserrors.ErrorIAMLimitExceeded)
require.NoError(t, p.put(svc, "aggregate", "Second", sizedPolicy(t, p.quota-first)))
})
}
}
// Replacing a policy counts only its new document, not the one it replaces.
func TestPutInlinePolicy_ReplacementCountsOnlyNewDocument(t *testing.T) {
t.Parallel()
for _, p := range inlinePutters {
t.Run(p.entityType, func(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)
p.create(t, svc, "replace")
require.NoError(t, p.put(svc, "replace", "Only", sizedPolicy(t, p.quota-10)))
require.NoError(t, p.put(svc, "replace", "Only", sizedPolicy(t, p.quota)))
})
}
}
func TestPutInlinePolicy_WhiteSpaceNotCounted(t *testing.T) {
t.Parallel()
for _, p := range inlinePutters {
t.Run(p.entityType, func(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)
p.create(t, svc, "spaced")
doc := strings.Replace(sizedPolicy(t, p.quota), `"Statement":`, `"Statement":`+strings.Repeat(" ", p.quota), 1)
require.NoError(t, p.put(svc, "spaced", "Spaced", doc))
})
}
}
// A PolicyDocument over AWS's API length bound fails parameter validation, as in
// AWS, rather than reading as a malformed document.
func TestPolicyDocumentLength_ValidationError(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)
tooLong := strings.Repeat("a", maxPolicyDocumentLength+1)
const want = "Member must have length less than or equal to 131072"
_, err := svc.CreatePolicy(testAccountID, &iam.CreatePolicyInput{
PolicyName: aws.String("TooLong"), PolicyDocument: aws.String(tooLong),
})
require.Error(t, err)
assert.Contains(t, err.Error(), awserrors.ErrorValidationError)
assert.Contains(t, err.Error(), want)
out, err := svc.CreatePolicy(testAccountID, &iam.CreatePolicyInput{
PolicyName: aws.String("Base"), PolicyDocument: aws.String(sizedPolicy(t, 200)),
})
require.NoError(t, err)
_, err = svc.CreatePolicyVersion(testAccountID, &iam.CreatePolicyVersionInput{
PolicyArn: out.Policy.Arn, PolicyDocument: aws.String(tooLong),
})
require.Error(t, err)
assert.Contains(t, err.Error(), want)
for _, p := range inlinePutters {
p.create(t, svc, "long-"+p.entityType)
err := p.put(svc, "long-"+p.entityType, "TooLong", tooLong)
require.Error(t, err, p.entityType)
assert.Contains(t, err.Error(), awserrors.ErrorValidationError, p.entityType)
assert.Contains(t, err.Error(), want, p.entityType)
}
}
func TestCreatePolicy_ManagedQuota(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)
_, err := svc.CreatePolicy(testAccountID, &iam.CreatePolicyInput{
PolicyName: aws.String("Over"), PolicyDocument: aws.String(sizedPolicy(t, 6145)),
})
require.Error(t, err)
assert.Contains(t, err.Error(), awserrors.ErrorIAMLimitExceeded)
assert.Contains(t, err.Error(), "Cannot exceed quota for PolicySize: 6144")
out, err := svc.CreatePolicy(testAccountID, &iam.CreatePolicyInput{
PolicyName: aws.String("AtQuota"), PolicyDocument: aws.String(sizedPolicy(t, 6144)),
})
require.NoError(t, err)
_, err = svc.CreatePolicyVersion(testAccountID, &iam.CreatePolicyVersionInput{
PolicyArn: out.Policy.Arn, PolicyDocument: aws.String(sizedPolicy(t, 6145)),
})
require.Error(t, err)
assert.Contains(t, err.Error(), awserrors.ErrorIAMLimitExceeded)
_, err = svc.CreatePolicyVersion(testAccountID, &iam.CreatePolicyVersionInput{
PolicyArn: out.Policy.Arn, PolicyDocument: aws.String(sizedPolicy(t, 6144)),
})
require.NoError(t, err)
}
// Under 2008-10-17 a ${...} reference is literal text, so a key no request
// supplies is accepted rather than rejected as unresolvable.
func TestValidatePolicyDocument_Version2008(t *testing.T) {
t.Parallel()
const legacy = `{"Version":"2008-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject",` +
`"Resource":"arn:aws:s3:::home/${unknown}/*","Condition":{"StringLike":{"s3:prefix":"${unknown}/*"}}},` +
`{"Effect":"Deny","Action":"s3:DeleteObject","NotResource":"arn:aws:s3:::${unknown}/*"}]}`
_, err := ValidatePolicyDocument(legacy)
require.NoError(t, err)
_, err = ValidatePolicyDocument(strings.Replace(legacy, "2008-10-17", "2012-10-17", 1))
require.Error(t, err)
assert.Contains(t, err.Error(), "no request supplies")
_, err = ValidatePolicyDocument(strings.Replace(legacy, "2008-10-17", "2010-01-01", 1))
require.Error(t, err)
assert.Contains(t, err.Error(), "unsupported policy version")
}
+6
View File
@@ -125,10 +125,16 @@ func policyCASTarget(accountID, policyARN string) (string, kvutil.CASConfig, err
func (s *IAMServiceImpl) CreatePolicyVersion(accountID string, input *iam.CreatePolicyVersionInput) (*iam.CreatePolicyVersionOutput, error) {
ctx := context.Background()
if err := checkPolicyDocumentLength(*input.PolicyDocument); err != nil {
return nil, err
}
if _, err := ValidatePolicyDocument(*input.PolicyDocument); err != nil {
return nil, awserrors.Errorf(awserrors.ErrorIAMMalformedPolicyDocument,
"policy %q: %w", *input.PolicyArn, err)
}
if err := checkManagedPolicySize(*input.PolicyDocument); err != nil {
return nil, err
}
setAsDefault := aws.BoolValue(input.SetAsDefault)
var created PolicyVersionRecord
+6
View File
@@ -373,6 +373,9 @@ func (s *IAMServiceImpl) PutRolePolicy(accountID string, input *iam.PutRolePolic
if err := validateIAMName("policyName", policyName, 128); err != nil {
return nil, err
}
if err := checkPolicyDocumentLength(policyDoc); err != nil {
return nil, err
}
if _, err := ValidatePolicyDocument(policyDoc); err != nil {
return nil, awserrors.Errorf(awserrors.ErrorIAMMalformedPolicyDocument,
"policy %q on role %q: %w", policyName, roleName, err)
@@ -389,6 +392,9 @@ func (s *IAMServiceImpl) PutRolePolicy(accountID string, input *iam.PutRolePolic
if !changed {
return false, nil
}
if err := checkInlinePolicySize("role", roleName, rolePolicySizeQuota, role.InlinePolicies, policyName, policyDoc); err != nil {
return false, err
}
role.InlinePolicies[policyName] = policyDoc
return true, nil
})
-15
View File
@@ -1053,21 +1053,6 @@ func TestPutRolePolicy_MalformedDocument(t *testing.T) {
assert.Contains(t, err.Error(), awserrors.ErrorIAMMalformedPolicyDocument)
}
func TestPutRolePolicy_OversizedDocument(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)
createTestRole(t, svc, "oversized-role")
huge := strings.Repeat("a", maxPolicyDocumentSize+1)
_, err := svc.PutRolePolicy(testAccountID, &iam.PutRolePolicyInput{
RoleName: aws.String("oversized-role"),
PolicyName: aws.String("Huge"),
PolicyDocument: aws.String(huge),
})
assert.Error(t, err)
assert.Contains(t, err.Error(), awserrors.ErrorIAMMalformedPolicyDocument)
}
func TestPutRolePolicy_RoleNotFound(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)
+101 -29
View File
@@ -15,6 +15,7 @@ import (
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/aws/aws-sdk-go/aws"
@@ -1286,10 +1287,16 @@ func (s *IAMServiceImpl) CreatePolicy(accountID string, input *iam.CreatePolicyI
}
}
if err := checkPolicyDocumentLength(*input.PolicyDocument); err != nil {
return nil, err
}
if _, err := ValidatePolicyDocument(*input.PolicyDocument); err != nil {
return nil, awserrors.Errorf(awserrors.ErrorIAMMalformedPolicyDocument,
"policy %q: %w", policyName, err)
}
if err := checkManagedPolicySize(*input.PolicyDocument); err != nil {
return nil, err
}
if err := validateTags(input.Tags, exactKeys); err != nil {
return nil, err
@@ -1705,6 +1712,9 @@ func (s *IAMServiceImpl) PutUserPolicy(accountID string, input *iam.PutUserPolic
if err := validateIAMName("policyName", policyName, 128); err != nil {
return nil, err
}
if err := checkPolicyDocumentLength(policyDoc); err != nil {
return nil, err
}
if _, err := ValidatePolicyDocument(policyDoc); err != nil {
return nil, awserrors.Errorf(awserrors.ErrorIAMMalformedPolicyDocument,
"policy %q on user %q: %w", policyName, userName, err)
@@ -1714,6 +1724,9 @@ func (s *IAMServiceImpl) PutUserPolicy(accountID string, input *iam.PutUserPolic
if err != nil {
return nil, err
}
if err := checkInlinePolicySize("user", userName, userPolicySizeQuota, user.InlinePolicies, policyName, policyDoc); err != nil {
return nil, err
}
if user.InlinePolicies == nil {
user.InlinePolicies = map[string]string{}
@@ -2204,7 +2217,64 @@ func parseCreatedAt(raw string) time.Time {
return t
}
const maxPolicyDocumentSize = 6144
// maxPolicyDocumentLength is AWS's API bound on a PolicyDocument parameter. The
// per-type limits below are the quotas a valid document is held to.
const maxPolicyDocumentLength = 131072
// AWS's policy size quotas, in characters excluding white space. Inline limits
// apply to the sum of an entity's inline policies.
const (
managedPolicySizeQuota = 6144
userPolicySizeQuota = 2048
groupPolicySizeQuota = 5120
rolePolicySizeQuota = 10240
)
// policySize is a document's size as AWS counts it against the quotas: its
// characters, leaving out white space.
func policySize(doc string) int {
n := 0
for _, r := range doc {
if !unicode.IsSpace(r) {
n++
}
}
return n
}
// checkPolicyDocumentLength refuses a PolicyDocument parameter over AWS's API
// bound with the ValidationError AWS returns, before the document is parsed.
func checkPolicyDocumentLength(doc string) error {
if utf8.RuneCountInString(doc) > maxPolicyDocumentLength {
return lengthViolation("policyDocument", fmt.Sprintf("less than or equal to %d", maxPolicyDocumentLength))
}
return nil
}
// checkManagedPolicySize refuses a managed policy document over its quota.
func checkManagedPolicySize(doc string) error {
if policySize(doc) > managedPolicySizeQuota {
return awserrors.Errorf(awserrors.ErrorIAMLimitExceeded, "Cannot exceed quota for PolicySize: %d", managedPolicySizeQuota)
}
return nil
}
// checkInlinePolicySize refuses putting doc as policyName when the entity's
// inline policies would then exceed quota in total. A policy of the same name
// is replaced, so only the new document counts.
func checkInlinePolicySize(entityType, entityName string, quota int, inline map[string]string, policyName, doc string) error {
total := policySize(doc)
for name, existing := range inline {
if name != policyName {
total += policySize(existing)
}
}
if total > quota {
return awserrors.Errorf(awserrors.ErrorIAMLimitExceeded,
"Maximum policy size of %d bytes exceeded for %s %s", quota, entityType, entityName)
}
return nil
}
// isIAMNameChar returns true if c is allowed in IAM user/policy names.
func isIAMNameChar(c byte) bool {
@@ -2288,8 +2358,8 @@ func validatePath(path string) error {
// ValidatePolicyDocument parses and validates an IAM policy document JSON string.
func ValidatePolicyDocument(docJSON string) (*PolicyDocument, error) {
if len(docJSON) > maxPolicyDocumentSize {
return nil, fmt.Errorf("policy document exceeds maximum size of %d bytes", maxPolicyDocumentSize)
if len(docJSON) > maxPolicyDocumentLength {
return nil, fmt.Errorf("policy document exceeds maximum length of %d", maxPolicyDocumentLength)
}
var doc PolicyDocument
@@ -2297,9 +2367,11 @@ func ValidatePolicyDocument(docJSON string) (*PolicyDocument, error) {
return nil, fmt.Errorf("invalid JSON: %w", err)
}
if doc.Version != "2012-10-17" {
if doc.Version != iampolicy.Version2012 && doc.Version != iampolicy.Version2008 {
return nil, fmt.Errorf("unsupported policy version: %q", doc.Version)
}
// Under 2008-10-17 a ${...} reference is literal text, so it cannot be unresolvable.
resolvesVariables := doc.Version == iampolicy.Version2012
if len(doc.Statement) == 0 {
return nil, fmt.Errorf("policy must contain at least one statement")
@@ -2318,7 +2390,7 @@ func ValidatePolicyDocument(docJSON string) (*PolicyDocument, error) {
if err := validateSelectorEntries(i, stmt); err != nil {
return nil, err
}
if err := validateStatementRestrictions(i, stmt); err != nil {
if err := validateStatementRestrictions(i, stmt, resolvesVariables); err != nil {
return nil, err
}
}
@@ -2362,19 +2434,22 @@ func validateSelectorEntries(i int, stmt Statement) error {
// validateStatementRestrictions rejects the clauses the evaluator cannot enforce,
// so an identity policy is never accepted with an inert restriction on it.
// Conditions inside the supported allowlist are accepted and enforced.
func validateStatementRestrictions(i int, stmt Statement) error {
// Conditions inside the supported allowlist are accepted and enforced. Policy
// variables are checked only when the document's Version resolves them.
func validateStatementRestrictions(i int, stmt Statement, resolvesVariables bool) error {
if isRawJSONNonEmpty(stmt.Principal) {
return fmt.Errorf("statement %d: Principal is not valid on an identity policy; use a resource or trust policy instead", i)
}
for _, resource := range stmt.Resource {
if err := validatePolicyVariables(i, "Resource", resource); err != nil {
return err
if resolvesVariables {
for _, resource := range stmt.Resource {
if err := validatePolicyVariables(i, "Resource", resource); err != nil {
return err
}
}
}
for _, resource := range stmt.NotResource {
if err := validatePolicyVariables(i, "NotResource", resource); err != nil {
return err
for _, resource := range stmt.NotResource {
if err := validatePolicyVariables(i, "NotResource", resource); err != nil {
return err
}
}
}
for op, keys := range stmt.Condition {
@@ -2382,7 +2457,7 @@ func validateStatementRestrictions(i int, stmt Statement) error {
if !iampolicy.SupportedCondition(op, key) {
return fmt.Errorf("statement %d: Condition operator %q on key %q is not supported in this release%s", i, op, key, unsupportedConditionKeyReason(key))
}
if err := validateConditionValues(i, op, key, values); err != nil {
if err := validateConditionValues(i, op, key, values, resolvesVariables); err != nil {
return err
}
}
@@ -2407,7 +2482,7 @@ func unsupportedConditionKeyReason(key string) string {
// validateConditionValues rejects leaf values the matcher can only ever compare
// false. An allowlisted operator over an unparseable value is still an inert
// restriction, which is the failure this validation exists to prevent.
func validateConditionValues(i int, op, key string, values ConditionValue) error {
func validateConditionValues(i int, op, key string, values ConditionValue, resolvesVariables bool) error {
if len(values) == 0 {
return fmt.Errorf("statement %d: Condition operator %q on key %q has no value", i, op, key)
}
@@ -2423,8 +2498,10 @@ func validateConditionValues(i int, op, key string, values ConditionValue) error
iampolicy.OpNumericEquals, iampolicy.OpNumericNotEquals, iampolicy.OpNumericLessThan,
iampolicy.OpNumericLessThanEquals, iampolicy.OpNumericGreaterThan, iampolicy.OpNumericGreaterThanEquals:
default:
if err := validatePolicyVariables(i, fmt.Sprintf("Condition %s on key %q", op, key), v); err != nil {
return err
if resolvesVariables {
if err := validatePolicyVariables(i, fmt.Sprintf("Condition %s on key %q", op, key), v); err != nil {
return err
}
}
}
switch base {
@@ -2491,13 +2568,8 @@ func validatePolicyVariables(i int, field, value string) error {
}
// summaryQuotaDefaults holds the static SummaryMap entries returned by
// GetAccountSummary. Spinifex's quota system (handlers/quota) only enforces
// infrastructure dimensions (vCPUs, VPCs, subnets, EIPs, EBS) and models no IAM
// entity limits, so these IAM quota values are AWS-parity constants for
// informational compatibility only, not enforced limits. Resource types
// Spinifex does not model are reported as 0 rather than omitted so CIS/audit
// tooling that reads these keys keeps working. Real per-account resource counts
// are overlaid on top of this table at call time.
// GetAccountSummary, at AWS's defaults. Unmodelled resource types report 0 so
// audit tooling finds every key; real per-account counts are overlaid at call time.
var summaryQuotaDefaults = map[string]int64{
// Account-wide quotas (AWS defaults).
"UsersQuota": 5000,
@@ -2515,12 +2587,12 @@ var summaryQuotaDefaults = map[string]int64{
"AttachedPoliciesPerGroupQuota": 10,
"AttachedPoliciesPerRoleQuota": 10,
"SigningCertificatesPerUserQuota": 2,
"UserPolicySizeQuota": 2048,
"GroupPolicySizeQuota": 5120,
"PolicySizeQuota": 6144,
"UserPolicySizeQuota": userPolicySizeQuota,
"GroupPolicySizeQuota": groupPolicySizeQuota,
"PolicySizeQuota": managedPolicySizeQuota,
"VersionsPerPolicyQuota": 5,
"AssumeRolePolicySizeQuota": maxTrustPolicyDocumentSize,
"RolePolicySizeQuota": 10240,
"RolePolicySizeQuota": rolePolicySizeQuota,
// AWS's default: STS global endpoint tokens valid only in default Regions.
"GlobalEndpointTokenVersion": 1,
+5 -20
View File
@@ -460,10 +460,10 @@ func TestCreatePolicy_EmptyPathRefusedBeforeDocument(t *testing.T) {
func TestValidatePolicyDocument_TooLarge(t *testing.T) {
t.Parallel()
largeDoc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"` + strings.Repeat("a", maxPolicyDocumentSize) + `"}]}`
largeDoc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"` + strings.Repeat("a", maxPolicyDocumentLength) + `"}]}`
_, err := ValidatePolicyDocument(largeDoc)
assert.Error(t, err)
assert.Contains(t, err.Error(), "exceeds maximum size")
assert.Contains(t, err.Error(), "exceeds maximum length")
}
// ============================================================================
@@ -1145,7 +1145,7 @@ func TestCreatePolicy_InvalidVersion(t *testing.T) {
_, err := svc.CreatePolicy(testAccountID, &iam.CreatePolicyInput{
PolicyName: aws.String("BadVersion"),
PolicyDocument: aws.String(`{"Version":"2008-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}`),
PolicyDocument: aws.String(`{"Version":"2010-01-01","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}`),
})
assert.Error(t, err)
assert.Contains(t, err.Error(), awserrors.ErrorIAMMalformedPolicyDocument)
@@ -2243,7 +2243,7 @@ func TestValidatePolicyDocument_BadJSON(t *testing.T) {
func TestValidatePolicyDocument_WrongVersion(t *testing.T) {
t.Parallel()
_, err := ValidatePolicyDocument(`{"Version":"2008-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}`)
_, err := ValidatePolicyDocument(`{"Version":"2010-01-01","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}`)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported policy version")
}
@@ -2504,7 +2504,7 @@ func TestValidateConditionValues_ArnOperators(t *testing.T) {
iampolicy.OpArnEquals, iampolicy.OpArnLike, iampolicy.OpArnNotEquals, iampolicy.OpArnNotLike,
} {
for _, tt := range tests {
err := validateConditionValues(0, op, key, ConditionValue{tt.value})
err := validateConditionValues(0, op, key, ConditionValue{tt.value}, true)
if tt.wantErr == "" {
assert.NoError(t, err, "%s %s", op, tt.name)
continue
@@ -2740,21 +2740,6 @@ func TestInputValidation_PathLength(t *testing.T) {
assert.Error(t, err, "513-char path should be rejected")
}
func TestInputValidation_PolicyDocumentSize(t *testing.T) {
t.Parallel()
// 6144 bytes — should pass
filler6144 := strings.Repeat("a", 6144-len(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"arn:aws:s3:::`)-len(`"}]}`))
doc6144 := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"arn:aws:s3:::` + filler6144 + `"}]}`
assert.Len(t, doc6144, 6144)
_, err := ValidatePolicyDocument(doc6144)
assert.NoError(t, err, "6144-byte policy document should be valid")
// 6145 bytes — should fail
doc6145 := doc6144 + " "
_, err = ValidatePolicyDocument(doc6145)
assert.Error(t, err, "6145-byte policy document should be rejected")
}
func TestValidatePolicyDocument_OverlappingDenyAllow(t *testing.T) {
t.Parallel()
doc, err := ValidatePolicyDocument(`{
-16
View File
@@ -2,7 +2,6 @@ package handlers_iam
import (
"encoding/json"
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws"
@@ -130,21 +129,6 @@ func TestPutUserPolicy_UnresolvableVariableNamesTheCause(t *testing.T) {
assert.Error(t, err)
}
func TestPutUserPolicy_OversizedDocument(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)
createTestUser(t, svc, "oversized-user")
huge := strings.Repeat("a", maxPolicyDocumentSize+1)
_, err := svc.PutUserPolicy(testAccountID, &iam.PutUserPolicyInput{
UserName: aws.String("oversized-user"),
PolicyName: aws.String("Huge"),
PolicyDocument: aws.String(huge),
})
assert.Error(t, err)
assert.Contains(t, err.Error(), awserrors.ErrorIAMMalformedPolicyDocument)
}
func TestPutUserPolicy_UserNotFound(t *testing.T) {
t.Parallel()
svc := setupTestIAMService(t)