From 2559404e980db7e960688034fc487dc427c65cb8 Mon Sep 17 00:00:00 2001 From: Ben Ash Date: Tue, 27 Sep 2022 16:31:32 -0400 Subject: [PATCH] Rework first-class AWS login support --- go.mod | 6 +- go.sum | 17 +- internal/consts/consts.go | 16 +- internal/provider/auth_aws.go | 252 ++++++++++++++++++++------- internal/provider/auth_aws_test.go | 265 +++++++++++++++++++++++++++++ internal/provider/auth_generic.go | 10 +- internal/provider/meta.go | 12 ++ website/docs/index.html.markdown | 37 ++-- 8 files changed, 524 insertions(+), 91 deletions(-) create mode 100644 internal/provider/auth_aws_test.go diff --git a/go.mod b/go.mod index c0b9c6e37..867c8717b 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.2 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v0.3.1 github.com/Azure/go-autorest/autorest v0.11.24 - github.com/aws/aws-sdk-go v1.41.8 + github.com/aws/aws-sdk-go v1.44.106 github.com/cenkalti/backoff/v4 v4.1.2 github.com/containerd/containerd v1.6.6 // indirect github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f @@ -22,7 +22,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 - github.com/hashicorp/go-hclog v1.2.2 + github.com/hashicorp/go-hclog v1.3.1 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-retryablehttp v0.7.0 github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 @@ -36,10 +36,12 @@ require ( github.com/hashicorp/vault/api v1.8.0 github.com/hashicorp/vault/sdk v0.6.0 github.com/jcmturner/gokrb5/v8 v8.4.2 + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.5.0 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 + golang.org/x/sys v0.0.0-20220927170352-d9d178bc13c6 // indirect google.golang.org/api v0.96.0 google.golang.org/genproto v0.0.0-20220808131553-a91ffa7f803e ) diff --git a/go.sum b/go.sum index 3436263bf..2b4699088 100644 --- a/go.sum +++ b/go.sum @@ -277,8 +277,8 @@ github.com/aws/aws-sdk-go v1.25.39/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpi github.com/aws/aws-sdk-go v1.25.41/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.37.19/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.41.8 h1:j6imzwVyWQYuQxbkPmg2MdMmLB+Zw+U3Ewi59YF8Rwk= -github.com/aws/aws-sdk-go v1.41.8/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/aws/aws-sdk-go v1.44.106 h1:FzINxRGt0gAzz01ixtKfkjiDOnnpd/uNbstW/qPW2QE= +github.com/aws/aws-sdk-go v1.44.106/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go-v2 v1.8.0/go.mod h1:xEFuWz+3TYdlPRuo+CqATbeDWIWyaT5uAPwPaWtgse0= github.com/aws/aws-sdk-go-v2/config v1.6.0/go.mod h1:TNtBVmka80lRPk5+S9ZqVfFszOQAGJJ9KbT3EM3CHNU= github.com/aws/aws-sdk-go-v2/credentials v1.3.2/go.mod h1:PACKuTJdt6AlXvEq8rFI4eDmoqDFC5DpVKQbWysaDgM= @@ -940,8 +940,9 @@ github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39 github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.2.2 h1:ihRI7YFwcZdiSD7SIenIhHfQH3OuDvWerAUBZbeQS3M= github.com/hashicorp/go-hclog v1.2.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.3.1 h1:vDwF1DFNZhntP4DAjuTpOw3uEgMUpXh1pB5fW9DqHpo= +github.com/hashicorp/go-hclog v1.3.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -1314,8 +1315,9 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -1325,8 +1327,9 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= @@ -2211,8 +2214,10 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220927170352-d9d178bc13c6 h1:cy1ko5847T/lJ45eyg/7uLprIE/amW5IXxGtEnQdYMI= +golang.org/x/sys v0.0.0-20220927170352-d9d178bc13c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/internal/consts/consts.go b/internal/consts/consts.go index cd864fe6e..5343c4a4d 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -79,14 +79,22 @@ const ( FieldAuthLoginOIDC = "auth_login_oidc" FieldAuthLoginJWT = "auth_login_jwt" FieldAuthLoginAzure = "auth_login_azure" - FieldIdentity = "identity" - FieldSignature = "signature" - FieldPKCS7 = "pkcs7" - FieldNonce = "nonce" FieldIAMHttpRequestMethod = "iam_http_request_method" FieldIAMHttpRequestURL = "iam_http_request_url" FieldIAMHttpRequestBody = "iam_http_request_body" FieldIAMHttpRequestHeaders = "iam_http_request_headers" + FieldAWSAccessKeyID = "aws_access_key_id" + FieldAWSSecretAccessKey = "aws_secret_access_key" + FieldAWSSessionToken = "aws_session_token" + FieldAWSRoleARN = "aws_role_arn" + FieldAWSRoleSessionName = "aws_role_session_name" + FieldAWSWebIdentityTokenFile = "aws_web_identity_token_file" + FieldAWSSTSEndpoint = "aws_sts_endpoint" + FieldAWSIAMEndpoint = "aws_iam_endpoint" + FieldAWSProfile = "aws_profile" + FieldAWSRegion = "aws_region" + FieldAWSSharedCredentialsFile = "aws_shared_credentials_file" + FieldHeaderValue = "header_value" FieldDisableRemount = "disable_remount" FieldCACertFile = "ca_cert_file" FieldCACertDir = "ca_cert_dir" diff --git a/internal/provider/auth_aws.go b/internal/provider/auth_aws.go index 980226861..95db51809 100644 --- a/internal/provider/auth_aws.go +++ b/internal/provider/auth_aws.go @@ -2,11 +2,9 @@ package provider import ( "fmt" - "net/http" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-secure-stdlib/awsutil" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/vault/api" @@ -23,57 +21,96 @@ func GetAWSLoginSchema(authField string) *schema.Schema { } // GetAWSLoginSchemaResource for the AWS authentication engine. -func GetAWSLoginSchemaResource(_ string) *schema.Resource { +func GetAWSLoginSchemaResource(authField string) *schema.Resource { return mustAddLoginSchema(&schema.Resource{ Schema: map[string]*schema.Schema{ consts.FieldRole: { Type: schema.TypeString, - Optional: true, - Description: `The IAM role to use when logging into Vault.`, + Required: true, + Description: `The Vault role to use when logging into Vault.`, }, - consts.FieldIdentity: { + // static credential fields + consts.FieldAWSAccessKeyID: { Type: schema.TypeString, - Description: `The base64 encoded EC2 instance identity document.`, Optional: true, + Description: `The AWS access key ID.`, + DefaultFunc: schema.EnvDefaultFunc("AWS_ACCESS_KEY_ID", nil), }, - consts.FieldSignature: { - Type: schema.TypeString, - Description: `The base64 encoded SHA256 RSA signature of the instance identity document.`, - Optional: true, + consts.FieldAWSSecretAccessKey: { + Type: schema.TypeString, + Optional: true, + Description: `The AWS secret access key.`, + DefaultFunc: schema.EnvDefaultFunc("AWS_SECRET_ACCESS_KEY", nil), + RequiredWith: []string{fmt.Sprintf("%s.0.%s", authField, consts.FieldAWSAccessKeyID)}, }, - consts.FieldPKCS7: { + consts.FieldAWSSessionToken: { Type: schema.TypeString, - Description: `PKCS#7 signature of the identity document.`, Optional: true, + Description: `The AWS session token.`, + DefaultFunc: schema.EnvDefaultFunc("AWS_SESSION_TOKEN", nil), }, - consts.FieldNonce: { + consts.FieldAWSProfile: { Type: schema.TypeString, - Description: `The nonce to be used for subsequent login requests.`, Optional: true, + Description: `The name of the AWS profile.`, + DefaultFunc: schema.EnvDefaultFunc("AWS_PROFILE", nil), }, - consts.FieldIAMHttpRequestMethod: { + consts.FieldAWSSharedCredentialsFile: { Type: schema.TypeString, - Description: `The HTTP method used in the signed request.`, Optional: true, - Default: http.MethodPost, + Description: `Path to the AWS shared credentials file.`, + DefaultFunc: schema.EnvDefaultFunc("AWS_SHARED_CREDENTIALS_FILE", nil), }, - consts.FieldIAMHttpRequestURL: { - Type: schema.TypeString, - Description: `The base64 encoded HTTP URL used in the signed request.`, - Optional: true, + consts.FieldAWSWebIdentityTokenFile: { + Type: schema.TypeString, + Optional: true, + Description: `Path to the file containing an OAuth 2.0 access token or OpenID ` + + `Connect ID token.`, + DefaultFunc: schema.EnvDefaultFunc("AWS_WEB_IDENTITY_TOKEN_FILE", nil), + }, + // STS assume role fields + consts.FieldAWSRoleARN: { + Type: schema.TypeString, + Optional: true, + Description: `The ARN of the AWS Role to assume.` + + `Used during STS AssumeRole`, + DefaultFunc: schema.EnvDefaultFunc("AWS_ROLE_ARN", nil), + }, + consts.FieldAWSRoleSessionName: { + Type: schema.TypeString, + Optional: true, + Description: `Specifies the name to attach to the AWS role session. ` + + `Used during STS AssumeRole`, + DefaultFunc: schema.EnvDefaultFunc("AWS_ROLE_SESSION_NAME", nil), }, - consts.FieldIAMHttpRequestBody: { + consts.FieldAWSRegion: { Type: schema.TypeString, - Description: `The base64 encoded body of the signed request.`, Optional: true, + Description: `The AWS region.`, + DefaultFunc: schema.MultiEnvDefaultFunc( + []string{ + "AWS_REGION", + "AWS_DEFAULT_REGION", + }, + nil, + ), }, - consts.FieldIAMHttpRequestHeaders: { - Type: schema.TypeMap, + consts.FieldAWSSTSEndpoint: { + Type: schema.TypeString, + Optional: true, + Description: `The STS endpoint URL.`, + ValidateDiagFunc: GetValidateDiagURI([]string{"https", "http"}), + }, + consts.FieldAWSIAMEndpoint: { + Type: schema.TypeString, + Optional: true, + Description: `The IAM endpoint URL.`, + ValidateDiagFunc: GetValidateDiagURI([]string{"https", "http"}), + }, + consts.FieldHeaderValue: { + Type: schema.TypeString, Optional: true, - Description: `Mapping of extra IAM specific HTTP request login headers.`, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, + Description: `The Vault header value to include in the STS signing request.`, }, }, }, consts.MountTypeAWS) @@ -85,7 +122,27 @@ type AuthLoginAWS struct { AuthLoginCommon } -// LoginPath for the AWS authentication engine. +func (l *AuthLoginAWS) Init(d *schema.ResourceData, authField string) error { + if err := l.AuthLoginCommon.Init(d, authField); err != nil { + return err + } + + if err := l.checkRequiredFields(d, consts.FieldRole); err != nil { + return err + } + + return nil +} + +// MountPath for the aws authentication engine. +func (l *AuthLoginAWS) MountPath() string { + if l.mount == "" { + return l.Method() + } + return l.mount +} + +// LoginPath for the aws authentication engine. func (l *AuthLoginAWS) LoginPath() string { return fmt.Sprintf("auth/%s/login", l.MountPath()) } @@ -95,53 +152,123 @@ func (l *AuthLoginAWS) Method() string { return consts.AuthMethodAWS } -// Login using the AWS authentication engine. +// Login using the aws authentication engine. func (l *AuthLoginAWS) Login(client *api.Client) (*api.Secret, error) { - params := l.copyParamsExcluding( - consts.FieldNamespace, - consts.FieldMount, - consts.FieldPasswordFile, + params, err := l.copyParams( + consts.FieldRole, ) + if err != nil { + return nil, err + } - logger := hclog.Default() - if logging.IsDebugOrHigher() { - logger.SetLevel(hclog.Debug) - } else { - logger.SetLevel(hclog.Error) + loginData, err := l.getLoginData(getHCLogger()) + if err != nil { + return nil, fmt.Errorf("failed to get AWS credentials required for Vault login, err=%w", err) } - if err := signAWSLogin(l.params, logger); err != nil { - return nil, fmt.Errorf("error signing AWS login request: %s", err) + for k, v := range loginData { + params[k] = v } return l.login(client, l.LoginPath(), params) } +func (l *AuthLoginAWS) getLoginData(logger hclog.Logger) (map[string]interface{}, error) { + config, err := l.getCredentialsConfig(logger) + if err != nil { + return nil, err + } + + creds, err := config.GenerateCredentialChain() + if err != nil { + return nil, err + } + + var headerValue string + if v, ok := l.params[consts.FieldHeaderValue].(string); ok { + headerValue = v + } + + return awsutil.GenerateLoginData(creds, headerValue, config.Region, logger) +} + +func (l *AuthLoginAWS) getCredentialsConfig(logger hclog.Logger) (*awsutil.CredentialsConfig, error) { + // we do not leverage awsutil.Options here since awsutil.NewCredentialsConfig + // does not currently support all that we do. + config, err := awsutil.NewCredentialsConfig() + if err != nil { + return nil, err + } + if v, ok := l.params[consts.FieldAWSAccessKeyID].(string); ok && v != "" { + config.AccessKey = v + } + if v, ok := l.params[consts.FieldAWSSecretAccessKey].(string); ok && v != "" { + config.SecretKey = v + } + if v, ok := l.params[consts.FieldAWSProfile].(string); ok && v != "" { + config.Profile = v + } + if v, ok := l.params[consts.FieldAWSSharedCredentialsFile].(string); ok && v != "" { + config.Filename = v + } + if v, ok := l.params[consts.FieldAWSWebIdentityTokenFile].(string); ok && v != "" { + config.WebIdentityTokenFile = v + } + if v, ok := l.params[consts.FieldAWSRoleARN].(string); ok && v != "" { + config.RoleARN = v + } + if v, ok := l.params[consts.FieldAWSRoleSessionName].(string); ok && v != "" { + config.RoleSessionName = v + } + if v, ok := l.params[consts.FieldAWSRegion].(string); ok && v != "" { + config.Region = v + } + if v, ok := l.params[consts.FieldAWSSessionToken].(string); ok && v != "" { + config.SessionToken = v + } + if v, ok := l.params[consts.FieldAWSSTSEndpoint].(string); ok && v != "" { + config.STSEndpoint = v + } + if v, ok := l.params[consts.FieldAWSIAMEndpoint].(string); ok && v != "" { + config.IAMEndpoint = v + } + + return config, nil +} + +// signAWSLogin is for use by the generic auth method func signAWSLogin(parameters map[string]interface{}, logger hclog.Logger) error { - var accessKey, secretKey, securityToken string - if val, ok := parameters["aws_access_key_id"].(string); ok { - accessKey = val + var accessKey string + if v, ok := parameters[consts.FieldAWSAccessKeyID].(string); ok { + accessKey = v } - if val, ok := parameters["aws_secret_access_key"].(string); ok { - secretKey = val + var secretKey string + if v, ok := parameters[consts.FieldAWSSecretAccessKey].(string); ok { + secretKey = v } - if val, ok := parameters["aws_security_token"].(string); ok { - securityToken = val + var sessionToken string + if v, ok := parameters[consts.FieldAWSSessionToken].(string); ok { + sessionToken = v + } else if v, ok := parameters["aws_security_token"].(string); ok { + // this is actually wrong, this should be the session token, + // leaving this here so that it does not break any pre-existing configurations. + sessionToken = v } - creds, err := awsutil.RetrieveCreds(accessKey, secretKey, securityToken, logger) + creds, err := awsutil.RetrieveCreds(accessKey, secretKey, sessionToken, logger) if err != nil { return fmt.Errorf("failed to retrieve AWS credentials: %s", err) } - var headerValue, stsRegion string - if val, ok := parameters["header_value"].(string); ok { - headerValue = val + var headerValue string + if v, ok := parameters[consts.FieldHeaderValue].(string); ok { + headerValue = v } - if val, ok := parameters["sts_region"].(string); ok { - stsRegion = val + var stsRegion string + if v, ok := parameters["sts_region"].(string); ok { + stsRegion = v } loginData, err := awsutil.GenerateLoginData(creds, headerValue, stsRegion, logger) @@ -149,10 +276,15 @@ func signAWSLogin(parameters map[string]interface{}, logger hclog.Logger) error return fmt.Errorf("failed to generate AWS login data: %s", err) } - parameters["iam_http_request_method"] = loginData["iam_http_request_method"] - parameters["iam_request_url"] = loginData["iam_request_url"] - parameters["iam_request_headers"] = loginData["iam_request_headers"] - parameters["iam_request_body"] = loginData["iam_request_body"] + headerFields := []string{ + consts.FieldIAMHttpRequestMethod, + consts.FieldIAMHttpRequestURL, + consts.FieldIAMHttpRequestBody, + consts.FieldIAMHttpRequestHeaders, + } + for _, k := range headerFields { + parameters[k] = loginData[k] + } return nil } diff --git a/internal/provider/auth_aws_test.go b/internal/provider/auth_aws_test.go new file mode 100644 index 000000000..a33309047 --- /dev/null +++ b/internal/provider/auth_aws_test.go @@ -0,0 +1,265 @@ +package provider + +import ( + "fmt" + "reflect" + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-secure-stdlib/awsutil" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" +) + +func TestAuthLoginAWS_Init(t *testing.T) { + tests := []struct { + name string + authField string + raw map[string]interface{} + wantErr bool + expectParams map[string]interface{} + expectErr error + }{ + { + name: "basic", + authField: consts.FieldAuthLoginAWS, + raw: map[string]interface{}{ + consts.FieldAuthLoginAWS: []interface{}{ + map[string]interface{}{ + consts.FieldNamespace: "ns1", + consts.FieldRole: "alice", + consts.FieldAWSAccessKeyID: "key-id", + consts.FieldAWSSecretAccessKey: "sa-key", + consts.FieldAWSSessionToken: "session-token", + consts.FieldAWSIAMEndpoint: "iam.us-east-2.amazonaws.com", + consts.FieldAWSSTSEndpoint: "sts.us-east-2.amazonaws.com", + consts.FieldAWSRegion: "us-east-2", + consts.FieldAWSSharedCredentialsFile: "credentials", + consts.FieldAWSProfile: "profile1", + consts.FieldAWSRoleARN: "role-arn", + consts.FieldAWSRoleSessionName: "session1", + consts.FieldAWSWebIdentityTokenFile: "web-token", + consts.FieldHeaderValue: "header1", + }, + }, + }, + expectParams: map[string]interface{}{ + consts.FieldNamespace: "ns1", + consts.FieldRole: "alice", + consts.FieldMount: consts.MountTypeAWS, + consts.FieldAWSAccessKeyID: "key-id", + consts.FieldAWSSecretAccessKey: "sa-key", + consts.FieldAWSSessionToken: "session-token", + consts.FieldAWSIAMEndpoint: "iam.us-east-2.amazonaws.com", + consts.FieldAWSSTSEndpoint: "sts.us-east-2.amazonaws.com", + consts.FieldAWSRegion: "us-east-2", + consts.FieldAWSSharedCredentialsFile: "credentials", + consts.FieldAWSProfile: "profile1", + consts.FieldAWSRoleARN: "role-arn", + consts.FieldAWSRoleSessionName: "session1", + consts.FieldAWSWebIdentityTokenFile: "web-token", + consts.FieldHeaderValue: "header1", + }, + wantErr: false, + }, + { + name: "error-missing-resource", + authField: consts.FieldAuthLoginAWS, + expectParams: nil, + wantErr: true, + expectErr: fmt.Errorf("resource data missing field %q", consts.FieldAuthLoginAWS), + }, + { + name: "error-missing-required", + authField: consts.FieldAuthLoginAWS, + raw: map[string]interface{}{ + consts.FieldAuthLoginAWS: []interface{}{ + map[string]interface{}{}, + }, + }, + expectParams: nil, + wantErr: true, + expectErr: fmt.Errorf("required fields are unset: %v", []string{ + consts.FieldRole, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := map[string]*schema.Schema{ + tt.authField: GetAWSLoginSchema(tt.authField), + } + + d := schema.TestResourceDataRaw(t, s, tt.raw) + l := &AuthLoginAWS{} + err := l.Init(d, tt.authField) + if (err != nil) != tt.wantErr { + t.Fatalf("Init() error = %v, wantErr %v", err, tt.wantErr) + } + + if err != nil { + if tt.expectErr != nil { + if !reflect.DeepEqual(tt.expectErr, err) { + t.Errorf("Init() expected error %#v, actual %#v", tt.expectErr, err) + } + } + } else { + if !reflect.DeepEqual(tt.expectParams, l.params) { + t.Errorf("Init() expected params %#v, actual %#v", tt.expectParams, l.params) + } + } + }) + } +} + +func TestAuthLoginAWS_LoginPath(t *testing.T) { + type fields struct { + AuthLoginCommon AuthLoginCommon + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "default", + fields: fields{ + AuthLoginCommon: AuthLoginCommon{ + params: map[string]interface{}{ + consts.FieldRole: "alice", + }, + }, + }, + want: "auth/aws/login", + }, + { + name: "other", + fields: fields{ + AuthLoginCommon: AuthLoginCommon{ + mount: "other", + params: map[string]interface{}{ + consts.FieldRole: "alice", + }, + }, + }, + want: "auth/other/login", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &AuthLoginAWS{ + AuthLoginCommon: tt.fields.AuthLoginCommon, + } + if got := l.LoginPath(); got != tt.want { + t.Errorf("LoginPath() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAuthLoginAWS_getCredentialsConfig(t *testing.T) { + type fields struct { + AuthLoginCommon AuthLoginCommon + } + tests := []struct { + name string + fields fields + logger hclog.Logger + want *awsutil.CredentialsConfig + wantErr bool + }{ + { + name: "static-creds", + fields: fields{ + AuthLoginCommon: AuthLoginCommon{ + params: map[string]interface{}{ + consts.FieldAWSAccessKeyID: "key-id", + consts.FieldAWSSecretAccessKey: "sa-key", + }, + }, + }, + logger: hclog.NewNullLogger(), + want: &awsutil.CredentialsConfig{ + Region: "us-east-1", + AccessKey: "key-id", + SecretKey: "sa-key", + }, + wantErr: false, + }, + { + name: "static-creds-with-profile", + fields: fields{ + AuthLoginCommon: AuthLoginCommon{ + params: map[string]interface{}{ + consts.FieldAWSSharedCredentialsFile: "credentials", + consts.FieldAWSProfile: "profile1", + }, + }, + }, + logger: hclog.NewNullLogger(), + want: &awsutil.CredentialsConfig{ + Region: "us-east-1", + Filename: "credentials", + Profile: "profile1", + }, + wantErr: false, + }, + { + name: "all", + fields: fields{ + AuthLoginCommon: AuthLoginCommon{ + params: map[string]interface{}{ + consts.FieldAWSAccessKeyID: "key-id", + consts.FieldAWSSecretAccessKey: "sa-key", + consts.FieldAWSSessionToken: "session-token", + consts.FieldAWSIAMEndpoint: "iam.us-east-2.amazonaws.com", + consts.FieldAWSSTSEndpoint: "sts.us-east-2.amazonaws.com", + consts.FieldAWSRegion: "us-east-2", + consts.FieldAWSSharedCredentialsFile: "credentials", + consts.FieldAWSProfile: "profile1", + consts.FieldAWSRoleARN: "role-arn", + consts.FieldAWSRoleSessionName: "session1", + consts.FieldAWSWebIdentityTokenFile: "web-token", + }, + }, + }, + logger: hclog.NewNullLogger(), + want: &awsutil.CredentialsConfig{ + AccessKey: "key-id", + SecretKey: "sa-key", + SessionToken: "session-token", + IAMEndpoint: "iam.us-east-2.amazonaws.com", + STSEndpoint: "sts.us-east-2.amazonaws.com", + Region: "us-east-2", + Filename: "credentials", + Profile: "profile1", + RoleARN: "role-arn", + RoleSessionName: "session1", + WebIdentityTokenFile: "web-token", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &AuthLoginAWS{ + AuthLoginCommon: tt.fields.AuthLoginCommon, + } + got, err := l.getCredentialsConfig(tt.logger) + if (err != nil) != tt.wantErr { + t.Errorf("getCredentialsConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if got.HTTPClient == nil { + t.Errorf("getCredentialsConfig() HTTPClient not initialized") + } + // set HTTPClient to nil + got.HTTPClient = nil + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getCredentialsConfig() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/provider/auth_generic.go b/internal/provider/auth_generic.go index a1fc9ce70..c409a76cb 100644 --- a/internal/provider/auth_generic.go +++ b/internal/provider/auth_generic.go @@ -3,8 +3,6 @@ package provider import ( "fmt" - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/vault/api" @@ -99,13 +97,7 @@ func (l *AuthLoginGeneric) Login(client *api.Client) (*api.Secret, error) { // the AWS auth method was previously handled by the auth_login generic // resource. case consts.AuthMethodAWS: - logger := hclog.Default() - if logging.IsDebugOrHigher() { - logger.SetLevel(hclog.Debug) - } else { - logger.SetLevel(hclog.Error) - } - if err := signAWSLogin(params, logger); err != nil { + if err := signAWSLogin(params, getHCLogger()); err != nil { return nil, fmt.Errorf("error signing AWS login request: %s", err) } } diff --git a/internal/provider/meta.go b/internal/provider/meta.go index 35cfc9a6e..142eab657 100644 --- a/internal/provider/meta.go +++ b/internal/provider/meta.go @@ -9,7 +9,9 @@ import ( "strings" "sync" + "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/vault/api" @@ -421,3 +423,13 @@ func GetToken(d *schema.ResourceData) (string, error) { } return strings.TrimSpace(token), nil } + +func getHCLogger() hclog.Logger { + logger := hclog.Default() + if logging.IsDebugOrHigher() { + logger.SetLevel(hclog.Debug) + } else { + logger.SetLevel(hclog.Error) + } + return logger +} diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index 7fce0af67..cc3dd4513 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -266,24 +266,41 @@ The `auth_login_aws` configuration block accepts the following arguments: * `mount` - (Optional) The name of the authentication engine mount. Default: `aws` -* `role` - (Optional) The IAM role to use when logging into Vault. +* `role` - (Required) The name of the role against which the login is being attempted. + +* `aws_access_key_id` - (Optional) The AWS access key ID. + *Can be specified with the `AWS_ACCESS_KEY_ID` environment variable.* + +* `aws_secret_access_key` - (Optional) The AWS secret access key. + *Can be specified with the `AWS_SECRET_ACCESS_KEY` environment variable.* + +* `aws_session_token` - (Optional) The AWS session token. + *Can be specified with the `AWS_SESSION_TOKEN` environment variable.* + +* `aws_profile` - (Optional) The name of the AWS profile. + *Can be specified with the `AWS_PROFILE` environment variable.* -* `identity` - (Optional) The base64 encoded EC2 instance identity document. +* `aws_shared_credentials_file` - (Optional) Path to the AWS shared credentials file. + *Can be specified with the `AWS_SHARED_CREDENTIALS_FILE` environment variable.* -* `signature` - (Optional) The base64 encoded SHA256 RSA signature of the instance identity document. +* `aws_web_identity_token_file` - (Optional) Path to the file containing an OAuth 2.0 access token or OpenID + Connect ID token. + *Can be specified with the `AWS_WEB_IDENTITY_TOKEN_FILE` environment variable.* -* `pkcs7` - (Optional) PKCS#7 signature of the identity document. +* `aws_region` - (Optional) The AWS region. + *Can be specified with the `AWS_REGION` or `AWS_DEFAULT_REGION` environment variables.* -* `nonce` - (Optional) The nonce to be used for subsequent login requests. +* `aws_role_arn` - (Optional) The ARN of the AWS Role to assume. *Used during STS AssumeRole* + *Can be specified with the `AWS_ROLE_ARN` environment variable.* -* `iam_http_request_method` - (Optional) The HTTP method used in the signed request. - `POST` is is the only supported method. +* `aws_role_session_name` - (Optional) Specifies the name to attach to the AWS role session. *Used during STS AssumeRole* + *Can be specified with the `AWS_ROLE_SESSION_NAME` environment variable.* -* `iam_http_request_url` - (Optional) The base64 encoded HTTP URL used in the signed request. +* `aws_sts_endpoint` - (Optional) The STS endpoint URL. -* `iam_http_request_body` - (Optional) The base64 encoded body of the signed request. +* `aws_iam_endpoint` - (Optional) The IAM endpoint URL. -* `iam_http_request_headers` - (Optional) Mapping of extra IAM specific HTTP request login headers. +* `header_value` - (Optional) The Vault header value to include in the STS signing request. ### TLS Certificate