diff --git a/.gitignore b/.gitignore index ed3a862..e64afd6 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ vendor/ .ignore* local/ .deps/ -.cache/ \ No newline at end of file +.cache/ +*.env diff --git a/aws-cli-auth.go b/aws-cli-auth.go index fa6577e..54c6fc9 100755 --- a/aws-cli-auth.go +++ b/aws-cli-auth.go @@ -2,29 +2,33 @@ package main import ( "context" - "log" "os" "os/signal" "syscall" + "time" "github.com/DevLabFoundry/aws-cli-auth/cmd" + "github.com/rs/zerolog" ) func main() { ctx, stop := signal.NotifyContext(context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM, os.Kill}...) defer stop() + logger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}). + Level(zerolog.ErrorLevel). + With().Timestamp(). + Logger() go func() { <-ctx.Done() stop() - // log.Printf("\x1b[31minterrupted: %s\x1b[0m", ctx.Err()) - os.Exit(0) + logger.Fatal().Msgf("\x1b[31minterrupted: %s\x1b[0m", ctx.Err()) }() - c := cmd.New() + c := cmd.New(logger) c.WithSubCommands(cmd.SubCommands()...) if err := c.Execute(ctx); err != nil { - log.Fatalf("\x1b[31m%s\x1b[0m", err) + logger.Fatal().Msgf("\x1b[31m%s\x1b[0m", err) } } diff --git a/cmd/awscliauth.go b/cmd/awscliauth.go index 39f4fcc..0e46063 100755 --- a/cmd/awscliauth.go +++ b/cmd/awscliauth.go @@ -9,6 +9,7 @@ import ( "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" "github.com/Ensono/eirctl/selfupdate" + "github.com/rs/zerolog" "github.com/spf13/cobra" ) @@ -23,6 +24,7 @@ type Root struct { // ChannelErr io.Writer // viperConf *viper.Viper rootFlags *RootCmdFlags + logger zerolog.Logger Datadir string } @@ -35,9 +37,10 @@ type RootCmdFlags struct { CustomIniLocation string } -func New() *Root { +func New(logger zerolog.Logger) *Root { rf := &RootCmdFlags{} r := &Root{ + logger: logger, rootFlags: rf, Cmd: &cobra.Command{ Use: "aws-cli-auth", diff --git a/cmd/awscliauth_test.go b/cmd/awscliauth_test.go index 14419dd..6145fd9 100644 --- a/cmd/awscliauth_test.go +++ b/cmd/awscliauth_test.go @@ -12,13 +12,14 @@ import ( "github.com/DevLabFoundry/aws-cli-auth/cmd" "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" "github.com/DevLabFoundry/aws-cli-auth/internal/web" + "github.com/rs/zerolog" ) func cmdHelperExecutor(t *testing.T, args []string) (stdOut *bytes.Buffer, errOut *bytes.Buffer, err error) { t.Helper() errOut = new(bytes.Buffer) stdOut = new(bytes.Buffer) - c := cmd.New() + c := cmd.New(zerolog.New(io.Discard)) c.WithSubCommands(cmd.SubCommands()...) c.Cmd.SetArgs(args) c.Cmd.SetErr(errOut) diff --git a/cmd/saml.go b/cmd/saml.go index e58c17e..f20bdf1 100755 --- a/cmd/saml.go +++ b/cmd/saml.go @@ -13,12 +13,15 @@ import ( "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" + validator "github.com/rezakhademix/govalidator/v2" + "github.com/rs/zerolog" "github.com/spf13/cobra" "gopkg.in/ini.v1" ) var ( ErrUnableToCreateSession = errors.New("sts - cannot start a new session") + ErrValidationFailed = errors.New("missing values") ) const ( @@ -64,17 +67,24 @@ func newSamlCmd(r *Root) { if err != nil { return err } - + if r.rootFlags.Verbose { + r.logger = r.logger.Level(zerolog.DebugLevel) + } + r.logger.Debug().Str("CustomIniLocation", r.rootFlags.CustomIniLocation).Msg("if empty using default ~/.aws-cli-auth.ini") iniFile, err := samlInitConfig(r.rootFlags.CustomIniLocation) if err != nil { return err } + r.logger.Debug().Msgf("iniFile: %+v", iniFile) + conf, err := credentialexchange.LoadCliConfig(iniFile, r.rootFlags.CfgSectionName) if err != nil { return err } + r.logger.Debug().Str("section", r.rootFlags.CfgSectionName).Msgf("loaded section: %+v", conf) + if err := ConfigFromFlags(conf, r.rootFlags, flags, user.Username); err != nil { return err } @@ -95,6 +105,11 @@ func newSamlCmd(r *Root) { saveRole = allRoles[len(allRoles)-1] } + r.logger.Debug().Str("saveRole", saveRole). + Str("SsoEndpoint", conf.SsoUserEndpoint). + Str("SsoCredFedEndpoint", conf.SsoCredFedEndpoint). + Msg("") + secretStore, err := credentialexchange.NewSecretStore(saveRole, fmt.Sprintf("%s-%s", credentialexchange.SELF_NAME, credentialexchange.RoleKeyConverter(saveRole)), os.TempDir(), user.Username) @@ -102,18 +117,16 @@ func newSamlCmd(r *Root) { return err } - // 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) + cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return fmt.Errorf("failed to create session %s, %w", err, ErrUnableToCreateSession) } - svc := sts.NewFromConfig(awsConf) + if cfg.Region == "" { + return fmt.Errorf("unable to deduce AWS region, AWS_REGION, AWS_DEFAULT_REGION, ~/.aws/config default or profile level region must be set") + } + + svc := sts.NewFromConfig(cfg) webConfig := web.NewWebConf(r.Datadir). WithTimeout(flags.SamlTimeout). WithCustomExecutable(conf.BaseConfig.BrowserExecutablePath) @@ -165,7 +178,7 @@ If this flag is specified the --sso-role must also be specified.`) // 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 CDP (ChromeDeubgProto)") // Add subcommand to root command r.Cmd.AddCommand(sc.cmd) } @@ -219,5 +232,24 @@ func ConfigFromFlags(fileConfig *credentialexchange.CredentialConfig, rf *RootCm fileConfig.BaseConfig = baseConf fileConfig.Duration = d + + return configValid(fileConfig) +} + +func configValid(config *credentialexchange.CredentialConfig) error { + v := validator.New() + ssoVal := !config.IsSso + if config.IsSso { + ssoVal = len(config.SsoRole) > 0 && len(config.SsoRegion) > 0 + } + v.RequiredString(config.ProviderUrl, "provider-url", "provider url must be specified"). + // RequiredString(config.BaseConfig.Role, "role", "role must be provided"). + RequiredString(config.PrincipalArn, "principal-arn", "principal ARN must be provided"). + CustomRule(ssoVal, "is-sso", "sso-role must be specified when is-sso is set"). + CustomRule((len(config.BaseConfig.Role) > 1 && len(config.SsoRole) < 1) || (len(config.BaseConfig.Role) < 1 && len(config.SsoRole) > 1), "role", "sso-role cannot be specified when role is also set") + + if v.IsFailed() { + return fmt.Errorf("%w %#q", ErrValidationFailed, v.Errors()) + } return nil } diff --git a/cmd/saml_test.go b/cmd/saml_test.go index bd7735f..275e693 100644 --- a/cmd/saml_test.go +++ b/cmd/saml_test.go @@ -1,6 +1,7 @@ package cmd_test import ( + "errors" "testing" "github.com/DevLabFoundry/aws-cli-auth/cmd" @@ -8,14 +9,15 @@ import ( "github.com/go-test/deep" ) -func Test_ConfigMerge(t *testing.T) { +func Test_ConfigMerge_succeeds(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", + PrincipalArn: "aw:arn:....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) @@ -28,8 +30,79 @@ func Test_ConfigMerge(t *testing.T) { RoleChain: []string{"role-123"}, Username: "me", }, + PrincipalArn: "aw:arn:....123", } if diff := deep.Equal(conf, want); len(diff) > 0 { t.Errorf("diff: %v", diff) } } + +func Test_ConfigMerge_fails_with_missing(t *testing.T) { + t.Run("provider not provided", func(t *testing.T) { + + conf := &credentialexchange.CredentialConfig{ + BaseConfig: credentialexchange.BaseConfig{ + BrowserExecutablePath: "/foo/path", + Role: "", + RoleChain: []string{"role-123"}, + }, + ProviderUrl: "", + } + err := cmd.ConfigFromFlags(conf, &cmd.RootCmdFlags{}, &cmd.SamlCmdFlags{Role: "role-overridden-from-flags"}, "me") + if !errors.Is(err, cmd.ErrValidationFailed) { + t.Error(err) + } + }) + t.Run("role not provided", func(t *testing.T) { + + conf := &credentialexchange.CredentialConfig{ + BaseConfig: credentialexchange.BaseConfig{ + BrowserExecutablePath: "/foo/path", + Role: "", + RoleChain: []string{"role-123"}, + }, + ProviderUrl: "https://my-idp.com/?app_id=testdd", + } + err := cmd.ConfigFromFlags(conf, &cmd.RootCmdFlags{}, &cmd.SamlCmdFlags{}, "me") + if !errors.Is(err, cmd.ErrValidationFailed) { + t.Error(err) + } + }) + t.Run("is-sso set but sso-role not set", func(t *testing.T) { + + conf := &credentialexchange.CredentialConfig{ + BaseConfig: credentialexchange.BaseConfig{ + BrowserExecutablePath: "/foo/path", + Role: "", + RoleChain: []string{"role-123"}, + }, + PrincipalArn: "some-arn", + IsSso: true, + SsoRegion: "", + SsoRole: "foo:bar", + ProviderUrl: "https://my-idp.com/?app_id=testdd", + } + err := cmd.ConfigFromFlags(conf, &cmd.RootCmdFlags{}, &cmd.SamlCmdFlags{}, "me") + if !errors.Is(err, cmd.ErrValidationFailed) { + t.Error(err) + } + }) + t.Run("role and sso-role both provided", func(t *testing.T) { + + conf := &credentialexchange.CredentialConfig{ + BaseConfig: credentialexchange.BaseConfig{ + BrowserExecutablePath: "/foo/path", + Role: "", + RoleChain: []string{"role-123"}, + }, + PrincipalArn: "some-arn", + SsoRegion: "foo", + SsoRole: "foo:bar", + ProviderUrl: "https://my-idp.com/?app_id=testdd", + } + err := cmd.ConfigFromFlags(conf, &cmd.RootCmdFlags{}, &cmd.SamlCmdFlags{Role: "wrong-role"}, "me") + if !errors.Is(err, cmd.ErrValidationFailed) { + t.Error(err) + } + }) +} diff --git a/eirctl.yaml b/eirctl.yaml index 678352e..a08a26e 100644 --- a/eirctl.yaml +++ b/eirctl.yaml @@ -1,5 +1,5 @@ import: - - https://raw.githubusercontent.com/Ensono/eirctl/e71dd9d66293e27e70fd0620e63a6d627579c060/shared/build/go/eirctl.yaml + - https://raw.githubusercontent.com/Ensono/eirctl/refs/tags/v0.9.7/shared/build/go/eirctl.yaml contexts: unit:test: @@ -12,7 +12,7 @@ contexts: - GO pipelines: - build: + build: - task: build:unix - task: build:win depends_on: build:unix @@ -20,6 +20,8 @@ pipelines: unit:test:run: - task: unit:test:prereqs - task: unit:test + env: + ROOT_PKG_NAME: github.com/DevLabFoundry depends_on: unit:test:prereqs bin:release: @@ -34,7 +36,7 @@ pipelines: tasks: tag: - command: + command: - | git tag -a ${VERSION} -m "ci tag release" ${REVISION} git push origin ${VERSION} @@ -48,6 +50,7 @@ tasks: description: | Unit test runner needs a bit of extra care in this case to ensure we have all the dependencies command: | + unset GOTOOLCHAIN export GOPATH=$PWD/.deps GOBIN=$PWD/.deps/bin 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 @@ -56,7 +59,7 @@ tasks: unit:test:prereqs: description: Installs coverage and junit tools context: unit:test - command: + command: - | mkdir -p .coverage export GOPATH="${PWD}/.deps" GOBIN="${PWD}/.deps/bin" @@ -65,13 +68,14 @@ tasks: go install github.com/AlekSi/gocov-xml@v1.0.0 clean:dir: - command: + command: - | rm -rf dist/ build:win: context: go1x description: Builds Go binary + reset_context: true command: - | mkdir -p .deps diff --git a/go.mod b/go.mod index 77132d8..9cd8fd2 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/DevLabFoundry/aws-cli-auth -go 1.25.4 +go 1.25.5 require ( github.com/aws/aws-sdk-go-v2 v1.39.6 @@ -8,6 +8,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 github.com/aws/smithy-go v1.23.2 github.com/go-rod/rod v0.116.2 + github.com/rezakhademix/govalidator/v2 v2.1.2 github.com/spf13/cobra v1.10.1 github.com/werf/lockgate v0.1.1 github.com/zalando/go-keyring v0.2.6 @@ -15,8 +16,11 @@ require ( ) require ( + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rs/zerolog v1.34.0 // indirect github.com/schollz/progressbar/v3 v3.18.0 // indirect golang.org/x/term v0.37.0 // indirect ) diff --git a/go.sum b/go.sum index 30bbfee..03fe12e 100644 --- a/go.sum +++ b/go.sum @@ -6,12 +6,8 @@ github.com/Ensono/eirctl v0.9.6 h1:G6S0ZJ2VtedGW2/nn8sbMQnNbLVXgjwNJnuLEHUjJRc= github.com/Ensono/eirctl v0.9.6/go.mod h1:pxX1iE+guf8Lyvs98FkNnMKqyTtHaLrJgB3f4foEROk= github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= -github.com/aws/aws-sdk-go-v2/config v1.31.19 h1:qdUtOw4JhZr2YcKO3g0ho/IcFXfXrrb8xlX05Y6EvSw= -github.com/aws/aws-sdk-go-v2/config v1.31.19/go.mod h1:tMJ8bur01t8eEm0atLadkIIFA154OJ4JCKZeQ+o+R7k= github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc= github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0= -github.com/aws/aws-sdk-go-v2/credentials v1.18.23 h1:IQILcxVgMO2BVLaJ2aAv21dKWvE1MduNrbvuK43XL2Q= -github.com/aws/aws-sdk-go-v2/credentials v1.18.23/go.mod h1:JRodHszhVdh5TPUknxDzJzrMiznG+M+FfR3WSWKgCI8= github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg= github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= @@ -26,16 +22,10 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/A github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.2 h1:/p6MxkbQoCzaGQT3WO0JwG0FlQyG9RD8VmdmoKc5xqU= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.2/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ= github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.6 h1:0dES42T2dhICCbVB3JSTTn7+Bz93wfJEK1b7jksZIyQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.6/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= -github.com/aws/aws-sdk-go-v2/service/sts v1.40.1 h1:5sbIM57lHLaEaNWdIx23JH30LNBsSDkjN/QXGcRLAFc= -github.com/aws/aws-sdk-go-v2/service/sts v1.40.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0= github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= @@ -50,6 +40,7 @@ github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7m github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= @@ -61,6 +52,7 @@ 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.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= @@ -75,18 +67,27 @@ github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcI github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rezakhademix/govalidator/v2 v2.1.2 h1:qqCIkWC6sWr8zeW9zCkYEJxbZMt/Dn1ASXkGIQe3rDI= +github.com/rezakhademix/govalidator/v2 v2.1.2/go.mod h1:be7JrYM3STiL5jYt1WrQN5ArR8xTov/DvWJ9yXtULj8= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= @@ -125,6 +126,9 @@ github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8u github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= diff --git a/internal/credentialexchange/config.go b/internal/credentialexchange/config.go index 430a2f4..93bcb55 100644 --- a/internal/credentialexchange/config.go +++ b/internal/credentialexchange/config.go @@ -1,5 +1,11 @@ package credentialexchange +import ( + "encoding/json" + "fmt" + "time" +) + const ( SELF_NAME = "aws-cli-auth" WEB_ID_TOKEN_VAR = "AWS_WEB_IDENTITY_TOKEN_FILE" @@ -29,3 +35,43 @@ type CredentialConfig struct { SsoUserEndpoint string `ini:"is-sso-endpoint"` SsoCredFedEndpoint string } + +// AWSRole aws role attributes +type AWSRoleConfig struct { + RoleARN string + PrincipalARN string + Name string +} + +// AWSCredentials is a representation of the returned credential +type AWSCredentials struct { + Version int + AWSAccessKey string `json:"AccessKeyId"` + AWSSecretKey string `json:"SecretAccessKey"` + AWSSessionToken string `json:"SessionToken"` + PrincipalARN string `json:"-"` + Expires time.Time `json:"Expiration"` +} + +// roleCreds can be encapsulated in this function +// never used outside of this scope for now +type roleCreds struct { + RoleCreds struct { + AccessKey string `json:"accessKeyId"` + SecretKey string `json:"secretAccessKey"` + SessionToken string `json:"sessionToken"` + Expiration int64 `json:"expiration"` + } `json:"roleCredentials"` +} + +func (a *AWSCredentials) FromRoleCredString(cred string) (*AWSCredentials, error) { + rc := &roleCreds{} + if err := json.Unmarshal([]byte(cred), rc); err != nil { + return nil, fmt.Errorf("%s, %w", err, ErrUnmarshalCred) + } + a.AWSAccessKey = rc.RoleCreds.AccessKey + a.AWSSecretKey = rc.RoleCreds.SecretKey + a.AWSSessionToken = rc.RoleCreds.SessionToken + a.Expires = time.UnixMilli(rc.RoleCreds.Expiration) + return a, nil +} diff --git a/internal/credentialexchange/credentialexchange.go b/internal/credentialexchange/credentialexchange.go index d9dc7f0..03ce845 100755 --- a/internal/credentialexchange/credentialexchange.go +++ b/internal/credentialexchange/credentialexchange.go @@ -2,7 +2,6 @@ package credentialexchange import ( "context" - "encoding/json" "errors" "fmt" "os" @@ -21,51 +20,16 @@ var ( ErrUnmarshalCred = errors.New("unable to unmarshal credential from string") ) -// AWSRole aws role attributes -type AWSRoleConfig struct { - RoleARN string - PrincipalARN string - Name string -} - -// AWSCredentials is a representation of the returned credential -type AWSCredentials struct { - Version int - AWSAccessKey string `json:"AccessKeyId"` - AWSSecretKey string `json:"SecretAccessKey"` - AWSSessionToken string `json:"SessionToken"` - PrincipalARN string `json:"-"` - Expires time.Time `json:"Expiration"` -} - -func (a *AWSCredentials) FromRoleCredString(cred string) (*AWSCredentials, error) { - // RoleCreds can be encapsulated in this function - // never used outside of this scope for now - type RoleCreds struct { - RoleCreds struct { - AccessKey string `json:"accessKeyId"` - SecretKey string `json:"secretAccessKey"` - SessionToken string `json:"sessionToken"` - Expiration int64 `json:"expiration"` - } `json:"roleCredentials"` - } - rc := &RoleCreds{} - if err := json.Unmarshal([]byte(cred), rc); err != nil { - return nil, fmt.Errorf("%s, %w", err, ErrUnmarshalCred) - } - a.AWSAccessKey = rc.RoleCreds.AccessKey - a.AWSSecretKey = rc.RoleCreds.SecretKey - a.AWSSessionToken = rc.RoleCreds.SessionToken - a.Expires = time.UnixMilli(rc.RoleCreds.Expiration) - return a, nil -} - type AuthSamlApi interface { AssumeRoleWithSAML(ctx context.Context, params *sts.AssumeRoleWithSAMLInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithSAMLOutput, error) GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) AssumeRole(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) } +type authWebTokenApi interface { + AssumeRoleWithWebIdentity(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error) +} + // LoginStsSaml exchanges saml response for STS creds func LoginStsSaml(ctx context.Context, samlResponse string, role AWSRole, svc AuthSamlApi) (*AWSCredentials, error) { @@ -78,7 +42,7 @@ func LoginStsSaml(ctx context.Context, samlResponse string, role AWSRole, svc Au resp, err := svc.AssumeRoleWithSAML(ctx, params) if err != nil { - return nil, fmt.Errorf("failed to retrieve STS credentials using SAML: %s, %w", err.Error(), ErrUnableAssume) + return nil, fmt.Errorf("%w, failed to retrieve STS credentials using SAML: %s", ErrUnableAssume, err.Error()) } return &AWSCredentials{ @@ -90,15 +54,6 @@ func LoginStsSaml(ctx context.Context, samlResponse string, role AWSRole, svc Au }, nil } -type credsProvider struct { - accessKey, secretKey, sessionToken string - expiry time.Time -} - -func (c *credsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { - return aws.Credentials{AccessKeyID: c.accessKey, SecretAccessKey: c.secretKey, SessionToken: c.sessionToken, CanExpire: true, Expires: c.expiry}, nil -} - // IsValid checks current credentials and // returns them if they are still valid // if reloadTimeBefore is less than time left on the creds @@ -109,11 +64,6 @@ func IsValid(ctx context.Context, currentCreds *AWSCredentials, reloadBeforeTime } if _, err := svc.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}, func(o *sts.Options) { - // set the default region for the - // if o.EndpointOptions.GetResolvedRegion() == "" { - // // cannot determine - // o.BaseEndpoint = aws.String("https://sts.amazonaws.com") - // } o.Credentials = &credsProvider{currentCreds.AWSAccessKey, currentCreds.AWSSecretKey, currentCreds.AWSSessionToken, currentCreds.Expires} }); err != nil { // var oe *smithy.OperationError @@ -130,10 +80,6 @@ func IsValid(ctx context.Context, currentCreds *AWSCredentials, reloadBeforeTime return !ReloadBeforeExpiry(currentCreds.Expires, reloadBeforeTime), nil } -type authWebTokenApi interface { - AssumeRoleWithWebIdentity(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error) -} - // LoginAwsWebToken func LoginAwsWebToken(ctx context.Context, username string, svc authWebTokenApi) (*AWSCredentials, error) { // var role string @@ -167,6 +113,23 @@ func LoginAwsWebToken(ctx context.Context, username string, svc authWebTokenApi) }, nil } +// AssumeRoleInChain loops over all the roles provided +// If none are provided it will return the baseCreds +func AssumeRoleInChain(ctx context.Context, baseCreds *AWSCredentials, svc AuthSamlApi, username string, roles []string, conf CredentialConfig) (*AWSCredentials, error) { + duration := int32(900) + for idx, r := range roles { + if len(roles) == idx+1 { + duration = int32(conf.Duration) + } + c, err := assumeRoleWithCreds(ctx, baseCreds, svc, username, r, duration) + if err != nil { + return nil, err + } + baseCreds = c + } + return baseCreds, nil +} + // AssumeRoleWithCreds uses existing creds retrieved from anywhere // to pass to a credential provider and assume a specific role // @@ -199,19 +162,11 @@ func assumeRoleWithCreds(ctx context.Context, currentCreds *AWSCredentials, svc }, nil } -// AssumeRoleInChain loops over all the roles provided -// If none are provided it will return the baseCreds -func AssumeRoleInChain(ctx context.Context, baseCreds *AWSCredentials, svc AuthSamlApi, username string, roles []string, conf CredentialConfig) (*AWSCredentials, error) { - duration := int32(900) - for idx, r := range roles { - if len(roles) == idx+1 { - duration = int32(conf.Duration) - } - c, err := assumeRoleWithCreds(ctx, baseCreds, svc, username, r, duration) - if err != nil { - return nil, err - } - baseCreds = c - } - return baseCreds, nil +type credsProvider struct { + accessKey, secretKey, sessionToken string + expiry time.Time +} + +func (c *credsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { + return aws.Credentials{AccessKeyID: c.accessKey, SecretAccessKey: c.secretKey, SessionToken: c.sessionToken, CanExpire: true, Expires: c.expiry}, nil }