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