From 82ce31626b86a563f3d38adc95fbeb7ec8c75758 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:51:29 +1000 Subject: [PATCH] fix: reject PermissionsBoundary on CreateRole and CreateUser (#944) --- spinifex/handlers/iam/roles.go | 4 +++ spinifex/handlers/iam/roles_test.go | 42 ++++++++++++++++++++++ spinifex/handlers/iam/service_impl.go | 15 ++++++++ spinifex/handlers/iam/service_impl_test.go | 40 +++++++++++++++++++++ 4 files changed, 101 insertions(+) diff --git a/spinifex/handlers/iam/roles.go b/spinifex/handlers/iam/roles.go index 7cb6f3fca..72f8a9f00 100644 --- a/spinifex/handlers/iam/roles.go +++ b/spinifex/handlers/iam/roles.go @@ -44,6 +44,10 @@ func (s *IAMServiceImpl) CreateRole(accountID string, input *iam.CreateRoleInput return nil, errors.New(awserrors.ErrorIAMInvalidInput) } + if err := validatePermissionsBoundary(input.PermissionsBoundary); err != nil { + return nil, err + } + path := "/" if input.Path != nil { path = *input.Path diff --git a/spinifex/handlers/iam/roles_test.go b/spinifex/handlers/iam/roles_test.go index 78053a391..979ef106d 100644 --- a/spinifex/handlers/iam/roles_test.go +++ b/spinifex/handlers/iam/roles_test.go @@ -108,6 +108,48 @@ func TestCreateRole_InvalidPath(t *testing.T) { assert.Contains(t, err.Error(), awserrors.ErrorIAMInvalidInput) } +func TestCreateRole_PermissionsBoundarySet(t *testing.T) { + svc := setupTestIAMService(t) + + _, err := svc.CreateRole(testAccountID, &iam.CreateRoleInput{ + RoleName: aws.String("bounded-role"), + AssumeRolePolicyDocument: aws.String(validTrustPolicy()), + PermissionsBoundary: aws.String("arn:aws:iam::" + testAccountID + ":policy/deny-all"), + }) + require.Error(t, err) + assert.Contains(t, err.Error(), awserrors.ErrorValidationError) + assert.Contains(t, err.Error(), "PermissionsBoundary") + + _, err = svc.GetRole(testAccountID, &iam.GetRoleInput{RoleName: aws.String("bounded-role")}) + require.Error(t, err) + assert.Contains(t, err.Error(), awserrors.ErrorIAMNoSuchEntity) +} + +func TestCreateRole_PermissionsBoundaryAbsent(t *testing.T) { + svc := setupTestIAMService(t) + + tests := []struct { + name string + roleName string + boundary *string + }{ + {name: "nil", roleName: "nil-boundary", boundary: nil}, + {name: "empty string", roleName: "empty-boundary", boundary: aws.String("")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out, err := svc.CreateRole(testAccountID, &iam.CreateRoleInput{ + RoleName: aws.String(tt.roleName), + AssumeRolePolicyDocument: aws.String(validTrustPolicy()), + PermissionsBoundary: tt.boundary, + }) + require.NoError(t, err) + assert.Equal(t, tt.roleName, *out.Role.RoleName) + }) + } +} + func TestCreateRole_MalformedTrustPolicy_InvalidJSON(t *testing.T) { svc := setupTestIAMService(t) diff --git a/spinifex/handlers/iam/service_impl.go b/spinifex/handlers/iam/service_impl.go index 71ddd9b09..5f7c915b9 100644 --- a/spinifex/handlers/iam/service_impl.go +++ b/spinifex/handlers/iam/service_impl.go @@ -323,6 +323,10 @@ func (s *IAMServiceImpl) CreateUser(accountID string, input *iam.CreateUserInput return nil, errors.New(awserrors.ErrorIAMInvalidInput) } + if err := validatePermissionsBoundary(input.PermissionsBoundary); err != nil { + return nil, err + } + path := "/" if input.Path != nil { path = *input.Path @@ -1984,6 +1988,17 @@ func validatePolicyName(name string) error { return nil } +// validatePermissionsBoundary rejects a boundary the evaluator cannot enforce. +// Storing an identity with an unenforced boundary would widen access silently, +// so the create fails visibly instead. +func validatePermissionsBoundary(boundary *string) error { + if aws.StringValue(boundary) == "" { + return nil + } + return awserrors.Errorf(awserrors.ErrorValidationError, + "PermissionsBoundary is not supported in this release; omit it and scope the identity with its attached policies instead") +} + func validatePath(path string) error { if !strings.HasPrefix(path, "/") || !strings.HasSuffix(path, "/") { return fmt.Errorf("path must begin and end with /") diff --git a/spinifex/handlers/iam/service_impl_test.go b/spinifex/handlers/iam/service_impl_test.go index f603ec1e4..2b4e44007 100644 --- a/spinifex/handlers/iam/service_impl_test.go +++ b/spinifex/handlers/iam/service_impl_test.go @@ -91,6 +91,46 @@ func TestCreateUser_Duplicate(t *testing.T) { assert.Contains(t, err.Error(), awserrors.ErrorIAMEntityAlreadyExists) } +func TestCreateUser_PermissionsBoundarySet(t *testing.T) { + svc := setupTestIAMService(t) + + _, err := svc.CreateUser(testAccountID, &iam.CreateUserInput{ + UserName: aws.String("boundeduser"), + PermissionsBoundary: aws.String("arn:aws:iam::" + testAccountID + ":policy/deny-all"), + }) + require.Error(t, err) + assert.Contains(t, err.Error(), awserrors.ErrorValidationError) + assert.Contains(t, err.Error(), "PermissionsBoundary") + + _, err = svc.GetUser(testAccountID, &iam.GetUserInput{UserName: aws.String("boundeduser")}) + require.Error(t, err) + assert.Contains(t, err.Error(), awserrors.ErrorIAMNoSuchEntity) +} + +func TestCreateUser_PermissionsBoundaryAbsent(t *testing.T) { + svc := setupTestIAMService(t) + + tests := []struct { + name string + userName string + boundary *string + }{ + {name: "nil", userName: "nilboundary", boundary: nil}, + {name: "empty string", userName: "emptyboundary", boundary: aws.String("")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out, err := svc.CreateUser(testAccountID, &iam.CreateUserInput{ + UserName: aws.String(tt.userName), + PermissionsBoundary: tt.boundary, + }) + require.NoError(t, err) + assert.Equal(t, tt.userName, *out.User.UserName) + }) + } +} + func TestGetUser(t *testing.T) { svc := setupTestIAMService(t) createTestUser(t, svc, "getuser")