Adds support for `sso` endpoint override

f-sso-endpoint
Graham Davison 3 years ago
parent 63fdeec004
commit fce0cf6d9c

@ -275,3 +275,5 @@ require (
)
go 1.21.3
replace github.com/hashicorp/aws-sdk-go-base/v2 => github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.38.0.20231103062846-b877e512ee57

@ -645,8 +645,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rH
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.38 h1:C5DvIFGNn7Lhu8SV6PAUY5WNq3aPYYqdnC4PT1tJc1o=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.38/go.mod h1:zjTe61MBV+nvdnW4MDP1NBFEC6qyCbYEd9tI0x8FY5s=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.38.0.20231103062846-b877e512ee57 h1:JUQYJk6eGLmsdh1/wk3xfqKH6ABd9CrQ87LsjmjkVqE=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.38.0.20231103062846-b877e512ee57/go.mod h1:2QDVxfGLmjkLE/eha2EyrkuaW6GcZSukY+ifkg5zclY=
github.com/hashicorp/consul/api v1.13.0 h1:2hnLQ0GjQvw7f3O61jMO8gbasZviZTrt9R8WzgiirHc=
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU=

@ -605,6 +605,19 @@ var endpointsSchema = singleNestedAttribute{
},
},
"sso": stringAttribute{
configschema.Attribute{
Type: cty.String,
Optional: true,
Description: "A custom endpoint for the IAM Identity Center (formerly known as SSO) API",
},
validateString{
Validators: []stringValidator{
validateStringValidURL,
},
},
},
"sts": stringAttribute{
configschema.Attribute{
Type: cty.String,
@ -1052,6 +1065,13 @@ func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics {
cfg.IamEndpoint = v
}
if v, ok := retrieveArgument(&diags,
newAttributeRetriever(obj, cty.GetAttrPath("endpoints").GetAttr("sso")),
newEnvvarRetriever("AWS_ENDPOINT_URL_SSO"),
); ok {
cfg.SsoEndpoint = v
}
if v, ok := retrieveArgument(&diags,
newAttributeRetriever(obj, cty.GetAttrPath("endpoints").GetAttr("sts")),
newAttributeRetriever(obj, cty.GetAttrPath("sts_endpoint")),

@ -626,8 +626,7 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey
tc := tc
t.Run(name, func(t *testing.T) {
oldEnv := servicemocks.InitSessionTestEnv()
defer servicemocks.PopEnv(oldEnv)
servicemocks.InitSessionTestEnv(t)
ctx := context.TODO()
@ -669,9 +668,9 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey
}
if tc.EnableWebIdentityEnvVars {
os.Setenv("AWS_ROLE_ARN", servicemocks.MockStsAssumeRoleWithWebIdentityArn)
os.Setenv("AWS_ROLE_SESSION_NAME", servicemocks.MockStsAssumeRoleWithWebIdentitySessionName)
os.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", file.Name())
t.Setenv("AWS_ROLE_ARN", servicemocks.MockStsAssumeRoleWithWebIdentityArn)
t.Setenv("AWS_ROLE_SESSION_NAME", servicemocks.MockStsAssumeRoleWithWebIdentitySessionName)
t.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", file.Name())
} /*else if tc.EnableWebIdentityConfig {
tc.Config.AssumeRoleWithWebIdentity = &AssumeRoleWithWebIdentity{
RoleARN: servicemocks.MockStsAssumeRoleWithWebIdentityArn,
@ -703,7 +702,7 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey
t.Fatalf("unexpected error writing shared configuration file: %s", err)
}
setSharedConfigFile(file.Name())
setSharedConfigFile(t, file.Name())
}
if tc.SharedCredentialsFile != "" {
@ -728,7 +727,7 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey
}
for k, v := range tc.EnvironmentVariables {
os.Setenv(k, v)
t.Setenv(k, v)
}
b, diags := configureBackend(t, tc.config)
@ -1112,8 +1111,7 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey
tc := tc
t.Run(name, func(t *testing.T) {
oldEnv := servicemocks.InitSessionTestEnv()
defer servicemocks.PopEnv(oldEnv)
servicemocks.InitSessionTestEnv(t)
ctx := context.TODO()
@ -1162,7 +1160,7 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey
t.Fatalf("unexpected error writing shared configuration file: %s", err)
}
setSharedConfigFile(file.Name())
setSharedConfigFile(t, file.Name())
}
if tc.SharedCredentialsFile != "" {
@ -1187,7 +1185,7 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey
}
for k, v := range tc.EnvironmentVariables {
os.Setenv(k, v)
t.Setenv(k, v)
}
b, diags := configureBackend(t, tc.config)
@ -1553,8 +1551,7 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey
tc := tc
t.Run(name, func(t *testing.T) {
oldEnv := servicemocks.InitSessionTestEnv()
defer servicemocks.PopEnv(oldEnv)
servicemocks.InitSessionTestEnv(t)
ctx := context.TODO()
@ -1603,7 +1600,7 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey
t.Fatalf("unexpected error writing shared configuration file: %s", err)
}
setSharedConfigFile(file.Name())
setSharedConfigFile(t, file.Name())
}
if tc.SharedCredentialsFile != "" {
@ -1628,7 +1625,7 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey
}
for k, v := range tc.EnvironmentVariables {
os.Setenv(k, v)
t.Setenv(k, v)
}
b, diags := configureBackend(t, tc.config)
@ -1904,8 +1901,7 @@ web_identity_token_file = no-such-file
tc := tc
t.Run(name, func(t *testing.T) {
oldEnv := servicemocks.InitSessionTestEnv()
defer servicemocks.PopEnv(oldEnv)
servicemocks.InitSessionTestEnv(t)
ctx := context.TODO()
@ -1919,7 +1915,7 @@ web_identity_token_file = no-such-file
}
for k, v := range tc.EnvironmentVariables {
os.Setenv(k, v)
t.Setenv(k, v)
}
ts := servicemocks.MockAwsApiServer("STS", tc.MockStsEndpoints)
@ -1934,7 +1930,7 @@ web_identity_token_file = no-such-file
t.Fatalf("error creating temp dir: %s", err)
}
defer os.Remove(tempdir)
os.Setenv("TMPDIR", tempdir)
t.Setenv("TMPDIR", tempdir)
tokenFile, err := os.CreateTemp("", "aws-sdk-go-base-web-identity-token-file")
if err != nil {
@ -1967,7 +1963,7 @@ web_identity_token_file = no-such-file
}
if tc.SetTokenFileEnvironmentVariable {
os.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", tokenFileName)
t.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", tokenFileName)
}
if tc.SharedConfigurationFile != "" {
@ -2014,6 +2010,228 @@ web_identity_token_file = no-such-file
}
}
func TestBackendConfig_Authentication_SSO(t *testing.T) {
const ssoSessionName = "test-sso-session"
testCases := map[string]struct {
config map[string]any
SharedConfigurationFile string
SetSharedConfigurationFile bool
ExpectedCredentialsValue aws.Credentials
ValidateDiags DiagsValidator
MockStsEndpoints []*servicemocks.MockEndpoint
}{
"shared configuration file": {
config: map[string]any{},
SharedConfigurationFile: fmt.Sprintf(`
[default]
sso_session = %s
sso_account_id = 123456789012
sso_role_name = testRole
region = us-east-1
[sso-session test-sso-session]
sso_region = us-east-1
sso_start_url = https://d-123456789a.awsapps.com/start
sso_registration_scopes = sso:account:access
`, ssoSessionName),
SetSharedConfigurationFile: true,
ExpectedCredentialsValue: mockdata.MockSsoCredentials,
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsGetCallerIdentityValidEndpoint,
},
},
}
for name, tc := range testCases {
tc := tc
t.Run(name, func(t *testing.T) {
servicemocks.InitSessionTestEnv(t)
ctx := context.TODO()
// Populate required fields
tc.config["region"] = "us-east-1"
tc.config["bucket"] = "bucket"
tc.config["key"] = "key"
if tc.ValidateDiags == nil {
tc.ValidateDiags = ExpectNoDiags
}
err := servicemocks.SsoTestSetup(t, ssoSessionName)
if err != nil {
t.Fatalf("setup: %s", err)
}
endpoints := map[string]any{}
closeSso, ssoEndpoint := servicemocks.SsoCredentialsApiMock()
defer closeSso()
endpoints["sso"] = ssoEndpoint
ts := servicemocks.MockAwsApiServer("STS", tc.MockStsEndpoints)
defer ts.Close()
endpoints["sts"] = ts.URL
tempdir, err := os.MkdirTemp("", "temp")
if err != nil {
t.Fatalf("error creating temp dir: %s", err)
}
defer os.Remove(tempdir)
t.Setenv("TMPDIR", tempdir)
if tc.SharedConfigurationFile != "" {
file, err := os.CreateTemp("", "aws-sdk-go-base-shared-configuration-file")
if err != nil {
t.Fatalf("unexpected error creating temporary shared configuration file: %s", err)
}
defer os.Remove(file.Name())
err = os.WriteFile(file.Name(), []byte(tc.SharedConfigurationFile), 0600)
if err != nil {
t.Fatalf("unexpected error writing shared configuration file: %s", err)
}
tc.config["shared_config_files"] = []any{file.Name()}
}
tc.config["skip_credentials_validation"] = true
tc.config["endpoints"] = endpoints
b, diags := configureBackend(t, tc.config)
tc.ValidateDiags(t, diags)
if diags.HasErrors() {
return
}
credentials, err := b.awsConfig.Credentials.Retrieve(ctx)
if err != nil {
t.Fatalf("Error when requesting credentials: %s", err)
}
if diff := cmp.Diff(credentials, tc.ExpectedCredentialsValue, cmpopts.IgnoreFields(aws.Credentials{}, "Expires")); diff != "" {
t.Fatalf("unexpected credentials: (- got, + expected)\n%s", diff)
}
})
}
}
func TestBackendConfig_Authentication_LegacySSO(t *testing.T) {
const ssoStartUrl = "https://d-123456789a.awsapps.com/start"
testCases := map[string]struct {
config map[string]any
SharedConfigurationFile string
SetSharedConfigurationFile bool
ExpectedCredentialsValue aws.Credentials
ValidateDiags DiagsValidator
MockStsEndpoints []*servicemocks.MockEndpoint
}{
"shared configuration file": {
config: map[string]any{},
SharedConfigurationFile: fmt.Sprintf(`
[default]
sso_start_url = %s
sso_region = us-east-1
sso_account_id = 123456789012
sso_role_name = testRole
region = us-east-1
`, ssoStartUrl),
SetSharedConfigurationFile: true,
ExpectedCredentialsValue: mockdata.MockSsoCredentials,
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsGetCallerIdentityValidEndpoint,
},
},
}
for name, tc := range testCases {
tc := tc
t.Run(name, func(t *testing.T) {
servicemocks.InitSessionTestEnv(t)
ctx := context.TODO()
// Populate required fields
tc.config["region"] = "us-east-1"
tc.config["bucket"] = "bucket"
tc.config["key"] = "key"
if tc.ValidateDiags == nil {
tc.ValidateDiags = ExpectNoDiags
}
err := servicemocks.SsoTestSetup(t, ssoStartUrl)
if err != nil {
t.Fatalf("setup: %s", err)
}
endpoints := map[string]any{}
closeSso, ssoEndpoint := servicemocks.SsoCredentialsApiMock()
defer closeSso()
endpoints["sso"] = ssoEndpoint
ts := servicemocks.MockAwsApiServer("STS", tc.MockStsEndpoints)
defer ts.Close()
endpoints["sts"] = ts.URL
tempdir, err := os.MkdirTemp("", "temp")
if err != nil {
t.Fatalf("error creating temp dir: %s", err)
}
defer os.Remove(tempdir)
t.Setenv("TMPDIR", tempdir)
if tc.SharedConfigurationFile != "" {
file, err := os.CreateTemp("", "aws-sdk-go-base-shared-configuration-file")
if err != nil {
t.Fatalf("unexpected error creating temporary shared configuration file: %s", err)
}
defer os.Remove(file.Name())
err = os.WriteFile(file.Name(), []byte(tc.SharedConfigurationFile), 0600)
if err != nil {
t.Fatalf("unexpected error writing shared configuration file: %s", err)
}
tc.config["shared_config_files"] = []any{file.Name()}
}
tc.config["skip_credentials_validation"] = true
tc.config["endpoints"] = endpoints
b, diags := configureBackend(t, tc.config)
tc.ValidateDiags(t, diags)
if diags.HasErrors() {
return
}
credentials, err := b.awsConfig.Credentials.Retrieve(ctx)
if err != nil {
t.Fatalf("Error when requesting credentials: %s", err)
}
if diff := cmp.Diff(credentials, tc.ExpectedCredentialsValue, cmpopts.IgnoreFields(aws.Credentials{}, "Expires")); diff != "" {
t.Fatalf("unexpected credentials: (- got, + expected)\n%s", diff)
}
})
}
}
func TestBackendConfig_Region(t *testing.T) {
testCases := map[string]struct {
config map[string]any
@ -2171,15 +2389,14 @@ region = us-west-2
tc := tc
t.Run(name, func(t *testing.T) {
oldEnv := servicemocks.InitSessionTestEnv()
defer servicemocks.PopEnv(oldEnv)
servicemocks.InitSessionTestEnv(t)
// Populate required fields
tc.config["bucket"] = "bucket"
tc.config["key"] = "key"
for k, v := range tc.EnvironmentVariables {
os.Setenv(k, v)
t.Setenv(k, v)
}
if tc.IMDSRegion != "" {
@ -2216,7 +2433,7 @@ region = us-west-2
t.Fatalf("unexpected error writing shared configuration file: %s", err)
}
setSharedConfigFile(file.Name())
setSharedConfigFile(t, file.Name())
}
tc.config["skip_credentials_validation"] = true
@ -2233,9 +2450,9 @@ region = us-west-2
}
}
func setSharedConfigFile(filename string) {
os.Setenv("AWS_SDK_LOAD_CONFIG", "1")
os.Setenv("AWS_CONFIG_FILE", filename)
func setSharedConfigFile(t *testing.T, filename string) {
t.Setenv("AWS_SDK_LOAD_CONFIG", "1")
t.Setenv("AWS_CONFIG_FILE", filename)
}
func configureBackend(t *testing.T, config map[string]any) (*Backend, tfdiags.Diagnostics) {

@ -1261,8 +1261,7 @@ func TestBackendConfig_PrepareConfigValidation(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
oldEnv := servicemocks.StashEnv()
defer servicemocks.PopEnv(oldEnv)
servicemocks.StashEnv(t)
b := New()
@ -1305,8 +1304,7 @@ func TestBackendConfig_PrepareConfigWithEnvVars(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
oldEnv := servicemocks.StashEnv()
defer servicemocks.PopEnv(oldEnv)
servicemocks.StashEnv(t)
b := New()

@ -198,6 +198,8 @@ The optional argument `endpoints` contains the following arguments:
This can also be sourced from the environment variable `AWS_ENDPOINT_URL_IAM` or the deprecated environment variable `AWS_IAM_ENDPOINT`.
* `s3` - (Optional) Custom endpoint URL for the AWS S3 API.
This can also be sourced from the environment variable `AWS_ENDPOINT_URL_S3` or the deprecated environment variable `AWS_S3_ENDPOINT`.
* `sso` - (Optional) Custom endpoint URL for the AWS IAM Identity Center (formerly known as AWS SSO) API.
This can also be sourced from the environment variable `AWS_ENDPOINT_URL_SSO`.
* `sts` - (Optional) Custom endpoint URL for the AWS STS API.
This can also be sourced from the environment variable `AWS_ENDPOINT_URL_STS` or the deprecated environment variable `AWS_STS_ENDPOINT`.

Loading…
Cancel
Save