1package aws
2
3import (
4	"context"
5	"fmt"
6	"math/rand"
7	"regexp"
8	"time"
9
10	"github.com/aws/aws-sdk-go/aws"
11	"github.com/aws/aws-sdk-go/service/iam"
12	"github.com/aws/aws-sdk-go/service/sts"
13	"github.com/hashicorp/errwrap"
14	"github.com/hashicorp/vault/helper/awsutil"
15	"github.com/hashicorp/vault/sdk/framework"
16	"github.com/hashicorp/vault/sdk/logical"
17)
18
19const secretAccessKeyType = "access_keys"
20
21func secretAccessKeys(b *backend) *framework.Secret {
22	return &framework.Secret{
23		Type: secretAccessKeyType,
24		Fields: map[string]*framework.FieldSchema{
25			"access_key": &framework.FieldSchema{
26				Type:        framework.TypeString,
27				Description: "Access Key",
28			},
29
30			"secret_key": &framework.FieldSchema{
31				Type:        framework.TypeString,
32				Description: "Secret Key",
33			},
34			"security_token": &framework.FieldSchema{
35				Type:        framework.TypeString,
36				Description: "Security Token",
37			},
38		},
39
40		Renew:  b.secretAccessKeysRenew,
41		Revoke: b.secretAccessKeysRevoke,
42	}
43}
44
45func genUsername(displayName, policyName, userType string) (ret string, warning string) {
46	var midString string
47
48	switch userType {
49	case "iam_user":
50		// IAM users are capped at 64 chars; this leaves, after the beginning and
51		// end added below, 42 chars to play with.
52		midString = fmt.Sprintf("%s-%s-",
53			normalizeDisplayName(displayName),
54			normalizeDisplayName(policyName))
55		if len(midString) > 42 {
56			midString = midString[0:42]
57			warning = "the calling token display name/IAM policy name were truncated to fit into IAM username length limits"
58		}
59	case "sts":
60		// Capped at 32 chars, which leaves only a couple of characters to play
61		// with, so don't insert display name or policy name at all
62	}
63
64	ret = fmt.Sprintf("vault-%s%d-%d", midString, time.Now().Unix(), rand.Int31n(10000))
65	return
66}
67
68func (b *backend) secretTokenCreate(ctx context.Context, s logical.Storage,
69	displayName, policyName, policy string,
70	lifeTimeInSeconds int64) (*logical.Response, error) {
71	stsClient, err := b.clientSTS(ctx, s)
72	if err != nil {
73		return logical.ErrorResponse(err.Error()), nil
74	}
75
76	username, usernameWarning := genUsername(displayName, policyName, "sts")
77
78	tokenResp, err := stsClient.GetFederationToken(
79		&sts.GetFederationTokenInput{
80			Name:            aws.String(username),
81			Policy:          aws.String(policy),
82			DurationSeconds: &lifeTimeInSeconds,
83		})
84
85	if err != nil {
86		return logical.ErrorResponse(fmt.Sprintf(
87			"Error generating STS keys: %s", err)), awsutil.CheckAWSError(err)
88	}
89
90	resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{
91		"access_key":     *tokenResp.Credentials.AccessKeyId,
92		"secret_key":     *tokenResp.Credentials.SecretAccessKey,
93		"security_token": *tokenResp.Credentials.SessionToken,
94	}, map[string]interface{}{
95		"username": username,
96		"policy":   policy,
97		"is_sts":   true,
98	})
99
100	// Set the secret TTL to appropriately match the expiration of the token
101	resp.Secret.TTL = tokenResp.Credentials.Expiration.Sub(time.Now())
102
103	// STS are purposefully short-lived and aren't renewable
104	resp.Secret.Renewable = false
105
106	if usernameWarning != "" {
107		resp.AddWarning(usernameWarning)
108	}
109
110	return resp, nil
111}
112
113func (b *backend) assumeRole(ctx context.Context, s logical.Storage,
114	displayName, roleName, roleArn, policy string,
115	lifeTimeInSeconds int64) (*logical.Response, error) {
116	stsClient, err := b.clientSTS(ctx, s)
117	if err != nil {
118		return logical.ErrorResponse(err.Error()), nil
119	}
120
121	username, usernameWarning := genUsername(displayName, roleName, "iam_user")
122
123	assumeRoleInput := &sts.AssumeRoleInput{
124		RoleSessionName: aws.String(username),
125		RoleArn:         aws.String(roleArn),
126		DurationSeconds: &lifeTimeInSeconds,
127	}
128	if policy != "" {
129		assumeRoleInput.SetPolicy(policy)
130	}
131	tokenResp, err := stsClient.AssumeRole(assumeRoleInput)
132
133	if err != nil {
134		return logical.ErrorResponse(fmt.Sprintf(
135			"Error assuming role: %s", err)), awsutil.CheckAWSError(err)
136	}
137
138	resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{
139		"access_key":     *tokenResp.Credentials.AccessKeyId,
140		"secret_key":     *tokenResp.Credentials.SecretAccessKey,
141		"security_token": *tokenResp.Credentials.SessionToken,
142	}, map[string]interface{}{
143		"username": username,
144		"policy":   roleArn,
145		"is_sts":   true,
146	})
147
148	// Set the secret TTL to appropriately match the expiration of the token
149	resp.Secret.TTL = tokenResp.Credentials.Expiration.Sub(time.Now())
150
151	// STS are purposefully short-lived and aren't renewable
152	resp.Secret.Renewable = false
153
154	if usernameWarning != "" {
155		resp.AddWarning(usernameWarning)
156	}
157
158	return resp, nil
159}
160
161func (b *backend) secretAccessKeysCreate(
162	ctx context.Context,
163	s logical.Storage,
164	displayName, policyName string, role *awsRoleEntry) (*logical.Response, error) {
165	iamClient, err := b.clientIAM(ctx, s)
166	if err != nil {
167		return logical.ErrorResponse(err.Error()), nil
168	}
169
170	username, usernameWarning := genUsername(displayName, policyName, "iam_user")
171
172	// Write to the WAL that this user will be created. We do this before
173	// the user is created because if switch the order then the WAL put
174	// can fail, which would put us in an awkward position: we have a user
175	// we need to rollback but can't put the WAL entry to do the rollback.
176	walID, err := framework.PutWAL(ctx, s, "user", &walUser{
177		UserName: username,
178	})
179	if err != nil {
180		return nil, errwrap.Wrapf("error writing WAL entry: {{err}}", err)
181	}
182
183	userPath := role.UserPath
184	if userPath == "" {
185		userPath = "/"
186	}
187
188	// Create the user
189	_, err = iamClient.CreateUser(&iam.CreateUserInput{
190		UserName: aws.String(username),
191		Path:     aws.String(userPath),
192	})
193	if err != nil {
194		if walErr := framework.DeleteWAL(ctx, s, walID); walErr != nil {
195			iamErr := errwrap.Wrapf("error creating IAM user: {{err}}", err)
196			return nil, errwrap.Wrap(errwrap.Wrapf("failed to delete WAL entry: {{err}}", walErr), iamErr)
197		}
198		return logical.ErrorResponse(fmt.Sprintf(
199			"Error creating IAM user: %s", err)), awsutil.CheckAWSError(err)
200	}
201
202	for _, arn := range role.PolicyArns {
203		// Attach existing policy against user
204		_, err = iamClient.AttachUserPolicy(&iam.AttachUserPolicyInput{
205			UserName:  aws.String(username),
206			PolicyArn: aws.String(arn),
207		})
208		if err != nil {
209			return logical.ErrorResponse(fmt.Sprintf(
210				"Error attaching user policy: %s", err)), awsutil.CheckAWSError(err)
211		}
212
213	}
214	if role.PolicyDocument != "" {
215		// Add new inline user policy against user
216		_, err = iamClient.PutUserPolicy(&iam.PutUserPolicyInput{
217			UserName:       aws.String(username),
218			PolicyName:     aws.String(policyName),
219			PolicyDocument: aws.String(role.PolicyDocument),
220		})
221		if err != nil {
222			return logical.ErrorResponse(fmt.Sprintf(
223				"Error putting user policy: %s", err)), awsutil.CheckAWSError(err)
224		}
225	}
226
227	// Create the keys
228	keyResp, err := iamClient.CreateAccessKey(&iam.CreateAccessKeyInput{
229		UserName: aws.String(username),
230	})
231	if err != nil {
232		return logical.ErrorResponse(fmt.Sprintf(
233			"Error creating access keys: %s", err)), awsutil.CheckAWSError(err)
234	}
235
236	// Remove the WAL entry, we succeeded! If we fail, we don't return
237	// the secret because it'll get rolled back anyways, so we have to return
238	// an error here.
239	if err := framework.DeleteWAL(ctx, s, walID); err != nil {
240		return nil, errwrap.Wrapf("failed to commit WAL entry: {{err}}", err)
241	}
242
243	// Return the info!
244	resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{
245		"access_key":     *keyResp.AccessKey.AccessKeyId,
246		"secret_key":     *keyResp.AccessKey.SecretAccessKey,
247		"security_token": nil,
248	}, map[string]interface{}{
249		"username": username,
250		"policy":   role,
251		"is_sts":   false,
252	})
253
254	lease, err := b.Lease(ctx, s)
255	if err != nil || lease == nil {
256		lease = &configLease{}
257	}
258
259	resp.Secret.TTL = lease.Lease
260	resp.Secret.MaxTTL = lease.LeaseMax
261
262	if usernameWarning != "" {
263		resp.AddWarning(usernameWarning)
264	}
265
266	return resp, nil
267}
268
269func (b *backend) secretAccessKeysRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
270	// STS already has a lifetime, and we don't support renewing it
271	isSTSRaw, ok := req.Secret.InternalData["is_sts"]
272	if ok {
273		isSTS, ok := isSTSRaw.(bool)
274		if ok {
275			if isSTS {
276				return nil, nil
277			}
278		}
279	}
280
281	lease, err := b.Lease(ctx, req.Storage)
282	if err != nil {
283		return nil, err
284	}
285	if lease == nil {
286		lease = &configLease{}
287	}
288
289	resp := &logical.Response{Secret: req.Secret}
290	resp.Secret.TTL = lease.Lease
291	resp.Secret.MaxTTL = lease.LeaseMax
292	return resp, nil
293}
294
295func (b *backend) secretAccessKeysRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
296
297	// STS cleans up after itself so we can skip this if is_sts internal data
298	// element set to true. If is_sts is not set, assumes old version
299	// and defaults to the IAM approach.
300	isSTSRaw, ok := req.Secret.InternalData["is_sts"]
301	if ok {
302		isSTS, ok := isSTSRaw.(bool)
303		if ok {
304			if isSTS {
305				return nil, nil
306			}
307		} else {
308			return nil, fmt.Errorf("secret has is_sts but value could not be understood")
309		}
310	}
311
312	// Get the username from the internal data
313	usernameRaw, ok := req.Secret.InternalData["username"]
314	if !ok {
315		return nil, fmt.Errorf("secret is missing username internal data")
316	}
317	username, ok := usernameRaw.(string)
318	if !ok {
319		return nil, fmt.Errorf("secret is missing username internal data")
320	}
321
322	// Use the user rollback mechanism to delete this user
323	err := b.pathUserRollback(ctx, req, "user", map[string]interface{}{
324		"username": username,
325	})
326	if err != nil {
327		return nil, err
328	}
329
330	return nil, nil
331}
332
333func normalizeDisplayName(displayName string) string {
334	re := regexp.MustCompile("[^a-zA-Z0-9+=,.@_-]")
335	return re.ReplaceAllString(displayName, "_")
336}
337