Skip to content

Commit

Permalink
Add Support AWS SSO Provider in Credential Chain
Browse files Browse the repository at this point in the history
  • Loading branch information
skmcgrail committed Jan 25, 2021
1 parent 428d4c5 commit 5bfa8d7
Show file tree
Hide file tree
Showing 15 changed files with 263 additions and 13 deletions.
9 changes: 9 additions & 0 deletions .changes/next-release/config-feature-1611597600517230000.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"ID": "config-feature-1611597600517230000",
"SchemaVersion": 1,
"Module": "config",
"Type": "feature",
"Description": "Add Support for AWS Single Sign-On (SSO) credential provider",
"MinVersion": "",
"AffectedModules": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"ID": "credentials-feature-1611597655218336000",
"SchemaVersion": 1,
"Module": "credentials",
"Type": "feature",
"Description": "Add AWS Single Sign-On (SSO) credential provider",
"MinVersion": "",
"AffectedModules": null
}
2 changes: 1 addition & 1 deletion config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package config

import (
"context"
"github.com/google/go-cmp/cmp"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/google/go-cmp/cmp"
)

func TestConfigs_SharedConfigOptions(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion config/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ require (
github.com/aws/aws-sdk-go-v2 v1.0.0
github.com/aws/aws-sdk-go-v2/credentials v1.0.0
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.0
github.com/aws/aws-sdk-go-v2/service/sso v1.0.0
github.com/aws/aws-sdk-go-v2/service/sts v1.0.0
github.com/aws/smithy-go v1.0.0
github.com/google/go-cmp v0.5.4
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.0
)

replace (
Expand Down
2 changes: 2 additions & 0 deletions config/go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/aws/aws-sdk-go-v2/service/sso v1.0.0 h1:eNwZL0deLt9ehrTpPAO/pvztJxa4RT6+E7sbDpgMGUQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.0.0/go.mod h1:qNdDupP6xoM//zL1JmPl2XGbyPL5kKrlsoYVh8XZxzQ=
github.com/aws/smithy-go v1.0.0 h1:hkhcRKG9rJ4Fn+RbfXY7Tz7b3ITLDyolBnLLBhwbg/c=
github.com/aws/smithy-go v1.0.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
29 changes: 29 additions & 0 deletions config/load_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go-v2/credentials/endpointcreds"
"github.com/aws/aws-sdk-go-v2/credentials/processcreds"
"github.com/aws/aws-sdk-go-v2/credentials/ssocreds"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/smithy-go/logging"
Expand Down Expand Up @@ -110,6 +111,10 @@ type LoadOptions struct {
// stscreds.AssumeRoleOptions
AssumeRoleCredentialOptions func(*stscreds.AssumeRoleOptions)

// SSOProviderOptions is a function for setting
// the ssocreds.Options
SSOProviderOptions func(options *ssocreds.Options)

// LogConfigurationWarnings when set to true, enables logging
// configuration warnings
LogConfigurationWarnings *bool
Expand Down Expand Up @@ -592,3 +597,27 @@ func WithS3UseARNRegion(v bool) LoadOptionsFunc {
return nil
}
}


// getSSOProviderOptions returns AssumeRoleCredentialOptions from LoadOptions
func (o LoadOptions) getSSOProviderOptions(context.Context) (func(options *ssocreds.Options), bool, error) {
if o.SSOProviderOptions == nil {
return nil, false, nil
}

return o.SSOProviderOptions, true, nil
}

// WithSSOProviderOptions is a helper function to construct
// functional options that sets a function to use ssocreds.Options
// on config's LoadOptions. If the SSO credential provider options is set to nil,
// the sso provider options value will be ignored. If multiple
// WithSSOProviderOptions calls are made, the last call overrides
// the previous call values.
func WithSSOProviderOptions(v func(*ssocreds.Options)) LoadOptionsFunc {
return func(o *LoadOptions) error {
o.SSOProviderOptions = v
return nil
}
}

19 changes: 19 additions & 0 deletions config/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go-v2/credentials/endpointcreds"
"github.com/aws/aws-sdk-go-v2/credentials/processcreds"
"github.com/aws/aws-sdk-go-v2/credentials/ssocreds"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/smithy-go/logging"
"github.com/aws/smithy-go/middleware"
Expand Down Expand Up @@ -406,3 +407,21 @@ func getLogConfigurationWarnings(ctx context.Context, configs configs) (v bool,
}
return
}

// ssoCredentialOptionsProvider is an interface for retrieving a function for setting
// the ssocreds.Options.
type ssoCredentialOptionsProvider interface {
getSSOProviderOptions(context.Context) (func(*ssocreds.Options), bool, error)
}

func getSSOProviderOptions(ctx context.Context, configs configs) (v func(options *ssocreds.Options), found bool, err error) {
for _, c := range configs {
if p, ok := c.(ssoCredentialOptionsProvider); ok {
v, found, err = p.getSSOProviderOptions(ctx)
if err != nil || found {
break
}
}
}
return
}
23 changes: 21 additions & 2 deletions config/resolve_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go-v2/credentials/endpointcreds"
"github.com/aws/aws-sdk-go-v2/credentials/processcreds"
"github.com/aws/aws-sdk-go-v2/credentials/ssocreds"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/service/sso"
"github.com/aws/aws-sdk-go-v2/service/sts"
)

