Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
skyzyx committed Mar 1, 2024
0 parents commit 57a59bd
Show file tree
Hide file tree
Showing 6 changed files with 370 additions and 0 deletions.
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# AWS Account List

Generates a list of all AWS accounts registered in an AWS Organizations account.

## Install as a CLI tool

1. You must have the Golang toolchain installed first.

```bash
brew install go
```

1. Add `$GOPATH/bin` to your `$PATH` environment variable. By default (i.e., without configuration), `$GOPATH` is defined as `$HOME/go`.

```bash
export PATH="$PATH:$GOPATH/bin"
```

1. Once you've done everything above, you can use `go get`.
```bash
go get -u github.com/northwood-labs/aws-account-list
```
## Usage as CLI Tool
Examples assume the use of [AWS Vault] and [AWS Identity Center].
Gets a list of AWS accounts that are part of the AWS Organization as JSON.
```bash
aws-account-list --help
```
Read directly from the AWS Organizations management account.
```bash
aws-vault exec management-account -- aws-account-list
```
Assume the `AWS_ORG_ROLE` IAM role first, then read the AWS Organizations management account using that IAM role.
```bash
AWS_ORG_ROLE="arn:aws:iam::0123456789012:role/OrganizationReadOnlyAccess"
aws-vault exec management-account -- aws-account-list
```
## Usage as Library
This can also be used as a library in your own applications for generating a list in-memory. The library should fetch data for accounts asynchronously for better performance, but does not yet. This has been tested on AWS Organizations up to ~200 accounts.
```go
import "github.com/northwood-labs/aws-account-list/accountlist"
```
See `main.go`, which implements this library to produce this very same CLI tool.
[AWS Identity Center]: https://aws.amazon.com/iam/identity-center/
[AWS Vault]: https://github.com/99designs/aws-vault
147 changes: 147 additions & 0 deletions accountlist/functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package accountlist

import (
"context"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/organizations"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/pkg/errors"
)

// GetSTSEnabledOrgClient accepts an AWS Config object, assumes the OrgRole, and
// returns an initialized client for AWS Organizations. See
// https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/organizations#Client
// for more information.
func GetSTSEnabledOrgClient(config *aws.Config, orgRole string) *organizations.Client {
// Connect STS as a credential provider.
stsClient := sts.NewFromConfig(*config)
config.Credentials = aws.NewCredentialsCache(
stscreds.NewAssumeRoleProvider(stsClient, orgRole),
)

// Create service client value configured for credentials from assumed role.
orgClient := organizations.NewFromConfig(*config)

return orgClient
}

// GetOrgListAccountsPaginator accepts an organizations.Client object and
// returns a paginator object for the ListAccounts operation.
func GetOrgListAccountsPaginator(
orgClient *organizations.Client,
orgResultCount int32,
) *organizations.ListAccountsPaginator {
paginator := organizations.NewListAccountsPaginator(
orgClient,
&organizations.ListAccountsInput{
MaxResults: aws.Int32(orgResultCount),
},
func(o *organizations.ListAccountsPaginatorOptions) {
o.Limit = orgResultCount
},
)

return paginator
}

// CollectAccountTags iterates over the paginator to produce a result set of
// AccountTag objects. Optionally, you can pass a callback function which
// receives a single AccountTag object in a streaming manner and can perform
// an action.
func CollectAccountTags(
ctx context.Context,
orgClient *organizations.Client,
paginator *organizations.ListAccountsPaginator,
optCallback ...func(*AccountTag),
) ([]AccountTag, error) {
accountTags := make([]AccountTag, 0)

// Execute the AWS requests in a paginated effort.
for paginator.HasMorePages() {
results, err := paginator.NextPage(ctx)
if err != nil {
return accountTags, errors.Wrap(err, "failed to next page of results")
}

for i := range results.Accounts {
account := &results.Accounts[i]

orgTagResponse, err := orgClient.ListTagsForResource(ctx, &organizations.ListTagsForResourceInput{
ResourceId: account.Id,
})
if err != nil {
return accountTags, errors.Wrapf(err, "failed to list tags for account %s", *account.Id)
}

accountTag := AccountTag{
ID: *account.Id,
ARN: *account.Arn,
Name: *account.Name,
Email: *account.Email,
}

for i := range orgTagResponse.Tags {
tag := &orgTagResponse.Tags[i]

accountTag.Tags = append(accountTag.Tags, TagValues{
Key: *tag.Key,
Value: *tag.Value,
})
}

accountTag.OUs, err = getOU(ctx, orgClient, *account.Id)
if err != nil {
return accountTags, err
}

// Run the callback.
if len(optCallback) >= 1 {
fn := optCallback[0]
fn(&accountTag)
}

accountTags = append(accountTags, accountTag)
}
}

return accountTags, nil
}

