mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
fix: return AWS's per-action field sets for IAM roles, policies, users and account summary
CreateRole, GetRole, ListRoles and instance-profile roles each get their own projection; GetRole carries an empty RoleLastUsed. Policies carry UpdateDate and PermissionsBoundaryUsageCount, CreatePolicy drops Description and ListPolicies drops Tags. CreateUser echoes Tags, list instance-profile actions drop profile Tags, and GetAccountSummary gains AWS's seven missing keys and loses SigningCertificatesPerUser.
This commit is contained in:
@@ -282,6 +282,30 @@ func TestIAMRequest_GetAccountSummary_Success(t *testing.T) {
|
||||
assert.Contains(t, xmlStr, "<value>2</value>")
|
||||
}
|
||||
|
||||
// AWS answers GetRole for a never-used role with an empty <RoleLastUsed/>
|
||||
// element; a nil-field struct must still marshal to that element, not vanish.
|
||||
func TestIAMRequest_GetRole_EmptyRoleLastUsed(t *testing.T) {
|
||||
svc := &flexMockIAMService{
|
||||
getRoleFn: func(_ string, input *iam.GetRoleInput) (*iam.GetRoleOutput, error) {
|
||||
return &iam.GetRoleOutput{Role: &iam.Role{
|
||||
RoleName: input.RoleName,
|
||||
AssumeRolePolicyDocument: aws.String("{}"),
|
||||
RoleLastUsed: &iam.RoleLastUsed{},
|
||||
}}, nil
|
||||
},
|
||||
}
|
||||
handler := setupIAMRequestHandler(svc)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("Action=GetRole&RoleName=app"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp := doRequest(handler, req)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
assert.Contains(t, string(body), "<RoleLastUsed></RoleLastUsed>")
|
||||
}
|
||||
|
||||
func TestIAMRequest_UnknownAction(t *testing.T) {
|
||||
handler := setupIAMRequestHandler(&flexMockIAMService{})
|
||||
|
||||
|
||||
@@ -108,3 +108,48 @@ func TestGetAccountSummary_EmptyAccount(t *testing.T) {
|
||||
// Quota parity constants are present even for an empty account.
|
||||
assert.Equal(t, int64(5000), summaryCount(t, out.SummaryMap, "UsersQuota"))
|
||||
}
|
||||
|
||||
func TestGetAccountSummary_AWSKeySet(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestIAMService(t)
|
||||
|
||||
acc, err := svc.CreateAccount("Key Set Org")
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := svc.GetAccountSummary(acc.AccountID, &iam.GetAccountSummaryInput{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotContains(t, out.SummaryMap, "SigningCertificatesPerUser", "AWS has no such key")
|
||||
for key, want := range map[string]int64{
|
||||
"AssumeRolePolicySizeQuota": 2048,
|
||||
"RolePolicySizeQuota": 10240,
|
||||
"GlobalEndpointTokenVersion": 1,
|
||||
"AccountAccessKeysPresent": 0,
|
||||
"AccountPasswordPresent": 0,
|
||||
"AccountSigningCertificatesPresent": 0,
|
||||
"Providers": 0,
|
||||
} {
|
||||
assert.Equal(t, want, summaryCount(t, out.SummaryMap, key), key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAccountSummary_ProvidersCountsOIDCPerAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestIAMService(t)
|
||||
|
||||
accA, err := svc.CreateAccount("Org A")
|
||||
require.NoError(t, err)
|
||||
accB, err := svc.CreateAccount("Org B")
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, issuer := range []string{"https://oidc.example/a", "https://oidc.example/b"} {
|
||||
_, err := svc.CreateOpenIDConnectProvider(accA.AccountID, &iam.CreateOpenIDConnectProviderInput{Url: aws.String(issuer)})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
_, err = svc.CreateOpenIDConnectProvider(accB.AccountID, &iam.CreateOpenIDConnectProviderInput{Url: aws.String("https://oidc.example/c")})
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := svc.GetAccountSummary(accA.AccountID, &iam.GetAccountSummaryInput{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), summaryCount(t, out.SummaryMap, "Providers"))
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ func (s *IAMServiceImpl) ListInstanceProfiles(accountID string, input *iam.ListI
|
||||
continue
|
||||
}
|
||||
|
||||
sdkProfile, err := s.profileToSDK(ctx, accountID, &profile)
|
||||
sdkProfile, err := s.listedProfileToSDK(ctx, accountID, &profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -237,7 +237,7 @@ func (s *IAMServiceImpl) ListInstanceProfilesForRole(accountID string, input *ia
|
||||
|
||||
out := make([]*iam.InstanceProfile, 0, len(profiles))
|
||||
for _, p := range profiles {
|
||||
sdkProfile, err := s.profileToSDK(ctx, accountID, p)
|
||||
sdkProfile, err := s.listedProfileToSDK(ctx, accountID, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -386,12 +386,7 @@ func (s *IAMServiceImpl) profileToSDK(ctx context.Context, accountID string, p *
|
||||
CreateDate: aws.Time(parseCreatedAt(p.CreatedAt)),
|
||||
Roles: []*iam.Role{},
|
||||
}
|
||||
for _, t := range p.Tags {
|
||||
out.Tags = append(out.Tags, &iam.Tag{
|
||||
Key: aws.String(t.Key),
|
||||
Value: aws.String(t.Value),
|
||||
})
|
||||
}
|
||||
out.Tags = tagsToSDK(p.Tags)
|
||||
if p.RoleName != "" {
|
||||
role, err := s.getRole(ctx, accountID, p.RoleName)
|
||||
if err != nil {
|
||||
@@ -401,3 +396,14 @@ func (s *IAMServiceImpl) profileToSDK(ctx context.Context, accountID string, p *
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// listedProfileToSDK is the shape ListInstanceProfiles and
|
||||
// ListInstanceProfilesForRole return: GetInstanceProfile's without Tags.
|
||||
func (s *IAMServiceImpl) listedProfileToSDK(ctx context.Context, accountID string, p *InstanceProfile) (*iam.InstanceProfile, error) {
|
||||
out, err := s.profileToSDK(ctx, accountID, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.Tags = nil
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package handlers_iam
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// AWS returns a different field set for a role from each action that carries one.
|
||||
func TestRoleShapes_PerAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestIAMService(t)
|
||||
|
||||
created, err := svc.CreateRole(testAccountID, &iam.CreateRoleInput{
|
||||
RoleName: aws.String("shaped"),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicy()),
|
||||
Description: aws.String("described"),
|
||||
MaxSessionDuration: aws.Int64(7200),
|
||||
Tags: []*iam.Tag{sdkTag("team", "core")},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, created.Role.Description, "CreateRole omits Description")
|
||||
assert.Nil(t, created.Role.MaxSessionDuration, "CreateRole omits MaxSessionDuration")
|
||||
assert.Nil(t, created.Role.RoleLastUsed)
|
||||
assert.Equal(t, map[string]string{"team": "core"}, tagsAsMap(created.Role.Tags))
|
||||
|
||||
got, err := svc.GetRole(testAccountID, &iam.GetRoleInput{RoleName: aws.String("shaped")})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "described", aws.StringValue(got.Role.Description))
|
||||
assert.Equal(t, int64(7200), aws.Int64Value(got.Role.MaxSessionDuration))
|
||||
assert.Equal(t, map[string]string{"team": "core"}, tagsAsMap(got.Role.Tags))
|
||||
assert.Equal(t, &iam.RoleLastUsed{}, got.Role.RoleLastUsed, "an unused role has an empty RoleLastUsed")
|
||||
|
||||
listed, err := svc.ListRoles(testAccountID, &iam.ListRolesInput{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listed.Roles, 1)
|
||||
assert.Equal(t, "described", aws.StringValue(listed.Roles[0].Description))
|
||||
assert.Equal(t, int64(7200), aws.Int64Value(listed.Roles[0].MaxSessionDuration))
|
||||
assert.Nil(t, listed.Roles[0].Tags, "ListRoles omits Tags")
|
||||
assert.Nil(t, listed.Roles[0].RoleLastUsed, "ListRoles omits RoleLastUsed")
|
||||
}
|
||||
|
||||
// A role inside an instance profile carries only its identity, trust policy and
|
||||
// creation date; only GetInstanceProfile returns the profile's own Tags.
|
||||
func TestInstanceProfileShapes_PerAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestIAMService(t)
|
||||
|
||||
_, err := svc.CreateRole(testAccountID, &iam.CreateRoleInput{
|
||||
RoleName: aws.String("embedded"),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicy()),
|
||||
Description: aws.String("described"),
|
||||
Tags: []*iam.Tag{sdkTag("team", "core")},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = svc.CreateInstanceProfile(testAccountID, &iam.CreateInstanceProfileInput{
|
||||
InstanceProfileName: aws.String("profile"),
|
||||
Tags: []*iam.Tag{sdkTag("env", "prod")},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = svc.AddRoleToInstanceProfile(testAccountID, &iam.AddRoleToInstanceProfileInput{
|
||||
InstanceProfileName: aws.String("profile"),
|
||||
RoleName: aws.String("embedded"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assertEmbeddedRole := func(t *testing.T, p *iam.InstanceProfile) {
|
||||
t.Helper()
|
||||
require.Len(t, p.Roles, 1)
|
||||
assert.Equal(t, &iam.Role{
|
||||
RoleName: aws.String("embedded"),
|
||||
RoleId: p.Roles[0].RoleId,
|
||||
Arn: aws.String("arn:aws:iam::" + testAccountID + ":role/embedded"),
|
||||
Path: aws.String("/"),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicy()),
|
||||
CreateDate: p.Roles[0].CreateDate,
|
||||
}, p.Roles[0])
|
||||
}
|
||||
|
||||
got, err := svc.GetInstanceProfile(testAccountID, &iam.GetInstanceProfileInput{InstanceProfileName: aws.String("profile")})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{"env": "prod"}, tagsAsMap(got.InstanceProfile.Tags))
|
||||
assertEmbeddedRole(t, got.InstanceProfile)
|
||||
|
||||
listed, err := svc.ListInstanceProfiles(testAccountID, &iam.ListInstanceProfilesInput{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listed.InstanceProfiles, 1)
|
||||
assert.Nil(t, listed.InstanceProfiles[0].Tags, "ListInstanceProfiles omits profile Tags")
|
||||
assertEmbeddedRole(t, listed.InstanceProfiles[0])
|
||||
|
||||
forRole, err := svc.ListInstanceProfilesForRole(testAccountID, &iam.ListInstanceProfilesForRoleInput{RoleName: aws.String("embedded")})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, forRole.InstanceProfiles, 1)
|
||||
assert.Nil(t, forRole.InstanceProfiles[0].Tags, "ListInstanceProfilesForRole omits profile Tags")
|
||||
assertEmbeddedRole(t, forRole.InstanceProfiles[0])
|
||||
}
|
||||
|
||||
func TestCreateUser_EchoesTags(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestIAMService(t)
|
||||
|
||||
tagged, err := svc.CreateUser(testAccountID, &iam.CreateUserInput{
|
||||
UserName: aws.String("tagged"),
|
||||
Tags: []*iam.Tag{sdkTag("env", "prod")},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{"env": "prod"}, tagsAsMap(tagged.User.Tags))
|
||||
|
||||
untagged, err := svc.CreateUser(testAccountID, &iam.CreateUserInput{UserName: aws.String("untagged")})
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, untagged.User.Tags)
|
||||
}
|
||||
|
||||
func TestPolicyShapes_PerAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestIAMService(t)
|
||||
|
||||
created, err := svc.CreatePolicy(testAccountID, &iam.CreatePolicyInput{
|
||||
PolicyName: aws.String("shaped"),
|
||||
PolicyDocument: aws.String(validPolicyDocument()),
|
||||
Description: aws.String("described"),
|
||||
Tags: []*iam.Tag{sdkTag("team", "core")},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, created.Policy.Description, "CreatePolicy omits Description")
|
||||
assert.Equal(t, map[string]string{"team": "core"}, tagsAsMap(created.Policy.Tags))
|
||||
assert.Equal(t, int64(0), aws.Int64Value(created.Policy.PermissionsBoundaryUsageCount))
|
||||
require.NotNil(t, created.Policy.UpdateDate)
|
||||
assert.Equal(t, *created.Policy.CreateDate, *created.Policy.UpdateDate, "a single-version policy was updated when it was created")
|
||||
|
||||
got, err := svc.GetPolicy(testAccountID, &iam.GetPolicyInput{PolicyArn: created.Policy.Arn})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "described", aws.StringValue(got.Policy.Description))
|
||||
assert.Equal(t, map[string]string{"team": "core"}, tagsAsMap(got.Policy.Tags))
|
||||
assert.Equal(t, int64(0), aws.Int64Value(got.Policy.PermissionsBoundaryUsageCount))
|
||||
assert.Equal(t, *created.Policy.CreateDate, aws.TimeValue(got.Policy.UpdateDate))
|
||||
|
||||
listed, err := svc.ListPolicies(testAccountID, &iam.ListPoliciesInput{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listed.Policies, 1)
|
||||
assert.Nil(t, listed.Policies[0].Tags, "ListPolicies omits Tags")
|
||||
assert.Nil(t, listed.Policies[0].Description, "ListPolicies omits Description")
|
||||
assert.Equal(t, int64(0), aws.Int64Value(listed.Policies[0].PermissionsBoundaryUsageCount))
|
||||
assert.Equal(t, *created.Policy.CreateDate, aws.TimeValue(listed.Policies[0].UpdateDate))
|
||||
|
||||
version, err := svc.CreatePolicyVersion(testAccountID, &iam.CreatePolicyVersionInput{
|
||||
PolicyArn: created.Policy.Arn,
|
||||
PolicyDocument: aws.String(validPolicyDocument()),
|
||||
SetAsDefault: aws.Bool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
got, err = svc.GetPolicy(testAccountID, &iam.GetPolicyInput{PolicyArn: created.Policy.Arn})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, *version.PolicyVersion.CreateDate, aws.TimeValue(got.Policy.UpdateDate),
|
||||
"UpdateDate follows the default version")
|
||||
}
|
||||
@@ -106,7 +106,7 @@ func (s *IAMServiceImpl) CreateRole(accountID string, input *iam.CreateRoleInput
|
||||
|
||||
slog.Info("IAM role created", "accountID", accountID, "roleName", roleName, "roleID", role.RoleID)
|
||||
|
||||
return &iam.CreateRoleOutput{Role: roleToSDK(&role)}, nil
|
||||
return &iam.CreateRoleOutput{Role: createdRoleToSDK(&role)}, nil
|
||||
}
|
||||
|
||||
func (s *IAMServiceImpl) GetRole(accountID string, input *iam.GetRoleInput) (*iam.GetRoleOutput, error) {
|
||||
@@ -116,7 +116,7 @@ func (s *IAMServiceImpl) GetRole(accountID string, input *iam.GetRoleInput) (*ia
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &iam.GetRoleOutput{Role: roleToSDK(role)}, nil
|
||||
return &iam.GetRoleOutput{Role: gotRoleToSDK(role)}, nil
|
||||
}
|
||||
|
||||
func (s *IAMServiceImpl) ListRoles(accountID string, input *iam.ListRolesInput) (*iam.ListRolesOutput, error) {
|
||||
@@ -169,7 +169,7 @@ func (s *IAMServiceImpl) ListRoles(accountID string, input *iam.ListRolesInput)
|
||||
continue
|
||||
}
|
||||
|
||||
roles = append(roles, roleToSDK(&role))
|
||||
roles = append(roles, listedRoleToSDK(&role))
|
||||
}
|
||||
|
||||
return &iam.ListRolesOutput{
|
||||
@@ -646,27 +646,43 @@ func (s *IAMServiceImpl) findInstanceProfilesForRole(ctx context.Context, accoun
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
// roleToSDK converts the internal Role record into the AWS SDK shape used by
|
||||
// CreateRole / GetRole / ListRoles responses.
|
||||
// roleToSDK is the field set AWS returns for a role embedded in an instance
|
||||
// profile. Each role action adds to it: AWS returns a different set per call.
|
||||
func roleToSDK(r *Role) *iam.Role {
|
||||
out := &iam.Role{
|
||||
return &iam.Role{
|
||||
RoleName: aws.String(r.RoleName),
|
||||
RoleId: aws.String(r.RoleID),
|
||||
Arn: aws.String(r.ARN),
|
||||
Path: aws.String(r.Path),
|
||||
AssumeRolePolicyDocument: aws.String(r.AssumeRolePolicyDocument),
|
||||
CreateDate: aws.Time(parseCreatedAt(r.CreatedAt)),
|
||||
MaxSessionDuration: aws.Int64(r.MaxSessionDuration),
|
||||
}
|
||||
}
|
||||
|
||||
// createdRoleToSDK is CreateRole's shape, which omits Description and
|
||||
// MaxSessionDuration even when they were supplied.
|
||||
func createdRoleToSDK(r *Role) *iam.Role {
|
||||
out := roleToSDK(r)
|
||||
out.Tags = tagsToSDK(r.Tags)
|
||||
return out
|
||||
}
|
||||
|
||||
// listedRoleToSDK is ListRoles' shape, which carries no Tags or RoleLastUsed.
|
||||
func listedRoleToSDK(r *Role) *iam.Role {
|
||||
out := roleToSDK(r)
|
||||
if r.Description != "" {
|
||||
out.Description = aws.String(r.Description)
|
||||
}
|
||||
for _, t := range r.Tags {
|
||||
out.Tags = append(out.Tags, &iam.Tag{
|
||||
Key: aws.String(t.Key),
|
||||
Value: aws.String(t.Value),
|
||||
})
|
||||
}
|
||||
out.MaxSessionDuration = aws.Int64(r.MaxSessionDuration)
|
||||
return out
|
||||
}
|
||||
|
||||
// gotRoleToSDK is GetRole's shape. RoleLastUsed is always empty, which is what
|
||||
// AWS returns for a role that has never been used.
|
||||
func gotRoleToSDK(r *Role) *iam.Role {
|
||||
out := listedRoleToSDK(r)
|
||||
out.Tags = tagsToSDK(r.Tags)
|
||||
out.RoleLastUsed = &iam.RoleLastUsed{}
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
@@ -52,14 +52,17 @@ func TestCreateRole(t *testing.T) {
|
||||
require.NotNil(t, out.Role)
|
||||
assert.Equal(t, "app-role", *out.Role.RoleName)
|
||||
assert.Equal(t, "/service-roles/", *out.Role.Path)
|
||||
assert.Equal(t, "Role for app servers", *out.Role.Description)
|
||||
assert.Equal(t, int64(7200), *out.Role.MaxSessionDuration)
|
||||
assert.Equal(t, "arn:aws:iam::"+testAccountID+":role/service-roles/app-role", *out.Role.Arn)
|
||||
require.Greater(t, len(*out.Role.RoleId), 4)
|
||||
assert.Equal(t, "AROA", (*out.Role.RoleId)[:4])
|
||||
require.Len(t, out.Role.Tags, 1)
|
||||
assert.Equal(t, "team", *out.Role.Tags[0].Key)
|
||||
assert.Equal(t, "backend", *out.Role.Tags[0].Value)
|
||||
|
||||
got, err := svc.GetRole(testAccountID, &iam.GetRoleInput{RoleName: aws.String("app-role")})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Role for app servers", *got.Role.Description)
|
||||
assert.Equal(t, int64(7200), *got.Role.MaxSessionDuration)
|
||||
}
|
||||
|
||||
func TestCreateRole_DefaultPath(t *testing.T) {
|
||||
@@ -79,11 +82,13 @@ func TestCreateRole_DefaultMaxSessionDuration(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupTestIAMService(t)
|
||||
|
||||
out, err := svc.CreateRole(testAccountID, &iam.CreateRoleInput{
|
||||
_, err := svc.CreateRole(testAccountID, &iam.CreateRoleInput{
|
||||
RoleName: aws.String("default-session"),
|
||||
AssumeRolePolicyDocument: aws.String(validTrustPolicy()),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
out, err := svc.GetRole(testAccountID, &iam.GetRoleInput{RoleName: aws.String("default-session")})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, defaultMaxSessionDuration, *out.Role.MaxSessionDuration)
|
||||
}
|
||||
|
||||
|
||||
@@ -383,6 +383,7 @@ func (s *IAMServiceImpl) CreateUser(accountID string, input *iam.CreateUserInput
|
||||
Arn: aws.String(user.ARN),
|
||||
Path: aws.String(user.Path),
|
||||
CreateDate: aws.Time(createdAt),
|
||||
Tags: tagsToSDK(user.Tags),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -1237,21 +1238,10 @@ func (s *IAMServiceImpl) CreatePolicy(accountID string, input *iam.CreatePolicyI
|
||||
|
||||
slog.Info("IAM policy created", "accountID", accountID, "policyName", policyName, "policyID", policy.PolicyID)
|
||||
|
||||
createdAt := parseCreatedAt(policy.CreatedAt)
|
||||
return &iam.CreatePolicyOutput{
|
||||
Policy: &iam.Policy{
|
||||
PolicyName: aws.String(policy.PolicyName),
|
||||
PolicyId: aws.String(policy.PolicyID),
|
||||
Arn: aws.String(policy.ARN),
|
||||
Path: aws.String(policy.Path),
|
||||
Description: aws.String(policy.Description),
|
||||
DefaultVersionId: aws.String(policy.DefaultVersion),
|
||||
CreateDate: aws.Time(createdAt),
|
||||
AttachmentCount: aws.Int64(0),
|
||||
IsAttachable: aws.Bool(true),
|
||||
Tags: tagsToSDK(policy.Tags),
|
||||
},
|
||||
}, nil
|
||||
// CreatePolicy omits Description even when it was supplied.
|
||||
out := policyToSDK(&policy, 0)
|
||||
out.Tags = tagsToSDK(policy.Tags)
|
||||
return &iam.CreatePolicyOutput{Policy: out}, nil
|
||||
}
|
||||
|
||||
func (s *IAMServiceImpl) GetPolicy(accountID string, input *iam.GetPolicyInput) (*iam.GetPolicyOutput, error) {
|
||||
@@ -1265,23 +1255,11 @@ func (s *IAMServiceImpl) GetPolicy(accountID string, input *iam.GetPolicyInput)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check policy attachments: %w", err)
|
||||
}
|
||||
attachmentCount := counts[policy.ARN]
|
||||
|
||||
createdAt := parseCreatedAt(policy.CreatedAt)
|
||||
return &iam.GetPolicyOutput{
|
||||
Policy: &iam.Policy{
|
||||
PolicyName: aws.String(policy.PolicyName),
|
||||
PolicyId: aws.String(policy.PolicyID),
|
||||
Arn: aws.String(policy.ARN),
|
||||
Path: aws.String(policy.Path),
|
||||
Description: aws.String(policy.Description),
|
||||
DefaultVersionId: aws.String(policy.DefaultVersion),
|
||||
CreateDate: aws.Time(createdAt),
|
||||
AttachmentCount: aws.Int64(attachmentCount),
|
||||
IsAttachable: aws.Bool(true),
|
||||
Tags: tagsToSDK(policy.Tags),
|
||||
},
|
||||
}, nil
|
||||
out := policyToSDK(policy, counts[policy.ARN])
|
||||
out.Description = aws.String(policy.Description)
|
||||
out.Tags = tagsToSDK(policy.Tags)
|
||||
return &iam.GetPolicyOutput{Policy: out}, nil
|
||||
}
|
||||
|
||||
func (s *IAMServiceImpl) ListPolicies(accountID string, input *iam.ListPoliciesInput) (*iam.ListPoliciesOutput, error) {
|
||||
@@ -1338,18 +1316,8 @@ func (s *IAMServiceImpl) ListPolicies(accountID string, input *iam.ListPoliciesI
|
||||
continue
|
||||
}
|
||||
|
||||
createdAt := parseCreatedAt(policy.CreatedAt)
|
||||
policies = append(policies, &iam.Policy{
|
||||
PolicyName: aws.String(policy.PolicyName),
|
||||
PolicyId: aws.String(policy.PolicyID),
|
||||
Arn: aws.String(policy.ARN),
|
||||
Path: aws.String(policy.Path),
|
||||
DefaultVersionId: aws.String(policy.DefaultVersion),
|
||||
CreateDate: aws.Time(createdAt),
|
||||
AttachmentCount: aws.Int64(attachCounts[policy.ARN]),
|
||||
IsAttachable: aws.Bool(true),
|
||||
Tags: tagsToSDK(policy.Tags),
|
||||
})
|
||||
// ListPolicies carries neither Tags nor Description.
|
||||
policies = append(policies, policyToSDK(&policy, attachCounts[policy.ARN]))
|
||||
}
|
||||
|
||||
return &iam.ListPoliciesOutput{
|
||||
@@ -1358,6 +1326,27 @@ func (s *IAMServiceImpl) ListPolicies(accountID string, input *iam.ListPoliciesI
|
||||
}, nil
|
||||
}
|
||||
|
||||
// policyToSDK is the field set every policy action returns. Permissions
|
||||
// boundaries are rejected, so no policy is ever in use as one.
|
||||
func policyToSDK(p *Policy, attachmentCount int64) *iam.Policy {
|
||||
updatedAt := p.UpdatedAt
|
||||
if updatedAt == "" {
|
||||
updatedAt = p.CreatedAt
|
||||
}
|
||||
return &iam.Policy{
|
||||
PolicyName: aws.String(p.PolicyName),
|
||||
PolicyId: aws.String(p.PolicyID),
|
||||
Arn: aws.String(p.ARN),
|
||||
Path: aws.String(p.Path),
|
||||
DefaultVersionId: aws.String(p.DefaultVersion),
|
||||
CreateDate: aws.Time(parseCreatedAt(p.CreatedAt)),
|
||||
UpdateDate: aws.Time(parseCreatedAt(updatedAt)),
|
||||
AttachmentCount: aws.Int64(attachmentCount),
|
||||
PermissionsBoundaryUsageCount: aws.Int64(0),
|
||||
IsAttachable: aws.Bool(true),
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -2323,21 +2312,27 @@ var summaryQuotaDefaults = map[string]int64{
|
||||
"GroupPolicySizeQuota": 5120,
|
||||
"PolicySizeQuota": 6144,
|
||||
"VersionsPerPolicyQuota": 5,
|
||||
"AssumeRolePolicySizeQuota": maxTrustPolicyDocumentSize,
|
||||
"RolePolicySizeQuota": 10240,
|
||||
|
||||
// AWS's default: STS global endpoint tokens valid only in default Regions.
|
||||
"GlobalEndpointTokenVersion": 1,
|
||||
|
||||
// Resource types Spinifex does not model — reported as 0.
|
||||
"MFADevices": 0,
|
||||
"MFADevicesInUse": 0,
|
||||
"AccountMFAEnabled": 0,
|
||||
"ServerCertificates": 0,
|
||||
"SigningCertificatesPerUser": 0,
|
||||
"PolicyVersionsInUse": 0,
|
||||
"MFADevices": 0,
|
||||
"MFADevicesInUse": 0,
|
||||
"AccountMFAEnabled": 0,
|
||||
"ServerCertificates": 0,
|
||||
"PolicyVersionsInUse": 0,
|
||||
"AccountAccessKeysPresent": 0,
|
||||
"AccountPasswordPresent": 0,
|
||||
"AccountSigningCertificatesPresent": 0,
|
||||
}
|
||||
|
||||
// countBucket counts records in a KV bucket that belong to accountID. Records
|
||||
// are keyed "accountID.name"; counting is by key prefix only (no Get/unmarshal)
|
||||
// so it stays cheap with many resources. The version key is skipped and a
|
||||
// missing bucket counts as zero.
|
||||
func countBucket(ctx context.Context, bucket jetstream.KeyValue, accountID string) (int64, error) {
|
||||
// countBucket counts the keys in bucket that start with prefix, without a
|
||||
// Get/unmarshal so it stays cheap with many resources. The version key is
|
||||
// skipped and an empty bucket counts as zero.
|
||||
func countBucket(ctx context.Context, bucket jetstream.KeyValue, prefix string) (int64, error) {
|
||||
keys, err := kvutil.Keys(ctx, bucket)
|
||||
if err != nil {
|
||||
if errors.Is(err, jetstream.ErrNoKeysFound) {
|
||||
@@ -2346,7 +2341,6 @@ func countBucket(ctx context.Context, bucket jetstream.KeyValue, accountID strin
|
||||
return 0, err
|
||||
}
|
||||
|
||||
prefix := accountID + "."
|
||||
var count int64
|
||||
for _, key := range keys {
|
||||
if key == utils.VersionKey {
|
||||
@@ -2377,12 +2371,31 @@ func (s *IAMServiceImpl) GetAccountSummary(accountID string, _ *iam.GetAccountSu
|
||||
summary[key] = aws.Int64(value)
|
||||
}
|
||||
for key, bucket := range counted {
|
||||
n, err := countBucket(ctx, bucket, accountID)
|
||||
n, err := countBucket(ctx, bucket, accountID+".")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("count %s: %w", key, err)
|
||||
}
|
||||
summary[key] = aws.Int64(n)
|
||||
}
|
||||
|
||||
providers, err := s.countOIDCProviders(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("count Providers: %w", err)
|
||||
}
|
||||
summary["Providers"] = aws.Int64(providers)
|
||||
|
||||
return &iam.GetAccountSummaryOutput{SummaryMap: summary}, nil
|
||||
}
|
||||
|
||||
// countOIDCProviders counts the account's OIDC providers. AWS also counts SAML
|
||||
// providers under Providers; Spinifex has none.
|
||||
func (s *IAMServiceImpl) countOIDCProviders(ctx context.Context, accountID string) (int64, error) {
|
||||
kv, err := s.js.KeyValue(ctx, IAMAccountBucketName(accountID))
|
||||
if err != nil {
|
||||
if errors.Is(err, jetstream.ErrBucketNotFound) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("open IAM account bucket: %w", err)
|
||||
}
|
||||
return countBucket(ctx, kv, oidcProvidersKeyPrefix)
|
||||
}
|
||||
|
||||
@@ -1085,7 +1085,7 @@ func TestCreatePolicy(t *testing.T) {
|
||||
require.NotNil(t, out.Policy)
|
||||
assert.Equal(t, "AllowEC2", *out.Policy.PolicyName)
|
||||
assert.Equal(t, "/devteam/", *out.Policy.Path)
|
||||
assert.Equal(t, "Allow EC2 describe", *out.Policy.Description)
|
||||
assert.Nil(t, out.Policy.Description, "AWS omits Description from CreatePolicy even when supplied")
|
||||
assert.Equal(t, "v1", *out.Policy.DefaultVersionId)
|
||||
assert.Contains(t, *out.Policy.Arn, "policy/devteam/AllowEC2")
|
||||
assert.Greater(t, len(*out.Policy.PolicyId), 4)
|
||||
@@ -1235,7 +1235,7 @@ func TestCreatePolicy_WithTags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
require.NotNil(t, listed, "TaggedPolicy missing from ListPolicies")
|
||||
require.Len(t, listed.Tags, 2)
|
||||
assert.Nil(t, listed.Tags, "AWS omits Tags from ListPolicies")
|
||||
|
||||
tagsOut, err := svc.ListPolicyTags(testAccountID, &iam.ListPolicyTagsInput{PolicyArn: createOut.Policy.Arn})
|
||||
require.NoError(t, err)
|
||||
@@ -1251,7 +1251,6 @@ func TestCreatePolicy_WithTags(t *testing.T) {
|
||||
want := map[string]string{"team": "platform", "env": "prod"}
|
||||
assert.Equal(t, want, byKey(createOut.Policy.Tags))
|
||||
assert.Equal(t, want, byKey(getOut.Policy.Tags))
|
||||
assert.Equal(t, want, byKey(listed.Tags))
|
||||
assert.Equal(t, want, byKey(tagsOut.Tags))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user