1package awsauth
2
3import (
4	"context"
5
6	"github.com/aws/aws-sdk-go/aws"
7	"github.com/hashicorp/vault/sdk/framework"
8	"github.com/hashicorp/vault/sdk/logical"
9)
10
11func (b *backend) pathConfigClient() *framework.Path {
12	return &framework.Path{
13		Pattern: "config/client$",
14		Fields: map[string]*framework.FieldSchema{
15			"access_key": {
16				Type:        framework.TypeString,
17				Default:     "",
18				Description: "AWS Access Key ID for the account used to make AWS API requests.",
19			},
20
21			"secret_key": {
22				Type:        framework.TypeString,
23				Default:     "",
24				Description: "AWS Secret Access Key for the account used to make AWS API requests.",
25			},
26
27			"endpoint": {
28				Type:        framework.TypeString,
29				Default:     "",
30				Description: "URL to override the default generated endpoint for making AWS EC2 API calls.",
31			},
32
33			"iam_endpoint": {
34				Type:        framework.TypeString,
35				Default:     "",
36				Description: "URL to override the default generated endpoint for making AWS IAM API calls.",
37			},
38
39			"sts_endpoint": {
40				Type:        framework.TypeString,
41				Default:     "",
42				Description: "URL to override the default generated endpoint for making AWS STS API calls.",
43			},
44
45			"iam_server_id_header_value": {
46				Type:        framework.TypeString,
47				Default:     "",
48				Description: "Value to require in the X-Vault-AWS-IAM-Server-ID request header",
49			},
50			"max_retries": {
51				Type:        framework.TypeInt,
52				Default:     aws.UseServiceDefaultRetries,
53				Description: "Maximum number of retries for recoverable exceptions of AWS APIs",
54			},
55		},
56
57		ExistenceCheck: b.pathConfigClientExistenceCheck,
58
59		Operations: map[logical.Operation]framework.OperationHandler{
60			logical.CreateOperation: &framework.PathOperation{
61				Callback: b.pathConfigClientCreateUpdate,
62			},
63			logical.UpdateOperation: &framework.PathOperation{
64				Callback: b.pathConfigClientCreateUpdate,
65			},
66			logical.DeleteOperation: &framework.PathOperation{
67				Callback: b.pathConfigClientDelete,
68			},
69			logical.ReadOperation: &framework.PathOperation{
70				Callback: b.pathConfigClientRead,
71			},
72		},
73
74		HelpSynopsis:    pathConfigClientHelpSyn,
75		HelpDescription: pathConfigClientHelpDesc,
76	}
77}
78
79// Establishes dichotomy of request operation between CreateOperation and UpdateOperation.
80// Returning 'true' forces an UpdateOperation, CreateOperation otherwise.
81func (b *backend) pathConfigClientExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
82	entry, err := b.lockedClientConfigEntry(ctx, req.Storage)
83	if err != nil {
84		return false, err
85	}
86	return entry != nil, nil
87}
88
89// Fetch the client configuration required to access the AWS API, after acquiring an exclusive lock.
90func (b *backend) lockedClientConfigEntry(ctx context.Context, s logical.Storage) (*clientConfig, error) {
91	b.configMutex.RLock()
92	defer b.configMutex.RUnlock()
93
94	return b.nonLockedClientConfigEntry(ctx, s)
95}
96
97// Fetch the client configuration required to access the AWS API.
98func (b *backend) nonLockedClientConfigEntry(ctx context.Context, s logical.Storage) (*clientConfig, error) {
99	entry, err := s.Get(ctx, "config/client")
100	if err != nil {
101		return nil, err
102	}
103	if entry == nil {
104		return nil, nil
105	}
106
107	var result clientConfig
108	if err := entry.DecodeJSON(&result); err != nil {
109		return nil, err
110	}
111	return &result, nil
112}
113
114func (b *backend) pathConfigClientRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
115	clientConfig, err := b.lockedClientConfigEntry(ctx, req.Storage)
116	if err != nil {
117		return nil, err
118	}
119
120	if clientConfig == nil {
121		return nil, nil
122	}
123
124	return &logical.Response{
125		Data: map[string]interface{}{
126			"access_key":                 clientConfig.AccessKey,
127			"endpoint":                   clientConfig.Endpoint,
128			"iam_endpoint":               clientConfig.IAMEndpoint,
129			"sts_endpoint":               clientConfig.STSEndpoint,
130			"iam_server_id_header_value": clientConfig.IAMServerIdHeaderValue,
131			"max_retries":                clientConfig.MaxRetries,
132		},
133	}, nil
134}
135
136func (b *backend) pathConfigClientDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
137	b.configMutex.Lock()
138	defer b.configMutex.Unlock()
139
140	if err := req.Storage.Delete(ctx, "config/client"); err != nil {
141		return nil, err
142	}
143
144	// Remove all the cached EC2 client objects in the backend.
145	b.flushCachedEC2Clients()
146
147	// Remove all the cached EC2 client objects in the backend.
148	b.flushCachedIAMClients()
149
150	// unset the cached default AWS account ID
151	b.defaultAWSAccountID = ""
152
153	return nil, nil
154}
155
156// pathConfigClientCreateUpdate is used to register the 'aws_secret_key' and 'aws_access_key'
157// that can be used to interact with AWS EC2 API.
158func (b *backend) pathConfigClientCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
159	b.configMutex.Lock()
160	defer b.configMutex.Unlock()
161
162	configEntry, err := b.nonLockedClientConfigEntry(ctx, req.Storage)
163	if err != nil {
164		return nil, err
165	}
166	if configEntry == nil {
167		configEntry = &clientConfig{}
168	}
169
170	// changedCreds is whether we need to flush the cached AWS clients and store in the backend
171	changedCreds := false
172	// changedOtherConfig is whether other config has changed that requires storing in the backend
173	// but does not require flushing the cached clients
174	changedOtherConfig := false
175
176	accessKeyStr, ok := data.GetOk("access_key")
177	if ok {
178		if configEntry.AccessKey != accessKeyStr.(string) {
179			changedCreds = true
180			configEntry.AccessKey = accessKeyStr.(string)
181		}
182	} else if req.Operation == logical.CreateOperation {
183		// Use the default
184		configEntry.AccessKey = data.Get("access_key").(string)
185	}
186
187	secretKeyStr, ok := data.GetOk("secret_key")
188	if ok {
189		if configEntry.SecretKey != secretKeyStr.(string) {
190			changedCreds = true
191			configEntry.SecretKey = secretKeyStr.(string)
192		}
193	} else if req.Operation == logical.CreateOperation {
194		configEntry.SecretKey = data.Get("secret_key").(string)
195	}
196
197	endpointStr, ok := data.GetOk("endpoint")
198	if ok {
199		if configEntry.Endpoint != endpointStr.(string) {
200			changedCreds = true
201			configEntry.Endpoint = endpointStr.(string)
202		}
203	} else if req.Operation == logical.CreateOperation {
204		configEntry.Endpoint = data.Get("endpoint").(string)
205	}
206
207	iamEndpointStr, ok := data.GetOk("iam_endpoint")
208	if ok {
209		if configEntry.IAMEndpoint != iamEndpointStr.(string) {
210			changedCreds = true
211			configEntry.IAMEndpoint = iamEndpointStr.(string)
212		}
213	} else if req.Operation == logical.CreateOperation {
214		configEntry.IAMEndpoint = data.Get("iam_endpoint").(string)
215	}
216
217	stsEndpointStr, ok := data.GetOk("sts_endpoint")
218	if ok {
219		if configEntry.STSEndpoint != stsEndpointStr.(string) {
220			// We don't directly cache STS clients as they are ever directly used.
221			// However, they are potentially indirectly used as credential providers
222			// for the EC2 and IAM clients, and thus we would be indirectly caching
223			// them there. So, if we change the STS endpoint, we should flush those
224			// cached clients.
225			changedCreds = true
226			configEntry.STSEndpoint = stsEndpointStr.(string)
227		}
228	} else if req.Operation == logical.CreateOperation {
229		configEntry.STSEndpoint = data.Get("sts_endpoint").(string)
230	}
231
232	headerValStr, ok := data.GetOk("iam_server_id_header_value")
233	if ok {
234		if configEntry.IAMServerIdHeaderValue != headerValStr.(string) {
235			// NOT setting changedCreds here, since this isn't really cached
236			configEntry.IAMServerIdHeaderValue = headerValStr.(string)
237			changedOtherConfig = true
238		}
239	} else if req.Operation == logical.CreateOperation {
240		configEntry.IAMServerIdHeaderValue = data.Get("iam_server_id_header_value").(string)
241	}
242
243	maxRetriesInt, ok := data.GetOk("max_retries")
244	if ok {
245		configEntry.MaxRetries = maxRetriesInt.(int)
246		changedOtherConfig = true
247	} else if req.Operation == logical.CreateOperation {
248		configEntry.MaxRetries = data.Get("max_retries").(int)
249	}
250
251	// Since this endpoint supports both create operation and update operation,
252	// the error checks for access_key and secret_key not being set are not present.
253	// This allows calling this endpoint multiple times to provide the values.
254	// Hence, the readers of this endpoint should do the validation on
255	// the validation of keys before using them.
256	entry, err := logical.StorageEntryJSON("config/client", configEntry)
257	if err != nil {
258		return nil, err
259	}
260
261	if changedCreds || changedOtherConfig || req.Operation == logical.CreateOperation {
262		if err := req.Storage.Put(ctx, entry); err != nil {
263			return nil, err
264		}
265	}
266
267	if changedCreds {
268		b.flushCachedEC2Clients()
269		b.flushCachedIAMClients()
270		b.defaultAWSAccountID = ""
271	}
272
273	return nil, nil
274}
275
276// Struct to hold 'aws_access_key' and 'aws_secret_key' that are required to
277// interact with the AWS EC2 API.
278type clientConfig struct {
279	AccessKey              string `json:"access_key"`
280	SecretKey              string `json:"secret_key"`
281	Endpoint               string `json:"endpoint"`
282	IAMEndpoint            string `json:"iam_endpoint"`
283	STSEndpoint            string `json:"sts_endpoint"`
284	IAMServerIdHeaderValue string `json:"iam_server_id_header_value"`
285	MaxRetries             int    `json:"max_retries"`
286}
287
288const pathConfigClientHelpSyn = `
289Configure AWS IAM credentials that are used to query instance and role details from the AWS API.
290`
291
292const pathConfigClientHelpDesc = `
293The aws-ec2 auth method makes AWS API queries to retrieve information
294regarding EC2 instances that perform login operations. The 'aws_secret_key' and
295'aws_access_key' parameters configured here should map to an AWS IAM user that
296has permission to make the following API queries:
297
298* ec2:DescribeInstances
299* iam:GetInstanceProfile (if IAM Role binding is used)
300`
301