func getOU(
ctx context.Context,
orgClient *organizations.Client,
accountID string,
) ([]OUType, error) {
var results []OUType

parents, err := orgClient.ListParents(ctx, &organizations.ListParentsInput{
ChildId: aws.String(accountID),
})
if err != nil {
return results, errors.Wrapf(err, "failed to list parents for account %s", accountID)
}

for i := range parents.Parents {
parent := parents.Parents[i]

if parent.Type == "ORGANIZATIONAL_UNIT" {
ou, err := orgClient.DescribeOrganizationalUnit(ctx, &organizations.DescribeOrganizationalUnitInput{
OrganizationalUnitId: parent.Id,
})
if err != nil {
return results, errors.Wrapf(err, "failed to describe OU %s", *parent.Id)
}

results = append(results, OUType{
accountID: accountID,
ID: *ou.OrganizationalUnit.Id,
ARN: *ou.OrganizationalUnit.Arn,
Name: *ou.OrganizationalUnit.Name,
})
}
}

return results, nil
}
26 changes: 26 additions & 0 deletions accountlist/structs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package accountlist

// AccountTag represents the data we collect with this app.
type AccountTag struct {
ID string `json:"id"`
ARN string `json:"arn,omitempty"`
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
Status string `json:"status,omitempty"`
Tags []TagValues `json:"tags,omitempty"`
OUs []OUType `json:"organizationalUnits,omitempty"`
}

// OUType represents the data we collect with this app.
type OUType struct {
accountID string
ID string `json:"id,omitempty"`
ARN string `json:"arn,omitempty"`
Name string `json:"name,omitempty"`
}

// TagValues represents tag key-value pairs.
type TagValues struct {
Key string `json:"key,omitempty"`
Value string `json:"value,omitempty"`
}
29 changes: 29 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module github.com/northwood-labs/aws-account-list

go 1.22

toolchain go1.22.0

require (
github.com/aws/aws-sdk-go-v2 v1.25.2
github.com/aws/aws-sdk-go-v2/credentials v1.17.4
github.com/aws/aws-sdk-go-v2/service/organizations v1.25.1
github.com/aws/aws-sdk-go-v2/service/sts v1.28.1
github.com/northwood-labs/awsutils v0.0.0-20220620172853-924504e83dfb
github.com/northwood-labs/golang-utils/exiterrorf v0.0.0-20240301191325-850f76df0fb0
github.com/pkg/errors v0.9.1
)

