open-vault/builtin/credential/aws/path_config_client.go

293 lines
9.6 KiB
Go

package awsauth
import (
"context"
"github.com/aws/aws-sdk-go/aws"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathConfigClient(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/client$",
Fields: map[string]*framework.FieldSchema{
"access_key": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "AWS Access Key ID for the account used to make AWS API requests.",
},
"secret_key": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "AWS Secret Access Key for the account used to make AWS API requests.",
},
"endpoint": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "URL to override the default generated endpoint for making AWS EC2 API calls.",
},
"iam_endpoint": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "URL to override the default generated endpoint for making AWS IAM API calls.",
},
"sts_endpoint": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "URL to override the default generated endpoint for making AWS STS API calls.",
},
"iam_server_id_header_value": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "Value to require in the X-Vault-AWS-IAM-Server-ID request header",
},
"max_retries": &framework.FieldSchema{
Type: framework.TypeInt,
Default: aws.UseServiceDefaultRetries,
Description: "Maximum number of retries for recoverable exceptions of AWS APIs",
},
},
ExistenceCheck: b.pathConfigClientExistenceCheck,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.CreateOperation: b.pathConfigClientCreateUpdate,
logical.UpdateOperation: b.pathConfigClientCreateUpdate,
logical.DeleteOperation: b.pathConfigClientDelete,
logical.ReadOperation: b.pathConfigClientRead,
},
HelpSynopsis: pathConfigClientHelpSyn,
HelpDescription: pathConfigClientHelpDesc,
}
}
// Establishes dichotomy of request operation between CreateOperation and UpdateOperation.
// Returning 'true' forces an UpdateOperation, CreateOperation otherwise.
func (b *backend) pathConfigClientExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
entry, err := b.lockedClientConfigEntry(ctx, req.Storage)
if err != nil {
return false, err
}
return entry != nil, nil
}
// Fetch the client configuration required to access the AWS API, after acquiring an exclusive lock.
func (b *backend) lockedClientConfigEntry(ctx context.Context, s logical.Storage) (*clientConfig, error) {
b.configMutex.RLock()
defer b.configMutex.RUnlock()
return b.nonLockedClientConfigEntry(ctx, s)
}
// Fetch the client configuration required to access the AWS API.
func (b *backend) nonLockedClientConfigEntry(ctx context.Context, s logical.Storage) (*clientConfig, error) {
entry, err := s.Get(ctx, "config/client")
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result clientConfig
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathConfigClientRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
clientConfig, err := b.lockedClientConfigEntry(ctx, req.Storage)
if err != nil {
return nil, err
}
if clientConfig == nil {
return nil, nil
}
return &logical.Response{
Data: map[string]interface{}{
"access_key": clientConfig.AccessKey,
"endpoint": clientConfig.Endpoint,
"iam_endpoint": clientConfig.IAMEndpoint,
"sts_endpoint": clientConfig.STSEndpoint,
"iam_server_id_header_value": clientConfig.IAMServerIdHeaderValue,
"max_retries": clientConfig.MaxRetries,
},
}, nil
}
func (b *backend) pathConfigClientDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.configMutex.Lock()
defer b.configMutex.Unlock()
if err := req.Storage.Delete(ctx, "config/client"); err != nil {
return nil, err
}
// Remove all the cached EC2 client objects in the backend.
b.flushCachedEC2Clients()
// Remove all the cached EC2 client objects in the backend.
b.flushCachedIAMClients()
// unset the cached default AWS account ID
b.defaultAWSAccountID = ""
return nil, nil
}
// pathConfigClientCreateUpdate is used to register the 'aws_secret_key' and 'aws_access_key'
// that can be used to interact with AWS EC2 API.
func (b *backend) pathConfigClientCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.configMutex.Lock()
defer b.configMutex.Unlock()
configEntry, err := b.nonLockedClientConfigEntry(ctx, req.Storage)
if err != nil {
return nil, err
}
if configEntry == nil {
configEntry = &clientConfig{}
}
// changedCreds is whether we need to flush the cached AWS clients and store in the backend
changedCreds := false
// changedOtherConfig is whether other config has changed that requires storing in the backend
// but does not require flushing the cached clients
changedOtherConfig := false
accessKeyStr, ok := data.GetOk("access_key")
if ok {
if configEntry.AccessKey != accessKeyStr.(string) {
changedCreds = true
configEntry.AccessKey = accessKeyStr.(string)
}
} else if req.Operation == logical.CreateOperation {
// Use the default
configEntry.AccessKey = data.Get("access_key").(string)
}
secretKeyStr, ok := data.GetOk("secret_key")
if ok {
if configEntry.SecretKey != secretKeyStr.(string) {
changedCreds = true
configEntry.SecretKey = secretKeyStr.(string)
}
} else if req.Operation == logical.CreateOperation {
configEntry.SecretKey = data.Get("secret_key").(string)
}
endpointStr, ok := data.GetOk("endpoint")
if ok {
if configEntry.Endpoint != endpointStr.(string) {
changedCreds = true
configEntry.Endpoint = endpointStr.(string)
}
} else if req.Operation == logical.CreateOperation {
configEntry.Endpoint = data.Get("endpoint").(string)
}
iamEndpointStr, ok := data.GetOk("iam_endpoint")
if ok {
if configEntry.IAMEndpoint != iamEndpointStr.(string) {
changedCreds = true
configEntry.IAMEndpoint = iamEndpointStr.(string)
}
} else if req.Operation == logical.CreateOperation {
configEntry.IAMEndpoint = data.Get("iam_endpoint").(string)
}
stsEndpointStr, ok := data.GetOk("sts_endpoint")
if ok {
if configEntry.STSEndpoint != stsEndpointStr.(string) {
// We don't directly cache STS clients as they are ever directly used.
// However, they are potentially indirectly used as credential providers
// for the EC2 and IAM clients, and thus we would be indirectly caching
// them there. So, if we change the STS endpoint, we should flush those
// cached clients.
changedCreds = true
configEntry.STSEndpoint = stsEndpointStr.(string)
}
} else if req.Operation == logical.CreateOperation {
configEntry.STSEndpoint = data.Get("sts_endpoint").(string)
}
headerValStr, ok := data.GetOk("iam_server_id_header_value")
if ok {
if configEntry.IAMServerIdHeaderValue != headerValStr.(string) {
// NOT setting changedCreds here, since this isn't really cached
configEntry.IAMServerIdHeaderValue = headerValStr.(string)
changedOtherConfig = true
}
} else if req.Operation == logical.CreateOperation {
configEntry.IAMServerIdHeaderValue = data.Get("iam_server_id_header_value").(string)
}
maxRetriesInt, ok := data.GetOk("max_retries")
if ok {
configEntry.MaxRetries = maxRetriesInt.(int)
changedOtherConfig = true
} else if req.Operation == logical.CreateOperation {
configEntry.MaxRetries = data.Get("max_retries").(int)
}
// Since this endpoint supports both create operation and update operation,
// the error checks for access_key and secret_key not being set are not present.
// This allows calling this endpoint multiple times to provide the values.
// Hence, the readers of this endpoint should do the validation on
// the validation of keys before using them.
entry, err := logical.StorageEntryJSON("config/client", configEntry)
if err != nil {
return nil, err
}
if changedCreds || changedOtherConfig || req.Operation == logical.CreateOperation {
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
}
if changedCreds {
b.flushCachedEC2Clients()
b.flushCachedIAMClients()
b.defaultAWSAccountID = ""
}
return nil, nil
}
// Struct to hold 'aws_access_key' and 'aws_secret_key' that are required to
// interact with the AWS EC2 API.
type clientConfig struct {
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
Endpoint string `json:"endpoint"`
IAMEndpoint string `json:"iam_endpoint"`
STSEndpoint string `json:"sts_endpoint"`
IAMServerIdHeaderValue string `json:"iam_server_id_header_value"`
MaxRetries int `json:"max_retries"`
}
const pathConfigClientHelpSyn = `
Configure AWS IAM credentials that are used to query instance and role details from the AWS API.
`
const pathConfigClientHelpDesc = `
The aws-ec2 auth method makes AWS API queries to retrieve information
regarding EC2 instances that perform login operations. The 'aws_secret_key' and
'aws_access_key' parameters configured here should map to an AWS IAM user that
has permission to make the following API queries:
* ec2:DescribeInstances
* iam:GetInstanceProfile (if IAM Role binding is used)
`