Expand Down Expand Up @@ -108,6 +110,9 @@ func resolveCredentialChain(ctx context.Context, cfg *aws.Config, configs config
func resolveCredsFromProfile(ctx context.Context, cfg *aws.Config, envConfig *EnvConfig, sharedConfig *SharedConfig, configs configs) (err error) {

switch {
case sharedConfig.hasSSOConfiguration():
err = resolveSSOCredentials(ctx, cfg, sharedConfig, configs)

case sharedConfig.Source != nil:
// Assume IAM role with credentials source from a different profile.
err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig.Source, configs)
Expand Down Expand Up @@ -151,6 +156,20 @@ func resolveCredsFromProfile(ctx context.Context, cfg *aws.Config, envConfig *En
return nil
}

func resolveSSOCredentials(ctx context.Context, cfg *aws.Config, sharedConfig *SharedConfig, configs configs) error {
var options []func(*ssocreds.Options)
v, found, err := getSSOProviderOptions(ctx, configs)
if err != nil {
return err
}
if found {
options = append(options, v)
}

cfg.Credentials = ssocreds.New(sso.NewFromConfig(*cfg), sharedConfig.SSOAccountID, sharedConfig.SSORegion, sharedConfig.SSORoleName, sharedConfig.SSOStartURL, options...)
return nil
}

func ecsContainerURI(path string) string {
return fmt.Sprintf("%s%s", ecsContainerEndpoint, path)
}
Expand Down Expand Up @@ -353,7 +372,7 @@ func assumeWebIdentity(ctx context.Context, cfg *aws.Config, filepath string, ro
optFns = append(optFns, optFn)
}

provider := stscreds.NewWebIdentityRoleProvider(sts.NewFromConfig(cfg.Copy()), roleARN, stscreds.IdentityTokenFile(filepath), optFns...)
provider := stscreds.NewWebIdentityRoleProvider(sts.NewFromConfig(*cfg), roleARN, stscreds.IdentityTokenFile(filepath), optFns...)

cfg.Credentials = provider

Expand Down Expand Up @@ -401,7 +420,7 @@ func credsFromAssumeRole(ctx context.Context, cfg *aws.Config, sharedCfg *Shared
}
}

cfg.Credentials = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg.Copy()), sharedCfg.RoleARN, optFns...)
cfg.Credentials = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(*cfg), sharedCfg.RoleARN, optFns...)

