aboutsummaryrefslogtreecommitdiff
path: root/cloud
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2021-11-16 14:46:24 -0800
committerMike Crute <mike@crute.us>2021-11-17 07:56:10 -0800
commitcc58a3da7d647de8520e33dc4356672d2ed1a366 (patch)
tree1b232a0d51446eb6370cfb13932190d31ce053df /cloud
parenta42d794a286154a3106551e6e483861af2a9ef16 (diff)
downloadcloud-identity-broker-cc58a3da7d647de8520e33dc4356672d2ed1a366.tar.bz2
cloud-identity-broker-cc58a3da7d647de8520e33dc4356672d2ed1a366.tar.xz
cloud-identity-broker-cc58a3da7d647de8520e33dc4356672d2ed1a366.zip
Import of source code
Diffstat (limited to 'cloud')
-rw-r--r--cloud/aws/aws.go218
-rw-r--r--cloud/aws/error.go11
2 files changed, 229 insertions, 0 deletions
diff --git a/cloud/aws/aws.go b/cloud/aws/aws.go
new file mode 100644
index 0000000..180b2c4
--- /dev/null
+++ b/cloud/aws/aws.go
@@ -0,0 +1,218 @@
1package aws
2
3import (
4 "encoding/json"
5 "fmt"
6 "io/ioutil"
7 "net/http"
8 "net/url"
9 "strconv"
10
11 "code.crute.us/mcrute/cloud-identity-broker/app/models"
12
13 "code.crute.us/mcrute/golib/vault"
14 "github.com/aws/aws-sdk-go/aws"
15 "github.com/aws/aws-sdk-go/aws/credentials"
16 "github.com/aws/aws-sdk-go/aws/session"
17 "github.com/aws/aws-sdk-go/service/ec2"
18 "github.com/aws/aws-sdk-go/service/sts"
19)
20
21// defaultRegion is the region where AWS currently has may authoritative
22// service like IAM and ancillary auth services.
23var defaultRegion = aws.String("us-east-1")
24
25type Region struct {
26 Name string
27 Enabled bool
28}
29
30type AWSClient interface {
31 AssumeRole(string, *string) (*sts.Credentials, error)
32 GetFederationURL(string, string) (string, error)
33 GetRegionList() ([]*Region, error)
34}
35
36// account models the account configuration stored in Vault for an AWS account
37// with assumable roles that are stored within a kv JSON record.
38type account struct {
39 AccessKeyId string
40 SecretAccessKey string
41 Roles map[string]struct {
42 ARN string
43 ExternalId string
44 }
45}
46
47type client struct {
48 AccessKeyId string
49 SecretAccessKey string
50 ARN string
51 ExternalId string
52 ConsoleSessionDurationSecs int64
53}
54
55var _ AWSClient = (*client)(nil)
56
57// NewAWSClientFromAccount returns a new AWSClient based on an account.
58//
59// An account is actually more accurately called an assumable role. Each
60// account contains a vault material set path which is used to fetch all of the
61// credentials for that AWS account and is filtered to one assumable role ARN
62// which is used as the scope for this AWS client. Thus even if an account has
63// multiple roles there must be one instance of the AWS client per account/role
64// pair.
65func NewAWSClientFromAccount(a *models.Account) (AWSClient, error) {
66 var ac account
67 if err := vault.GetVaultKeyStruct(a.VaultMaterial, &ac); err != nil {
68 return nil, err
69 }
70
71 r, ok := ac.Roles[a.ShortName]
72 if !ok {
73 return nil, fmt.Errorf("No roles for account %s in vault response", a.ShortName)
74 }
75
76 return &client{
77 AccessKeyId: ac.AccessKeyId,
78 SecretAccessKey: ac.SecretAccessKey,
79 ARN: r.ARN,
80 ExternalId: r.ExternalId,
81 ConsoleSessionDurationSecs: a.ConsoleSessionDurationSecs(),
82 }, nil
83}
84
85// AssumeRole uses an IAM user credential with higher privilege to assume a
86// role in an AWS account and region. It returns the STS credentials.
87//
88// Not all credentials work for all regions. For newer regions and opt-in
89// regions AWS has been siloing assumed role credentials to that region so it's
90// important to use the correct regional endpoint to fetch the credentials.
91//
92// Note that this is not simply a passthrough to Vault's AWS backend because
93// the Vault backend works by assuming roles and when assuming roles with an
94// assumed role AWS limits the chained role lifetime to 1 hour which doesn't
95// work depending on how the upstream web application tied to this client
96// works. This method instead uses a long-lived IAM user credential to assume a
97// role, which has a limited lifetime which is typically greater than 1 hour.
98func (a *client) AssumeRole(user string, region *string) (*sts.Credentials, error) {
99 if region != nil && *region == "global" {
100 region = nil
101 }
102
103 c := sts.New(session.New(&aws.Config{
104 Region: region,
105 Credentials: credentials.NewStaticCredentials(
106 a.AccessKeyId,
107 a.SecretAccessKey,
108 "",
109 ),
110 }))
111 result, err := c.AssumeRole(&sts.AssumeRoleInput{
112 ExternalId: aws.String(a.ExternalId),
113 RoleArn: aws.String(a.ARN),
114 RoleSessionName: aws.String(user),
115 DurationSeconds: aws.Int64(a.ConsoleSessionDurationSecs),
116 })
117 if err != nil {
118 return nil, err
119 }
120
121 return result.Credentials, nil
122}
123
124// GetRegionList returns all of the EC2 regions that are enabled for an
125// account.
126//
127// All regions should return the same answer to this query so just send it to
128// the default region.
129func (a *client) GetRegionList() ([]*Region, error) {
130 r, err := a.AssumeRole("region-list", defaultRegion)
131 if err != nil {
132 return nil, err
133 }
134
135 ec2c := ec2.New(session.New(&aws.Config{
136 Region: defaultRegion,
137 Credentials: credentials.NewStaticCredentials(
138 *r.AccessKeyId,
139 *r.SecretAccessKey,
140 *r.SessionToken,
141 ),
142 }))
143
144 ro, err := ec2c.DescribeRegions(&ec2.DescribeRegionsInput{AllRegions: aws.Bool(true)})
145 if err != nil {
146 return nil, err
147 }
148
149 out := []*Region{}
150 for _, r := range ro.Regions {
151 out = append(out, &Region{
152 Name: *r.RegionName,
153 Enabled: (*r.OptInStatus != "not-opted-in"),
154 })
155 }
156
157 return out, nil
158}
159
160// GetFederationURL assumes a role and returns a URL that can be used to login
161// to the AWS console for that role.
162//
163// This URL can be used for any region but the endpoints to return the URL only
164// work in us-east-1.
165func (a *client) GetFederationURL(user string, issuerEndpoint string) (string, error) {
166 r, err := a.AssumeRole(user, defaultRegion)
167 if err != nil {
168 return "", err
169 }
170
171 sp, _ := json.Marshal(map[string]string{
172 "sessionId": *r.AccessKeyId,
173 "sessionKey": *r.SecretAccessKey,
174 "sessionToken": *r.SessionToken,
175 })
176
177 client := &http.Client{}
178 fedResp, err := client.Do(&http.Request{
179 Method: http.MethodGet,
180 URL: &url.URL{
181 Scheme: "https",
182 Host: "signin.aws.amazon.com",
183 Path: "/federation",
184 RawQuery: url.Values{
185 "Action": []string{"getSigninToken"},
186 "Session": []string{string(sp)},
187 "SessionDuration": []string{strconv.FormatInt(a.ConsoleSessionDurationSecs, 10)},
188 }.Encode(),
189 },
190 })
191 if err != nil {
192 return "", err
193 }
194
195 if fedResp.StatusCode != 200 {
196 return "", fmt.Errorf("federation endpoint returned HTTP %d", fedResp.StatusCode)
197 }
198
199 body, err := ioutil.ReadAll(fedResp.Body)
200 if err != nil {
201 return "", err
202 }
203
204 var fedData map[string]string
205 if err = json.Unmarshal(body, &fedData); err != nil {
206 return "", err
207 }
208
209 u, _ := url.Parse("https://signin.aws.amazon.com/federation")
210 u.RawQuery = url.Values{
211 "Action": []string{"login"},
212 "Issuer": []string{issuerEndpoint},
213 "Destination": []string{"https://console.aws.amazon.com/"},
214 "SigninToken": []string{fedData["SigninToken"]},
215 }.Encode()
216
217 return u.String(), nil
218}
diff --git a/cloud/aws/error.go b/cloud/aws/error.go
new file mode 100644
index 0000000..62fda48
--- /dev/null
+++ b/cloud/aws/error.go
@@ -0,0 +1,11 @@
1package aws
2
3import (
4 "strings"
5)
6
7// IsRegionNotExist tries to determine if the error is caused by a region not
8// existing, as would be the case in a user typo.
9func IsRegionNotExist(err error) bool {
10 return strings.Contains(err.Error(), "no such host")
11}