From 50aa2c9fe25594bce581e2324c50dc78f25e9964 Mon Sep 17 00:00:00 2001 From: dnitsch Date: Sat, 27 Sep 2025 12:34:28 +0100 Subject: [PATCH 1/3] fix: add more tests rework how the config file is passed in perform merges from config.specific sections to the global config +semver: feature +semver: FEATURE --- cmd/awscliauth.go | 30 ++-- cmd/awscliauth_test.go | 52 +++++++ cmd/clear.go | 19 +-- cmd/saml.go | 171 +++++++++++++-------- cmd/saml_test.go | 35 +++++ cmd/specific.go | 13 +- eirctl.yaml | 10 +- go.mod | 2 + go.sum | 4 + internal/cmdutils/cmdutils.go | 4 +- internal/cmdutils/cmdutils_test.go | 21 ++- internal/credentialexchange/config.go | 38 +++-- internal/credentialexchange/helper.go | 115 ++++++++------ internal/credentialexchange/helper_test.go | 40 +++++ internal/credentialexchange/secret.go | 92 +++++------ internal/credentialexchange/secret_test.go | 9 +- 16 files changed, 440 insertions(+), 215 deletions(-) create mode 100644 cmd/saml_test.go diff --git a/cmd/awscliauth.go b/cmd/awscliauth.go index 59a890d..9e77856 100755 --- a/cmd/awscliauth.go +++ b/cmd/awscliauth.go @@ -20,21 +20,21 @@ type Root struct { // ChannelOut io.Writer // ChannelErr io.Writer // viperConf *viper.Viper - rootFlags *rootCmdFlags + rootFlags *RootCmdFlags Datadir string } -type rootCmdFlags struct { - cfgSectionName string - storeInProfile bool - killHangingProcess bool - roleChain []string - verbose bool - duration int +type RootCmdFlags struct { + CfgSectionName string + StoreInProfile bool + RoleChain []string + Verbose bool + Duration int + CustomIniLocation string } func New() *Root { - rf := &rootCmdFlags{} + rf := &RootCmdFlags{} r := &Root{ rootFlags: rf, Cmd: &cobra.Command{ @@ -49,15 +49,17 @@ Stores them under the $HOME/.aws/credentials file under a specified path or retu }, } - r.Cmd.PersistentFlags().StringSliceVarP(&rf.roleChain, "role-chain", "", []string{}, "If specified it will assume the roles from the base credentials, in order they are specified in") - r.Cmd.PersistentFlags().BoolVarP(&rf.storeInProfile, "store-profile", "s", false, `By default the credentials are returned to stdout to be used by the credential_process. + r.Cmd.PersistentFlags().StringSliceVarP(&rf.RoleChain, "role-chain", "", []string{}, "If specified it will assume the roles from the base credentials, in order they are specified in") + r.Cmd.PersistentFlags().BoolVarP(&rf.StoreInProfile, "store-profile", "s", false, `By default the credentials are returned to stdout to be used by the credential_process. Set this flag to instead store the credentials under a named profile section. You can then reference that profile name via the CLI or for use in an SDK`) - r.Cmd.PersistentFlags().StringVarP(&rf.cfgSectionName, "cfg-section", "", "", "Config section name in the default AWS credentials file. To enable priofi") + r.Cmd.PersistentFlags().StringVarP(&rf.CfgSectionName, "cfg-section", "", "", "Config section name to use in the look up of the config ini file (~/.aws-cli-auth.ini) and in the AWS credentials file") // When specifying store in profile the config section name must be provided r.Cmd.MarkFlagsRequiredTogether("store-profile", "cfg-section") - r.Cmd.PersistentFlags().IntVarP(&rf.duration, "max-duration", "d", 900, `Override default max session duration, in seconds, of the role session [900-43200]. + r.Cmd.PersistentFlags().IntVarP(&rf.Duration, "max-duration", "d", 900, `Override default max session duration, in seconds, of the role session [900-43200]. NB: This cannot be higher than the 3600 as the API does not allow for AssumeRole for sessions longer than an hour`) - r.Cmd.PersistentFlags().BoolVarP(&rf.verbose, "verbose", "v", false, "Verbose output") + r.Cmd.PersistentFlags().BoolVarP(&rf.Verbose, "verbose", "v", false, "Verbose output") + r.Cmd.PersistentFlags().StringVarP(&rf.CustomIniLocation, "config-file", "c", "", "Specify the custom location of config file") + _ = r.dataDirInit() return r } diff --git a/cmd/awscliauth_test.go b/cmd/awscliauth_test.go index 7cc60ca..742b1dc 100644 --- a/cmd/awscliauth_test.go +++ b/cmd/awscliauth_test.go @@ -5,9 +5,12 @@ import ( "context" "errors" "io" + "os" + "strings" "testing" "github.com/DevLabFoundry/aws-cli-auth/cmd" + "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" "github.com/DevLabFoundry/aws-cli-auth/internal/web" ) @@ -81,3 +84,52 @@ func Test_Saml_timeout(t *testing.T) { // } }) } + +func Test_SpecificCommand(t *testing.T) { + + t.Run("Sepcific command should fail with wrong method", func(t *testing.T) { + _, _, err := cmdHelperExecutor(t, []string{"specific", "--method=unknown", "--role", + "arn:aws:iam::1234111111111:role/Role-ReadOnly"}) + if err == nil { + t.Error("got nil, wanted an error") + } + if !errors.Is(err, cmd.ErrUnsupportedMethod) { + t.Errorf("got %v, wanted %v", err, cmd.ErrUnsupportedMethod) + } + }) + + t.Run("Sepcific command fails on missing env AWS_WEB_IDENTITY_TOKEN_FILE", func(t *testing.T) { + os.Setenv("AWS_ROLE_ARN", "arn:aws:iam::1234111111111:role/Role-ReadOnly") + defer os.Unsetenv("AWS_ROLE_ARN") + _, _, err := cmdHelperExecutor(t, []string{"specific", "--method=WEB_ID", "--role", + "arn:aws:iam::1234111111111:role/Role-ReadOnly"}) + if err == nil { + t.Error("got nil, wanted an error") + } + if !errors.Is(err, credentialexchange.ErrMissingEnvVar) { + t.Errorf("got %v, wanted %v", err, cmd.ErrUnsupportedMethod) + } + }) +} + +func Test_ClearCommand(t *testing.T) { + + t.Run("should pass without --force", func(t *testing.T) { + _, _, err := cmdHelperExecutor(t, []string{"clear-cache"}) + if err != nil { + t.Error("got nil, wanted an error") + } + }) + t.Run("should warn user to manually delete data dir", func(t *testing.T) { + stdout, _, err := cmdHelperExecutor(t, []string{"clear-cache", "--force"}) + if err != nil { + t.Error("got nil, wanted an error") + } + if len(stdout.String()) < 1 { + t.Fatal("got nil, wanted output") + } + if !strings.Contains(stdout.String(), "manually") { + t.Errorf("incorrect messasge displayed, got %s, wanted to include manually", stdout.String()) + } + }) +} diff --git a/cmd/clear.go b/cmd/clear.go index 8965c9a..743533e 100644 --- a/cmd/clear.go +++ b/cmd/clear.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "os" "os/user" @@ -13,6 +14,10 @@ type clearFlags struct { force bool } +var ( + ErrCannotReadConfig = errors.New("cannot open config file") +) + func newClearCmd(r *Root) { flags := &clearFlags{} @@ -26,7 +31,8 @@ func newClearCmd(r *Root) { if err != nil { return err } - if err := samlInitConfig(); err != nil { + iniCfg, err := samlInitConfig("") + if err != nil { return err } secretStore, err := credentialexchange.NewSecretStore("", @@ -38,15 +44,11 @@ func newClearCmd(r *Root) { } if flags.force { - fmt.Fprint(os.Stderr, "delete ~/.aws-cli-auth-data/ manually") + fmt.Fprint(cmd.OutOrStderr(), "delete ~/.aws-cli-auth-data/ manually") } - if err := secretStore.ClearAll(); err != nil { - fmt.Fprint(os.Stderr, err.Error()) - } - - if err := os.Remove(credentialexchange.ConfigIniFile("")); err != nil { - return err + if err := secretStore.ClearAll(iniCfg); err != nil { + fmt.Fprint(cmd.OutOrStderr(), err.Error()) } return nil @@ -64,6 +66,5 @@ Use with caution. If for any reason the local ini file and the secret store on your OS (keyring on GNU, keychain MacOS, windows secret store) are out of sync and the secrets cannot be retrieved by name but still exists, you might want to use CLI or GUI interface to the secret backing store on your OS and search for a secret prefixed with aws-cli-* and delete manually `) - r.Cmd.AddCommand(cmd) } diff --git a/cmd/saml.go b/cmd/saml.go index 415e2fd..a775bb5 100755 --- a/cmd/saml.go +++ b/cmd/saml.go @@ -7,12 +7,14 @@ import ( "os/user" "strings" + "dario.cat/mergo" "github.com/DevLabFoundry/aws-cli-auth/internal/cmdutils" "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" "github.com/DevLabFoundry/aws-cli-auth/internal/web" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/spf13/cobra" + "gopkg.in/ini.v1" ) var ( @@ -25,29 +27,29 @@ const ( SsoCredsEndpointQuery = "?account_id=%s&role_name=%s&debug=true" ) -type samlFlags struct { - providerUrl string - principalArn string - acsUrl string - isSso bool - role string - ssoRegion string - ssoRole string - ssoUserEndpoint string - ssoFedCredEndpoint string - customExecutablePath string - samlTimeout int32 - reloadBeforeTime int +type SamlCmdFlags struct { + ProviderUrl string + PrincipalArn string + AcsUrl string + IsSso bool + Role string + SsoRegion string + SsoRole string + SsoUserEndpoint string + SsoFedCredEndpoint string + CustomExecutablePath string + SamlTimeout int32 + ReloadBeforeTime int } type samlCmd struct { - flags *samlFlags + flags *SamlCmdFlags ssoRoleAccount, ssoRoleName string cmd *cobra.Command } func newSamlCmd(r *Root) { - flags := &samlFlags{} + flags := &SamlCmdFlags{} sc := &samlCmd{ flags: flags, } @@ -63,33 +65,28 @@ func newSamlCmd(r *Root) { return err } - if err := samlInitConfig(); err != nil { + iniFile, err := samlInitConfig(r.rootFlags.CustomIniLocation) + if err != nil { + return err + } + + conf, err := credentialexchange.LoadCliConfig(iniFile, r.rootFlags.CfgSectionName) + if err != nil { return err } - allRoles := credentialexchange.MergeRoleChain(flags.role, r.rootFlags.roleChain, flags.isSso) - conf := credentialexchange.CredentialConfig{ - ProviderUrl: flags.providerUrl, - PrincipalArn: flags.principalArn, - Duration: r.rootFlags.duration, - AcsUrl: flags.acsUrl, - IsSso: flags.isSso, - SsoRegion: flags.ssoRegion, - SsoRole: flags.ssoRole, - BaseConfig: credentialexchange.BaseConfig{ - StoreInProfile: r.rootFlags.storeInProfile, - Role: flags.role, - RoleChain: allRoles, - Username: user.Username, - CfgSectionName: r.rootFlags.cfgSectionName, - DoKillHangingProcess: r.rootFlags.killHangingProcess, - ReloadBeforeTime: flags.reloadBeforeTime, - }, + if err := ConfigFromFlags(conf, r.rootFlags, flags, user.Username); err != nil { + return err } - saveRole := flags.role - if flags.isSso { - saveRole = flags.ssoRole + allRoles := credentialexchange.MergeRoleChain(flags.Role, r.rootFlags.RoleChain, flags.IsSso) + + conf.BaseConfig.RoleChain = allRoles + + // now we want to overwrite anything set via the command line + saveRole := flags.Role + if flags.IsSso { + saveRole = flags.SsoRole conf.SsoUserEndpoint = fmt.Sprintf(UserEndpoint, conf.SsoRegion) conf.SsoCredFedEndpoint = fmt.Sprintf( CredsEndpoint, conf.SsoRegion) + fmt.Sprintf( @@ -107,21 +104,29 @@ func newSamlCmd(r *Root) { return err } - cfg, err := config.LoadDefaultConfig(ctx) + // we want to remove any AWS_* env vars that could interfere with the default config + for _, envVar := range []string{"AWS_PROFILE", "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"} { + os.Unsetenv(envVar) + } + + awsConf, err := config.LoadDefaultConfig(ctx) if err != nil { return fmt.Errorf("failed to create session %s, %w", err, ErrUnableToCreateSession) } - svc := sts.NewFromConfig(cfg) - webConfig := web.NewWebConf(r.Datadir).WithTimeout(flags.samlTimeout) - webConfig.CustomChromeExecutable = flags.customExecutablePath - return cmdutils.GetCredsWebUI(ctx, svc, secretStore, conf, webConfig) + + svc := sts.NewFromConfig(awsConf) + webConfig := web.NewWebConf(r.Datadir).WithTimeout(flags.SamlTimeout) + webConfig.CustomChromeExecutable = flags.CustomExecutablePath + + return cmdutils.GetCredsWebUI(ctx, svc, secretStore, *conf, webConfig) }, PreRunE: func(cmd *cobra.Command, args []string) error { - if flags.reloadBeforeTime != 0 && flags.reloadBeforeTime > r.rootFlags.duration { - return fmt.Errorf("reload-before: %v, must be less than duration (-d): %v", flags.reloadBeforeTime, r.rootFlags.duration) + if flags.ReloadBeforeTime != 0 && flags.ReloadBeforeTime > r.rootFlags.Duration { + return fmt.Errorf("reload-before: %v, must be less than duration (-d): %v", flags.ReloadBeforeTime, r.rootFlags.Duration) } - if len(flags.ssoRole) > 0 { - sr := strings.Split(flags.ssoRole, ":") + if len(flags.SsoRole) > 0 { + sr := strings.Split(flags.SsoRole, ":") if len(sr) != 2 { return fmt.Errorf("incorrectly formatted role for AWS SSO - must only be ACCOUNT:ROLE_NAME") } @@ -131,47 +136,81 @@ func newSamlCmd(r *Root) { }, } - sc.cmd.PersistentFlags().StringVarP(&flags.providerUrl, "provider", "p", "", `Saml Entity StartSSO Url. + sc.cmd.PersistentFlags().StringVarP(&flags.ProviderUrl, "provider", "p", "", `Saml Entity StartSSO Url. This is the URL your Idp will make the first call to e.g.: https://company-xyz.okta.com/home/amazon_aws/12345SomeRandonId6789 `) - _ = sc.cmd.MarkPersistentFlagRequired("provider") - sc.cmd.PersistentFlags().StringVarP(&flags.principalArn, "principal", "", "", `Principal Arn of the SAML IdP in AWS + // _ = sc.cmd.MarkPersistentFlagRequired("provider") + sc.cmd.PersistentFlags().StringVarP(&flags.PrincipalArn, "principal", "", "", `Principal Arn of the SAML IdP in AWS You should find it in the IAM portal e.g.: arn:aws:iam::1234567891012:saml-provider/MyCompany-Idp `) // samlCmd.MarkPersistentFlagRequired("principal") - sc.cmd.PersistentFlags().StringVarP(&flags.role, "role", "r", "", `Set the role you want to assume when SAML or OIDC process completes`) - sc.cmd.PersistentFlags().StringVarP(&flags.acsUrl, "acsurl", "a", "https://signin.aws.amazon.com/saml", "Override the default ACS Url, used for checkin the post of the SAMLResponse") - sc.cmd.PersistentFlags().StringVarP(&flags.ssoUserEndpoint, "sso-user-endpoint", "", UserEndpoint, "UserEndpoint in a go style fmt.Sprintf string with a region placeholder") - sc.cmd.PersistentFlags().StringVarP(&flags.ssoRole, "sso-role", "", "", "Sso Role name must be in this format - 12345678910:PowerUser") - sc.cmd.PersistentFlags().StringVarP(&flags.ssoFedCredEndpoint, "sso-fed-endpoint", "", CredsEndpoint, "FederationCredEndpoint in a go style fmt.Sprintf string with a region placeholder") - sc.cmd.PersistentFlags().StringVarP(&flags.ssoRegion, "sso-region", "", "eu-west-1", "If using SSO, you must set the region") - sc.cmd.PersistentFlags().StringVarP(&flags.customExecutablePath, "executable-path", "", "", `Custom path to an executable + sc.cmd.PersistentFlags().StringVarP(&flags.Role, "role", "r", "", `Set the role you want to assume when SAML or OIDC process completes`) + sc.cmd.PersistentFlags().StringVarP(&flags.AcsUrl, "acsurl", "a", "https://signin.aws.amazon.com/saml", "Override the default ACS Url, used for checkin the post of the SAMLResponse") + sc.cmd.PersistentFlags().StringVarP(&flags.SsoUserEndpoint, "sso-user-endpoint", "", UserEndpoint, "UserEndpoint in a go style fmt.Sprintf string with a region placeholder") + sc.cmd.PersistentFlags().StringVarP(&flags.SsoRole, "sso-role", "", "", "Sso Role name must be in this format - 12345678910:PowerUser") + sc.cmd.PersistentFlags().StringVarP(&flags.SsoFedCredEndpoint, "sso-fed-endpoint", "", CredsEndpoint, "FederationCredEndpoint in a go style fmt.Sprintf string with a region placeholder") + sc.cmd.PersistentFlags().StringVarP(&flags.SsoRegion, "sso-region", "", "eu-west-1", "If using SSO, you must set the region") + sc.cmd.PersistentFlags().StringVarP(&flags.CustomExecutablePath, "executable-path", "", "", `Custom path to an executable This needs to be a chromium like executable - e.g. Chrome, Chromium, Brave, Edge. You can find out the path by opening your browser and typing in chrome|brave|edge://version `) - sc.cmd.PersistentFlags().BoolVarP(&flags.isSso, "is-sso", "", false, `Enables the new AWS User portal login. + sc.cmd.PersistentFlags().BoolVarP(&flags.IsSso, "is-sso", "", false, `Enables the new AWS User portal login. If this flag is specified the --sso-role must also be specified.`) - sc.cmd.PersistentFlags().IntVarP(&flags.reloadBeforeTime, "reload-before", "", 0, "Triggers a credentials refresh before the specified max-duration. Value provided in seconds. Should be less than the max-duration of the session") + sc.cmd.PersistentFlags().IntVarP(&flags.ReloadBeforeTime, "reload-before", "", 0, "Triggers a credentials refresh before the specified max-duration. Value provided in seconds. Should be less than the max-duration of the session") // sc.cmd.MarkFlagsMutuallyExclusive("role", "sso-role") - // samlCmd.MarkFlagsMutuallyExclusive("principal", "sso-role") // Non-SSO flow for SAML - sc.cmd.MarkFlagsRequiredTogether("principal", "role") + // sc.cmd.MarkFlagsRequiredTogether("principal", "role") // SSO flow for SAML sc.cmd.MarkFlagsRequiredTogether("is-sso", "sso-role", "sso-region") - sc.cmd.PersistentFlags().Int32VarP(&flags.samlTimeout, "saml-timeout", "", 120, "Timeout in seconds, before the operation of waiting for a response is cancelled via the chrome driver") + sc.cmd.PersistentFlags().Int32VarP(&flags.SamlTimeout, "saml-timeout", "", 120, "Timeout in seconds, before the operation of waiting for a response is cancelled via the chrome driver") // Add subcommand to root command r.Cmd.AddCommand(sc.cmd) - } -func samlInitConfig() error { - if _, err := os.Stat(credentialexchange.ConfigIniFile("")); err != nil { +func samlInitConfig(customPath string) (*ini.File, error) { + configPath := credentialexchange.ConfigIniFile(customPath) + if _, err := os.Stat(configPath); err != nil { // creating a file - rolesInit := []byte(fmt.Sprintf("[%s]\n", credentialexchange.INI_CONF_SECTION)) - return os.WriteFile(credentialexchange.ConfigIniFile(""), rolesInit, 0644) + rolesInit := []byte(fmt.Sprintf("; aws-cli-auth generated [role] section\n[%s]\n", credentialexchange.INI_CONF_SECTION)) + if err := os.WriteFile(configPath, rolesInit, 0644); err != nil { + return nil, err + } + } + return ini.Load(configPath) +} + +func ConfigFromFlags(fileConfig *credentialexchange.CredentialConfig, rf *RootCmdFlags, sf *SamlCmdFlags, user string) error { + + flagSamlConf := &credentialexchange.CredentialConfig{ + ProviderUrl: sf.ProviderUrl, + PrincipalArn: sf.PrincipalArn, + Duration: rf.Duration, + AcsUrl: sf.AcsUrl, + IsSso: sf.IsSso, + SsoRegion: sf.SsoRegion, + SsoRole: sf.SsoRole, + } + + flagBaseConfig := &credentialexchange.BaseConfig{ + StoreInProfile: rf.StoreInProfile, + Role: sf.Role, + // RoleChain is added in the command function + // RoleChain: allRoles, + Username: user, + CfgSectionName: rf.CfgSectionName, + ReloadBeforeTime: sf.ReloadBeforeTime, + } + + if err := mergo.Merge(&fileConfig.BaseConfig, flagBaseConfig, mergo.WithOverride); err != nil { + return err + } + baseConf := fileConfig.BaseConfig + if err := mergo.Merge(fileConfig, flagSamlConf, mergo.WithOverride, mergo.WithOverrideEmptySlice); err != nil { + return err } + fileConfig.BaseConfig = baseConf return nil } diff --git a/cmd/saml_test.go b/cmd/saml_test.go new file mode 100644 index 0000000..bd7735f --- /dev/null +++ b/cmd/saml_test.go @@ -0,0 +1,35 @@ +package cmd_test + +import ( + "testing" + + "github.com/DevLabFoundry/aws-cli-auth/cmd" + "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" + "github.com/go-test/deep" +) + +func Test_ConfigMerge(t *testing.T) { + conf := &credentialexchange.CredentialConfig{ + BaseConfig: credentialexchange.BaseConfig{ + BrowserExecutablePath: "/foo/path", + Role: "role1", + RoleChain: []string{"role-123"}, + }, + ProviderUrl: "https://my-idp.com/?app_id=testdd", + } + if err := cmd.ConfigFromFlags(conf, &cmd.RootCmdFlags{}, &cmd.SamlCmdFlags{Role: "role-overridden-from-flags"}, "me"); err != nil { + t.Fatal(err) + } + want := &credentialexchange.CredentialConfig{ + ProviderUrl: "https://my-idp.com/?app_id=testdd", + BaseConfig: credentialexchange.BaseConfig{ + BrowserExecutablePath: "/foo/path", + Role: "role-overridden-from-flags", + RoleChain: []string{"role-123"}, + Username: "me", + }, + } + if diff := deep.Equal(conf, want); len(diff) > 0 { + t.Errorf("diff: %v", diff) + } +} diff --git a/cmd/specific.go b/cmd/specific.go index 6048b88..1665418 100644 --- a/cmd/specific.go +++ b/cmd/specific.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "os/user" @@ -10,6 +11,10 @@ import ( "github.com/spf13/cobra" ) +var ( + ErrUnsupportedMethod = errors.New("method is not supported") +) + type specificCmdFlags struct { method string role string @@ -47,21 +52,21 @@ Returns the same JSON object as the call to the AWS CLI for any of the sts Assum return err } default: - return fmt.Errorf("unsupported Method: %s", flags.method) + return fmt.Errorf("%s\n%w", flags.method, ErrUnsupportedMethod) } } config := credentialexchange.CredentialConfig{ BaseConfig: credentialexchange.BaseConfig{ - StoreInProfile: r.rootFlags.storeInProfile, + StoreInProfile: r.rootFlags.StoreInProfile, Username: user.Username, Role: flags.role, - RoleChain: credentialexchange.MergeRoleChain(flags.role, r.rootFlags.roleChain, false), + RoleChain: credentialexchange.MergeRoleChain(flags.role, r.rootFlags.RoleChain, false), }, } conf := credentialexchange.CredentialConfig{ - Duration: r.rootFlags.duration, + Duration: r.rootFlags.Duration, } awsCreds, err = credentialexchange.AssumeRoleInChain(ctx, awsCreds, svc, config.BaseConfig.Username, config.BaseConfig.RoleChain, conf) diff --git a/eirctl.yaml b/eirctl.yaml index 4e3c2d0..5782f57 100644 --- a/eirctl.yaml +++ b/eirctl.yaml @@ -6,9 +6,6 @@ contexts: container: name: ghcr.io/devlabfoundry/aws-cli-auth-ci:0.3.0 entrypoint: /usr/bin/env - shell: sh - shell_args: - - -c envfile: exclude: - HOME @@ -30,6 +27,11 @@ pipelines: - pipeline: build depends_on: clean:dir + coverage: + - pipeline: unit:test:run + - task: show_coverage + depends_on: unit:test:run + tasks: tag: command: @@ -47,7 +49,7 @@ tasks: Unit test runner needs a bit of extra care in this case to ensure we have all the dependencies command: | export GOPATH=$PWD/.deps GOBIN=$PWD/.deps/bin - CGO_ENABLED=1 go test $(go list ./... | grep -v /local/) -v -coverpkg=./... -race -mod=readonly -timeout=1m -shuffle=on -buildvcs=false -coverprofile=.coverage/out -count=1 -run=$GO_TEST_RUN_ARGS | tee .coverage/test.out + CGO_ENABLED=1 go test ./... -v -coverpkg=github.com/DevLabFoundry/... -race -mod=readonly -timeout=1m -shuffle=on -buildvcs=false -coverprofile=.coverage/out -count=1 -run=$GO_TEST_RUN_ARGS | tee .coverage/test.out cat .coverage/test.out | .deps/bin/go-junit-report > .coverage/report-junit.xml .deps/bin/gocov convert .coverage/out | .deps/bin/gocov-xml > .coverage/report-cobertura.xml diff --git a/go.mod b/go.mod index 2ff3338..3c303a8 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( require ( al.essio.dev/pkg/shellescape v1.6.0 // indirect + dario.cat/mergo v1.0.2 github.com/aws/aws-sdk-go-v2/credentials v1.18.6 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 // indirect @@ -26,6 +27,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 // indirect github.com/danieljoos/wincred v1.2.2 // indirect + github.com/go-test/deep v1.1.1 github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index 6c2cc4d..6269b26 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8= github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= github.com/aws/aws-sdk-go-v2/config v1.31.2 h1:NOaSZpVGEH2Np/c1toSeW0jooNl+9ALmsUTZ8YvkJR0= @@ -33,6 +35,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= diff --git a/internal/cmdutils/cmdutils.go b/internal/cmdutils/cmdutils.go index c05dbab..9f4d70e 100644 --- a/internal/cmdutils/cmdutils.go +++ b/internal/cmdutils/cmdutils.go @@ -7,6 +7,7 @@ import ( "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" "github.com/DevLabFoundry/aws-cli-auth/internal/web" + "gopkg.in/ini.v1" ) var ( @@ -16,8 +17,7 @@ var ( type SecretStorageImpl interface { AWSCredential() (*credentialexchange.AWSCredentials, error) - Clear() error - ClearAll() error + ClearAll(cfg *ini.File) error SaveAWSCredential(cred *credentialexchange.AWSCredentials) error } diff --git a/internal/cmdutils/cmdutils_test.go b/internal/cmdutils/cmdutils_test.go index 50f7ffd..5dd77e2 100644 --- a/internal/cmdutils/cmdutils_test.go +++ b/internal/cmdutils/cmdutils_test.go @@ -16,6 +16,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/aws/aws-sdk-go-v2/service/sts/types" + "gopkg.in/ini.v1" ) func AwsMockHandler(t *testing.T, mux *http.ServeMux) http.Handler { @@ -165,7 +166,7 @@ func (m *mockAuthApi) AssumeRole(ctx context.Context, params *sts.AssumeRoleInpu type mockSecretApi struct { mCred func() (*credentialexchange.AWSCredentials, error) mclear func() error - mClearAll func() error + mClearAll func(cfg *ini.File) error mSave func(cred *credentialexchange.AWSCredentials) error } @@ -177,8 +178,8 @@ func (s *mockSecretApi) Clear() error { return s.mclear() } -func (s *mockSecretApi) ClearAll() error { - return s.mClearAll() +func (s *mockSecretApi) ClearAll(cfg *ini.File) error { + return s.mClearAll(cfg) } func (s *mockSecretApi) SaveAWSCredential(cred *credentialexchange.AWSCredentials) error { @@ -186,8 +187,6 @@ func (s *mockSecretApi) SaveAWSCredential(cred *credentialexchange.AWSCredential } func Test_GetSamlCreds_With(t *testing.T) { - t.Parallel() - ttests := map[string]struct { config func(t *testing.T) credentialexchange.CredentialConfig handler func(t *testing.T, awsMock bool) http.Handler @@ -341,7 +340,15 @@ func Test_GetSamlCreds_With(t *testing.T) { return &mockAuthApi{} }, secretStore: func(t *testing.T) cmdutils.SecretStorageImpl { - return &mockSecretApi{} + ss := &mockSecretApi{} + ss.mCred = func() (*credentialexchange.AWSCredentials, error) { + return &credentialexchange.AWSCredentials{ + AWSAccessKey: "123", + AWSSecretKey: "12312s", + AWSSessionToken: "session-token", + PrincipalARN: "some-arn"}, nil + } + return ss }, expectErr: true, errTyp: cmdutils.ErrMissingArg, @@ -402,6 +409,8 @@ func Test_GetSamlCreds_With(t *testing.T) { } for name, tt := range ttests { t.Run(name, func(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(tt.handler(t, true)) defer ts.Close() conf := tt.config(t) diff --git a/internal/credentialexchange/config.go b/internal/credentialexchange/config.go index efa75d7..56a30f6 100644 --- a/internal/credentialexchange/config.go +++ b/internal/credentialexchange/config.go @@ -8,24 +8,34 @@ const ( ) type BaseConfig struct { - Role string - RoleChain []string - Username string - CfgSectionName string - StoreInProfile bool - DoKillHangingProcess bool - ReloadBeforeTime int + Role string `ini:"role"` + RoleChain []string `ini:"role-chain"` + BrowserExecutablePath string `ini:"browser-executable-path"` + Username string + CfgSectionName string + StoreInProfile bool + ReloadBeforeTime int } type CredentialConfig struct { BaseConfig BaseConfig - ProviderUrl string - PrincipalArn string + ProviderUrl string `ini:"provider-url"` + PrincipalArn string `ini:"principal"` AcsUrl string - Duration int - IsSso bool - SsoRegion string - SsoRole string - SsoUserEndpoint string + Duration int `ini:"duration"` + IsSso bool `ini:"is-sso"` + SsoRegion string `ini:"sso-region"` + SsoRole string `ini:"sso-role"` + SsoUserEndpoint string `ini:"is-sso-endpoint"` SsoCredFedEndpoint string } + +// --cfg-section aws_travelodge_ssvc +// --store-profile +// -p "https://accounts.google.com/o/saml2/initsso?idpid=C03uqod6r&spid=759219486523&forceauthn=false" +// --principal "arn:aws:iam::881490129763:saml-provider/GoogleIdP" +// --role "arn:aws:iam::881490129763:role/IdP-admin" +// --role-chain "arn:aws:iam::881490129763:role/SSO-admin" +// -d 3600 +// --reload-before 120 +// --executable-path="/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" diff --git a/internal/credentialexchange/helper.go b/internal/credentialexchange/helper.go index b5c6f37..8a7a5c6 100644 --- a/internal/credentialexchange/helper.go +++ b/internal/credentialexchange/helper.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "dario.cat/mergo" ini "gopkg.in/ini.v1" ) @@ -32,14 +33,13 @@ func HomeDir() string { return home } +// ConfigIniFile returns the ini file if specified a path or default one +// located in `~/.aws-cli-auth.ini` func ConfigIniFile(basePath string) string { - var base string if basePath != "" { - base = basePath - } else { - base = HomeDir() + return basePath } - return path.Join(base, fmt.Sprintf(".%s.ini", SELF_NAME)) + return path.Join(HomeDir(), fmt.Sprintf(".%s.ini", SELF_NAME)) } func SessionName(username, selfName string) string { @@ -72,45 +72,6 @@ func SetCredentials(creds *AWSCredentials, config CredentialConfig) error { return returnStdOutAsJson(*creds) } -func storeCredentialsInProfile(creds AWSCredentials, configSection string) error { - basePath := path.Join(HomeDir(), ".aws") - awsConfPath := path.Join(basePath, "credentials") - - if _, err := os.Stat(basePath); os.IsNotExist(err) { - if err := os.Mkdir(basePath, 0755); err != nil { - return err - } - if err := os.WriteFile(awsConfPath, []byte(``), 0755); err != nil { - return err - } - } - - if overriddenpath, exists := os.LookupEnv("AWS_SHARED_CREDENTIALS_FILE"); exists { - awsConfPath = overriddenpath - } - - cfg, err := ini.Load(awsConfPath) - if err != nil { - return err - } - cfg.Section(configSection).Key(awsAccessKeySection).SetValue(creds.AWSAccessKey) - cfg.Section(configSection).Key(awsSecretKeyIdSection).SetValue(creds.AWSSecretKey) - cfg.Section(configSection).Key(awsSessionTokenSection).SetValue(creds.AWSSessionToken) - return cfg.SaveTo(awsConfPath) -} - -func returnStdOutAsJson(creds AWSCredentials) error { - creds.Version = 1 - - jsonBytes, err := json.Marshal(creds) - if err != nil { - // Errorf("Unexpected AWS credential response") - return err - } - _, _ = fmt.Fprint(os.Stdout, string(jsonBytes)) - return nil -} - // GetWebIdTokenFileContents reads the contents of the `AWS_WEB_IDENTITY_TOKEN_FILE` environment variable. // Used only with specific assume func GetWebIdTokenFileContents() (string, error) { @@ -136,6 +97,33 @@ func ReloadBeforeExpiry(expiry time.Time, reloadBeforeSeconds int) bool { return diff.Seconds() < float64(reloadBeforeSeconds) } +func LoadCliConfig(cfg *ini.File, cfgSection string) (*CredentialConfig, error) { + if cfg.HasSection("config") { + configSection, err := cfg.GetSection("config") + if err != nil { + return nil, err + } + mainBaseConfig := &BaseConfig{} + mainConfig := &CredentialConfig{} + _ = configSection.MapTo(mainConfig) + _ = configSection.MapTo(mainBaseConfig) + for _, section := range configSection.ChildSections() { + if fmt.Sprintf("config.%s", cfgSection) == section.Name() { + sectionBaseConfig := &BaseConfig{} + sectionConfig := &CredentialConfig{} + _ = section.MapTo(sectionConfig) + _ = section.MapTo(sectionBaseConfig) + _ = mergo.Merge(mainBaseConfig, sectionBaseConfig, mergo.WithOverride) + _ = mergo.Merge(mainConfig, sectionConfig, mergo.WithOverride) + mainConfig.BaseConfig = *mainBaseConfig + break + } + } + return mainConfig, nil + } + return &CredentialConfig{}, nil +} + // WriteIniSection update ini sections in own config file func WriteIniSection(role string) error { section := fmt.Sprintf("%s.%s", INI_CONF_SECTION, RoleKeyConverter(role)) @@ -154,3 +142,42 @@ func WriteIniSection(role string) error { return nil } + +func storeCredentialsInProfile(creds AWSCredentials, configSection string) error { + basePath := path.Join(HomeDir(), ".aws") + awsConfPath := path.Join(basePath, "credentials") + + if _, err := os.Stat(basePath); os.IsNotExist(err) { + if err := os.Mkdir(basePath, 0755); err != nil { + return err + } + if err := os.WriteFile(awsConfPath, []byte(``), 0755); err != nil { + return err + } + } + + if overriddenpath, exists := os.LookupEnv("AWS_SHARED_CREDENTIALS_FILE"); exists { + awsConfPath = overriddenpath + } + + cfg, err := ini.Load(awsConfPath) + if err != nil { + return err + } + cfg.Section(configSection).Key(awsAccessKeySection).SetValue(creds.AWSAccessKey) + cfg.Section(configSection).Key(awsSecretKeyIdSection).SetValue(creds.AWSSecretKey) + cfg.Section(configSection).Key(awsSessionTokenSection).SetValue(creds.AWSSessionToken) + return cfg.SaveTo(awsConfPath) +} + +func returnStdOutAsJson(creds AWSCredentials) error { + creds.Version = 1 + + jsonBytes, err := json.Marshal(creds) + if err != nil { + // Errorf("Unexpected AWS credential response") + return err + } + _, _ = fmt.Fprint(os.Stdout, string(jsonBytes)) + return nil +} diff --git a/internal/credentialexchange/helper_test.go b/internal/credentialexchange/helper_test.go index cd38a7d..fcbf9cd 100644 --- a/internal/credentialexchange/helper_test.go +++ b/internal/credentialexchange/helper_test.go @@ -249,3 +249,43 @@ func Test_SetCredentials_with(t *testing.T) { }) } } + +func Test_Helper_LoadCliConfig(t *testing.T) { + + f, err := os.CreateTemp(os.TempDir(), "ini-conf-*") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + f.Write([]byte(` +[config] +browser-executable-path = "/my/Browser" +duration = 3600 + +[config.specific_section] +role = arn:aws:iam::1233444555:role/SSO-admin +role-chain = arn:aws:iam::99993444555:role/SSO-admin +principal = arn:aws:iam::1233444555:saml-provider/GoogleIdP +provider-url = https://accounts.google.com/o/saml2/initsso?idpid=some-foo-id123&forceauthn=false +`)) + ini, err := ini.Load(credentialexchange.ConfigIniFile(f.Name())) + if err != nil { + t.Fatal() + } + cfg, err := credentialexchange.LoadCliConfig(ini, "specific_section") + if err != nil { + t.Fatal(err) + } + if cfg.Duration != 3600 { + t.Error() + } + if cfg.BaseConfig.Role != "arn:aws:iam::1233444555:role/SSO-admin" { + t.Error() + } + if cfg.BaseConfig.BrowserExecutablePath != "/my/Browser" { + t.Error() + } + if cfg.ProviderUrl != "https://accounts.google.com/o/saml2/initsso?idpid=some-foo-id123&forceauthn=false" { + t.Error() + } +} diff --git a/internal/credentialexchange/secret.go b/internal/credentialexchange/secret.go index 23f7b29..d78c59e 100644 --- a/internal/credentialexchange/secret.go +++ b/internal/credentialexchange/secret.go @@ -90,6 +90,48 @@ func NewSecretStore(roleArn, namer, baseDir, username string) (*SecretStore, err }, nil } +func (s *SecretStore) AWSCredential() (*AWSCredentials, error) { + if err := s.load(); err != nil { + return nil, fmt.Errorf("secret store: %s, %w", err, ErrUnableToLoadAWSCred) + } + + if s.AWSCredentials == nil && s.AWSCredJson == "" { + return nil, nil + } + + return s.AWSCredentials, nil +} + +func (s *SecretStore) SaveAWSCredential(cred *AWSCredentials) error { + s.AWSCredentials = cred + jsonStr, err := json.Marshal(cred) + if err != nil { + return err + } + s.AWSCredJson = string(jsonStr) + return s.save() +} + +// ClearAll loops through all the sections in the INI file +// deletes them from the keychain implementation on the OS +func (s *SecretStore) ClearAll(cfg *ini.File) error { + srvSections := []string{} + + for _, v := range cfg.Section(INI_CONF_SECTION).ChildSections() { + srvSections = append(srvSections, strings.ReplaceAll(v.Name(), fmt.Sprintf("%s.", INI_CONF_SECTION), "")) + } + + for _, v := range srvSections { + srv := fmt.Sprintf("%s-%s", SELF_NAME, v) + // fmt.Fprintf(os.Stderr, "username: %s\ncredentialsecret: %s\n", s.secretUser, srv) + if err := s.keyring.Delete(srv, s.secretUser); err != nil { + return fmt.Errorf("%s, %w", err, ErrFailedToClearSecretStorage) + } + } + + return nil +} + func (s *SecretStore) ensureLock() (func(), error) { acquired, lock, err := s.locker.Acquire(s.lockResource, lockgate.AcquireOptions{Shared: false, Timeout: 1 * time.Minute}) @@ -154,56 +196,6 @@ func (s *SecretStore) save() error { return s.keyring.Set(s.secretService, s.secretUser, s.AWSCredJson) } -func (s *SecretStore) AWSCredential() (*AWSCredentials, error) { - if err := s.load(); err != nil { - return nil, fmt.Errorf("secret store: %s, %w", err, ErrUnableToLoadAWSCred) - } - - if s.AWSCredentials == nil && s.AWSCredJson == "" { - return nil, nil - } - - return s.AWSCredentials, nil -} - -func (s *SecretStore) SaveAWSCredential(cred *AWSCredentials) error { - s.AWSCredentials = cred - jsonStr, err := json.Marshal(cred) - if err != nil { - return err - } - s.AWSCredJson = string(jsonStr) - return s.save() -} - -func (s *SecretStore) Clear() error { - return s.keyring.Delete(s.secretService, s.secretUser) -} - -// ClearAll loops through all the sections in the INI file -// deletes them from the keychain implementation on the OS -func (s *SecretStore) ClearAll() error { - srvSections := []string{} - cfg, err := ini.Load(ConfigIniFile("")) - if err != nil { - return fmt.Errorf("unable to get sections from ini: %s, %w", err, ErrUnableToRetrieveSections) - } - - for _, v := range cfg.Section(INI_CONF_SECTION).ChildSections() { - srvSections = append(srvSections, strings.ReplaceAll(v.Name(), fmt.Sprintf("%s.", INI_CONF_SECTION), "")) - } - - for _, v := range srvSections { - srv := fmt.Sprintf("%s-%s", SELF_NAME, v) - // fmt.Fprintf(os.Stderr, "username: %s\ncredentialsecret: %s\n", s.secretUser, srv) - if err := s.keyring.Delete(srv, s.secretUser); err != nil { - return fmt.Errorf("%s, %w", err, ErrFailedToClearSecretStorage) - } - } - - return nil -} - // RoleKeyConverter converts a role to a key used for storing in key store func RoleKeyConverter(role string) string { return strings.ReplaceAll(strings.ReplaceAll(role, ":", "_"), "/", "____") diff --git a/internal/credentialexchange/secret_test.go b/internal/credentialexchange/secret_test.go index 92759ef..6c08645 100644 --- a/internal/credentialexchange/secret_test.go +++ b/internal/credentialexchange/secret_test.go @@ -11,6 +11,7 @@ import ( "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" "github.com/werf/lockgate" "github.com/zalando/go-keyring" + "gopkg.in/ini.v1" ) var roleTest string = "arn:aws:iam::111122342343:role/DevAdmin" @@ -292,7 +293,7 @@ func Test_ClearAll_with(t *testing.T) { } for name, tt := range ttests { t.Run(name, func(t *testing.T) { - tmpDir, _ := os.MkdirTemp(os.TempDir(), "saml-cred-test") + tmpDir, _ := os.MkdirTemp(os.TempDir(), "saml-cred-test-*") iniFile := path.Join(tmpDir, fmt.Sprintf(".%s.ini", credentialexchange.SELF_NAME)) _ = os.WriteFile(iniFile, []byte(` [role] @@ -313,8 +314,12 @@ name = "arn:aws:iam::111122342343:role/DevAdmin" } crde.WithKeyring(tt.keyring(t)).WithLocker(tt.locker(t)) + cfgFile, err := ini.Load(iniFile) + if err != nil { + t.Fatal(err) + } - err := crde.ClearAll() + err = crde.ClearAll(cfgFile) if tt.expectErr { if err == nil { From e018fd8832d8a12f18043393310d48747c2f4f58 Mon Sep 17 00:00:00 2001 From: dnitsch Date: Sat, 27 Sep 2025 14:25:39 +0100 Subject: [PATCH 2/3] fix: clean up --- cmd/saml.go | 33 ++++++++++++++--------- docs/usage.md | 39 +++++++++++++++++++++++++++ internal/credentialexchange/config.go | 10 ------- internal/web/web.go | 5 ++++ 4 files changed, 64 insertions(+), 23 deletions(-) diff --git a/cmd/saml.go b/cmd/saml.go index a775bb5..e58c17e 100755 --- a/cmd/saml.go +++ b/cmd/saml.go @@ -79,10 +79,6 @@ func newSamlCmd(r *Root) { return err } - allRoles := credentialexchange.MergeRoleChain(flags.Role, r.rootFlags.RoleChain, flags.IsSso) - - conf.BaseConfig.RoleChain = allRoles - // now we want to overwrite anything set via the command line saveRole := flags.Role if flags.IsSso { @@ -93,6 +89,8 @@ func newSamlCmd(r *Root) { SsoCredsEndpointQuery, sc.ssoRoleAccount, sc.ssoRoleName) } + allRoles := credentialexchange.MergeRoleChain(conf.BaseConfig.Role, conf.BaseConfig.RoleChain, flags.IsSso) + if len(allRoles) > 0 { saveRole = allRoles[len(allRoles)-1] } @@ -105,10 +103,10 @@ func newSamlCmd(r *Root) { } // we want to remove any AWS_* env vars that could interfere with the default config - for _, envVar := range []string{"AWS_PROFILE", "AWS_ACCESS_KEY_ID", - "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"} { - os.Unsetenv(envVar) - } + // for _, envVar := range []string{"AWS_PROFILE", "AWS_ACCESS_KEY_ID", + // "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"} { + // os.Unsetenv(envVar) + // } awsConf, err := config.LoadDefaultConfig(ctx) if err != nil { @@ -116,10 +114,12 @@ func newSamlCmd(r *Root) { } svc := sts.NewFromConfig(awsConf) - webConfig := web.NewWebConf(r.Datadir).WithTimeout(flags.SamlTimeout) - webConfig.CustomChromeExecutable = flags.CustomExecutablePath + webConfig := web.NewWebConf(r.Datadir). + WithTimeout(flags.SamlTimeout). + WithCustomExecutable(conf.BaseConfig.BrowserExecutablePath) return cmdutils.GetCredsWebUI(ctx, svc, secretStore, *conf, webConfig) + }, PreRunE: func(cmd *cobra.Command, args []string) error { if flags.ReloadBeforeTime != 0 && flags.ReloadBeforeTime > r.rootFlags.Duration { @@ -183,11 +183,15 @@ func samlInitConfig(customPath string) (*ini.File, error) { } func ConfigFromFlags(fileConfig *credentialexchange.CredentialConfig, rf *RootCmdFlags, sf *SamlCmdFlags, user string) error { - + d := fileConfig.Duration + // 900 is the default + if rf.Duration != 900 { + d = rf.Duration + } flagSamlConf := &credentialexchange.CredentialConfig{ ProviderUrl: sf.ProviderUrl, PrincipalArn: sf.PrincipalArn, - Duration: rf.Duration, + Duration: d, AcsUrl: sf.AcsUrl, IsSso: sf.IsSso, SsoRegion: sf.SsoRegion, @@ -198,7 +202,7 @@ func ConfigFromFlags(fileConfig *credentialexchange.CredentialConfig, rf *RootCm StoreInProfile: rf.StoreInProfile, Role: sf.Role, // RoleChain is added in the command function - // RoleChain: allRoles, + RoleChain: rf.RoleChain, Username: user, CfgSectionName: rf.CfgSectionName, ReloadBeforeTime: sf.ReloadBeforeTime, @@ -207,10 +211,13 @@ func ConfigFromFlags(fileConfig *credentialexchange.CredentialConfig, rf *RootCm if err := mergo.Merge(&fileConfig.BaseConfig, flagBaseConfig, mergo.WithOverride); err != nil { return err } + baseConf := fileConfig.BaseConfig if err := mergo.Merge(fileConfig, flagSamlConf, mergo.WithOverride, mergo.WithOverrideEmptySlice); err != nil { return err } + fileConfig.BaseConfig = baseConf + fileConfig.Duration = d return nil } diff --git a/docs/usage.md b/docs/usage.md index 5d67342..434f1d3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -101,6 +101,45 @@ To give it a quick test. aws sts get-caller-identity --profile=nonprod_saml_admin ``` +### Configuration file + +You can specify the same parameters through the INI config file. + +```ini +; global config will be applied to config sections +[config] +browser-executable-path = "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" +duration = 3600 +provider-url = https://my-idp-url.com +; +; you can specify the below on the top level config +; NB: it does make more sense to have these in the specific sections +; anything set in the specific section will overwrite the global property +; anything set in the commandline will overwrite the conf file property +; +; role = main-assume-role +; role-chain = chain-role1,chain-role2 +; principal = +; is-sso = +; sso-region = +; sso-role = +; is-sso-endpoint = + +; Specific section overrides +[config.cfg-section-name] +role = arn:aws:iam::123456789101:role/IdP-admin +role-chain = arn:aws:iam::123456789101:role/SSO-admin +principal = arn:aws:iam::123456789101:saml-provider/GoogleIdP +provider-url = https://accounts.google.com/o/saml2/initsso?idpid=abc123&spid=1234567&forceauthn=false +; is-sso = false +; sso-region = eu-west-1 +; sso-role = +; is-sso-endpoint = + +; generated by aws-cli-auth +[role] +``` + ## AWS SSO Portal **NOW** Includes support for AWS User Portal, largely remains the same with a few exceptions/additions: diff --git a/internal/credentialexchange/config.go b/internal/credentialexchange/config.go index 56a30f6..430a2f4 100644 --- a/internal/credentialexchange/config.go +++ b/internal/credentialexchange/config.go @@ -29,13 +29,3 @@ type CredentialConfig struct { SsoUserEndpoint string `ini:"is-sso-endpoint"` SsoCredFedEndpoint string } - -// --cfg-section aws_travelodge_ssvc -// --store-profile -// -p "https://accounts.google.com/o/saml2/initsso?idpid=C03uqod6r&spid=759219486523&forceauthn=false" -// --principal "arn:aws:iam::881490129763:saml-provider/GoogleIdP" -// --role "arn:aws:iam::881490129763:role/IdP-admin" -// --role-chain "arn:aws:iam::881490129763:role/SSO-admin" -// -d 3600 -// --reload-before 120 -// --executable-path="/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" diff --git a/internal/web/web.go b/internal/web/web.go index cabdfa9..3a24150 100755 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -56,6 +56,11 @@ func (wc *WebConfig) WithNoSandbox() *WebConfig { return wc } +func (wc *WebConfig) WithCustomExecutable(browserPath string) *WebConfig { + wc.CustomChromeExecutable = browserPath + return wc +} + type Web struct { conf *WebConfig launcher *launcher.Launcher From 577d8642301dd9c1e7ecc6ad670a67edcd1e2aa5 Mon Sep 17 00:00:00 2001 From: dnitsch Date: Mon, 29 Sep 2025 09:08:24 +0100 Subject: [PATCH 3/3] fix: corrected spelling --- cmd/awscliauth_test.go | 4 ++-- cmd/clear.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/awscliauth_test.go b/cmd/awscliauth_test.go index 742b1dc..14419dd 100644 --- a/cmd/awscliauth_test.go +++ b/cmd/awscliauth_test.go @@ -87,7 +87,7 @@ func Test_Saml_timeout(t *testing.T) { func Test_SpecificCommand(t *testing.T) { - t.Run("Sepcific command should fail with wrong method", func(t *testing.T) { + t.Run("Specific command should fail with wrong method", func(t *testing.T) { _, _, err := cmdHelperExecutor(t, []string{"specific", "--method=unknown", "--role", "arn:aws:iam::1234111111111:role/Role-ReadOnly"}) if err == nil { @@ -98,7 +98,7 @@ func Test_SpecificCommand(t *testing.T) { } }) - t.Run("Sepcific command fails on missing env AWS_WEB_IDENTITY_TOKEN_FILE", func(t *testing.T) { + t.Run("Specific command fails on missing env AWS_WEB_IDENTITY_TOKEN_FILE", func(t *testing.T) { os.Setenv("AWS_ROLE_ARN", "arn:aws:iam::1234111111111:role/Role-ReadOnly") defer os.Unsetenv("AWS_ROLE_ARN") _, _, err := cmdHelperExecutor(t, []string{"specific", "--method=WEB_ID", "--role", diff --git a/cmd/clear.go b/cmd/clear.go index 743533e..80022b7 100644 --- a/cmd/clear.go +++ b/cmd/clear.go @@ -31,7 +31,7 @@ func newClearCmd(r *Root) { if err != nil { return err } - iniCfg, err := samlInitConfig("") + iniCfg, err := samlInitConfig(r.rootFlags.CustomIniLocation) if err != nil { return err }