-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 57a59bd
Showing
6 changed files
with
370 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |