feat: ListAccountAliases

This commit is contained in:
Julian Sommer
2026-10-01 14:28:14 +10:00
parent 6a873d3c3c
commit 1ab714b125
12 changed files with 113 additions and 7 deletions
+3 -3
View File
@@ -17,7 +17,7 @@ tags:
## Overview
The platform serves **397 operations** across the AWS APIs below. Every page names the operations its service implements from the pinned model, generated from the dispatch tables on each build rather than written by hand.
The platform serves **398 operations** across the AWS APIs below. Every page names the operations its service implements from the pinned model, generated from the dispatch tables on each build rather than written by hand.
| Service | Operations |
|---|---:|
@@ -27,8 +27,8 @@ The platform serves **397 operations** across the AWS APIs below. Every page nam
| [ECS](/coverage/ecs) | 31 |
| [EKS](/coverage/eks) | 36 |
| [ELBv2](/coverage/elbv2) | 36 |
| [IAM](/coverage/iam) | 79 |
| [IAM](/coverage/iam) | 80 |
| [RDS](/coverage/rds) | 26 |
| [S3](/coverage/s3) | 26 |
| [STS](/coverage/sts) | 5 |
| **Total** | **397** |
| **Total** | **398** |
+2 -1
View File
@@ -2862,6 +2862,7 @@
"GetUser",
"GetUserPolicy",
"ListAccessKeys",
"ListAccountAliases",
"ListAttachedGroupPolicies",
"ListAttachedRolePolicies",
"ListAttachedUserPolicies",
@@ -2943,6 +2944,7 @@
"GetUser",
"GetUserPolicy",
"ListAccessKeys",
"ListAccountAliases",
"ListAttachedGroupPolicies",
"ListAttachedRolePolicies",
"ListAttachedUserPolicies",
@@ -3042,7 +3044,6 @@
"GetServiceLastAccessedDetails",
"GetServiceLastAccessedDetailsWithEntities",
"GetServiceLinkedRoleDeletionStatus",
"ListAccountAliases",
"ListDelegationRequests",
"ListMFADeviceTags",
"ListMFADevices",
+2 -1
View File
@@ -18,7 +18,7 @@ tags:
## Overview
Spinifex implements **79 operations** in the IAM `2010-05-08` API model.
Spinifex implements **80 operations** in the IAM `2010-05-08` API model.
### Scope
@@ -67,6 +67,7 @@ All IAM operations are account-scoped. The root user of an account bypasses poli
| `GetUser` |
| `GetUserPolicy` |
| `ListAccessKeys` |
| `ListAccountAliases` |
| `ListAttachedGroupPolicies` |
| `ListAttachedRolePolicies` |
| `ListAttachedUserPolicies` |
+1 -2
View File
@@ -957,7 +957,7 @@ spot-price: On owned hardware there is no spot-to-on-demand price differential,
## iam
`2010-05-08` — 79 of 180 modelled operations implemented (43.9%), 0 stubbed, 19 not applicable, 0 registered outside the pinned model.
`2010-05-08` — 80 of 180 modelled operations implemented (44.4%), 0 stubbed, 19 not applicable, 0 registered outside the pinned model.
### Not implemented
@@ -1006,7 +1006,6 @@ spot-price: On owned hardware there is no spot-to-on-demand price differential,
- `GetServerCertificate`
- `GetServiceLastAccessedDetails`
- `GetServiceLastAccessedDetailsWithEntities`
- `ListAccountAliases`
- `ListDelegationRequests`
- `ListMFADeviceTags`
- `ListMFADevices`
+3
View File
@@ -323,6 +323,9 @@ var iamActions = map[string]iamAction{
"GetAccountSummary": iamHandler(func(accountID string, input *iam.GetAccountSummaryInput, gw *GatewayConfig) (any, error) {
return gateway_iam.GetAccountSummary(accountID, input, gw.IAMService)
}),
"ListAccountAliases": iamHandler(func(accountID string, input *iam.ListAccountAliasesInput, gw *GatewayConfig) (any, error) {
return gateway_iam.ListAccountAliases(accountID, input, gw.IAMService)
}),
}
func (gw *GatewayConfig) IAM_Request(w http.ResponseWriter, r *http.Request) error {
+15
View File
@@ -1,6 +1,7 @@
package gateway_iam
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
@@ -10,3 +11,17 @@ import (
func GetAccountSummary(accountID string, input *iam.GetAccountSummaryInput, svc handlers_iam.IAMService) (*iam.GetAccountSummaryOutput, error) {
return svc.GetAccountSummary(accountID, input)
}
func ListAccountAliases(accountID string, input *iam.ListAccountAliasesInput, svc handlers_iam.IAMService) (*iam.ListAccountAliasesOutput, error) {
p, err := newPager(input.Marker, input.MaxItems)
if err != nil {
return nil, err
}
out, err := svc.ListAccountAliases(accountID, input)
if err != nil {
return nil, err
}
out.AccountAliases, out.Marker = paginate(p, out.AccountAliases, aws.StringValue)
out.IsTruncated = aws.Bool(out.Marker != nil)
return out, nil
}
+1
View File
@@ -136,6 +136,7 @@ var iamScopes = map[string]resourceScope{
"ListOpenIDConnectProviders": {source: sourceAccount},
"ListGroups": {source: sourceAccount},
"GetAccountSummary": {source: sourceAccount},
"ListAccountAliases": {source: sourceAccount},
}
// HasScope reports whether action has an explicit IAM scope-table entry.
+32
View File
@@ -132,6 +132,38 @@ func TestListUsers_RejectsBadPagingBeforeListing(t *testing.T) {
}
}
// listAliasesStub counts calls so a rejected page proves the service never ran.
type listAliasesStub struct {
handlers_iam.IAMService
calls int
}
func (s *listAliasesStub) ListAccountAliases(_ string, _ *iam.ListAccountAliasesInput) (*iam.ListAccountAliasesOutput, error) {
s.calls++
return &iam.ListAccountAliasesOutput{AccountAliases: []*string{}}, nil
}
func TestListAccountAliases_RejectsBadPagingBeforeListing(t *testing.T) {
tests := []struct {
name string
input *iam.ListAccountAliasesInput
}{
{"MaxItems zero", &iam.ListAccountAliasesInput{MaxItems: aws.Int64(0)}},
{"MaxItems above 1000", &iam.ListAccountAliasesInput{MaxItems: aws.Int64(1001)}},
{"undecodable Marker", &iam.ListAccountAliasesInput{Marker: aws.String("not a marker!")}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
svc := &listAliasesStub{}
_, err := gateway_iam.ListAccountAliases(testAccountID, tc.input, svc)
require.Error(t, err)
assert.True(t, awserrors.IsErrorCode(err, awserrors.ErrorValidationError), "got %v", err)
assert.Zero(t, svc.calls)
})
}
}
func TestListUsers_MaxItemsBoundsAccepted(t *testing.T) {
svc := &listStub{users: []string{"a", "b"}}
assert.Len(t, listUsers(t, svc, nil, 1).Users, 1)
@@ -242,6 +242,10 @@ func (svc *echoIAMService) GetAccountSummary(accountID string, input *iam.GetAcc
return echoIAMCall[iam.GetAccountSummaryInput, iam.GetAccountSummaryOutput](svc, accountID, input)
}
func (svc *echoIAMService) ListAccountAliases(accountID string, input *iam.ListAccountAliasesInput) (*iam.ListAccountAliasesOutput, error) {
return echoIAMCall[iam.ListAccountAliasesInput, iam.ListAccountAliasesOutput](svc, accountID, input)
}
func (svc *echoIAMService) GetInstanceProfile(accountID string, input *iam.GetInstanceProfileInput) (*iam.GetInstanceProfileOutput, error) {
if _, ok := svc.wantOutput.(*iam.GetInstanceProfileOutput); !ok {
return &iam.GetInstanceProfileOutput{InstanceProfile: &iam.InstanceProfile{}}, nil
+40
View File
@@ -10,11 +10,14 @@ import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/mulgadc/spinifex/spinifex/arn"
"github.com/mulgadc/spinifex/spinifex/awserrors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// flexMockIAMService is a configurable mock with per-method overrides. It
@@ -125,6 +128,9 @@ func (m *flexMockIAMService) GetAccountSummary(accountID string, input *iam.GetA
}
return &iam.GetAccountSummaryOutput{}, nil
}
func (m *flexMockIAMService) ListAccountAliases(_ string, _ *iam.ListAccountAliasesInput) (*iam.ListAccountAliasesOutput, error) {
return &iam.ListAccountAliasesOutput{}, nil
}
func (m *flexMockIAMService) CreateGroup(_ string, _ *iam.CreateGroupInput) (*iam.CreateGroupOutput, error) {
return &iam.CreateGroupOutput{}, nil
}
@@ -282,6 +288,40 @@ func TestIAMRequest_GetAccountSummary_Success(t *testing.T) {
assert.Contains(t, xmlStr, "<value>2</value>")
}
// realAliasesIAMService answers ListAccountAliases from the real service, which
// needs no storage, so the test sees the response the gateway actually sends.
type realAliasesIAMService struct {
flexMockIAMService
}
func (realAliasesIAMService) ListAccountAliases(accountID string, input *iam.ListAccountAliasesInput) (*iam.ListAccountAliasesOutput, error) {
return (&handlers_iam.IAMServiceImpl{}).ListAccountAliases(accountID, input)
}
// AWS answers an account with no alias with an empty AccountAliases list and
// IsTruncated false, and no Marker.
func TestIAMRequest_ListAccountAliases_Empty(t *testing.T) {
server := httptest.NewServer(setupIAMRequestHandler(&realAliasesIAMService{}))
defer server.Close()
client := iam.New(session.Must(session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials("test", "test", ""),
DisableSSL: aws.Bool(true),
Endpoint: aws.String(server.URL),
MaxRetries: aws.Int(0),
Region: aws.String("us-east-1"),
})))
out, err := client.ListAccountAliases(&iam.ListAccountAliasesInput{})
require.NoError(t, err)
assert.Empty(t, out.AccountAliases)
assert.False(t, aws.BoolValue(out.IsTruncated))
assert.Nil(t, out.Marker)
_, err = client.ListAccountAliases(&iam.ListAccountAliasesInput{MaxItems: aws.Int64(1001)})
require.Error(t, err)
assert.Contains(t, err.Error(), awserrors.ErrorValidationError)
}
// 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) {
+4
View File
@@ -151,4 +151,8 @@ type IAMService interface {
// GetAccountSummary returns account-wide IAM usage counts plus AWS-parity
// quota values as a SummaryMap. Read-only and account-scoped.
GetAccountSummary(accountID string, input *iam.GetAccountSummaryInput) (*iam.GetAccountSummaryOutput, error)
// ListAccountAliases lists the account's aliases. Spinifex has no account
// aliases, so every account has none.
ListAccountAliases(accountID string, input *iam.ListAccountAliasesInput) (*iam.ListAccountAliasesOutput, error)
}
+6
View File
@@ -2666,6 +2666,12 @@ func (s *IAMServiceImpl) GetAccountSummary(accountID string, _ *iam.GetAccountSu
return &iam.GetAccountSummaryOutput{SummaryMap: summary}, nil
}
// ListAccountAliases returns no aliases, which is what AWS returns for an account
// that has none. Spinifex does not serve CreateAccountAlias.
func (s *IAMServiceImpl) ListAccountAliases(_ string, _ *iam.ListAccountAliasesInput) (*iam.ListAccountAliasesOutput, error) {
return &iam.ListAccountAliasesOutput{AccountAliases: []*string{}}, 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) {