1package awsauth
2
3import (
4	"context"
5	"fmt"
6
7	"github.com/hashicorp/vault/sdk/framework"
8	"github.com/hashicorp/vault/sdk/logical"
9)
10
11// awsStsEntry is used to store details of an STS role for assumption
12type awsStsEntry struct {
13	StsRole string `json:"sts_role"`
14}
15
16func pathListSts(b *backend) *framework.Path {
17	return &framework.Path{
18		Pattern: "config/sts/?",
19
20		Callbacks: map[logical.Operation]framework.OperationFunc{
21			logical.ListOperation: b.pathStsList,
22		},
23
24		HelpSynopsis:    pathListStsHelpSyn,
25		HelpDescription: pathListStsHelpDesc,
26	}
27}
28
29func pathConfigSts(b *backend) *framework.Path {
30	return &framework.Path{
31		Pattern: "config/sts/" + framework.GenericNameRegex("account_id"),
32		Fields: map[string]*framework.FieldSchema{
33			"account_id": {
34				Type: framework.TypeString,
35				Description: `AWS account ID to be associated with STS role. If set,
36Vault will use assumed credentials to verify any login attempts from EC2
37instances in this account.`,
38			},
39			"sts_role": {
40				Type: framework.TypeString,
41				Description: `AWS ARN for STS role to be assumed when interacting with the account specified.
42The Vault server must have permissions to assume this role.`,
43			},
44		},
45
46		ExistenceCheck: b.pathConfigStsExistenceCheck,
47
48		Callbacks: map[logical.Operation]framework.OperationFunc{
49			logical.CreateOperation: b.pathConfigStsCreateUpdate,
50			logical.UpdateOperation: b.pathConfigStsCreateUpdate,
51			logical.ReadOperation:   b.pathConfigStsRead,
52			logical.DeleteOperation: b.pathConfigStsDelete,
53		},
54
55		HelpSynopsis:    pathConfigStsSyn,
56		HelpDescription: pathConfigStsDesc,
57	}
58}
59
60// Establishes dichotomy of request operation between CreateOperation and UpdateOperation.
61// Returning 'true' forces an UpdateOperation, CreateOperation otherwise.
62func (b *backend) pathConfigStsExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
63	accountID := data.Get("account_id").(string)
64	if accountID == "" {
65		return false, fmt.Errorf("missing account_id")
66	}
67
68	entry, err := b.lockedAwsStsEntry(ctx, req.Storage, accountID)
69	if err != nil {
70		return false, err
71	}
72
73	return entry != nil, nil
74}
75
76// pathStsList is used to list all the AWS STS role configurations
77func (b *backend) pathStsList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
78	b.configMutex.RLock()
79	defer b.configMutex.RUnlock()
80	sts, err := req.Storage.List(ctx, "config/sts/")
81	if err != nil {
82		return nil, err
83	}
84	return logical.ListResponse(sts), nil
85}
86
87// nonLockedSetAwsStsEntry creates or updates an STS role association with the given accountID
88// This method does not acquire the write lock before creating or updating. If locking is
89// desired, use lockedSetAwsStsEntry instead
90func (b *backend) nonLockedSetAwsStsEntry(ctx context.Context, s logical.Storage, accountID string, stsEntry *awsStsEntry) error {
91	if accountID == "" {
92		return fmt.Errorf("missing AWS account ID")
93	}
94
95	if stsEntry == nil {
96		return fmt.Errorf("missing AWS STS Role ARN")
97	}
98
99	entry, err := logical.StorageEntryJSON("config/sts/"+accountID, stsEntry)
100	if err != nil {
101		return err
102	}
103
104	if entry == nil {
105		return fmt.Errorf("failed to create storage entry for AWS STS configuration")
106	}
107
108	return s.Put(ctx, entry)
109}
110
111// lockedSetAwsStsEntry creates or updates an STS role association with the given accountID
112// This method acquires the write lock before creating or updating the STS entry.
113func (b *backend) lockedSetAwsStsEntry(ctx context.Context, s logical.Storage, accountID string, stsEntry *awsStsEntry) error {
114	if accountID == "" {
115		return fmt.Errorf("missing AWS account ID")
116	}
117
118	if stsEntry == nil {
119		return fmt.Errorf("missing sts entry")
120	}
121
122	b.configMutex.Lock()
123	defer b.configMutex.Unlock()
124
125	return b.nonLockedSetAwsStsEntry(ctx, s, accountID, stsEntry)
126}
127
128// nonLockedAwsStsEntry returns the STS role associated with the given accountID.
129// This method does not acquire the read lock before returning information. If locking is
130// desired, use lockedAwsStsEntry instead
131func (b *backend) nonLockedAwsStsEntry(ctx context.Context, s logical.Storage, accountID string) (*awsStsEntry, error) {
132	entry, err := s.Get(ctx, "config/sts/"+accountID)
133	if err != nil {
134		return nil, err
135	}
136	if entry == nil {
137		return nil, nil
138	}
139	var stsEntry awsStsEntry
140	if err := entry.DecodeJSON(&stsEntry); err != nil {
141		return nil, err
142	}
143
144	return &stsEntry, nil
145}
146
147// lockedAwsStsEntry returns the STS role associated with the given accountID.
148// This method acquires the read lock before returning the association.
149func (b *backend) lockedAwsStsEntry(ctx context.Context, s logical.Storage, accountID string) (*awsStsEntry, error) {
150	b.configMutex.RLock()
151	defer b.configMutex.RUnlock()
152
153	return b.nonLockedAwsStsEntry(ctx, s, accountID)
154}
155
156// pathConfigStsRead is used to return information about an STS role/AWS accountID association
157func (b *backend) pathConfigStsRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
158	accountID := data.Get("account_id").(string)
159	if accountID == "" {
160		return logical.ErrorResponse("missing account id"), nil
161	}
162
163	stsEntry, err := b.lockedAwsStsEntry(ctx, req.Storage, accountID)
164	if err != nil {
165		return nil, err
166	}
167	if stsEntry == nil {
168		return nil, nil
169	}
170
171	return &logical.Response{
172		Data: map[string]interface{}{
173			"sts_role": stsEntry.StsRole,
174		},
175	}, nil
176}
177
178// pathConfigStsCreateUpdate is used to associate an STS role with a given AWS accountID
179func (b *backend) pathConfigStsCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
180	accountID := data.Get("account_id").(string)
181	if accountID == "" {
182		return logical.ErrorResponse("missing AWS account ID"), nil
183	}
184
185	b.configMutex.Lock()
186	defer b.configMutex.Unlock()
187
188	// Check if an STS role is already registered
189	stsEntry, err := b.nonLockedAwsStsEntry(ctx, req.Storage, accountID)
190	if err != nil {
191		return nil, err
192	}
193	if stsEntry == nil {
194		stsEntry = &awsStsEntry{}
195	}
196
197	// Check that an STS role has actually been provided
198	stsRole, ok := data.GetOk("sts_role")
199	if ok {
200		stsEntry.StsRole = stsRole.(string)
201	} else if req.Operation == logical.CreateOperation {
202		return logical.ErrorResponse("missing sts role"), nil
203	}
204
205	if stsEntry.StsRole == "" {
206		return logical.ErrorResponse("sts role cannot be empty"), nil
207	}
208
209	// save the provided STS role
210	if err := b.nonLockedSetAwsStsEntry(ctx, req.Storage, accountID, stsEntry); err != nil {
211		return nil, err
212	}
213
214	return nil, nil
215}
216
217// pathConfigStsDelete is used to delete a previously configured STS configuration
218func (b *backend) pathConfigStsDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
219	b.configMutex.Lock()
220	defer b.configMutex.Unlock()
221
222	accountID := data.Get("account_id").(string)
223	if accountID == "" {
224		return logical.ErrorResponse("missing account id"), nil
225	}
226
227	return nil, req.Storage.Delete(ctx, "config/sts/"+accountID)
228}
229
230const pathConfigStsSyn = `
231Specify STS roles to be assumed for certain AWS accounts.
232`
233
234const pathConfigStsDesc = `
235Allows the explicit association of STS roles to satellite AWS accounts (i.e. those
236which are not the account in which the Vault server is running.) Login attempts from
237EC2 instances running in these accounts will be verified using credentials obtained
238by assumption of these STS roles.
239
240The environment in which the Vault server resides must have access to assume the
241given STS roles.
242`
243const pathListStsHelpSyn = `
244List all the AWS account/STS role relationships registered with Vault.
245`
246
247const pathListStsHelpDesc = `
248AWS accounts will be listed by account ID, along with their respective role names.
249`
250