require (
github.com/aws/aws-sdk-go-v2/config v1.27.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 // indirect
github.com/aws/smithy-go v1.20.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
)
46 changes: 46 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w=
github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo=
github.com/aws/aws-sdk-go-v2/config v1.27.4 h1:AhfWb5ZwimdsYTgP7Od8E9L1u4sKmDW2ZVeLcf2O42M=
github.com/aws/aws-sdk-go-v2/config v1.27.4/go.mod h1:zq2FFXK3A416kiukwpsd+rD4ny6JC7QSkp4QdN1Mp2g=
github.com/aws/aws-sdk-go-v2/credentials v1.17.4 h1:h5Vztbd8qLppiPwX+y0Q6WiwMZgpd9keKe2EAENgAuI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.4/go.mod h1:+30tpwrkOgvkJL1rUZuRLoxcJwtI/OkeBLYnHxJtVe0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 h1:AK0J8iYBFeUk2Ax7O8YpLtFsfhdOByh2QIkHmigpRYk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2/go.mod h1:iRlGzMix0SExQEviAyptRWRGdYNo3+ufW/lCzvKVTUc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 h1:bNo4LagzUKbjdxE0tIcR9pMzLR2U/Tgie1Hq1HQ3iH8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2/go.mod h1:wRQv0nN6v9wDXuWThpovGQjqF1HFdcgWjporw14lS8k=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 h1:EtOU5jsPdIQNP+6Q2C5e3d65NKT1PeCiQk+9OdzO12Q=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2/go.mod h1:tyF5sKccmDz0Bv4NrstEr+/9YkSPJHrcO7UsUKf7pWM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2 h1:5ffmXjPtwRExp1zc7gENLgCPyHFbhEPwVTkTiH9niSk=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2/go.mod h1:Ru7vg1iQ7cR4i7SZ/JTLYN9kaXtbL69UdgG0OQWQxW0=
github.com/aws/aws-sdk-go-v2/service/organizations v1.25.1 h1:e0QjG+mWYv44qWv9CWdEGuUaPdNg7/62zNAm7QFLbOA=
github.com/aws/aws-sdk-go-v2/service/organizations v1.25.1/go.mod h1:NdwwMJq5OqnAhAUtXJLfMI3qkX0abduGGOpzYO5Kk8U=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 h1:utEGkfdQ4L6YW/ietH7111ZYglLJvS+sLriHJ1NBJEQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.1/go.mod h1:RsYqzYr2F2oPDdpy+PdhephuZxTfjHQe7SOBcZGoAU8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 h1:9/GylMS45hGGFCcMrUZDVayQE1jYSIN6da9jo7RAYIw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1/go.mod h1:YjAPFn4kGFqKC54VsHs5fn5B6d+PCY2tziEa3U/GB5Y=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.1 h1:3I2cBEYgKhrWlwyZgfpSO2BpaMY1LHPqXYk/QGlu2ew=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.1/go.mod h1:uQ7YYKZt3adCRrdCBREm1CD3efFLOUNH77MrUCvx5oA=
github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw=
github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/northwood-labs/awsutils v0.0.0-20220620172853-924504e83dfb h1:pZyIaM7pGlXplAaSy/0SGYgSa5Rt5SjDhVIF0xVc7I8=
github.com/northwood-labs/awsutils v0.0.0-20220620172853-924504e83dfb/go.mod h1:3yNQ3Fwync1OZXb9djMObgqZtk8/BidO5HqYVn40+t8=
github.com/northwood-labs/golang-utils/exiterrorf v0.0.0-20230302161720-ec685e2f274a h1:IvtxXAdMfCPmjDiUNhVKn/qoNDSU6xmutr9rlul4V9Q=
github.com/northwood-labs/golang-utils/exiterrorf v0.0.0-20230302161720-ec685e2f274a/go.mod h1:DZOF/zxKfLJhhFfPhDNrUEU0/MvT5GpFeX3HL1UdYTY=
github.com/northwood-labs/golang-utils/exiterrorf v0.0.0-20240301191325-850f76df0fb0 h1:8WUeGPPwayhDZ/Zabh2Ic/cpRyx9MwAep5WeSbHw590=
github.com/northwood-labs/golang-utils/exiterrorf v0.0.0-20240301191325-850f76df0fb0/go.mod h1:DZOF/zxKfLJhhFfPhDNrUEU0/MvT5GpFeX3HL1UdYTY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
63 changes: 63 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"os"

"github.com/aws/aws-sdk-go-v2/service/organizations"
"github.com/northwood-labs/aws-account-list/accountlist"
"github.com/northwood-labs/awsutils"
"github.com/northwood-labs/golang-utils/exiterrorf"
)

const (
// The number of results to request per page.
orgResultCount = 20
)

var (
// Global flags.
retries *int
verbose *bool

// Context.
ctx = context.Background()

orgRole = os.Getenv("AWS_ORG_ROLE")
)

func main() {
// Flags
retries = flag.Int("retries", 5, "The number of times to retry failed requests to AWS.")
verbose = flag.Bool("verbose", false, "Output internal data to stdout.")
flag.Parse()

config, err := awsutils.GetAWSConfig(ctx, "", "", *retries, *verbose)
if err != nil {
exiterrorf.ExitErrorf(err)
}

var orgClient *organizations.Client

if orgRole == "" {
orgClient = organizations.NewFromConfig(config)
} else {
orgClient = accountlist.GetSTSEnabledOrgClient(&config, orgRole)
}
paginator := accountlist.GetOrgListAccountsPaginator(orgClient, orgResultCount)

accountTags, err := accountlist.CollectAccountTags(ctx, orgClient, paginator)
if err != nil {
exiterrorf.ExitErrorf(err)
}

b, err := json.Marshal(accountTags)
if err != nil {
exiterrorf.ExitErrorf(err)
}

fmt.Println(string(b))
}

0 comments on commit 57a59bd

Please sign in to comment.