1package alicloud
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"github.com/hashicorp/vault/sdk/helper/jsonutil"
8	"math/rand"
9	"time"
10
11	"github.com/hashicorp/vault-plugin-secrets-alicloud/clients"
12	"github.com/hashicorp/vault/sdk/framework"
13	"github.com/hashicorp/vault/sdk/logical"
14)
15
16func (b *backend) pathCreds() *framework.Path {
17	return &framework.Path{
18		Pattern: "creds/" + framework.GenericNameRegex("name"),
19		Fields: map[string]*framework.FieldSchema{
20			"name": {
21				Type:        framework.TypeLowerCaseString,
22				Description: "The name of the role.",
23			},
24		},
25		Callbacks: map[logical.Operation]framework.OperationFunc{
26			logical.ReadOperation: b.operationCredsRead,
27		},
28		HelpSynopsis:    pathCredsHelpSyn,
29		HelpDescription: pathCredsHelpDesc,
30	}
31}
32
33func (b *backend) operationCredsRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
34	roleName := data.Get("name").(string)
35	if roleName == "" {
36		return nil, errors.New("name is required")
37	}
38
39	role, err := readRole(ctx, req.Storage, roleName)
40	if err != nil {
41		return nil, err
42	}
43	if role == nil {
44		// Attempting to read a role that doesn't exist.
45		return nil, nil
46	}
47
48	creds, err := readCredentials(ctx, req.Storage)
49	if err != nil {
50		return nil, err
51	}
52	if creds == nil {
53		return nil, errors.New("unable to create secret because no credentials are configured")
54	}
55
56	switch role.Type() {
57
58	case roleTypeSTS:
59		client, err := clients.NewSTSClient(b.sdkConfig, creds.AccessKey, creds.SecretKey)
60		if err != nil {
61			return nil, err
62		}
63		assumeRoleResp, err := client.AssumeRole(generateRoleSessionName(req.DisplayName, roleName), role.RoleARN)
64		if err != nil {
65			return nil, err
66		}
67		// Parse the expiration into a time, so that when we return it from our API it's formatted
68		// the same way as how _we_ format times, which could differ from this over time.
69		expiration, err := time.Parse("2006-01-02T15:04:05Z", assumeRoleResp.Credentials.Expiration)
70		if err != nil {
71			return nil, err
72		}
73		resp := b.Secret(secretType).Response(map[string]interface{}{
74			"access_key":     assumeRoleResp.Credentials.AccessKeyId,
75			"secret_key":     assumeRoleResp.Credentials.AccessKeySecret,
76			"security_token": assumeRoleResp.Credentials.SecurityToken,
77			"expiration":     expiration,
78		}, map[string]interface{}{
79			"role_type": roleTypeSTS.String(),
80		})
81
82		// Set the secret TTL to appropriately match the expiration of the token.
83		ttl := expiration.Sub(time.Now())
84		resp.Secret.TTL = ttl
85		resp.Secret.MaxTTL = ttl
86
87		// STS credentials are purposefully short-lived and aren't renewable.
88		resp.Secret.Renewable = false
89		return resp, nil
90
91	case roleTypeRAM:
92		client, err := clients.NewRAMClient(b.sdkConfig, creds.AccessKey, creds.SecretKey)
93		if err != nil {
94			return nil, err
95		}
96
97		/*
98			Now we're embarking upon a multi-step process that could fail at any time.
99			If it does, let's do our best to clean up after ourselves. Success will be
100			our flag at the end indicating whether we should leave things be, or clean
101			things up, based on how we exit this method. Since defer statements are
102			last-in-first-out, it will perfectly reverse the order of everything just
103			like we need.
104		*/
105		success := false
106
107		createUserResp, err := client.CreateUser(generateUsername(req.DisplayName, roleName))
108		if err != nil {
109			return nil, err
110		}
111		defer func() {
112			if success {
113				return
114			}
115			if err := client.DeleteUser(createUserResp.User.UserName); err != nil {
116				if b.Logger().IsError() {
117					b.Logger().Error(fmt.Sprintf("unable to delete user %s", createUserResp.User.UserName), err)
118				}
119			}
120		}()
121
122		// We need to gather up all the names and types of the remote policies we're
123		// about to create so we can detach and delete them later.
124		inlinePolicies := make([]*remotePolicy, len(role.InlinePolicies))
125
126		for i, inlinePolicy := range role.InlinePolicies {
127
128			// By combining the userName with the particular policy's UUID,
129			// it'll be possible to figure out who this policy is for and which one
130			// it is using the policy name alone. The max length of a policy name is
131			// 128, but given the max lengths of our username and inline policy UUID,
132			// we will always remain well under that here.
133			policyName := createUserResp.User.UserName + "-" + inlinePolicy.UUID
134
135			policyDoc, err := jsonutil.EncodeJSON(inlinePolicy.PolicyDocument)
136			if err != nil {
137				return nil, err
138			}
139
140			createPolicyResp, err := client.CreatePolicy(policyName, string(policyDoc))
141			if err != nil {
142				return nil, err
143			}
144
145			inlinePolicies[i] = &remotePolicy{
146				Name: createPolicyResp.Policy.PolicyName,
147				Type: createPolicyResp.Policy.PolicyType,
148			}
149
150			// This defer is in this loop on purpose.
151			defer func() {
152				if success {
153					return
154				}
155				if err := client.DeletePolicy(createPolicyResp.Policy.PolicyName); err != nil {
156					if b.Logger().IsError() {
157						b.Logger().Error(fmt.Sprintf("unable to delete policy %s", createPolicyResp.Policy.PolicyName), err)
158					}
159				}
160			}()
161
162			if err := client.AttachPolicy(createUserResp.User.UserName, createPolicyResp.Policy.PolicyName, createPolicyResp.Policy.PolicyType); err != nil {
163				return nil, err
164			}
165			// This defer is also in this loop on purpose.
166			defer func() {
167				if success {
168					return
169				}
170				if err := client.DetachPolicy(createUserResp.User.UserName, createPolicyResp.Policy.PolicyName, createPolicyResp.Policy.PolicyType); err != nil {
171					if b.Logger().IsError() {
172						b.Logger().Error(fmt.Sprintf(
173							"unable to detach policy name:%s, type:%s from user:%s", createPolicyResp.Policy.PolicyName, createPolicyResp.Policy.PolicyType, createUserResp.User.UserName))
174					}
175				}
176			}()
177		}
178
179		for _, remotePol := range role.RemotePolicies {
180			if err := client.AttachPolicy(createUserResp.User.UserName, remotePol.Name, remotePol.Type); err != nil {
181				return nil, err
182			}
183			// This defer is also in this loop on purpose.
184			// Separate these strings from the remotePol pointer so the defer statement will retain the correct values
185			// due to pointer reuse for the remotePol var on each iteration of the loop.
186			remotePolName := remotePol.Name
187			remotePolType := remotePol.Type
188			defer func() {
189				if success {
190					return
191				}
192				if err := client.DetachPolicy(createUserResp.User.UserName, remotePolName, remotePolType); err != nil {
193					if b.Logger().IsError() {
194						b.Logger().Error(fmt.Sprintf("unable to detach policy name:%s, type:%s from user:%s", remotePolName, remotePolType, createUserResp.User.UserName))
195					}
196				}
197			}()
198		}
199
200		accessKeyResp, err := client.CreateAccessKey(createUserResp.User.UserName)
201		if err != nil {
202			return nil, err
203		}
204		// It's unlikely we wouldn't have success at this point because there are no further errors returned below, but
205		// there could be a panic if somehow one of the objects below were missing a pointer, so let's play it safe and
206		// add a defer rolling back the access key if that happens.
207		defer func() {
208			if success {
209				return
210			}
211			if err := client.DeleteAccessKey(createUserResp.User.UserName, accessKeyResp.AccessKey.AccessKeyId); err != nil {
212				if b.Logger().IsError() {
213					b.Logger().Error(fmt.Sprintf("unable to delete access key for username:%s", createUserResp.User.UserName))
214				}
215			}
216		}()
217
218		resp := b.Secret(secretType).Response(map[string]interface{}{
219			"access_key": accessKeyResp.AccessKey.AccessKeyId,
220			"secret_key": accessKeyResp.AccessKey.AccessKeySecret,
221		}, map[string]interface{}{
222			"role_type":       roleTypeRAM.String(),
223			"role_name":       roleName,
224			"username":        createUserResp.User.UserName,
225			"access_key_id":   accessKeyResp.AccessKey.AccessKeyId,
226			"inline_policies": inlinePolicies,
227			"remote_policies": role.RemotePolicies,
228		})
229		if role.TTL != 0 {
230			resp.Secret.TTL = role.TTL
231		}
232		if role.MaxTTL != 0 {
233			resp.Secret.MaxTTL = role.MaxTTL
234		}
235
236		success = true
237		return resp, nil
238
239	default:
240		return nil, fmt.Errorf("unsupported role type: %s", role.Type())
241	}
242}
243
244// The max length of a username per AliCloud is 64.
245func generateUsername(displayName, roleName string) string {
246	return generateName(displayName, roleName, 64)
247}
248
249// The max length of a role session name per AliCloud is 32.
250func generateRoleSessionName(displayName, roleName string) string {
251	return generateName(displayName, roleName, 32)
252}
253
254func generateName(displayName, roleName string, maxLength int) string {
255	name := fmt.Sprintf("%s-%s-", displayName, roleName)
256
257	// The time and random number take up to 15 more in length, so if the name
258	// is too long we need to trim it.
259	if len(name) > maxLength-15 {
260		name = name[:maxLength-15]
261	}
262	r := rand.New(rand.NewSource(time.Now().UnixNano()))
263	return fmt.Sprintf("%s%d-%d", name, time.Now().Unix(), r.Intn(10000))
264}
265
266const pathCredsHelpSyn = `
267Generate an API key or STS credential using the given role's configuration.'
268`
269
270const pathCredsHelpDesc = `
271This path will generate a new API key or STS credential for
272accessing AliCloud. The RAM policies used to back this key pair will be
273configured on the role. For example, if this backend is mounted at "alicloud",
274then "alicloud/creds/deploy" would generate access keys for the "deploy" role.
275
276The API key or STS credential will have a ttl associated with it. API keys can
277be renewed or revoked as described here:
278https://www.vaultproject.io/docs/concepts/lease.html,
279but STS credentials do not support renewal or revocation.
280`
281