return nil
}
Expand Down
2 changes: 1 addition & 1 deletion config/resolve_credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func TestSharedConfigCredentialSource(t *testing.T) {
}{
"credential source and source profile": {
envProfile: "invalid_source_and_credential_source",
expectedError: "only source profile or credential source can be specified",
expectedError: "only one credential type may be specified per profile",
init: func() {
os.Setenv("AWS_ACCESS_KEY", "access_key")
os.Setenv("AWS_SECRET_KEY", "secret_key")
Expand Down
80 changes: 76 additions & 4 deletions config/shared_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ const (
roleSessionNameKey = `role_session_name` // optional
roleDurationSecondsKey = "duration_seconds" // optional

// AWS Single Sign-On (AWS SSO) group
ssoAccountIDKey = "sso_account_id"
ssoRegionKey = "sso_region"
ssoRoleNameKey = "sso_role_name"
ssoStartURL = "sso_start_url"

// Additional Config fields
regionKey = `region`

Expand Down Expand Up @@ -110,6 +116,11 @@ type SharedConfig struct {
CredentialProcess string
WebIdentityTokenFile string

SSOAccountID string
SSORegion string
SSORoleName string
SSOStartURL string

RoleARN string
ExternalID string
MFASerial string
Expand Down Expand Up @@ -750,13 +761,13 @@ func (c *SharedConfig) setFromIniSections(profiles map[string]struct{}, profile
// First time a profile has been seen, It must either be a assume role
// or credentials. Assert if the credential type requires a role ARN,
// the ARN is also set.
if err := c.validateCredentialsRequireARN(profile); err != nil {
if err := c.validateCredentialsConfig(profile); err != nil {
return err
}
}

// if not top level profile and has credentials, return with credentials.
if len(profiles) != 0 && c.Credentials.HasKeys() {
if len(profiles) != 0 && (c.Credentials.HasKeys() || c.hasSSOConfiguration()) {
return nil
}

Expand Down Expand Up @@ -787,7 +798,7 @@ func (c *SharedConfig) setFromIniSections(profiles map[string]struct{}, profile
return err
}

if !srcCfg.hasCredentials() {
if !srcCfg.hasCredentials() && !srcCfg.hasSSOConfiguration() {
return SharedConfigAssumeRoleError{
RoleARN: c.RoleARN,
Profile: c.SourceProfileName,
Expand Down Expand Up @@ -835,6 +846,12 @@ func (c *SharedConfig) setFromIniSection(profile string, section ini.Section) er
updateString(&c.CredentialSource, section, credentialSourceKey)
updateString(&c.Region, section, regionKey)

// AWS Single Sign-On (AWS SSO)
updateString(&c.SSOAccountID, section, ssoAccountIDKey)
updateString(&c.SSORegion, section, ssoRegionKey)
updateString(&c.SSORoleName, section, ssoRoleNameKey)
updateString(&c.SSOStartURL, section, ssoStartURL)

if section.Has(roleDurationSecondsKey) {
d := time.Duration(section.Int(roleDurationSecondsKey)) * time.Second
c.RoleDurationSeconds = &d
Expand All @@ -861,6 +878,18 @@ func (c *SharedConfig) setFromIniSection(profile string, section ini.Section) er
return nil
}

func (c *SharedConfig) validateCredentialsConfig(profile string) error {
if err := c.validateCredentialsRequireARN(profile); err != nil {
return err
}

if err := c.validateSSOConfiguration(profile); err != nil {
return err
}

return nil
}

func (c *SharedConfig) validateCredentialsRequireARN(profile string) error {
var credSource string

Expand Down Expand Up @@ -890,8 +919,39 @@ func (c *SharedConfig) validateCredentialType() error {
len(c.CredentialSource) != 0,
len(c.CredentialProcess) != 0,
len(c.WebIdentityTokenFile) != 0,
c.hasSSOConfiguration(),
) {
return fmt.Errorf("only source profile or credential source can be specified, not both")
return fmt.Errorf("only one credential type may be specified per profile: source profile, credential source, credential process, web identity token, or sso")
}

return nil
}

func (c *SharedConfig) validateSSOConfiguration(profile string) error {
if !c.hasSSOConfiguration() {
return nil
}

var missing []string
if len(c.SSOAccountID) == 0 {
missing = append(missing, ssoAccountIDKey)
}

if len(c.SSORegion) == 0 {
missing = append(missing, ssoRegionKey)
}

if len(c.SSORoleName) == 0 {
missing = append(missing, ssoRoleNameKey)
}

if len(c.SSOStartURL) == 0 {
missing = append(missing, ssoStartURL)
}

if len(missing) > 0 {
return fmt.Errorf("profile %q is configured to use SSO but is missing required configuration: %s",
profile, strings.Join(missing, ","))
}

return nil
Expand All @@ -911,6 +971,18 @@ func (c *SharedConfig) hasCredentials() bool {
return true
}

func (c *SharedConfig) hasSSOConfiguration() bool {
switch {
case len(c.SSOAccountID) != 0:
case len(c.SSORegion) != 0:
case len(c.SSORoleName) != 0:
case len(c.SSOStartURL) != 0:
default:
return false
}
return true
}

func (c *SharedConfig) clearAssumeRoleOptions() {
c.RoleARN = ""
c.ExternalID = ""
Expand Down
27 changes: 27 additions & 0 deletions config/shared_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,33 @@ func TestNewSharedConfig(t *testing.T) {
},
},
},
"AWS SSO Profile": {
Filenames: []string{testConfigFilename},
Profile: "sso_creds",
Expected: SharedConfig{
Profile: "sso_creds",
SSOAccountID: "012345678901",
SSORegion: "us-west-2",
SSORoleName: "TestRole",
SSOStartURL: "https://127.0.0.1/start",
},
},
"Assume Role with AWS SSO Credentials": {
Filenames: []string{testConfigFilename},
Profile: "source_sso_creds",
Expected: SharedConfig{
Profile: "source_sso_creds",
RoleARN: "source_sso_creds_arn",
SourceProfileName: "sso_creds",
Source: &SharedConfig{
Profile: "sso_creds",
SSOAccountID: "012345678901",
SSORegion: "us-west-2",
SSORoleName: "TestRole",
SSOStartURL: "https://127.0.0.1/start",
},
},
},
}

for name, c := range cases {
Expand Down
13 changes: 13 additions & 0 deletions config/testdata/shared_config
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,16 @@ source_profile = assume_role_with_credential_source
[profile multiple_assume_role_with_credential_source2]
role_arn = multiple_assume_role_with_credential_source2_role_arn
source_profile = multiple_assume_role_with_credential_source

[profile sso_creds]
sso_account_id = 012345678901
sso_region = us-west-2
sso_role_name = TestRole
sso_start_url = https://127.0.0.1/start

[profile source_sso_creds]
role_arn = source_sso_creds_arn
source_profile = sso_creds

[profile invalid_sso_creds]
sso_account_id = 012345678901
Loading

0 comments on commit 5bfa8d7

Please sign in to comment.