From dd291f09e98984922dab71949441c7b778edf61e Mon Sep 17 00:00:00 2001 From: dnitsch Date: Wed, 25 Oct 2023 16:37:30 +0100 Subject: [PATCH 1/5] fix: add refactor --- Makefile | 5 +- cmd/clear.go | 18 ++- cmd/root.go | 16 +-- cmd/saml.go | 24 +++- cmd/specific.go | 41 +++++-- go.mod | 9 +- go.sum | 25 ++-- internal/auth/saml.go | 51 -------- internal/cmdutils/cmdutils.go | 85 +++++++++++++ .../{config => credentialexchange}/config.go | 2 +- .../credentialexchange.go} | 80 ++++++------ internal/credentialexchange/doc.go | 2 + .../{util => credentialexchange}/helper.go | 75 ++++++------ .../helper_test.go | 45 +++++-- .../{util => credentialexchange}/secret.go | 114 ++++++++++++------ .../secret_test.go | 12 +- internal/util/log.go | 67 ---------- internal/util/types.go | 21 ---- internal/web/web.go | 22 ++-- 19 files changed, 371 insertions(+), 343 deletions(-) delete mode 100644 internal/auth/saml.go create mode 100644 internal/cmdutils/cmdutils.go rename internal/{config => credentialexchange}/config.go (94%) rename internal/{auth/awssts.go => credentialexchange/credentialexchange.go} (53%) create mode 100644 internal/credentialexchange/doc.go rename internal/{util => credentialexchange}/helper.go (69%) rename internal/{util => credentialexchange}/helper_test.go (50%) rename internal/{util => credentialexchange}/secret.go (53%) rename internal/{util => credentialexchange}/secret_test.go (63%) delete mode 100644 internal/util/log.go delete mode 100644 internal/util/types.go diff --git a/Makefile b/Makefile index 95b64fd..ec3f396 100644 --- a/Makefile +++ b/Makefile @@ -21,11 +21,8 @@ test_prereq: go install github.com/axw/gocov/gocov@v1.0.0 && \ go install github.com/AlekSi/gocov-xml@v1.0.0 -tidy: install - go mod tidy - install: - go mod vendor + go mod tidy .PHONY: clean clean: diff --git a/cmd/clear.go b/cmd/clear.go index 86e8ef0..bd50bd9 100644 --- a/cmd/clear.go +++ b/cmd/clear.go @@ -1,9 +1,10 @@ package cmd import ( + "fmt" "os" - "github.com/dnitsch/aws-cli-auth/internal/util" + "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" "github.com/dnitsch/aws-cli-auth/internal/web" "github.com/spf13/cobra" ) @@ -24,19 +25,24 @@ func init() { } func clear(cmd *cobra.Command, args []string) error { - web := web.New() - secretStore := util.NewSecretStore("") + + web := web.New("") + + secretStore, err := credentialexchange.NewSecretStore("") + if err != nil { + return err + } if force { if err := web.ClearCache(); err != nil { - util.Exit(err) + return err } - util.Debugf("Chromium Cache cleared") + fmt.Fprint(os.Stderr, "Chromium Cache cleared") } secretStore.ClearAll() - if err := os.Remove(util.ConfigIniFile("")); err != nil { + if err := os.Remove(credentialexchange.ConfigIniFile("")); err != nil { return err } diff --git a/cmd/root.go b/cmd/root.go index ddf4620..27d13f3 100755 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,8 +4,8 @@ import ( "fmt" "os" - "github.com/dnitsch/aws-cli-auth/internal/config" - "github.com/dnitsch/aws-cli-auth/internal/util" + "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" + "github.com/spf13/cobra" ) @@ -31,9 +31,10 @@ Stores them under the $HOME/.aws/credentials file under a specified path or retu func Execute() { if err := rootCmd.Execute(); err != nil { - util.Exit(err) + fmt.Errorf("cli error: %v", err) + os.Exit(1) } - util.CleanExit() + os.Exit(0) } func init() { @@ -45,11 +46,10 @@ func init() { } func initConfig() { - util.IsTraceEnabled = verbose - if _, err := os.Stat(util.ConfigIniFile("")); err != nil { + if _, err := os.Stat(credentialexchange.ConfigIniFile("")); err != nil { // creating a file - rolesInit := []byte(fmt.Sprintf("[%s]\n", config.INI_CONF_SECTION)) - err := os.WriteFile(util.ConfigIniFile(""), rolesInit, 0644) + rolesInit := []byte(fmt.Sprintf("[%s]\n", credentialexchange.INI_CONF_SECTION)) + err := os.WriteFile(credentialexchange.ConfigIniFile(""), rolesInit, 0644) cobra.CheckErr(err) } } diff --git a/cmd/saml.go b/cmd/saml.go index c3e5275..5bc5ed3 100755 --- a/cmd/saml.go +++ b/cmd/saml.go @@ -1,13 +1,20 @@ package cmd import ( + "errors" "fmt" - "github.com/dnitsch/aws-cli-auth/internal/auth" - "github.com/dnitsch/aws-cli-auth/internal/config" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/sts" + "github.com/dnitsch/aws-cli-auth/internal/cmdutils" + "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" "github.com/spf13/cobra" ) +var ( + ErrUnableToCreateSession = errors.New("sts - cannot start a new session") +) + var ( providerUrl string principalArn string @@ -42,12 +49,12 @@ func init() { } func getSaml(cmd *cobra.Command, args []string) error { - conf := config.SamlConfig{ + conf := credentialexchange.SamlConfig{ ProviderUrl: providerUrl, PrincipalArn: principalArn, Duration: duration, AcsUrl: acsUrl, - BaseConfig: config.BaseConfig{ + BaseConfig: credentialexchange.BaseConfig{ StoreInProfile: storeInProfile, Role: role, CfgSectionName: cfgSectionName, @@ -56,7 +63,14 @@ func getSaml(cmd *cobra.Command, args []string) error { }, } - if err := auth.GetSamlCreds(conf); err != nil { + sess, err := session.NewSession() + if err != nil { + return fmt.Errorf("failed to create session %s, %w", err, ErrUnableToCreateSession) + } + + svc := sts.New(sess) + + if err := cmdutils.GetSamlCreds(svc, conf); err != nil { return err } return nil diff --git a/cmd/specific.go b/cmd/specific.go index 686e216..56ae0ed 100644 --- a/cmd/specific.go +++ b/cmd/specific.go @@ -2,11 +2,13 @@ package cmd import ( "fmt" - "os" + "os/user" - "github.com/dnitsch/aws-cli-auth/internal/auth" - "github.com/dnitsch/aws-cli-auth/internal/config" - "github.com/dnitsch/aws-cli-auth/internal/util" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/sts" + "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" "github.com/spf13/cobra" ) @@ -28,12 +30,24 @@ func init() { } func specific(cmd *cobra.Command, args []string) error { - var awsCreds *util.AWSCredentials - var err error + var awsCreds *credentialexchange.AWSCredentials + sess, err := session.NewSession() + if err != nil { + return fmt.Errorf("failed to create session %s, %w", err, ErrUnableToCreateSession) + } + + svc := sts.New(sess) + + user, err := user.Current() + + if err != nil { + return err + } + if method != "" { switch method { case "WEB_ID": - awsCreds, err = auth.LoginAwsWebToken(os.Getenv("USER")) // TODO: redo this getUser implementation + awsCreds, err = credentialexchange.LoginAwsWebToken(user.Name, svc) if err != nil { return err } @@ -41,16 +55,23 @@ func specific(cmd *cobra.Command, args []string) error { return fmt.Errorf("unsupported Method: %s", method) } } - config := config.SamlConfig{BaseConfig: config.BaseConfig{StoreInProfile: storeInProfile}} + config := credentialexchange.SamlConfig{BaseConfig: credentialexchange.BaseConfig{StoreInProfile: storeInProfile}} if role != "" { - awsCreds, err = auth.AssumeRoleWithCreds(awsCreds, os.Getenv("USER"), role) + specificCreds := credentials.NewStaticCredentialsFromCreds(credentials.Value{ + AccessKeyID: awsCreds.AWSAccessKey, + SecretAccessKey: awsCreds.AWSSecretKey, + SessionToken: awsCreds.AWSSessionToken, + }) + + svc := sts.New(sess, aws.NewConfig().WithCredentials(specificCreds)) + awsCreds, err = credentialexchange.AssumeRoleWithCreds(svc, user.Name, role) if err != nil { return err } } - if err := util.SetCredentials(awsCreds, config); err != nil { + if err := credentialexchange.SetCredentials(awsCreds, config); err != nil { return err } return nil diff --git a/go.mod b/go.mod index 671e244..8dd3c11 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,11 @@ module github.com/dnitsch/aws-cli-auth -go 1.20 +go 1.21 require ( github.com/aws/aws-sdk-go v1.46.3 github.com/mitchellh/go-ps v1.0.0 - github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.7.0 - // github.com/spf13/viper v1.10.1 github.com/zalando/go-keyring v0.2.3 ) @@ -17,12 +15,10 @@ require ( github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/google/uuid v1.3.1 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/ysmood/fetchup v0.2.3 // indirect github.com/ysmood/goob v0.4.0 // indirect - github.com/ysmood/got v0.34.1 // indirect + github.com/ysmood/got v0.37.0 // indirect github.com/ysmood/gson v0.7.3 // indirect github.com/ysmood/leakless v0.8.0 // indirect golang.org/x/crypto v0.14.0 // indirect @@ -32,7 +28,6 @@ require ( github.com/go-rod/rod v0.114.4 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/rs/zerolog v1.31.0 github.com/spf13/pflag v1.0.5 // indirect github.com/werf/lockgate v0.1.1 golang.org/x/sys v0.13.0 // indirect diff --git a/go.sum b/go.sum index 67d6e55..7911557 100644 --- a/go.sum +++ b/go.sum @@ -2,15 +2,14 @@ github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4u github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/aws/aws-sdk-go v1.46.3 h1:zcrCu14ANOji6m38bUTxYdPqne4EXIvJQ2KXZ5oi9k0= github.com/aws/aws-sdk-go v1.46.3/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.114.4 h1:FpkNFukjCuZLwnoLs+S9aCL95o/EMec6M+41UmvQay8= github.com/go-rod/rod v0.114.4/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw= -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.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= @@ -23,21 +22,11 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-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/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -47,17 +36,21 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/werf/lockgate v0.1.1 h1:S400JFYjtWfE4i4LY9FA8zx0fMdfui9DPrBiTciCrx4= github.com/werf/lockgate v0.1.1/go.mod h1:0yIFSLq9ausy6ejNxF5uUBf/Ib6daMAfXuCaTMZJzIE= github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= -github.com/ysmood/gop v0.0.2 h1:VuWweTmXK+zedLqYufJdh3PlxDNBOfFHjIZlPT2T5nw= github.com/ysmood/gop v0.0.2/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= -github.com/ysmood/got v0.34.1 h1:IrV2uWLs45VXNvZqhJ6g2nIhY+pgIG1CUoOcqfXFl1s= +github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= +github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= github.com/ysmood/got v0.34.1/go.mod h1:yddyjq/PmAf08RMLSwDjPyCvHvYed+WjHnQxpH851LM= +github.com/ysmood/got v0.37.0 h1:ZUt6YWpxbfXrZfCbkqk2EQFC96OdWD05ZNkULTDo4Co= +github.com/ysmood/got v0.37.0/go.mod h1:dkEyTWZWvj5OJxtJdNlK2xme1EitYf8GOePYS6XQQBg= github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= @@ -83,10 +76,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -106,5 +96,6 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/auth/saml.go b/internal/auth/saml.go deleted file mode 100644 index 5473a56..0000000 --- a/internal/auth/saml.go +++ /dev/null @@ -1,51 +0,0 @@ -package auth - -import ( - "os/user" - - "github.com/dnitsch/aws-cli-auth/internal/config" - "github.com/dnitsch/aws-cli-auth/internal/util" - "github.com/dnitsch/aws-cli-auth/internal/web" -) - -// GetSamlCreds -func GetSamlCreds(conf config.SamlConfig) error { - if conf.BaseConfig.CfgSectionName == "" && conf.BaseConfig.StoreInProfile { - util.Debug("Config-Section name must be provided if store-profile is enabled") - util.Exit(nil) - } - - secretStore := util.NewSecretStore(conf.BaseConfig.Role) - var awsCreds *util.AWSCredentials - var webBrowser *web.Web - var err error - - // Try to reuse stored credential in secret - awsCreds, err = secretStore.AWSCredential() - - if !util.IsValid(awsCreds, conf.BaseConfig.ReloadBeforeTime) || err != nil { - webBrowser = web.New() - - t, err := webBrowser.GetSamlLogin(conf) - if err != nil { - return err - } - user, err := user.Current() - if err != nil { - return err - } - - roleObj := &util.AWSRole{RoleARN: conf.BaseConfig.Role, PrincipalARN: conf.PrincipalArn, Name: util.SessionName(user.Username, config.SELF_NAME), Duration: conf.Duration} - - awsCreds, err = LoginStsSaml(t, roleObj) - if err != nil { - return err - } - - awsCreds.Version = 1 - secretStore.SaveAWSCredential(awsCreds) - } - - util.SetCredentials(awsCreds, conf) - return nil -} diff --git a/internal/cmdutils/cmdutils.go b/internal/cmdutils/cmdutils.go new file mode 100644 index 0000000..b105552 --- /dev/null +++ b/internal/cmdutils/cmdutils.go @@ -0,0 +1,85 @@ +package cmdutils + +import ( + "errors" + "fmt" + "os" + "os/user" + "path" + + "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" + "github.com/dnitsch/aws-cli-auth/internal/web" +) + +var ( + ErrMissingArg = errors.New("missing arg") +) + +// GetSamlCreds +func GetSamlCreds(svc credentialexchange.AuthSamlApi, conf credentialexchange.SamlConfig) error { + if conf.BaseConfig.CfgSectionName == "" && conf.BaseConfig.StoreInProfile { + // Debug("Config-Section name must be provided if store-profile is enabled") + return fmt.Errorf("Config-Section name must be provided if store-profile is enabled %w", ErrMissingArg) + } + + secretStore, err := credentialexchange.NewSecretStore(conf.BaseConfig.Role) + if err != nil { + return err + } + + // Try to reuse stored credential in secret + storedCreds, err := secretStore.AWSCredential() + if err != nil { + return err + } + + // creds := credentials.NewStaticCredentialsFromCreds(credentials.Value{ + // AccessKeyID: storedCreds.AWSAccessKey, + // SecretAccessKey: storedCreds.AWSSecretKey, + // SessionToken: storedCreds.AWSSessionToken, + // }) + // svc.Config.Credentials = creds + + credsValid, err := credentialexchange.IsValid(storedCreds, conf.BaseConfig.ReloadBeforeTime) + if err != nil { + return err + } + if !credsValid || err != nil { + if err := refreshCreds(conf, secretStore, svc); err != nil { + return err + } + } + + credentialexchange.SetCredentials(storedCreds, conf) + return nil +} + +func refreshCreds(conf credentialexchange.SamlConfig, secretStore *credentialexchange.SecretStore, svc credentialexchange.AuthSamlApi) error { + + datadir := path.Join(credentialexchange.HomeDir(), fmt.Sprintf(".%s-data", credentialexchange.SELF_NAME)) + os.MkdirAll(datadir, 0755) + + webBrowser := web.New(datadir) + + samlResp, err := webBrowser.GetSamlLogin(conf) + if err != nil { + return err + } + user, err := user.Current() + if err != nil { + return err + } + + roleObj := &credentialexchange.AWSRole{RoleARN: conf.BaseConfig.Role, PrincipalARN: conf.PrincipalArn, Name: credentialexchange.SessionName(user.Username, credentialexchange.SELF_NAME), Duration: conf.Duration} + + awsCreds, err := credentialexchange.LoginStsSaml(samlResp, *roleObj, svc) + if err != nil { + return err + } + + awsCreds.Version = 1 + if err := secretStore.SaveAWSCredential(awsCreds); err != nil { + return err + } + return nil +} diff --git a/internal/config/config.go b/internal/credentialexchange/config.go similarity index 94% rename from internal/config/config.go rename to internal/credentialexchange/config.go index 9dacb19..78cb54e 100644 --- a/internal/config/config.go +++ b/internal/credentialexchange/config.go @@ -1,4 +1,4 @@ -package config +package credentialexchange const ( SELF_NAME = "aws-cli-auth" diff --git a/internal/auth/awssts.go b/internal/credentialexchange/credentialexchange.go similarity index 53% rename from internal/auth/awssts.go rename to internal/credentialexchange/credentialexchange.go index 65f5988..c4b64b0 100755 --- a/internal/auth/awssts.go +++ b/internal/credentialexchange/credentialexchange.go @@ -1,33 +1,32 @@ -package auth +package credentialexchange import ( + "errors" "fmt" "os" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/sts" - "github.com/dnitsch/aws-cli-auth/internal/config" - "github.com/dnitsch/aws-cli-auth/internal/util" - "github.com/pkg/errors" +) + +var ( + ErrUnableAssume = errors.New("unable to assume") + ErrUnableSessionCreate = errors.New("unable to create a sesion") ) // AWSRole aws role attributes -type AWSRole struct { +type AWSRoleConfig struct { RoleARN string PrincipalARN string Name string } -// LoginStsSaml exchanges saml response for STS creds -func LoginStsSaml(samlResponse string, role *util.AWSRole) (*util.AWSCredentials, error) { - sess, err := session.NewSession() - if err != nil { - return nil, errors.Wrap(err, "Failed to create session") - } +type AuthSamlApi interface { + AssumeRoleWithSAML(input *sts.AssumeRoleWithSAMLInput) (*sts.AssumeRoleWithSAMLOutput, error) +} - svc := sts.New(sess) +// LoginStsSaml exchanges saml response for STS creds +func LoginStsSaml(samlResponse string, role AWSRole, svc AuthSamlApi) (*AWSCredentials, error) { params := &sts.AssumeRoleWithSAMLInput{ PrincipalArn: aws.String(role.PrincipalARN), // Required @@ -38,10 +37,10 @@ func LoginStsSaml(samlResponse string, role *util.AWSRole) (*util.AWSCredentials resp, err := svc.AssumeRoleWithSAML(params) if err != nil { - return nil, errors.Wrap(err, "Failed to retrieve STS credentials using SAML") + return nil, fmt.Errorf("failed to retrieve STS credentials using SAML: %s, %w", err.Error(), ErrUnableAssume) } - return &util.AWSCredentials{ + return &AWSCredentials{ AWSAccessKey: aws.StringValue(resp.Credentials.AccessKeyId), AWSSecretKey: aws.StringValue(resp.Credentials.SecretAccessKey), AWSSessionToken: aws.StringValue(resp.Credentials.SessionToken), @@ -50,23 +49,22 @@ func LoginStsSaml(samlResponse string, role *util.AWSRole) (*util.AWSCredentials }, nil } -func LoginAwsWebToken(username string) (*util.AWSCredentials, error) { - // var role string - sess, err := session.NewSession() - if err != nil { - return nil, errors.Wrap(err, "Failed to create session") - } +type authWebTokenApi interface { + AssumeRoleWithWebIdentity(input *sts.AssumeRoleWithWebIdentityInput) (*sts.AssumeRoleWithWebIdentityOutput, error) +} - svc := sts.New(sess) - r, exists := os.LookupEnv(config.AWS_ROLE_ARN) +func LoginAwsWebToken(username string, svc authWebTokenApi) (*AWSCredentials, error) { + // var role string + r, exists := os.LookupEnv(AWS_ROLE_ARN) if !exists { - util.Exit(fmt.Errorf("Role Var Not Found")) + return nil, fmt.Errorf("roleVar not found, %s is empty", AWS_ROLE_ARN) } - token, err := util.GetWebIdTokenFileContents() + token, err := GetWebIdTokenFileContents() if err != nil { - util.Exit(err) + return nil, err } - sessionName := util.SessionName(username, config.SELF_NAME) + + sessionName := SessionName(username, SELF_NAME) input := &sts.AssumeRoleWithWebIdentityInput{ RoleArn: &r, RoleSessionName: &sessionName, @@ -75,10 +73,10 @@ func LoginAwsWebToken(username string) (*util.AWSCredentials, error) { resp, err := svc.AssumeRoleWithWebIdentity(input) if err != nil { - return nil, errors.Wrap(err, "Failed to retrieve STS credentials using SAML") + return nil, fmt.Errorf("failed to retrieve STS credentials using token file: %s, %w", err.Error(), ErrUnableAssume) } - return &util.AWSCredentials{ + return &AWSCredentials{ AWSAccessKey: aws.StringValue(resp.Credentials.AccessKeyId), AWSSecretKey: aws.StringValue(resp.Credentials.SecretAccessKey), AWSSessionToken: aws.StringValue(resp.Credentials.SessionToken), @@ -87,20 +85,14 @@ func LoginAwsWebToken(username string) (*util.AWSCredentials, error) { }, nil } -func AssumeRoleWithCreds(creds *util.AWSCredentials, username, role string) (*util.AWSCredentials, error) { - sess, err := session.NewSession() - if err != nil { - return nil, errors.Wrap(err, "Failed to create session") - } +type authAssumeRoleCredsApi interface { + AssumeRole(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) +} - specificCreds := credentials.NewStaticCredentialsFromCreds(credentials.Value{ - AccessKeyID: creds.AWSAccessKey, - SecretAccessKey: creds.AWSSecretKey, - SessionToken: creds.AWSSessionToken, - }) +// AssumeRoleWithCreds +func AssumeRoleWithCreds(svc authAssumeRoleCredsApi, username, role string) (*AWSCredentials, error) { - svc := sts.New(sess, aws.NewConfig().WithCredentials(specificCreds)) - sessionName := util.SessionName(username, config.SELF_NAME) + sessionName := SessionName(username, SELF_NAME) input := &sts.AssumeRoleInput{ RoleArn: &role, @@ -109,10 +101,10 @@ func AssumeRoleWithCreds(creds *util.AWSCredentials, username, role string) (*ut roleCreds, err := svc.AssumeRole(input) if err != nil { - return nil, errors.Wrap(err, "Failed to retrieve STS credentials using Role Provided") + return nil, fmt.Errorf("failed to retrieve STS credentials using Role Provided, %w", ErrUnableAssume) } - return &util.AWSCredentials{ + return &AWSCredentials{ AWSAccessKey: aws.StringValue(roleCreds.Credentials.AccessKeyId), AWSSecretKey: aws.StringValue(roleCreds.Credentials.SecretAccessKey), AWSSessionToken: aws.StringValue(roleCreds.Credentials.SessionToken), diff --git a/internal/credentialexchange/doc.go b/internal/credentialexchange/doc.go new file mode 100644 index 0000000..96774a1 --- /dev/null +++ b/internal/credentialexchange/doc.go @@ -0,0 +1,2 @@ +// credentialexchange +package credentialexchange diff --git a/internal/util/helper.go b/internal/credentialexchange/helper.go similarity index 69% rename from internal/util/helper.go rename to internal/credentialexchange/helper.go index c7fc1da..4d8e04b 100644 --- a/internal/util/helper.go +++ b/internal/credentialexchange/helper.go @@ -1,7 +1,8 @@ -package util +package credentialexchange import ( "encoding/json" + "errors" "fmt" "log" "os" @@ -10,13 +11,18 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/sts" - "github.com/dnitsch/aws-cli-auth/internal/config" ini "gopkg.in/ini.v1" ) +var ( + ErrSectionNotFound = errors.New("section not found") + ErrConfigFailure = errors.New("config error") +) + func HomeDir() string { home, err := os.UserHomeDir() if err != nil { @@ -32,22 +38,17 @@ func ConfigIniFile(basePath string) string { } else { base = HomeDir() } - return path.Join(base, fmt.Sprintf(".%s.ini", config.SELF_NAME)) -} - -func WriteDataDir(datadir string) { - os.MkdirAll(datadir, 0755) + return path.Join(base, fmt.Sprintf(".%s.ini", SELF_NAME)) } func SessionName(username, selfName string) string { return fmt.Sprintf("%s-%s", username, selfName) } -func SetCredentials(creds *AWSCredentials, config config.SamlConfig) error { +func SetCredentials(creds *AWSCredentials, config SamlConfig) error { if config.BaseConfig.StoreInProfile { if err := storeCredentialsInProfile(*creds, config.BaseConfig.CfgSectionName); err != nil { - Errorf("Error: %s", err.Error()) return err } return nil @@ -71,8 +72,7 @@ func storeCredentialsInProfile(creds AWSCredentials, configSection string) error cfg, err := ini.Load(awsConfPath) if err != nil { - Errorf("Fail to read file: %v", err) - Exit(err) + return err } cfg.Section(configSection).Key("aws_access_key_id").SetValue(creds.AWSAccessKey) cfg.Section(configSection).Key("aws_secret_access_key").SetValue(creds.AWSSecretKey) @@ -87,7 +87,7 @@ func returnStdOutAsJson(creds AWSCredentials) error { jsonBytes, err := json.Marshal(creds) if err != nil { - Errorf("Unexpected AWS credential response") + // Errorf("Unexpected AWS credential response") return err } fmt.Println(string(jsonBytes)) @@ -96,30 +96,33 @@ func returnStdOutAsJson(creds AWSCredentials) error { func GetWebIdTokenFileContents() (string, error) { // var content *string - file, exists := os.LookupEnv(config.WEB_ID_TOKEN_VAR) + file, exists := os.LookupEnv(WEB_ID_TOKEN_VAR) if !exists { - Exit(fmt.Errorf("FileNotPresent: %s", config.WEB_ID_TOKEN_VAR)) + return "", fmt.Errorf("FileNotPresent: %s", WEB_ID_TOKEN_VAR) } content, err := os.ReadFile(file) if err != nil { - Exit(err) + return "", err } return string(content), nil } +type callerIdApi interface { + GetCallerIdentity(input *sts.GetCallerIdentityInput) (*sts.GetCallerIdentityOutput, error) +} + // IsValid checks current credentials and // returns them if they are still valid // if reloadTimeBefore is less than time left on the creds // then it will re-request a login -func IsValid(currentCreds *AWSCredentials, relaodBeforeTime int) bool { +func IsValid(currentCreds *AWSCredentials, relaodBeforeTime int) (bool, error) { if currentCreds == nil { - return false + return false, nil } sess, err := session.NewSession() if err != nil { - Errorf("Failed to create aws client session") - Exit(err) + return false, fmt.Errorf("session error: %s, %w", err, ErrUnableSessionCreate) } creds := credentials.NewStaticCredentialsFromCreds(credentials.Value{ @@ -129,23 +132,27 @@ func IsValid(currentCreds *AWSCredentials, relaodBeforeTime int) bool { }) svc := sts.New(sess, aws.NewConfig().WithCredentials(creds)) - + svc.Config.Credentials = creds // aws.NewConfig().WithCredentials(creds) input := &sts.GetCallerIdentityInput{} - _, err = svc.GetCallerIdentity(input) - - if err != nil { - Errorf("The previous credential isn't valid") + if _, err := svc.GetCallerIdentity(input); err != nil { + // Errorf("The previous credential isn't valid") + if aerr, ok := err.(awserr.Error); ok { + if aerr.Code() == sts.ErrCodeExpiredTokenException { + return false, nil + } + } + return false, fmt.Errorf("the previous credential isn't valid: %w", ErrUnableAssume) } - return err == nil && !reloadBeforeExpiry(currentCreds.Expires, relaodBeforeTime) + return !ReloadBeforeExpiry(currentCreds.Expires, relaodBeforeTime), nil } -// reloadBeforeExpiry returns true if the time +// ReloadBeforeExpiry returns true if the time // to expiry is less than the specified time in seconds // false if there is more than required time in seconds // before needing to recycle credentials -func reloadBeforeExpiry(expiry time.Time, reloadBeforeSeconds int) bool { +func ReloadBeforeExpiry(expiry time.Time, reloadBeforeSeconds int) bool { now := time.Now() diff := expiry.Sub(now) return diff.Seconds() < float64(reloadBeforeSeconds) @@ -153,11 +160,10 @@ func reloadBeforeExpiry(expiry time.Time, reloadBeforeSeconds int) bool { // WriteIniSection update ini sections in own config file func WriteIniSection(role string) error { - section := fmt.Sprintf("%s.%s", config.INI_CONF_SECTION, RoleKeyConverter(role)) + section := fmt.Sprintf("%s.%s", INI_CONF_SECTION, RoleKeyConverter(role)) cfg, err := ini.Load(ConfigIniFile("")) if err != nil { - Errorf("Fail to read Ini file: %v", err) - Exit(err) + return fmt.Errorf("Fail to read Ini file: %v, %w", err, ErrConfigFailure) } if !cfg.HasSection(section) { sct, err := cfg.NewSection(section) @@ -177,13 +183,8 @@ func GetAllIniSections() ([]string, error) { if err != nil { return nil, err } - for _, v := range cfg.Section(config.INI_CONF_SECTION).ChildSections() { - sections = append(sections, strings.Replace(v.Name(), fmt.Sprintf("%s.", config.INI_CONF_SECTION), "", -1)) + for _, v := range cfg.Section(INI_CONF_SECTION).ChildSections() { + sections = append(sections, strings.Replace(v.Name(), fmt.Sprintf("%s.", INI_CONF_SECTION), "", -1)) } return sections, nil } - -// CleanExit signals 0 exit code and should clean up any current process -func CleanExit() { - os.Exit(0) -} diff --git a/internal/util/helper_test.go b/internal/credentialexchange/helper_test.go similarity index 50% rename from internal/util/helper_test.go rename to internal/credentialexchange/helper_test.go index f99e9eb..236b22a 100644 --- a/internal/util/helper_test.go +++ b/internal/credentialexchange/helper_test.go @@ -1,11 +1,13 @@ -package util +package credentialexchange_test import ( "fmt" + "os" + "strings" "testing" "time" - "github.com/dnitsch/aws-cli-auth/internal/config" + "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" ini "gopkg.in/ini.v1" ) @@ -30,14 +32,14 @@ func TestCreateEntryInIni(t *testing.T) { t.Fatalf("Fail to read file: %v", err) } - section := cfg.Section(config.INI_CONF_SECTION) // - if !cfg.HasSection(fmt.Sprintf("%s.%s", config.INI_CONF_SECTION, RoleKeyConverter(roleTest))) { + section := cfg.Section(credentialexchange.INI_CONF_SECTION) // + if !cfg.HasSection(fmt.Sprintf("%s.%s", credentialexchange.INI_CONF_SECTION, credentialexchange.RoleKeyConverter(roleTest))) { t.Errorf("section NOT Exists") } roles := section.ChildSections() subSectionExists := false for _, v := range roles { - if v.Name() == fmt.Sprintf("role.%s", RoleKeyConverter(roleTest)) { + if v.Name() == fmt.Sprintf("role.%s", credentialexchange.RoleKeyConverter(roleTest)) { subSectionExists = true break } @@ -52,7 +54,7 @@ func TestReloadBeforeExpirySuccess(t *testing.T) { expiry := (time.Now()).Add(time.Second * 305) - got := reloadBeforeExpiry(expiry, 300) + got := credentialexchange.ReloadBeforeExpiry(expiry, 300) if got { t.Errorf("Expected %v, got: %v", false, got) @@ -63,9 +65,38 @@ func TestReloadBeforeExpiryNeedToRefresh(t *testing.T) { expiry := (time.Now()).Add(time.Second * 299) - got := reloadBeforeExpiry(expiry, 300) + got := credentialexchange.ReloadBeforeExpiry(expiry, 300) if !got { t.Errorf("Expected %v, got: %v", false, got) } } + +func Test_HomeDirOverwritten(t *testing.T) { + ttests := map[string]struct { + setUpCleanUp func() func() + }{ + "test1": { + setUpCleanUp: func() func() { + orignalEnv := os.Environ() + os.Setenv("HOME", "./.ignore-delete") + return func() { + for _, e := range orignalEnv { + pair := strings.SplitN(e, "=", 2) + os.Setenv(pair[0], pair[1]) + } + } + }, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + cleanUp := tt.setUpCleanUp() + defer cleanUp() + got := credentialexchange.HomeDir() + if got != "./.ignore-delete" { + t.Fail() + } + }) + } +} diff --git a/internal/util/secret.go b/internal/credentialexchange/secret.go similarity index 53% rename from internal/util/secret.go rename to internal/credentialexchange/secret.go index f6f9cba..e7b9209 100644 --- a/internal/util/secret.go +++ b/internal/credentialexchange/secret.go @@ -1,19 +1,36 @@ -// taken from AWS-CLI-OIDC - initially -package util +package credentialexchange import ( "encoding/json" + "errors" "fmt" "os" "strings" "time" - "github.com/dnitsch/aws-cli-auth/internal/config" "github.com/werf/lockgate" "github.com/werf/lockgate/pkg/file_locker" "github.com/zalando/go-keyring" ) +// bit of an antipattern to store types away from their business objects +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"` +} + +// AWSRole aws role attributes +type AWSRole struct { + RoleARN string + PrincipalARN string + Name string + Duration int +} + type SecretStore struct { AWSCredentials *AWSCredentials AWSCredJson string @@ -23,16 +40,14 @@ type SecretStore struct { lockResource string secretService string secretUser string - // keyring keyring.Keyring } -func NewSecretStore(role string) *SecretStore { - namer := fmt.Sprintf("%s-%s", config.SELF_NAME, RoleKeyConverter(role)) +func NewSecretStore(role string) (*SecretStore, error) { + namer := fmt.Sprintf("%s-%s", SELF_NAME, RoleKeyConverter(role)) lockDir := os.TempDir() + "/aws-clie-auth-lock" locker, err := file_locker.NewFileLocker(lockDir) if err != nil { - Errorf("Can't setup lock dir: %s", lockDir) - Exit(err) + return nil, fmt.Errorf("Can't setup lock dir: %s", lockDir) } return &SecretStore{ lockDir: lockDir, @@ -41,100 +56,119 @@ func NewSecretStore(role string) *SecretStore { secretService: namer, roleArn: role, secretUser: os.Getenv("USER"), - } + }, nil } -func (s *SecretStore) load() { +func (s *SecretStore) load() error { acquired, lock, err := s.locker.Acquire(s.lockResource, lockgate.AcquireOptions{Shared: false, Timeout: 3 * time.Minute}) if err != nil { - Errorf("Can't load secret due to locked now") - Exit(err) + // Errorf("Can't load secret due to locked now") + // Exit(err) + return err } defer func() { if acquired { if err := s.locker.Release(lock); err != nil { - Errorf("Can't unlock") - Exit(err) + // Errorf("Can't unlock") + // Exit(err) + fmt.Fprintf(os.Stderr, "") } } }() if !acquired { - Errorf("Can't load secret due to locked now") - Exit(err) + // Errorf("Can't load secret due to locked now") + // Exit(err) + return err } creds := &AWSCredentials{} jsonStr, err := keyring.Get(s.secretService, s.secretUser) if err != nil { - if err == keyring.ErrNotFound { - return + if errors.Is(err, keyring.ErrNotFound) { + return nil } - Errorf("Can't load secret due to unexpected error: %v", err) - Exit(err) + // Errorf("Can't load secret due to unexpected error: %v", err) + // Exit(err) + return err } if err := json.Unmarshal([]byte(jsonStr), &creds); err != nil { - Errorf("Can't load secret due to broken data: %v", err) - Exit(err) + // Errorf("Can't load secret due to broken data: %v", err) + // Exit(err) + return err } if err := WriteIniSection(s.roleArn); err != nil { - Errorf("Can't save role to ") - Exit(err) + // Errorf("Can't save role to ") + // Exit(err) + return err } s.AWSCredentials = creds s.AWSCredJson = jsonStr + return nil } -func (s *SecretStore) save() { +func (s *SecretStore) save() error { acquired, lock, err := s.locker.Acquire(s.lockResource, lockgate.AcquireOptions{Shared: false, Timeout: 3 * time.Minute}) if err != nil { - Errorf("Can't save secret due to lock") - Exit(err) + // Errorf("Can't save secret due to lock") + // Exit(err) + return err + } defer func() { if acquired { if err := s.locker.Release(lock); err != nil { - Errorf("Can't unlock") - Exit(err) + // Errorf("Can't unlock") + // Exit(err) + // return err + fmt.Fprintf(os.Stderr, "Can't unlock: %s", err) } } }() if err := keyring.Set(s.secretService, s.secretUser, s.AWSCredJson); err != nil { - Errorf("Can't save secret: %v", err) - Exit(err) + // Errorf("Can't save secret: %v", err) + // Exit(err) + return err } + return nil } func (s *SecretStore) AWSCredential() (*AWSCredentials, error) { - s.load() + if err := s.load(); err != nil { + return nil, err + } if s.AWSCredentials == nil && s.AWSCredJson == "" { - Infof("Not found the credential for %s", s.roleArn) + // Infof("Not found the credential for %s", s.roleArn) return nil, nil } - Debugf("Got credential from OS secret store for %s", s.roleArn) + fmt.Fprintf(os.Stderr, "Got credential from OS secret store for %s", s.roleArn) return s.AWSCredentials, nil } -func (s *SecretStore) SaveAWSCredential(cred *AWSCredentials) { +func (s *SecretStore) SaveAWSCredential(cred *AWSCredentials) error { s.AWSCredentials = cred jsonStr, err := json.Marshal(cred) if err != nil { - Errorf("Can't save secret due to the broken data") - Exit(err) + // Errorf("Can't save secret due to the broken data") + // Exit(err) + return err } s.AWSCredJson = string(jsonStr) - s.save() + if err := s.save(); err != nil { + return err + } - Debug("The AWS credentials has been saved in OS secret store") + fmt.Fprint(os.Stderr, "The AWS credentials has been saved in OS secret store") + return nil } func (s *SecretStore) Clear() error { @@ -148,7 +182,7 @@ func (s *SecretStore) ClearAll() error { } for _, v := range secretServices { - keyring.Delete(fmt.Sprintf("%s-%s", config.SELF_NAME, v), s.secretUser) + keyring.Delete(fmt.Sprintf("%s-%s", SELF_NAME, v), s.secretUser) } return nil } diff --git a/internal/util/secret_test.go b/internal/credentialexchange/secret_test.go similarity index 63% rename from internal/util/secret_test.go rename to internal/credentialexchange/secret_test.go index 7dc242d..4bdf1c5 100644 --- a/internal/util/secret_test.go +++ b/internal/credentialexchange/secret_test.go @@ -1,13 +1,17 @@ -package util +package credentialexchange_test -import "testing" +import ( + "testing" + + "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" +) var roleTest string = "arn:aws:iam::111122342343:role/DevAdmin" var keyTest string = "arn_aws_iam__111122342343_role____DevAdmin" func TestConvertRoleToKey(t *testing.T) { - got := RoleKeyConverter(roleTest) + got := credentialexchange.RoleKeyConverter(roleTest) want := keyTest if got != want { t.Errorf("Wanted: %s, Got: %s", want, got) @@ -16,7 +20,7 @@ func TestConvertRoleToKey(t *testing.T) { func TestConvertKeyToRole(t *testing.T) { - got := KeyRoleConverter(keyTest) + got := credentialexchange.KeyRoleConverter(keyTest) want := roleTest if got != want { t.Errorf("Wanted: %s, Got: %s", want, got) diff --git a/internal/util/log.go b/internal/util/log.go deleted file mode 100644 index d5a8dc5..0000000 --- a/internal/util/log.go +++ /dev/null @@ -1,67 +0,0 @@ -package util - -import ( - "fmt" - "os" - "strings" - - "github.com/rs/zerolog" -) - -var ( - Logger zerolog.Logger -) - -var IsTraceEnabled bool - -func Debugf(format string, args ...any) { - Debug(fmt.Sprintf(format, args...)) -} - -func Debug(msg string) { - Logger.Debug().Msg(msg) -} - -func Infof(format string, args ...any) { - Info(fmt.Sprintf(format, args...)) -} - -func Info(msg string) { - Logger.Info().Msg(msg) -} - -func Warnf(format string, args ...any) { - Warn(fmt.Sprintf(format, args...)) -} - -func Warn(msg string) { - Logger.Warn().Msg(msg) -} - -func Errorf(format string, args ...any) { - Error(fmt.Errorf(format, args...)) -} - -func Error(err error) { - Logger.Error().Err(err).Msg("") -} - -func Exit(err error) { - if err != nil { - Error(err) - } - os.Exit(1) -} - -func init() { - logLevel := "error" - // Set global log level - if level, found := os.LookupEnv("AWS_AUTH_CLI_LOG_LEVEL"); found { - logLevel = strings.ToLower(level) - } - lvl, err := zerolog.ParseLevel(logLevel) - if err != nil { - panic(fmt.Errorf("StartUpLoggerFailed: %v", err)) - } - Logger = zerolog.New(os.Stderr).With().Timestamp().Logger().Level(lvl) -} diff --git a/internal/util/types.go b/internal/util/types.go deleted file mode 100644 index 046db31..0000000 --- a/internal/util/types.go +++ /dev/null @@ -1,21 +0,0 @@ -package util - -import "time" - -// bit of an antipattern to store types away from their business objects -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"` -} - -// AWSRole aws role attributes -type AWSRole struct { - RoleARN string - PrincipalARN string - Name string - Duration int -} diff --git a/internal/web/web.go b/internal/web/web.go index 190c8b2..315ede5 100755 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -5,12 +5,9 @@ import ( "net/http" nurl "net/url" "os" - "path" "strings" - "github.com/dnitsch/aws-cli-auth/internal/config" - "github.com/dnitsch/aws-cli-auth/internal/util" - + "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" @@ -24,35 +21,32 @@ type Web struct { } // New returns an initialised instance of Web struct -func New() *Web { - ddir := path.Join(util.HomeDir(), fmt.Sprintf(".%s-data", config.SELF_NAME)) +func New(datadir string) *Web { l := launcher.New(). Headless(false). Devtools(false). Leakless(true) - url := l.UserDataDir(ddir).MustLaunch() + url := l.UserDataDir(datadir).MustLaunch() browser := rod.New(). ControlURL(url). MustConnect().NoDefaultDevice() return &Web{ - datadir: &ddir, + datadir: &datadir, launcher: l, browser: browser, } } -// GetSamlLogin performs a saml login -func (web *Web) GetSamlLogin(conf config.SamlConfig) (string, error) { +// GetSamlLogin performs a saml login for a given +func (web *Web) GetSamlLogin(conf credentialexchange.SamlConfig) (string, error) { // do not clean up userdata - - // datadir := path.Join(util.GetHomeDir(), fmt.Sprintf(".%s-data", config.SELF_NAME)) - util.WriteDataDir(*web.datadir) + // credentialexchange.WriteDataDir(*web.datadir) defer web.browser.MustClose() @@ -110,7 +104,7 @@ func checkRodProcess() error { } } for _, pid := range pids { - util.Debugf("Process to be killed as part of clean up: %d", pid) + fmt.Fprintf(os.Stderr, "Process to be killed as part of clean up: %d", pid) if proc, _ := os.FindProcess(pid); proc != nil { proc.Kill() } From dd45f3175e7434bb6694f116a0d9799a1c98da3e Mon Sep 17 00:00:00 2001 From: dnitsch Date: Sat, 28 Oct 2023 09:44:18 +0100 Subject: [PATCH 2/5] fix: add bulk of unit tests integration mock test --- aws-cli-auth.go | 8 +- cmd/clear.go | 5 +- cmd/root.go | 16 +- cmd/saml.go | 40 +- cmd/specific.go | 31 +- go.mod | 14 +- go.sum | 61 ++- internal/cmdutils/cmdutils.go | 60 ++- internal/cmdutils/cmdutils_test.go | 421 ++++++++++++++++++ .../credentialexchange/credentialexchange.go | 87 +++- internal/credentialexchange/helper.go | 58 +-- internal/credentialexchange/secret.go | 8 +- internal/web/web.go | 80 ++-- 13 files changed, 675 insertions(+), 214 deletions(-) create mode 100644 internal/cmdutils/cmdutils_test.go diff --git a/aws-cli-auth.go b/aws-cli-auth.go index 359b6d0..a0eb540 100755 --- a/aws-cli-auth.go +++ b/aws-cli-auth.go @@ -1,7 +1,11 @@ package main -import "github.com/dnitsch/aws-cli-auth/cmd" +import ( + "context" + + "github.com/dnitsch/aws-cli-auth/cmd" +) func main() { - cmd.Execute() + cmd.Execute(context.Background()) } diff --git a/cmd/clear.go b/cmd/clear.go index bd50bd9..d324a31 100644 --- a/cmd/clear.go +++ b/cmd/clear.go @@ -19,14 +19,14 @@ var ( ) func init() { - cobra.OnInitialize(initConfig) + cobra.OnInitialize(samlInitConfig) clearCmd.PersistentFlags().BoolVarP(&force, "force", "f", false, "If aws-cli-auth exited improprely in a previous run there is a chance that there could be hanging processes left over - this will clean them up forcefully") rootCmd.AddCommand(clearCmd) } func clear(cmd *cobra.Command, args []string) error { - web := web.New("") + web := web.New(web.NewWebConf(datadir)) secretStore, err := credentialexchange.NewSecretStore("") if err != nil { @@ -34,7 +34,6 @@ func clear(cmd *cobra.Command, args []string) error { } if force { - if err := web.ClearCache(); err != nil { return err } diff --git a/cmd/root.go b/cmd/root.go index 27d13f3..0f76e84 100755 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,11 +1,10 @@ package cmd import ( + "context" "fmt" "os" - "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" - "github.com/spf13/cobra" ) @@ -29,7 +28,8 @@ Stores them under the $HOME/.aws/credentials file under a specified path or retu } ) -func Execute() { +func Execute(ctx context.Context) { + rootCmd.ExecuteContext(ctx) if err := rootCmd.Execute(); err != nil { fmt.Errorf("cli error: %v", err) os.Exit(1) @@ -38,18 +38,8 @@ func Execute() { } func init() { - cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringVarP(&role, "role", "r", "", "Set the role you want to assume when SAML or OIDC process completes") rootCmd.PersistentFlags().StringVarP(&cfgSectionName, "cfg-section", "", "", "config section name in the yaml config file") rootCmd.PersistentFlags().BoolVarP(&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") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") } - -func initConfig() { - if _, err := os.Stat(credentialexchange.ConfigIniFile("")); err != nil { - // creating a file - rolesInit := []byte(fmt.Sprintf("[%s]\n", credentialexchange.INI_CONF_SECTION)) - err := os.WriteFile(credentialexchange.ConfigIniFile(""), rolesInit, 0644) - cobra.CheckErr(err) - } -} diff --git a/cmd/saml.go b/cmd/saml.go index 5bc5ed3..40e3a8d 100755 --- a/cmd/saml.go +++ b/cmd/saml.go @@ -3,11 +3,14 @@ package cmd import ( "errors" "fmt" + "os" + "path" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/sts" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/dnitsch/aws-cli-auth/internal/cmdutils" "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" + "github.com/dnitsch/aws-cli-auth/internal/web" "github.com/spf13/cobra" ) @@ -20,6 +23,7 @@ var ( principalArn string acsUrl string role string + datadir string duration int reloadBeforeTime int samlCmd = &cobra.Command{ @@ -37,6 +41,7 @@ var ( ) func init() { + cobra.OnInitialize(samlInitConfig) samlCmd.PersistentFlags().StringVarP(&providerUrl, "provider", "p", "", "Saml Entity StartSSO Url") samlCmd.MarkPersistentFlagRequired("provider") samlCmd.PersistentFlags().StringVarP(&principalArn, "principal", "", "", "Principal Arn of the SAML IdP in AWS") @@ -49,6 +54,8 @@ func init() { } func getSaml(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + conf := credentialexchange.SamlConfig{ ProviderUrl: providerUrl, PrincipalArn: principalArn, @@ -63,15 +70,34 @@ func getSaml(cmd *cobra.Command, args []string) error { }, } - sess, err := session.NewSession() + datadir := path.Join(credentialexchange.HomeDir(), fmt.Sprintf(".%s-data", credentialexchange.SELF_NAME)) + os.MkdirAll(datadir, 0755) + + secretStore, err := credentialexchange.NewSecretStore(conf.BaseConfig.Role) + if err != nil { + return err + } + + cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return fmt.Errorf("failed to create session %s, %w", err, ErrUnableToCreateSession) } + svc := sts.NewFromConfig(cfg) - svc := sts.New(sess) + return cmdutils.GetSamlCreds(ctx, svc, secretStore, conf, web.NewWebConf(datadir)) +} - if err := cmdutils.GetSamlCreds(svc, conf); err != nil { - return err +func samlInitConfig() { + if _, err := os.Stat(credentialexchange.ConfigIniFile("")); err != nil { + // creating a file + rolesInit := []byte(fmt.Sprintf("[%s]\n", credentialexchange.INI_CONF_SECTION)) + err := os.WriteFile(credentialexchange.ConfigIniFile(""), rolesInit, 0644) + cobra.CheckErr(err) + } + + datadir = path.Join(credentialexchange.HomeDir(), fmt.Sprintf(".%s-data", credentialexchange.SELF_NAME)) + + if _, err := os.Stat(datadir); err != nil { + cobra.CheckErr(os.MkdirAll(datadir, 0755)) } - return nil } diff --git a/cmd/specific.go b/cmd/specific.go index 56ae0ed..4a3892e 100644 --- a/cmd/specific.go +++ b/cmd/specific.go @@ -4,10 +4,8 @@ import ( "fmt" "os/user" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/sts" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" "github.com/spf13/cobra" ) @@ -31,12 +29,13 @@ func init() { func specific(cmd *cobra.Command, args []string) error { var awsCreds *credentialexchange.AWSCredentials - sess, err := session.NewSession() + ctx := cmd.Context() + + cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return fmt.Errorf("failed to create session %s, %w", err, ErrUnableToCreateSession) } - - svc := sts.New(sess) + svc := sts.NewFromConfig(cfg) user, err := user.Current() @@ -47,7 +46,7 @@ func specific(cmd *cobra.Command, args []string) error { if method != "" { switch method { case "WEB_ID": - awsCreds, err = credentialexchange.LoginAwsWebToken(user.Name, svc) + awsCreds, err = credentialexchange.LoginAwsWebToken(ctx, user.Name, svc) if err != nil { return err } @@ -57,15 +56,15 @@ func specific(cmd *cobra.Command, args []string) error { } config := credentialexchange.SamlConfig{BaseConfig: credentialexchange.BaseConfig{StoreInProfile: storeInProfile}} + // IF role is provided it can be assumed from the WEB_ID credentials + // if role != "" { - specificCreds := credentials.NewStaticCredentialsFromCreds(credentials.Value{ - AccessKeyID: awsCreds.AWSAccessKey, - SecretAccessKey: awsCreds.AWSSecretKey, - SessionToken: awsCreds.AWSSessionToken, - }) - - svc := sts.New(sess, aws.NewConfig().WithCredentials(specificCreds)) - awsCreds, err = credentialexchange.AssumeRoleWithCreds(svc, user.Name, role) + // svc.Config.Credentials = credentials. (credentials.Value{ + // AccessKeyID: awsCreds.AWSAccessKey, + // SecretAccessKey: awsCreds.AWSSecretKey, + // SessionToken: awsCreds.AWSSessionToken, + // }) + awsCreds, err = credentialexchange.AssumeRoleWithCreds(ctx, svc, user.Name, role) if err != nil { return err } diff --git a/go.mod b/go.mod index 8dd3c11..cb6d615 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module github.com/dnitsch/aws-cli-auth go 1.21 require ( - github.com/aws/aws-sdk-go v1.46.3 + github.com/aws/aws-sdk-go-v2 v1.21.2 + github.com/aws/smithy-go v1.15.0 github.com/mitchellh/go-ps v1.0.0 github.com/spf13/cobra v1.7.0 github.com/zalando/go-keyring v0.2.3 @@ -11,6 +12,14 @@ require ( require ( github.com/alessio/shellescape v1.4.2 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.13.43 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 // indirect github.com/danieljoos/wincred v1.2.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gofrs/flock v0.8.1 // indirect @@ -25,9 +34,10 @@ require ( ) require ( + github.com/aws/aws-sdk-go-v2/config v1.19.1 + github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 github.com/go-rod/rod v0.114.4 github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/werf/lockgate v0.1.1 golang.org/x/sys v0.13.0 // indirect diff --git a/go.sum b/go.sum index 7911557..4dfa22f 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,29 @@ github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -github.com/aws/aws-sdk-go v1.46.3 h1:zcrCu14ANOji6m38bUTxYdPqne4EXIvJQ2KXZ5oi9k0= -github.com/aws/aws-sdk-go v1.46.3/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA= +github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= +github.com/aws/aws-sdk-go-v2/config v1.19.1 h1:oe3vqcGftyk40icfLymhhhNysAwk0NfiwkDi2GTPMXs= +github.com/aws/aws-sdk-go-v2/config v1.19.1/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43 h1:LU8vo40zBlo3R7bAvBVy/ku4nxGEyZe9N8MqAeFTzF8= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43/go.mod h1:zWJBz1Yf1ZtX5NGax9ZdNjhhI4rgjfgsyk6vTY1yfVg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 h1:PIktER+hwIG286DqXyvVENjgLTAwGgoeriLDD5C+YlQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 h1:nFBQlGtkbPzp/NjZLuFxRqmT91rLJkgvsEQs68h962Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 h1:JRVhO25+r3ar2mKGP7E0LDl8K9/G36gjlqca5iQbaqc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 h1:hze8YsjSh8Wl1rYa1CJpRmXP21BvOBuc76YhW0HsuQ4= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 h1:WWZA/I2K4ptBS1kg0kV1JbBtG/umed0vwHRrmcr9z7k= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 h1:JuPGc7IkOP4AaqcZSIcyqLpFSqBWK32rM9+a1g6u73k= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 h1:HFiiRkf1SdaAmV3/BHOFZ9DjFynPHj8G/UIO1lQS+fk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 h1:0BkLfgeDjfZnZ+MhB3ONb01u9pwFYTCZVhlsSSBvlbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ= +github.com/aws/smithy-go v1.15.0 h1:PS/durmlzvAFpQHDs4wi4sNNP9ExsqZh6IlfdHXgKK8= +github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= @@ -14,17 +36,17 @@ 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.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= -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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -57,45 +79,16 @@ github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= github.com/ysmood/leakless v0.8.0 h1:BzLrVoiwxikpgEQR0Lk8NyBN5Cit2b1z+u0mgL4ZJak= github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cmdutils/cmdutils.go b/internal/cmdutils/cmdutils.go index b105552..1bc5dc5 100644 --- a/internal/cmdutils/cmdutils.go +++ b/internal/cmdutils/cmdutils.go @@ -1,65 +1,55 @@ package cmdutils import ( + "context" "errors" "fmt" - "os" "os/user" - "path" "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" "github.com/dnitsch/aws-cli-auth/internal/web" ) var ( - ErrMissingArg = errors.New("missing arg") + ErrMissingArg = errors.New("missing arg") + ErrUnableToValidate = errors.New("unable to validate token") ) +type SecretStorageImpl interface { + AWSCredential() (*credentialexchange.AWSCredentials, error) + Clear() error + ClearAll() error + SaveAWSCredential(cred *credentialexchange.AWSCredentials) error +} + // GetSamlCreds -func GetSamlCreds(svc credentialexchange.AuthSamlApi, conf credentialexchange.SamlConfig) error { +func GetSamlCreds(ctx context.Context, svc credentialexchange.AuthSamlApi, secretStore SecretStorageImpl, conf credentialexchange.SamlConfig, webConfig *web.WebConfig) error { if conf.BaseConfig.CfgSectionName == "" && conf.BaseConfig.StoreInProfile { // Debug("Config-Section name must be provided if store-profile is enabled") return fmt.Errorf("Config-Section name must be provided if store-profile is enabled %w", ErrMissingArg) } - secretStore, err := credentialexchange.NewSecretStore(conf.BaseConfig.Role) - if err != nil { - return err - } - // Try to reuse stored credential in secret storedCreds, err := secretStore.AWSCredential() if err != nil { return err } - // creds := credentials.NewStaticCredentialsFromCreds(credentials.Value{ - // AccessKeyID: storedCreds.AWSAccessKey, - // SecretAccessKey: storedCreds.AWSSecretKey, - // SessionToken: storedCreds.AWSSessionToken, - // }) - // svc.Config.Credentials = creds - - credsValid, err := credentialexchange.IsValid(storedCreds, conf.BaseConfig.ReloadBeforeTime) + credsValid, err := credentialexchange.IsValid(ctx, storedCreds, conf.BaseConfig.ReloadBeforeTime, svc) if err != nil { - return err + return fmt.Errorf("failed to validate: %s, %w", err, ErrUnableToValidate) } - if !credsValid || err != nil { - if err := refreshCreds(conf, secretStore, svc); err != nil { - return err - } + + if !credsValid { + return refreshCreds(ctx, conf, secretStore, svc, webConfig) } - credentialexchange.SetCredentials(storedCreds, conf) - return nil + return credentialexchange.SetCredentials(storedCreds, conf) } -func refreshCreds(conf credentialexchange.SamlConfig, secretStore *credentialexchange.SecretStore, svc credentialexchange.AuthSamlApi) error { +func refreshCreds(ctx context.Context, conf credentialexchange.SamlConfig, secretStore SecretStorageImpl, svc credentialexchange.AuthSamlApi, webConfig *web.WebConfig) error { - datadir := path.Join(credentialexchange.HomeDir(), fmt.Sprintf(".%s-data", credentialexchange.SELF_NAME)) - os.MkdirAll(datadir, 0755) - - webBrowser := web.New(datadir) + webBrowser := web.New(webConfig) samlResp, err := webBrowser.GetSamlLogin(conf) if err != nil { @@ -70,9 +60,13 @@ func refreshCreds(conf credentialexchange.SamlConfig, secretStore *credentialexc return err } - roleObj := &credentialexchange.AWSRole{RoleARN: conf.BaseConfig.Role, PrincipalARN: conf.PrincipalArn, Name: credentialexchange.SessionName(user.Username, credentialexchange.SELF_NAME), Duration: conf.Duration} - - awsCreds, err := credentialexchange.LoginStsSaml(samlResp, *roleObj, svc) + roleObj := credentialexchange.AWSRole{ + RoleARN: conf.BaseConfig.Role, + PrincipalARN: conf.PrincipalArn, + Name: credentialexchange.SessionName(user.Username, credentialexchange.SELF_NAME), + Duration: conf.Duration, + } + awsCreds, err := credentialexchange.LoginStsSaml(ctx, samlResp, roleObj, svc) if err != nil { return err } @@ -81,5 +75,5 @@ func refreshCreds(conf credentialexchange.SamlConfig, secretStore *credentialexc if err := secretStore.SaveAWSCredential(awsCreds); err != nil { return err } - return nil + return credentialexchange.SetCredentials(awsCreds, conf) } diff --git a/internal/cmdutils/cmdutils_test.go b/internal/cmdutils/cmdutils_test.go new file mode 100644 index 0000000..39406e4 --- /dev/null +++ b/internal/cmdutils/cmdutils_test.go @@ -0,0 +1,421 @@ +package cmdutils_test + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "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" + "github.com/dnitsch/aws-cli-auth/internal/cmdutils" + "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" + "github.com/dnitsch/aws-cli-auth/internal/web" +) + +func AwsMockHandler(t *testing.T, mux *http.ServeMux) http.Handler { + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + + for k, v := range r.URL.Query() { + fmt.Println(k, " => ", v) + } + fmt.Println(r.URL.Query().Get("Action")) + // if r.Form.Get("Action") == "AssumeRoleWithSAML" { + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + w.Write([]byte(` + + https://integ.example.com/idp/shibboleth + + arn:aws:sts::1122223334:assumed-role/some-role + ARO456EXAMPLE789:some-role + + + ASIAV3ZUEFP6EXAMPLE + 8P+SQvWIuLnKhh8d++jpw0nNmQRBZvNEXAMPLEKEY + IQoJb3JpZ2luX2VjEOz////////////////////wEXAMPLEtMSJHMEUCIDoKK3JH9uG + QE1z0sINr5M4jk+Na8KHDcCYRVjJCZEvOAiEA3OvJGtw1EcViOleS2vhs8VdCKFJQWP + QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU + 9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz + +scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA== + 2030-11-01T20:26:47Z + + https://signin.aws.amazon.com/saml + transient + 6 + SbdGOnUkh1i4+EXAMPLExL/jEvs= + SourceIdentityValue + SamlExample + + + c6104cbe-af31-11e0-8154-cbc7ccf896c7 + +`)) + // w.Write([]byte(`{"Credentials":{"AccessKeyId":"AWSGFDDFSDESFRFRE123112","Expiration":"1239792839344","SecretAccessKey":"SDFSDJHWUJFWE322342323WEFFDWEF@£423rERVedfvvr342","SessionToken":"fdsdf23r4234werfedsfvfvee43g5r354grtrtv"}}`)) + // } + }) + return mux +} + +func IdpHandler(t *testing.T, addAwsMock bool) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/saml", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Server", "Server") + w.Header().Set("X-Amzn-Requestid", "9363fdebc232c348b71c8ba5b59f9a34") + // w.WriteHeader(http.StatusOK) + w.Write([]byte(` + + + + SAMLResponse=dsicisud99u2ubf92e9euhre&RelayState= + + + `)) + }) + mux.HandleFunc("/idp-redirect", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(` + + + + + +
+ + `)) + }) + mux.HandleFunc("/idp-onload", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(` + + +
+ + + `)) + }) + mux.HandleFunc("/some-app", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(` + + +
SomeApp
+ + `)) + }) + if addAwsMock { + return AwsMockHandler(t, mux) + } + return mux +} + +func testConfig() credentialexchange.SamlConfig { + return credentialexchange.SamlConfig{ + BaseConfig: credentialexchange.BaseConfig{ + Role: "arn:aws:iam::1122223334:role/some-role", + StoreInProfile: false, + ReloadBeforeTime: 850, + }, + PrincipalArn: "arn:aws:iam::1122223334:saml-provider/some-provider", + Duration: 900, + } +} + +type mockAuthApi struct { + assumeRoleWSaml func(ctx context.Context, params *sts.AssumeRoleWithSAMLInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithSAMLOutput, error) + getCallId func(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) +} + +func (m *mockAuthApi) AssumeRoleWithSAML(ctx context.Context, params *sts.AssumeRoleWithSAMLInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithSAMLOutput, error) { + return m.assumeRoleWSaml(ctx, params, optFns...) +} + +func (m *mockAuthApi) GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) { + return m.getCallId(ctx, params, optFns...) +} + +type mockSecretApi struct { + mCred func() (*credentialexchange.AWSCredentials, error) + mclear func() error + mClearAll func() error + mSave func(cred *credentialexchange.AWSCredentials) error +} + +func (s *mockSecretApi) AWSCredential() (*credentialexchange.AWSCredentials, error) { + return s.mCred() +} + +func (s *mockSecretApi) Clear() error { + return s.mclear() +} + +func (s *mockSecretApi) ClearAll() error { + return s.mClearAll() +} + +func (s *mockSecretApi) SaveAWSCredential(cred *credentialexchange.AWSCredentials) error { + return s.mSave(cred) +} + +func Test_GetSamlCreds_With(t *testing.T) { + ttests := map[string]struct { + config func(t *testing.T) credentialexchange.SamlConfig + handler func(t *testing.T, awsMock bool) http.Handler + authApi func(t *testing.T) credentialexchange.AuthSamlApi + secretStore func(t *testing.T) cmdutils.SecretStorageImpl + expectErr bool + errTyp error + }{ + "correct config and extracted creds but not valid anymore": { + config: func(t *testing.T) credentialexchange.SamlConfig { + return testConfig() + }, + handler: IdpHandler, + authApi: func(t *testing.T) credentialexchange.AuthSamlApi { + m := &mockAuthApi{} + m.assumeRoleWSaml = func(ctx context.Context, params *sts.AssumeRoleWithSAMLInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithSAMLOutput, error) { + return &sts.AssumeRoleWithSAMLOutput{ + AssumedRoleUser: &types.AssumedRoleUser{ + AssumedRoleId: aws.String("some-role"), + Arn: aws.String("arn"), + }, + Audience: new(string), + Credentials: &types.Credentials{ + AccessKeyId: aws.String("123213"), + SecretAccessKey: aws.String("32798hewf"), + SessionToken: aws.String("49hefusdSOM_LONG_TOKEN_HERE"), + Expiration: aws.Time(time.Now().Local().Add(time.Minute * time.Duration(5))), + }, + Issuer: new(string), + NameQualifier: new(string), + PackedPolicySize: new(int32), + SourceIdentity: new(string), + Subject: new(string), + SubjectType: new(string), + }, nil + } + + m.getCallId = func(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) { + // t.Error() + return &sts.GetCallerIdentityOutput{ + Account: aws.String("1122223334"), + Arn: aws.String("arn:aws:iam::1122223334:role/some-role"), + UserId: aws.String("some-user-id"), + }, nil + } + + return m + }, + secretStore: func(t *testing.T) cmdutils.SecretStorageImpl { + ss := &mockSecretApi{} + ss.mCred = func() (*credentialexchange.AWSCredentials, error) { + return &credentialexchange.AWSCredentials{ + Version: 1, + AWSAccessKey: "3212321", + AWSSecretKey: "23fsd2332", + AWSSessionToken: "LONG_TOKEN", + Expires: time.Now().Local().Add(time.Minute * time.Duration(-1)), + }, nil + } + ss.mSave = func(cred *credentialexchange.AWSCredentials) error { + return nil + } + return ss + }, + expectErr: false, + errTyp: nil, + }, + "correct config and extracted creds an IsValid": { + config: func(t *testing.T) credentialexchange.SamlConfig { + conf := testConfig() + conf.BaseConfig.ReloadBeforeTime = 60 + return conf + }, + handler: IdpHandler, + authApi: func(t *testing.T) credentialexchange.AuthSamlApi { + m := &mockAuthApi{} + m.assumeRoleWSaml = func(ctx context.Context, params *sts.AssumeRoleWithSAMLInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithSAMLOutput, error) { + return &sts.AssumeRoleWithSAMLOutput{ + AssumedRoleUser: &types.AssumedRoleUser{ + AssumedRoleId: aws.String("some-role"), + Arn: aws.String("arn"), + }, + Audience: new(string), + Credentials: &types.Credentials{ + AccessKeyId: aws.String("123213"), + SecretAccessKey: aws.String("32798hewf"), + SessionToken: aws.String("49hefusdSOM_LONG_TOKEN_HERE"), + Expiration: aws.Time(time.Now().Local().Add(time.Minute * time.Duration(5))), + }, + Issuer: new(string), + NameQualifier: new(string), + PackedPolicySize: new(int32), + SourceIdentity: new(string), + Subject: new(string), + SubjectType: new(string), + }, nil + } + + m.getCallId = func(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) { + // t.Error() + return &sts.GetCallerIdentityOutput{ + Account: aws.String("1122223334"), + Arn: aws.String("arn:aws:iam::1122223334:role/some-role"), + UserId: aws.String("some-user-id"), + }, nil + } + + return m + }, + secretStore: func(t *testing.T) cmdutils.SecretStorageImpl { + ss := &mockSecretApi{} + ss.mCred = func() (*credentialexchange.AWSCredentials, error) { + return &credentialexchange.AWSCredentials{ + Version: 1, + AWSAccessKey: "3212321", + AWSSecretKey: "23fsd2332", + AWSSessionToken: "LONG_TOKEN", + Expires: time.Now().Local().Add(time.Minute * time.Duration(10)), + }, nil + } + ss.mSave = func(cred *credentialexchange.AWSCredentials) error { + return nil + } + return ss + }, + expectErr: false, + errTyp: nil, + }, + "mising config section name and --store-in-profile set": { + config: func(t *testing.T) credentialexchange.SamlConfig { + tc := testConfig() + tc.BaseConfig.CfgSectionName = "" + tc.BaseConfig.StoreInProfile = true + return tc + }, + handler: IdpHandler, + authApi: func(t *testing.T) credentialexchange.AuthSamlApi { + return &mockAuthApi{} + }, + secretStore: func(t *testing.T) cmdutils.SecretStorageImpl { + return &mockSecretApi{} + }, + expectErr: true, + errTyp: cmdutils.ErrMissingArg, + }, + "failure on unable to retrieve existing credential": { + config: func(t *testing.T) credentialexchange.SamlConfig { + tc := testConfig() + tc.BaseConfig.CfgSectionName = "" + tc.BaseConfig.StoreInProfile = false + return tc + }, + handler: IdpHandler, + authApi: func(t *testing.T) credentialexchange.AuthSamlApi { + return &mockAuthApi{} + }, + secretStore: func(t *testing.T) cmdutils.SecretStorageImpl { + ss := &mockSecretApi{} + ss.mCred = func() (*credentialexchange.AWSCredentials, error) { + return nil, fmt.Errorf("%w", credentialexchange.ErrUnableToLoadAWSCred) + } + return ss + }, + expectErr: true, + errTyp: credentialexchange.ErrUnableToLoadAWSCred, + }, + "fails on isValid": { + config: func(t *testing.T) credentialexchange.SamlConfig { + tc := testConfig() + tc.BaseConfig.CfgSectionName = "" + tc.BaseConfig.StoreInProfile = false + return tc + }, + handler: IdpHandler, + authApi: func(t *testing.T) credentialexchange.AuthSamlApi { + m := &mockAuthApi{} + m.getCallId = func(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) { + return nil, fmt.Errorf("get caller error") + } + + return m + }, + secretStore: func(t *testing.T) cmdutils.SecretStorageImpl { + ss := &mockSecretApi{} + ss.mCred = func() (*credentialexchange.AWSCredentials, error) { + return &credentialexchange.AWSCredentials{ + Version: 1, + AWSAccessKey: "3212321", + AWSSecretKey: "23fsd2332", + AWSSessionToken: "LONG_TOKEN", + Expires: time.Now().Local().Add(time.Minute * time.Duration(-1)), + }, nil + } + return ss + }, + expectErr: true, + errTyp: cmdutils.ErrUnableToValidate, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + ts := httptest.NewServer(tt.handler(t, true)) + defer ts.Close() + conf := tt.config(t) + conf.AcsUrl = fmt.Sprintf("%s/saml", ts.URL) + conf.ProviderUrl = fmt.Sprintf("%s/idp-onload", ts.URL) + + tempDir, _ := os.MkdirTemp(os.TempDir(), "saml-tester") + + defer func() { + os.RemoveAll(tempDir) + }() + + ss := tt.secretStore(t) + + err := cmdutils.GetSamlCreds( + context.TODO(), tt.authApi(t), ss, conf, + web.NewWebConf(tempDir).WithHeadless().WithTimeout(10)) + + if tt.expectErr { + if err == nil { + t.Errorf("got , wanted %s", tt.errTyp) + return + } + if !errors.Is(err, tt.errTyp) { + t.Errorf("got %s, wanted %s", err, tt.errTyp) + return + } + } + + if err != nil && !tt.expectErr { + t.Errorf("got %s, wanted ", err) + } + }) + } +} diff --git a/internal/credentialexchange/credentialexchange.go b/internal/credentialexchange/credentialexchange.go index c4b64b0..5aa2770 100755 --- a/internal/credentialexchange/credentialexchange.go +++ b/internal/credentialexchange/credentialexchange.go @@ -1,17 +1,21 @@ package credentialexchange import ( + "context" "errors" "fmt" "os" + "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/sts" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/aws/smithy-go" ) var ( ErrUnableAssume = errors.New("unable to assume") ErrUnableSessionCreate = errors.New("unable to create a sesion") + ErrTokenExpired = errors.New("token expired") ) // AWSRole aws role attributes @@ -22,38 +26,73 @@ type AWSRoleConfig struct { } type AuthSamlApi interface { - AssumeRoleWithSAML(input *sts.AssumeRoleWithSAMLInput) (*sts.AssumeRoleWithSAMLOutput, error) + 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) } // LoginStsSaml exchanges saml response for STS creds -func LoginStsSaml(samlResponse string, role AWSRole, svc AuthSamlApi) (*AWSCredentials, error) { +func LoginStsSaml(ctx context.Context, samlResponse string, role AWSRole, svc AuthSamlApi) (*AWSCredentials, error) { params := &sts.AssumeRoleWithSAMLInput{ PrincipalArn: aws.String(role.PrincipalARN), // Required RoleArn: aws.String(role.RoleARN), // Required SAMLAssertion: aws.String(samlResponse), // Required - DurationSeconds: aws.Int64(int64(role.Duration)), + DurationSeconds: aws.Int32(int32(role.Duration)), } - resp, err := svc.AssumeRoleWithSAML(params) + 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 &AWSCredentials{ - AWSAccessKey: aws.StringValue(resp.Credentials.AccessKeyId), - AWSSecretKey: aws.StringValue(resp.Credentials.SecretAccessKey), - AWSSessionToken: aws.StringValue(resp.Credentials.SessionToken), - PrincipalARN: aws.StringValue(resp.AssumedRoleUser.Arn), + AWSAccessKey: *resp.Credentials.AccessKeyId, + AWSSecretKey: *resp.Credentials.SecretAccessKey, + AWSSessionToken: *resp.Credentials.SessionToken, + PrincipalARN: *resp.AssumedRoleUser.Arn, Expires: resp.Credentials.Expiration.Local(), }, 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 +// then it will re-request a login +func IsValid(ctx context.Context, currentCreds *AWSCredentials, reloadBeforeTime int, svc AuthSamlApi) (bool, error) { + if currentCreds == nil { + return false, nil + } + + if _, err := svc.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}, func(o *sts.Options) { + o.Credentials = &credsProvider{currentCreds.AWSAccessKey, currentCreds.AWSSecretKey, currentCreds.AWSSessionToken, currentCreds.Expires} + }); err != nil { + // var oe *smithy.OperationError + var oe smithy.APIError + if errors.As(err, &oe) { + if oe.ErrorCode() == "ExpiredToken" { + return false, nil + } + } + return false, fmt.Errorf("the previous credential is invalid: %s, %w", err, ErrUnableAssume) + } + + return !ReloadBeforeExpiry(currentCreds.Expires, reloadBeforeTime), nil +} + type authWebTokenApi interface { - AssumeRoleWithWebIdentity(input *sts.AssumeRoleWithWebIdentityInput) (*sts.AssumeRoleWithWebIdentityOutput, error) + AssumeRoleWithWebIdentity(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error) } -func LoginAwsWebToken(username string, svc authWebTokenApi) (*AWSCredentials, error) { +func LoginAwsWebToken(ctx context.Context, username string, svc authWebTokenApi) (*AWSCredentials, error) { // var role string r, exists := os.LookupEnv(AWS_ROLE_ARN) if !exists { @@ -71,26 +110,26 @@ func LoginAwsWebToken(username string, svc authWebTokenApi) (*AWSCredentials, er WebIdentityToken: &token, } - resp, err := svc.AssumeRoleWithWebIdentity(input) + resp, err := svc.AssumeRoleWithWebIdentity(ctx, input) if err != nil { return nil, fmt.Errorf("failed to retrieve STS credentials using token file: %s, %w", err.Error(), ErrUnableAssume) } return &AWSCredentials{ - AWSAccessKey: aws.StringValue(resp.Credentials.AccessKeyId), - AWSSecretKey: aws.StringValue(resp.Credentials.SecretAccessKey), - AWSSessionToken: aws.StringValue(resp.Credentials.SessionToken), - PrincipalARN: aws.StringValue(resp.AssumedRoleUser.Arn), + AWSAccessKey: *resp.Credentials.AccessKeyId, + AWSSecretKey: *resp.Credentials.SecretAccessKey, + AWSSessionToken: *resp.Credentials.SessionToken, + PrincipalARN: *resp.AssumedRoleUser.Arn, Expires: resp.Credentials.Expiration.Local(), }, nil } type authAssumeRoleCredsApi interface { - AssumeRole(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) + AssumeRole(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) } // AssumeRoleWithCreds -func AssumeRoleWithCreds(svc authAssumeRoleCredsApi, username, role string) (*AWSCredentials, error) { +func AssumeRoleWithCreds(ctx context.Context, svc authAssumeRoleCredsApi, username, role string) (*AWSCredentials, error) { sessionName := SessionName(username, SELF_NAME) @@ -98,17 +137,17 @@ func AssumeRoleWithCreds(svc authAssumeRoleCredsApi, username, role string) (*AW RoleArn: &role, RoleSessionName: &sessionName, } - roleCreds, err := svc.AssumeRole(input) + roleCreds, err := svc.AssumeRole(ctx, input) if err != nil { return nil, fmt.Errorf("failed to retrieve STS credentials using Role Provided, %w", ErrUnableAssume) } return &AWSCredentials{ - AWSAccessKey: aws.StringValue(roleCreds.Credentials.AccessKeyId), - AWSSecretKey: aws.StringValue(roleCreds.Credentials.SecretAccessKey), - AWSSessionToken: aws.StringValue(roleCreds.Credentials.SessionToken), - PrincipalARN: aws.StringValue(roleCreds.AssumedRoleUser.Arn), + AWSAccessKey: *roleCreds.Credentials.AccessKeyId, + AWSSecretKey: *roleCreds.Credentials.SecretAccessKey, + AWSSessionToken: *roleCreds.Credentials.SessionToken, + PrincipalARN: *roleCreds.AssumedRoleUser.Arn, Expires: roleCreds.Credentials.Expiration.Local(), }, nil } diff --git a/internal/credentialexchange/helper.go b/internal/credentialexchange/helper.go index 4d8e04b..832b807 100644 --- a/internal/credentialexchange/helper.go +++ b/internal/credentialexchange/helper.go @@ -10,11 +10,6 @@ import ( "strings" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/sts" ini "gopkg.in/ini.v1" ) @@ -46,7 +41,6 @@ func SessionName(username, selfName string) string { } func SetCredentials(creds *AWSCredentials, config SamlConfig) error { - if config.BaseConfig.StoreInProfile { if err := storeCredentialsInProfile(*creds, config.BaseConfig.CfgSectionName); err != nil { return err @@ -62,7 +56,6 @@ func storeCredentialsInProfile(creds AWSCredentials, configSection string) error if overriddenpath, exists := os.LookupEnv("AWS_SHARED_CREDENTIALS_FILE"); exists { awsConfPath = overriddenpath } else { - // os.MkdirAll(datadir, 0755) awsCredsPath := path.Join(HomeDir(), ".aws", "credentials") if _, err := os.Stat(awsCredsPath); os.IsNotExist(err) { os.Mkdir(awsCredsPath, 0655) @@ -90,7 +83,7 @@ func returnStdOutAsJson(creds AWSCredentials) error { // Errorf("Unexpected AWS credential response") return err } - fmt.Println(string(jsonBytes)) + fmt.Fprint(os.Stdout, string(jsonBytes)) return nil } @@ -98,7 +91,7 @@ func GetWebIdTokenFileContents() (string, error) { // var content *string file, exists := os.LookupEnv(WEB_ID_TOKEN_VAR) if !exists { - return "", fmt.Errorf("FileNotPresent: %s", WEB_ID_TOKEN_VAR) + return "", fmt.Errorf("fileNotPresent: %s", WEB_ID_TOKEN_VAR) } content, err := os.ReadFile(file) if err != nil { @@ -107,54 +100,13 @@ func GetWebIdTokenFileContents() (string, error) { return string(content), nil } -type callerIdApi interface { - GetCallerIdentity(input *sts.GetCallerIdentityInput) (*sts.GetCallerIdentityOutput, error) -} - -// IsValid checks current credentials and -// returns them if they are still valid -// if reloadTimeBefore is less than time left on the creds -// then it will re-request a login -func IsValid(currentCreds *AWSCredentials, relaodBeforeTime int) (bool, error) { - if currentCreds == nil { - return false, nil - } - - sess, err := session.NewSession() - if err != nil { - return false, fmt.Errorf("session error: %s, %w", err, ErrUnableSessionCreate) - } - - creds := credentials.NewStaticCredentialsFromCreds(credentials.Value{ - AccessKeyID: currentCreds.AWSAccessKey, - SecretAccessKey: currentCreds.AWSSecretKey, - SessionToken: currentCreds.AWSSessionToken, - }) - - svc := sts.New(sess, aws.NewConfig().WithCredentials(creds)) - svc.Config.Credentials = creds // aws.NewConfig().WithCredentials(creds) - input := &sts.GetCallerIdentityInput{} - - if _, err := svc.GetCallerIdentity(input); err != nil { - // Errorf("The previous credential isn't valid") - if aerr, ok := err.(awserr.Error); ok { - if aerr.Code() == sts.ErrCodeExpiredTokenException { - return false, nil - } - } - return false, fmt.Errorf("the previous credential isn't valid: %w", ErrUnableAssume) - } - - return !ReloadBeforeExpiry(currentCreds.Expires, relaodBeforeTime), nil -} - // ReloadBeforeExpiry returns true if the time // to expiry is less than the specified time in seconds // false if there is more than required time in seconds // before needing to recycle credentials func ReloadBeforeExpiry(expiry time.Time, reloadBeforeSeconds int) bool { - now := time.Now() - diff := expiry.Sub(now) + now := time.Now().Local() + diff := expiry.Local().Sub(now) return diff.Seconds() < float64(reloadBeforeSeconds) } @@ -163,7 +115,7 @@ func WriteIniSection(role string) error { section := fmt.Sprintf("%s.%s", INI_CONF_SECTION, RoleKeyConverter(role)) cfg, err := ini.Load(ConfigIniFile("")) if err != nil { - return fmt.Errorf("Fail to read Ini file: %v, %w", err, ErrConfigFailure) + return fmt.Errorf("fail to read Ini file: %v, %w", err, ErrConfigFailure) } if !cfg.HasSection(section) { sct, err := cfg.NewSection(section) diff --git a/internal/credentialexchange/secret.go b/internal/credentialexchange/secret.go index e7b9209..ebd4753 100644 --- a/internal/credentialexchange/secret.go +++ b/internal/credentialexchange/secret.go @@ -13,6 +13,10 @@ import ( "github.com/zalando/go-keyring" ) +var ( + ErrUnableToLoadAWSCred = errors.New("unable to laod AWS credential") +) + // bit of an antipattern to store types away from their business objects type AWSCredentials struct { Version int @@ -141,7 +145,7 @@ func (s *SecretStore) save() error { func (s *SecretStore) AWSCredential() (*AWSCredentials, error) { if err := s.load(); err != nil { - return nil, err + return nil, fmt.Errorf("secret store: %s, %w", err, ErrUnableToLoadAWSCred) } if s.AWSCredentials == nil && s.AWSCredJson == "" { @@ -167,7 +171,7 @@ func (s *SecretStore) SaveAWSCredential(cred *AWSCredentials) error { return err } - fmt.Fprint(os.Stderr, "The AWS credentials has been saved in OS secret store") + fmt.Fprint(os.Stderr, "The AWS credential has been saved in OS secret store") return nil } diff --git a/internal/web/web.go b/internal/web/web.go index 315ede5..6c9cdc7 100755 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -2,82 +2,112 @@ package web import ( "fmt" - "net/http" nurl "net/url" "os" "strings" + "time" "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" - "github.com/go-rod/rod/lib/proto" ps "github.com/mitchellh/go-ps" ) +type WebConfig struct { + datadir string + timeout int32 + headless bool +} + +func NewWebConf(datadir string) *WebConfig { + return &WebConfig{ + datadir: datadir, + headless: false, + timeout: 120, + } +} + +func (wc *WebConfig) WithTimeout(timeoutSeconds int32) *WebConfig { + wc.timeout = timeoutSeconds + return wc +} + +func (wc *WebConfig) WithHeadless() *WebConfig { + wc.headless = true + return wc +} + type Web struct { - datadir *string + conf *WebConfig launcher *launcher.Launcher browser *rod.Browser } // New returns an initialised instance of Web struct -func New(datadir string) *Web { +func New(conf *WebConfig) *Web { l := launcher.New(). - Headless(false). Devtools(false). + Headless(conf.headless). + UserDataDir(conf.datadir). Leakless(true) - url := l.UserDataDir(datadir).MustLaunch() + url := l.MustLaunch() browser := rod.New(). ControlURL(url). MustConnect().NoDefaultDevice() return &Web{ - datadir: &datadir, + conf: conf, launcher: l, browser: browser, } - } // GetSamlLogin performs a saml login for a given func (web *Web) GetSamlLogin(conf credentialexchange.SamlConfig) (string, error) { // do not clean up userdata - // credentialexchange.WriteDataDir(*web.datadir) - defer web.browser.MustClose() - page := web.browser.MustPage(conf.ProviderUrl) + web.browser.MustPage(conf.ProviderUrl) router := web.browser.HijackRequests() defer router.MustStop() - router.MustAdd(conf.AcsUrl, func(ctx *rod.Hijack) { - body := ctx.Request.Body() - _ = ctx.LoadResponse(http.DefaultClient, true) - ctx.Response.SetBody(body) - }) - - go router.Run() + capturedSaml := make(chan string) - wait := page.EachEvent(func(e *proto.PageFrameRequestedNavigation) (stop bool) { - return e.URL == conf.AcsUrl + router.MustAdd(fmt.Sprintf("%s*", conf.AcsUrl), func(ctx *rod.Hijack) { + // TODO: support both REDIRECT AND POST + if ctx.Request.Method() == "POST" || ctx.Request.Method() == "GET" { + cp := ctx.Request.Body() + capturedSaml <- cp + } }) - wait() - saml := strings.Split(page.MustElement(`body`).MustText(), "SAMLResponse=")[1] - saml = strings.Split(saml, "&")[0] - return nurl.QueryUnescape(saml) + go router.Run() + // forever loop wait for either a successfully + // extracted SAMLResponse + // + // Timesout after a specified timeout - default 120s + for { + select { + case saml := <-capturedSaml: + saml = strings.Split(saml, "SAMLResponse=")[1] + saml = strings.Split(saml, "&")[0] + return nurl.QueryUnescape(saml) + case <-time.After(time.Duration(web.conf.timeout) * time.Second): + return "", fmt.Errorf("timed out") + } + } } func (web *Web) ClearCache() error { errs := []error{} - if err := os.RemoveAll(*web.datadir); err != nil { + if err := os.RemoveAll(web.conf.datadir); err != nil { errs = append(errs, err) } if err := checkRodProcess(); err != nil { From 20f1662a6a6478acab1ced1a03fa8bc8d7df280e Mon Sep 17 00:00:00 2001 From: dnitsch Date: Tue, 31 Oct 2023 18:47:47 +0000 Subject: [PATCH 3/5] feat: add majority of unit tests +semver: feature --- Makefile | 2 +- cmd/clear.go | 2 +- cmd/root.go | 3 +- cmd/saml.go | 2 +- cmd/specific.go | 7 +- .../credentialexchange/credentialexchange.go | 29 +- .../credentialexchange_test.go | 470 ++++++++++++++++++ internal/credentialexchange/doc.go | 7 + internal/credentialexchange/helper.go | 2 +- internal/credentialexchange/secret.go | 141 +++--- internal/credentialexchange/secret_test.go | 221 ++++++++ internal/web/web.go | 13 +- internal/web/web_test.go | 149 ++++++ 13 files changed, 955 insertions(+), 93 deletions(-) create mode 100644 internal/credentialexchange/credentialexchange_test.go create mode 100644 internal/web/web_test.go diff --git a/Makefile b/Makefile index ec3f396..955cf4c 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ LDFLAGS := -ldflags="-s -w -X \"github.com/$(OWNER)/$(NAME)/cmd.Version=$(VERSIO .PHONY: test test_ci tidy install buildprep build buildmac buildwin test: test_prereq - go test `go list ./... | grep -v */generated/` -v -mod=readonly -coverprofile=.coverage/out | go-junit-report > .coverage/report-junit.xml && \ + go test ./... -v -mod=readonly -coverprofile=.coverage/out | go-junit-report > .coverage/report-junit.xml && \ gocov convert .coverage/out | gocov-xml > .coverage/report-cobertura.xml test_ci: diff --git a/cmd/clear.go b/cmd/clear.go index d324a31..841e4dd 100644 --- a/cmd/clear.go +++ b/cmd/clear.go @@ -28,7 +28,7 @@ func clear(cmd *cobra.Command, args []string) error { web := web.New(web.NewWebConf(datadir)) - secretStore, err := credentialexchange.NewSecretStore("") + secretStore, err := credentialexchange.NewSecretStore("", fmt.Sprintf("%s-%s", credentialexchange.SELF_NAME, credentialexchange.RoleKeyConverter("")), os.TempDir()+"/aws-clie-auth-lock") if err != nil { return err } diff --git a/cmd/root.go b/cmd/root.go index 0f76e84..d78d5c5 100755 --- a/cmd/root.go +++ b/cmd/root.go @@ -29,8 +29,7 @@ Stores them under the $HOME/.aws/credentials file under a specified path or retu ) func Execute(ctx context.Context) { - rootCmd.ExecuteContext(ctx) - if err := rootCmd.Execute(); err != nil { + if err := rootCmd.ExecuteContext(ctx); err != nil { fmt.Errorf("cli error: %v", err) os.Exit(1) } diff --git a/cmd/saml.go b/cmd/saml.go index 40e3a8d..40a0146 100755 --- a/cmd/saml.go +++ b/cmd/saml.go @@ -73,7 +73,7 @@ func getSaml(cmd *cobra.Command, args []string) error { datadir := path.Join(credentialexchange.HomeDir(), fmt.Sprintf(".%s-data", credentialexchange.SELF_NAME)) os.MkdirAll(datadir, 0755) - secretStore, err := credentialexchange.NewSecretStore(conf.BaseConfig.Role) + secretStore, err := credentialexchange.NewSecretStore(conf.BaseConfig.Role, fmt.Sprintf("%s-%s", credentialexchange.SELF_NAME, credentialexchange.RoleKeyConverter(conf.BaseConfig.Role)), os.TempDir()+"/aws-clie-auth-lock") if err != nil { return err } diff --git a/cmd/specific.go b/cmd/specific.go index 4a3892e..2d77842 100644 --- a/cmd/specific.go +++ b/cmd/specific.go @@ -59,12 +59,7 @@ func specific(cmd *cobra.Command, args []string) error { // IF role is provided it can be assumed from the WEB_ID credentials // if role != "" { - // svc.Config.Credentials = credentials. (credentials.Value{ - // AccessKeyID: awsCreds.AWSAccessKey, - // SecretAccessKey: awsCreds.AWSSecretKey, - // SessionToken: awsCreds.AWSSessionToken, - // }) - awsCreds, err = credentialexchange.AssumeRoleWithCreds(ctx, svc, user.Name, role) + awsCreds, err = credentialexchange.AssumeRoleWithCreds(ctx, awsCreds, svc, user.Name, role) if err != nil { return err } diff --git a/internal/credentialexchange/credentialexchange.go b/internal/credentialexchange/credentialexchange.go index 5aa2770..1794809 100755 --- a/internal/credentialexchange/credentialexchange.go +++ b/internal/credentialexchange/credentialexchange.go @@ -16,6 +16,7 @@ var ( ErrUnableAssume = errors.New("unable to assume") ErrUnableSessionCreate = errors.New("unable to create a sesion") ErrTokenExpired = errors.New("token expired") + ErrMissingEnvVar = errors.New("missing env var") ) // AWSRole aws role attributes @@ -25,6 +26,16 @@ type AWSRoleConfig struct { 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"` +} + 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) @@ -96,7 +107,7 @@ func LoginAwsWebToken(ctx context.Context, username string, svc authWebTokenApi) // var role string r, exists := os.LookupEnv(AWS_ROLE_ARN) if !exists { - return nil, fmt.Errorf("roleVar not found, %s is empty", AWS_ROLE_ARN) + return nil, fmt.Errorf("roleVar not found, %s is empty, %w", AWS_ROLE_ARN, ErrMissingEnvVar) } token, err := GetWebIdTokenFileContents() if err != nil { @@ -128,16 +139,20 @@ type authAssumeRoleCredsApi interface { AssumeRole(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) } -// AssumeRoleWithCreds -func AssumeRoleWithCreds(ctx context.Context, svc authAssumeRoleCredsApi, username, role string) (*AWSCredentials, error) { - - sessionName := SessionName(username, SELF_NAME) +// AssumeRoleWithCreds uses existing creds retrieved from anywhere +// to pass to a credential provider and assume a specific role +// +// Most common use case is role chaining an WeBId role to a specific one +func AssumeRoleWithCreds(ctx context.Context, currentCreds *AWSCredentials, svc authAssumeRoleCredsApi, username, role string) (*AWSCredentials, error) { input := &sts.AssumeRoleInput{ RoleArn: &role, - RoleSessionName: &sessionName, + RoleSessionName: aws.String(SessionName(username, SELF_NAME)), } - roleCreds, err := svc.AssumeRole(ctx, input) + + roleCreds, err := svc.AssumeRole(ctx, input, func(o *sts.Options) { + o.Credentials = &credsProvider{currentCreds.AWSAccessKey, currentCreds.AWSSecretKey, currentCreds.AWSSessionToken, currentCreds.Expires} + }) if err != nil { return nil, fmt.Errorf("failed to retrieve STS credentials using Role Provided, %w", ErrUnableAssume) diff --git a/internal/credentialexchange/credentialexchange_test.go b/internal/credentialexchange/credentialexchange_test.go new file mode 100644 index 0000000..3ac3481 --- /dev/null +++ b/internal/credentialexchange/credentialexchange_test.go @@ -0,0 +1,470 @@ +package credentialexchange_test + +import ( + "context" + "errors" + "fmt" + "os" + "path" + "testing" + "time" + + "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" + "github.com/aws/smithy-go" + "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" +) + +type mockAuthApi struct { + assumeRoleWSaml func(ctx context.Context, params *sts.AssumeRoleWithSAMLInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithSAMLOutput, error) + getCallId func(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) +} + +func (m *mockAuthApi) AssumeRoleWithSAML(ctx context.Context, params *sts.AssumeRoleWithSAMLInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithSAMLOutput, error) { + return m.assumeRoleWSaml(ctx, params, optFns...) +} + +func (m *mockAuthApi) GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) { + return m.getCallId(ctx, params, optFns...) +} + +var mockSuccessAwsCreds = &types.Credentials{ + AccessKeyId: aws.String("123"), + SecretAccessKey: aws.String("456"), + SessionToken: aws.String("abcd"), + Expiration: aws.Time(time.Now().Local().Add(time.Duration(15) * time.Minute)), +} + +func Test_AssumeWithSaml_(t *testing.T) { + ttests := map[string]struct { + srv func(t *testing.T) credentialexchange.AuthSamlApi + expectErr bool + errTyp error + }{ + "succeeds with correct input": { + srv: func(t *testing.T) credentialexchange.AuthSamlApi { + m := &mockAuthApi{} + m.assumeRoleWSaml = func(ctx context.Context, params *sts.AssumeRoleWithSAMLInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithSAMLOutput, error) { + if *params.RoleArn != "somerole" { + t.Errorf("expected role: %s got: %s", "somerole", *params.RoleArn) + } + return &sts.AssumeRoleWithSAMLOutput{ + AssumedRoleUser: &types.AssumedRoleUser{Arn: aws.String("somearn")}, + Credentials: mockSuccessAwsCreds, + }, nil + } + return m + }, + expectErr: false, + errTyp: nil, + }, + "fails on input": { + srv: func(t *testing.T) credentialexchange.AuthSamlApi { + m := &mockAuthApi{} + m.assumeRoleWSaml = func(ctx context.Context, params *sts.AssumeRoleWithSAMLInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithSAMLOutput, error) { + if *params.RoleArn != "somerole" { + t.Errorf("expected role: %s got: %s", "somerole", *params.RoleArn) + } + return nil, fmt.Errorf("some error") + } + return m + }, + expectErr: true, + errTyp: credentialexchange.ErrUnableAssume, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + got, err := credentialexchange.LoginStsSaml(context.TODO(), "samlAssertion...372dgh8ybjsdfviwehfiu9rwfe", + credentialexchange.AWSRole{ + RoleARN: "somerole", + PrincipalARN: "someprincipal", + Duration: 900, + }, + tt.srv(t), + ) + + if tt.expectErr { + if err == nil { + t.Errorf("got , wanted %s", tt.errTyp) + } + if !errors.Is(err, tt.errTyp) { + t.Errorf("got %s, wanted %s", err, tt.errTyp) + } + return + } + + if err != nil { + t.Fatalf("got %s, wanted ", err) + } + if err != nil { + t.Errorf("expected error: nil\n\ngot: %s", err) + } + if got.AWSSessionToken != "abcd" { + t.Errorf("incorrect session token\nwanted: %s\ngot: %s", "abcd", got.AWSSessionToken) + } + }) + } +} + +type smithyErrTyp struct { + err func() string + errCode func() string + errMsg func() string + errFault func() smithy.ErrorFault +} + +func (e *smithyErrTyp) Error() string { + return e.err() +} +func (e *smithyErrTyp) ErrorCode() string { + return e.errCode() +} + +// ErrorMessage returns the error message for the API exception. +func (e *smithyErrTyp) ErrorMessage() string { + return e.errMsg() +} + +// ErrorFault returns the fault for the API exception. +func (e *smithyErrTyp) ErrorFault() smithy.ErrorFault { + return e.errFault() +} + +func Test_IsValid_with(t *testing.T) { + ttests := map[string]struct { + srv func(t *testing.T) credentialexchange.AuthSamlApi + currCred *credentialexchange.AWSCredentials + reloadBefore int + expectValid bool + expectErr bool + errTyp error + }{ + "non expired credential with enough time before reload required": { + func(t *testing.T) credentialexchange.AuthSamlApi { + m := &mockAuthApi{} + m.getCallId = func(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) { + return &sts.GetCallerIdentityOutput{ + Account: aws.String("account"), + Arn: aws.String("arn"), + }, nil + } + return m + }, + &credentialexchange.AWSCredentials{ + AWSAccessKey: "stringjsonAccessKey", + AWSSecretKey: "stringjsonSecretAccessKey", + AWSSessionToken: "stringjsonSessionToken", + Expires: time.Now().Local().Add(time.Duration(15) * time.Minute), + }, + 120, + true, + false, + nil, + }, + "credentials valid but need to reload before time fails": { + func(t *testing.T) credentialexchange.AuthSamlApi { + m := &mockAuthApi{} + m.getCallId = func(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) { + return &sts.GetCallerIdentityOutput{ + Account: aws.String("account"), + Arn: aws.String("arn"), + }, nil + } + return m + }, + &credentialexchange.AWSCredentials{ + AWSAccessKey: "stringjsonAccessKey", + AWSSecretKey: "stringjsonSecretAccessKey", + AWSSessionToken: "stringjsonSessionToken", + Expires: time.Now().Local().Add(time.Duration(-15) * time.Minute), + }, + 120, + false, + false, + nil, + }, + "expired credential": { + func(t *testing.T) credentialexchange.AuthSamlApi { + m := &mockAuthApi{} + m.getCallId = func(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) { + return nil, &smithyErrTyp{ + err: func() string { return "some errr" }, + errCode: func() string { return "ExpiredToken" }, + } + } + return m + }, + &credentialexchange.AWSCredentials{ + AWSAccessKey: "stringjsonAccessKey", + AWSSecretKey: "stringjsonSecretAccessKey", + AWSSessionToken: "stringjsonSessionToken", + Expires: time.Now().Local().Add(time.Duration(-15) * time.Minute), + }, + 120, + false, + false, + nil, + }, + "another error when chekcing credential": { + func(t *testing.T) credentialexchange.AuthSamlApi { + m := &mockAuthApi{} + m.getCallId = func(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) { + return nil, &smithyErrTyp{ + err: func() string { return "some errr" }, + errCode: func() string { return "SomeOTherErr" }, + } + } + return m + }, + &credentialexchange.AWSCredentials{ + AWSAccessKey: "stringjsonAccessKey", + AWSSecretKey: "stringjsonSecretAccessKey", + AWSSessionToken: "stringjsonSessionToken", + Expires: time.Now().Local().Add(time.Duration(-15) * time.Minute), + }, + 120, + false, + true, + credentialexchange.ErrUnableAssume, + }, + "no existing credential": { + func(t *testing.T) credentialexchange.AuthSamlApi { + m := &mockAuthApi{} + return m + }, + nil, + 120, + false, + false, + nil, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + valid, err := credentialexchange.IsValid(context.TODO(), tt.currCred, tt.reloadBefore, tt.srv(t)) + + if tt.expectErr { + if err == nil { + t.Errorf("got , wanted %s", tt.errTyp) + return + } + if !errors.Is(err, tt.errTyp) { + t.Errorf("got %s, wanted %s", err, tt.errTyp) + return + } + } + + if err != nil && !tt.expectErr { + t.Errorf("got %s, wanted ", err) + } + + if valid != tt.expectValid { + t.Errorf("expected %v, got %v", tt.expectValid, valid) + } + }) + } +} + +type authWebTokenApi struct { + assumewithwebId func(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error) +} + +func (a *authWebTokenApi) AssumeRoleWithWebIdentity(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error) { + return a.assumewithwebId(ctx, params, optFns...) +} + +func Test_LoginAwsWebToken_with(t *testing.T) { + ttests := map[string]struct { + srv func(t *testing.T) *authWebTokenApi + setup func() func() + currCred *credentialexchange.AWSCredentials + expectErr bool + errTyp error + }{ + "succeeds with correct input": { + srv: func(t *testing.T) *authWebTokenApi { + a := &authWebTokenApi{} + a.assumewithwebId = func(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error) { + return &sts.AssumeRoleWithWebIdentityOutput{ + AssumedRoleUser: &types.AssumedRoleUser{Arn: aws.String("assumedRoleUser")}, + Credentials: mockSuccessAwsCreds, + }, nil + } + return a + }, + setup: func() func() { + tmpDir, _ := os.MkdirTemp(os.TempDir(), "web-id") + tokenFile := path.Join(tmpDir, ".ignore-token") + os.WriteFile(tokenFile, []byte(`sometoikonsebjsxd`), 0777) + os.Setenv(credentialexchange.WEB_ID_TOKEN_VAR, tokenFile) + os.Setenv("AWS_ROLE_ARN", "somerole") + return func() { + os.Clearenv() + os.RemoveAll(tmpDir) + } + }, + currCred: mockSuccessCreds, + expectErr: false, + errTyp: nil, + }, + "fails on rest call to assume": { + srv: func(t *testing.T) *authWebTokenApi { + a := &authWebTokenApi{} + a.assumewithwebId = func(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error) { + return nil, fmt.Errorf("some err") + } + return a + }, + setup: func() func() { + tmpDir, _ := os.MkdirTemp(os.TempDir(), "web-id") + tokenFile := path.Join(tmpDir, ".ignore-token") + os.WriteFile(tokenFile, []byte(`sometoikonsebjsxd`), 0777) + os.Setenv(credentialexchange.WEB_ID_TOKEN_VAR, tokenFile) + os.Setenv("AWS_ROLE_ARN", "somerole") + return func() { + os.Clearenv() + os.RemoveAll(tmpDir) + } + }, + currCred: mockSuccessCreds, + expectErr: true, + errTyp: credentialexchange.ErrUnableAssume, + }, + "fails on missing role env VARS": { + srv: func(t *testing.T) *authWebTokenApi { + a := &authWebTokenApi{} + a.assumewithwebId = func(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error) { + return &sts.AssumeRoleWithWebIdentityOutput{ + AssumedRoleUser: &types.AssumedRoleUser{Arn: aws.String("assumedRoleUser")}, + Credentials: mockSuccessAwsCreds, + }, nil + } + return a + }, + setup: func() func() { + return func() {} + }, + currCred: mockSuccessCreds, + expectErr: true, + errTyp: credentialexchange.ErrMissingEnvVar, + }, + "fails on missing token file env VARS": { + srv: func(t *testing.T) *authWebTokenApi { + a := &authWebTokenApi{} + a.assumewithwebId = func(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error) { + return &sts.AssumeRoleWithWebIdentityOutput{ + AssumedRoleUser: &types.AssumedRoleUser{Arn: aws.String("assumedRoleUser")}, + Credentials: mockSuccessAwsCreds, + }, nil + } + return a + }, + setup: func() func() { + // tmpDir, _ := os.MkdirTemp(os.TempDir(), "web-id") + // tokenFile := path.Join(tmpDir, ".ignore-token") + // os.WriteFile(tokenFile, []byte(`sometoikonsebjsxd`), 0777) + // os.Setenv(credentialexchange.WEB_ID_TOKEN_VAR, tokenFile) + os.Setenv("AWS_ROLE_ARN", "somerole") + return func() { + os.Clearenv() + // os.RemoveAll(tmpDir) + } + }, + currCred: mockSuccessCreds, + expectErr: true, + errTyp: credentialexchange.ErrMissingEnvVar, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + tearDown := tt.setup() + defer tearDown() + + got, err := credentialexchange.LoginAwsWebToken(context.TODO(), "username", tt.srv(t)) + + if tt.expectErr { + if err == nil { + t.Errorf("got , wanted %s", tt.errTyp) + } + if !errors.Is(err, tt.errTyp) { + t.Errorf("got %s, wanted %s", err, tt.errTyp) + } + return + } + + if err != nil && !tt.expectErr { + t.Fatalf("got %s, wanted ", err) + } + + if got.AWSAccessKey != *mockSuccessAwsCreds.AccessKeyId { + t.Fatalf("expected %v, got %v", mockSuccessAwsCreds, got) + } + }) + } +} + +type mockAssumeRole struct { + assume func(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) +} + +func (m *mockAssumeRole) AssumeRole(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) { + return m.assume(ctx, params, optFns...) +} + +func Test_AssumeSpecifiedCreds_with(t *testing.T) { + ttests := map[string]struct { + srv func(t *testing.T) *mockAssumeRole + currCred *credentialexchange.AWSCredentials + expectErr bool + errTyp error + }{ + "successfully passed in creds from somewhere": { + srv: func(t *testing.T) *mockAssumeRole { + m := &mockAssumeRole{} + m.assume = func(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) { + return &sts.AssumeRoleOutput{ + AssumedRoleUser: &types.AssumedRoleUser{Arn: aws.String("somearn")}, + Credentials: mockSuccessAwsCreds, + }, nil + } + return m + }, + }, + "error on calling AssumeRole API": { + srv: func(t *testing.T) *mockAssumeRole { + m := &mockAssumeRole{} + m.assume = func(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) { + return nil, fmt.Errorf("some error") + } + return m + }, + expectErr: true, + errTyp: credentialexchange.ErrUnableAssume, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + got, err := credentialexchange.AssumeRoleWithCreds(context.TODO(), tt.currCred, tt.srv(t), "foo", "barrole") + + if tt.expectErr { + if err == nil { + t.Errorf("got , wanted %s", tt.errTyp) + } + if !errors.Is(err, tt.errTyp) { + t.Errorf("got %s, wanted %s", err, tt.errTyp) + } + return + } + + if err != nil { + t.Fatalf("got %s, wanted ", err) + } + + if got.AWSAccessKey != *mockSuccessAwsCreds.AccessKeyId { + t.Fatalf("expected %v, got %v", mockSuccessAwsCreds, got) + } + }) + } +} diff --git a/internal/credentialexchange/doc.go b/internal/credentialexchange/doc.go index 96774a1..0640b7f 100644 --- a/internal/credentialexchange/doc.go +++ b/internal/credentialexchange/doc.go @@ -1,2 +1,9 @@ // credentialexchange +// +// Handles all the main flows for exchanging credentials for AWS temporary creds. +// +// Currently supports SAML as posted by an IdP to an ACS endpoint in AWS +// AWS_WEB_IDENTITY_TOKEN_FILE and optionally can specify the exact role to choose, +// +// if the TOKEN corresponds to the `chained role`. package credentialexchange diff --git a/internal/credentialexchange/helper.go b/internal/credentialexchange/helper.go index 832b807..c5ab8c3 100644 --- a/internal/credentialexchange/helper.go +++ b/internal/credentialexchange/helper.go @@ -91,7 +91,7 @@ func GetWebIdTokenFileContents() (string, error) { // var content *string file, exists := os.LookupEnv(WEB_ID_TOKEN_VAR) if !exists { - return "", fmt.Errorf("fileNotPresent: %s", WEB_ID_TOKEN_VAR) + return "", fmt.Errorf("fileNotPresent: %s, %w", WEB_ID_TOKEN_VAR, ErrMissingEnvVar) } content, err := os.ReadFile(file) if err != nil { diff --git a/internal/credentialexchange/secret.go b/internal/credentialexchange/secret.go index ebd4753..99de9ff 100644 --- a/internal/credentialexchange/secret.go +++ b/internal/credentialexchange/secret.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "os/user" "strings" "time" @@ -14,19 +15,14 @@ import ( ) var ( - ErrUnableToLoadAWSCred = errors.New("unable to laod AWS credential") + ErrUnableToLoadAWSCred = errors.New("unable to laod AWS credential") + ErrCannotLockDir = errors.New("unable to create lock dir") + ErrUnableToRetrieveSections = errors.New("unable to retrieve sections") + ErrUnableToLoadDueToLock = errors.New("cannot load secret due to lock error") + ErrUnableToAcquireLock = errors.New("cannot acquire lock") + ErrUnmarshallingSecret = errors.New("cannot unmarshal secret") ) -// bit of an antipattern to store types away from their business objects -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"` -} - // AWSRole aws role attributes type AWSRole struct { RoleARN string @@ -35,9 +31,11 @@ type AWSRole struct { Duration int } +// SecretStore type SecretStore struct { AWSCredentials *AWSCredentials AWSCredJson string + keyring keyring.Keyring roleArn string lockDir string locker lockgate.Locker @@ -46,66 +44,92 @@ type SecretStore struct { secretUser string } -func NewSecretStore(role string) (*SecretStore, error) { - namer := fmt.Sprintf("%s-%s", SELF_NAME, RoleKeyConverter(role)) - lockDir := os.TempDir() + "/aws-clie-auth-lock" +func (s *SecretStore) WithLocker(locker lockgate.Locker) *SecretStore { + s.locker = locker + return s +} + +func (s *SecretStore) WithKeyring(keyring keyring.Keyring) *SecretStore { + s.keyring = keyring + return s +} + +// keyRingImpl is the default keyring implementation +type keyRingImpl struct{} + +func (k *keyRingImpl) Set(service, user, password string) error { + return keyring.Set(service, user, password) +} +func (k *keyRingImpl) Get(service, user string) (string, error) { + return keyring.Get(service, user) +} +func (k *keyRingImpl) Delete(service, user string) error { + return keyring.Delete(service, user) +} + +func NewSecretStore(roleArn, namer, lockDir string) (*SecretStore, error) { locker, err := file_locker.NewFileLocker(lockDir) if err != nil { - return nil, fmt.Errorf("Can't setup lock dir: %s", lockDir) + return nil, fmt.Errorf("cannot setup lock dir: %s", lockDir) } + + user, err := user.Current() + if err != nil { + return nil, fmt.Errorf("cannot setup lock dir: %s", lockDir) + } + return &SecretStore{ lockDir: lockDir, locker: locker, + keyring: &keyRingImpl{}, lockResource: namer, secretService: namer, - roleArn: role, - secretUser: os.Getenv("USER"), + roleArn: roleArn, + secretUser: user.Name, }, nil } -func (s *SecretStore) load() error { - acquired, lock, err := s.locker.Acquire(s.lockResource, lockgate.AcquireOptions{Shared: false, Timeout: 3 * time.Minute}) +func (s *SecretStore) ensureLock() (func(), error) { + + acquired, lock, err := s.locker.Acquire(s.lockResource, lockgate.AcquireOptions{Shared: false, Timeout: 1 * time.Minute}) if err != nil { - // Errorf("Can't load secret due to locked now") - // Exit(err) - return err + return nil, fmt.Errorf("%s, %w", err, ErrUnableToAcquireLock) } - defer func() { + + if !acquired { + return nil, fmt.Errorf("%s, %w", err, ErrUnableToLoadDueToLock) + } + return func() { if acquired { if err := s.locker.Release(lock); err != nil { - // Errorf("Can't unlock") - // Exit(err) fmt.Fprintf(os.Stderr, "") } } - }() + }, nil +} - if !acquired { - // Errorf("Can't load secret due to locked now") - // Exit(err) +func (s *SecretStore) load() error { + release, err := s.ensureLock() + if err != nil { return err } + defer release() creds := &AWSCredentials{} - jsonStr, err := keyring.Get(s.secretService, s.secretUser) + jsonStr, err := s.keyring.Get(s.secretService, s.secretUser) if err != nil { if errors.Is(err, keyring.ErrNotFound) { return nil } - // Errorf("Can't load secret due to unexpected error: %v", err) - // Exit(err) return err } if err := json.Unmarshal([]byte(jsonStr), &creds); err != nil { - // Errorf("Can't load secret due to broken data: %v", err) - // Exit(err) - return err + return fmt.Errorf("%s, %w", err, ErrUnmarshallingSecret) } + if err := WriteIniSection(s.roleArn); err != nil { - // Errorf("Can't save role to ") - // Exit(err) return err } @@ -115,32 +139,13 @@ func (s *SecretStore) load() error { } func (s *SecretStore) save() error { - acquired, lock, err := s.locker.Acquire(s.lockResource, lockgate.AcquireOptions{Shared: false, Timeout: 3 * time.Minute}) - + release, err := s.ensureLock() if err != nil { - // Errorf("Can't save secret due to lock") - // Exit(err) return err - } + defer release() - defer func() { - if acquired { - if err := s.locker.Release(lock); err != nil { - // Errorf("Can't unlock") - // Exit(err) - // return err - fmt.Fprintf(os.Stderr, "Can't unlock: %s", err) - } - } - }() - - if err := keyring.Set(s.secretService, s.secretUser, s.AWSCredJson); err != nil { - // Errorf("Can't save secret: %v", err) - // Exit(err) - return err - } - return nil + return s.keyring.Set(s.secretService, s.secretUser, s.AWSCredJson) } func (s *SecretStore) AWSCredential() (*AWSCredentials, error) { @@ -149,7 +154,6 @@ func (s *SecretStore) AWSCredential() (*AWSCredentials, error) { } if s.AWSCredentials == nil && s.AWSCredJson == "" { - // Infof("Not found the credential for %s", s.roleArn) return nil, nil } @@ -162,31 +166,26 @@ func (s *SecretStore) SaveAWSCredential(cred *AWSCredentials) error { s.AWSCredentials = cred jsonStr, err := json.Marshal(cred) if err != nil { - // Errorf("Can't save secret due to the broken data") - // Exit(err) return err } s.AWSCredJson = string(jsonStr) - if err := s.save(); err != nil { - return err - } - - fmt.Fprint(os.Stderr, "The AWS credential has been saved in OS secret store") - return nil + return s.save() } func (s *SecretStore) Clear() error { - return keyring.Delete(s.secretService, s.secretUser) + 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 { secretServices, err := GetAllIniSections() if err != nil { - return err + return fmt.Errorf("unable to get sections from ini: %s, %w", err, ErrUnableToRetrieveSections) } for _, v := range secretServices { - keyring.Delete(fmt.Sprintf("%s-%s", SELF_NAME, v), s.secretUser) + s.keyring.Delete(fmt.Sprintf("%s-%s", SELF_NAME, v), s.secretUser) } return nil } diff --git a/internal/credentialexchange/secret_test.go b/internal/credentialexchange/secret_test.go index 4bdf1c5..54fface 100644 --- a/internal/credentialexchange/secret_test.go +++ b/internal/credentialexchange/secret_test.go @@ -1,9 +1,16 @@ package credentialexchange_test import ( + "errors" + "fmt" + "os" + "path" "testing" + "time" "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" + "github.com/werf/lockgate" + "github.com/zalando/go-keyring" ) var roleTest string = "arn:aws:iam::111122342343:role/DevAdmin" @@ -26,3 +33,217 @@ func TestConvertKeyToRole(t *testing.T) { t.Errorf("Wanted: %s, Got: %s", want, got) } } + +type mockKeyRing struct { + set func(service, user, password string) error + get func(service, user string) (string, error) + delete func(service, user string) error +} + +func (m *mockKeyRing) Set(service, user, password string) error { + return m.set(service, user, password) +} +func (m *mockKeyRing) Get(service, user string) (string, error) { + return m.get(service, user) +} +func (m *mockKeyRing) Delete(service, user string) error { + return m.delete(service, user) +} + +type mockLocker struct { + acquire func(lockName string, opts lockgate.AcquireOptions) (bool, lockgate.LockHandle, error) + release func(lock lockgate.LockHandle) error +} + +func (m *mockLocker) Acquire(lockName string, opts lockgate.AcquireOptions) (bool, lockgate.LockHandle, error) { + return m.acquire(lockName, opts) +} + +func (m *mockLocker) Release(lock lockgate.LockHandle) error { + return m.release(lock) +} + +var mockSuccessCreds = &credentialexchange.AWSCredentials{ + AWSAccessKey: "12345", + AWSSecretKey: "67890", + AWSSessionToken: "SOME_LONG_TOKEN", + Expires: time.Now().Add(time.Duration(10) * time.Minute), +} + +func Test_SecretStore_AWSCredential_(t *testing.T) { + ttests := map[string]struct { + keyring func(t *testing.T) keyring.Keyring + locker func(t *testing.T) lockgate.Locker + expect *credentialexchange.AWSCredentials + errTyp error + expectErr bool + }{ + "succeeds with correctly retrieved credential": { + keyring: func(t *testing.T) keyring.Keyring { + k := &mockKeyRing{} + k.get = func(service, user string) (string, error) { + return fmt.Sprintf(`{"AccessKeyId":"12345","SecretAccessKey":"67890","SessionToken":"SOME_LONG_TOKEN","Expiration":"%v"}`, mockSuccessCreds.Expires.Format("2006-01-02T15:04:05.000Z")), nil + } + return k + }, + locker: func(t *testing.T) lockgate.Locker { + l := &mockLocker{} + l.acquire = func(lockName string, opts lockgate.AcquireOptions) (bool, lockgate.LockHandle, error) { + return true, lockgate.LockHandle{UUID: "123123321dsdd", LockName: "somename"}, nil + } + l.release = func(lock lockgate.LockHandle) error { + return nil + } + return l + }, + expect: mockSuccessCreds, + errTyp: nil, + expectErr: false, + }, + "succeeds with not found on keychain": { + keyring: func(t *testing.T) keyring.Keyring { + k := &mockKeyRing{} + k.get = func(service, user string) (string, error) { + return "", fmt.Errorf("some err %w", keyring.ErrNotFound) + } + k.set = func(service, user, password string) error { + return nil + } + return k + }, + locker: func(t *testing.T) lockgate.Locker { + l := &mockLocker{} + l.acquire = func(lockName string, opts lockgate.AcquireOptions) (bool, lockgate.LockHandle, error) { + return true, lockgate.LockHandle{UUID: "123123321dsdd", LockName: "somename"}, nil + } + l.release = func(lock lockgate.LockHandle) error { + return nil + } + return l + }, + expect: mockSuccessCreds, + errTyp: nil, + expectErr: false, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + + tmpDir, _ := os.MkdirTemp(os.TempDir(), "saml-cred-test") + os.WriteFile(path.Join(tmpDir, fmt.Sprintf(".%s.ini", credentialexchange.SELF_NAME)), []byte(` +[role] +[role.roleArn] +name = "arn:aws:iam::111122342343:role/DevAdmin" +`), 0777) + os.Setenv("HOME", tmpDir) + defer func() { + os.Clearenv() + os.RemoveAll(tmpDir) + }() + + crde, err := credentialexchange.NewSecretStore("roleArn", "namer", "lockDir") + if err != nil { + t.Fail() + } + crde.WithKeyring(tt.keyring(t)).WithLocker(tt.locker(t)) + got, err := crde.AWSCredential() + if tt.expectErr { + if err == nil { + t.Errorf("got , wanted %s", tt.errTyp) + } + if !errors.Is(err, tt.errTyp) { + t.Errorf("got %s, wanted %s", err, tt.errTyp) + } + return + } + + if err != nil { + t.Errorf("got %s, wanted ", err) + } + + if got != nil { + if got.AWSSessionToken != tt.expect.AWSSessionToken { + t.Errorf("expected: \n%+v\n\n and got: \n%+v\n\n to be equal", tt.expect, got) + } + } + }) + } +} + +func Test_SaveAwsCredential_with(t *testing.T) { + ttests := map[string]struct { + keyring func(t *testing.T) keyring.Keyring + locker func(t *testing.T) lockgate.Locker + cred *credentialexchange.AWSCredentials + errTyp error + expectErr bool + }{ + "correct input": { + keyring: func(t *testing.T) keyring.Keyring { + k := &mockKeyRing{} + k.get = func(service, user string) (string, error) { + return fmt.Sprintf(`{"AccessKeyId":"12345","SecretAccessKey":"67890","SessionToken":"SOME_LONG_TOKEN","Expiration":"%v"}`, mockSuccessCreds.Expires.Format("2006-01-02T15:04:05.000Z")), nil + } + k.set = func(service, user, password string) error { + return nil + } + return k + }, + locker: func(t *testing.T) lockgate.Locker { + l := &mockLocker{} + l.acquire = func(lockName string, opts lockgate.AcquireOptions) (bool, lockgate.LockHandle, error) { + return true, lockgate.LockHandle{UUID: "123123321dsdd", LockName: "somename"}, nil + } + l.release = func(lock lockgate.LockHandle) error { + return nil + } + return l + }, + cred: mockSuccessCreds, + errTyp: nil, + expectErr: false, + }, + } + for name, tt := range ttests { + t.Run(name, func(t *testing.T) { + tmpDir, _ := os.MkdirTemp(os.TempDir(), "saml-cred-test") + iniFile := path.Join(tmpDir, fmt.Sprintf(".%s.ini", credentialexchange.SELF_NAME)) + os.WriteFile(iniFile, []byte(` +[role] +[role.someotherRole] +name = "arn:aws:iam::111122342343:role/DevAdmin" +`), 0777) + os.Setenv("HOME", tmpDir) + defer func() { + os.Clearenv() + os.RemoveAll(tmpDir) + }() + + crde, errInit := credentialexchange.NewSecretStore("roleArn", "namer", "lockDir") + + if errInit != nil { + t.Fatal(errInit) + return + } + + crde.WithKeyring(tt.keyring(t)).WithLocker(tt.locker(t)) + + err := crde.SaveAWSCredential(tt.cred) + + if tt.expectErr { + if err == nil { + t.Errorf("got , wanted %s", tt.errTyp) + } + if !errors.Is(err, tt.errTyp) { + t.Errorf("got %s, wanted %s", err, tt.errTyp) + } + return + } + + if err != nil { + t.Errorf("got %s, wanted ", err) + } + + }) + } +} diff --git a/internal/web/web.go b/internal/web/web.go index 6c9cdc7..1b0ee53 100755 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -1,6 +1,7 @@ package web import ( + "errors" "fmt" nurl "net/url" "os" @@ -13,8 +14,14 @@ import ( ps "github.com/mitchellh/go-ps" ) +var ( + ErrTimedOut = errors.New("timed out waiting for input") +) + +// WebConb type WebConfig struct { - datadir string + datadir string + // timeout value in seconds timeout int32 headless bool } @@ -98,8 +105,8 @@ func (web *Web) GetSamlLogin(conf credentialexchange.SamlConfig) (string, error) saml = strings.Split(saml, "SAMLResponse=")[1] saml = strings.Split(saml, "&")[0] return nurl.QueryUnescape(saml) - case <-time.After(time.Duration(web.conf.timeout) * time.Second): - return "", fmt.Errorf("timed out") + case <-time.After(time.Duration(web.conf.timeout*1000) * time.Millisecond): + return "", fmt.Errorf("%w", ErrTimedOut) } } } diff --git a/internal/web/web_test.go b/internal/web/web_test.go new file mode 100644 index 0000000..0aadb59 --- /dev/null +++ b/internal/web/web_test.go @@ -0,0 +1,149 @@ +package web_test + +import ( + "errors" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/dnitsch/aws-cli-auth/internal/credentialexchange" + "github.com/dnitsch/aws-cli-auth/internal/web" +) + +func mockIdpHandler(t *testing.T) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/saml", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Server", "Server") + w.Header().Set("X-Amzn-Requestid", "9363fdebc232c348b71c8ba5b59f9a34") + // w.WriteHeader(http.StatusOK) + w.Write([]byte(` + + + +SAMLResponse=dsicisud99u2ubf92e9euhre&RelayState= + + + `)) + }) + mux.HandleFunc("/idp-redirect", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(` + + + + + +
+ + `)) + }) + mux.HandleFunc("/idp-onload", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(` + + +
+ + + `)) + }) + mux.HandleFunc("/some-app", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(` + + +
SomeApp
+ + `)) + }) + return mux +} + +func Test_WebUI_with_succesful_saml(t *testing.T) { + ts := httptest.NewServer(mockIdpHandler(t)) + defer ts.Close() + conf := credentialexchange.SamlConfig{BaseConfig: credentialexchange.BaseConfig{}} + conf.AcsUrl = fmt.Sprintf("%s/saml", ts.URL) + conf.ProviderUrl = fmt.Sprintf("%s/idp-onload", ts.URL) + + tempDir, _ := os.MkdirTemp(os.TempDir(), "web-saml-tester") + + defer func() { + os.RemoveAll(tempDir) + }() + + webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(10)) + saml, err := webUi.GetSamlLogin(conf) + if err != nil { + t.Errorf("expected err to be got: %s", err) + } + if saml != "dsicisud99u2ubf92e9euhre" { + t.Errorf("incorrect saml returned\n expected \"dsicisud99u2ubf92e9euhre\", got: %s", saml) + } +} + +// 2023-10-27T09:54:59+01:00 + +func Test_WebUI_timeout_and_return_error(t *testing.T) { + ts := httptest.NewServer(mockIdpHandler(t)) + defer ts.Close() + conf := credentialexchange.SamlConfig{BaseConfig: credentialexchange.BaseConfig{}} + conf.AcsUrl = fmt.Sprintf("%s/saml", ts.URL) + conf.ProviderUrl = fmt.Sprintf("%s/idp-onload", ts.URL) + + tempDir, _ := os.MkdirTemp(os.TempDir(), "web-saml-tester") + + defer func() { + os.RemoveAll(tempDir) + }() + + webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(0)) + _, err := webUi.GetSamlLogin(conf) + + if !errors.Is(err, web.ErrTimedOut) { + t.Errorf("incorrect error returned\n expected: %s, got: %s", web.ErrTimedOut, err) + } +} + +func Test_ClearCache(t *testing.T) { + ts := httptest.NewServer(mockIdpHandler(t)) + defer ts.Close() + conf := credentialexchange.SamlConfig{BaseConfig: credentialexchange.BaseConfig{}} + conf.AcsUrl = fmt.Sprintf("%s/unknown", ts.URL) + conf.ProviderUrl = fmt.Sprintf("%s/idp-onload", ts.URL) + + tempDir, _ := os.MkdirTemp(os.TempDir(), "web-clear-saml-tester") + + defer func() { + os.RemoveAll(tempDir) + }() + + webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(20)) + + if err := webUi.ClearCache(); err != nil { + t.Errorf("expected , got: %s", err) + } + +} From 7c8531bc2f55258b50bc8bb2363b0fa1fb88f1b7 Mon Sep 17 00:00:00 2001 From: dnitsch Date: Tue, 31 Oct 2023 18:53:40 +0000 Subject: [PATCH 4/5] fix: add deps for headless chromium --- .github/workflows/build.yml | 10 +++++++++- .github/workflows/pr.yml | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7794eaa..00f8782 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,7 +42,15 @@ jobs: fetch-depth: 1 - name: install deps run: | - apt update && apt install -y jq git + apt update && apt install -y jq git \ + # Chromium dependencies + libnss3 \ + libxss1 \ + libasound2 \ + libxtst6 \ + libgtk-3-0 \ + libgbm1 \ + ca-certificates git config --global --add safe.directory "$GITHUB_WORKSPACE" git config user.email ${{ github.actor }}-ci@gha.org git config user.name ${{ github.actor }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 30e5d06..41eb193 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -34,7 +34,15 @@ jobs: - uses: actions/checkout@v3 - name: install deps run: | - apt-get update && apt-get install -y jq git + apt-get update && apt-get install -y jq git \ + # Chromium dependencies + libnss3 \ + libxss1 \ + libasound2 \ + libxtst6 \ + libgtk-3-0 \ + libgbm1 \ + ca-certificates git config --global --add safe.directory "$GITHUB_WORKSPACE" git config user.email ${{ github.actor }}-ci@gha.org git config user.name ${{ github.actor }} From a839f6aeb58533046ea093f8f5dcb8b93c2aae67 Mon Sep 17 00:00:00 2001 From: dnitsch Date: Wed, 1 Nov 2023 08:23:58 +0000 Subject: [PATCH 5/5] fix: ci defs --- .github/workflows/build.yml | 7 ++++--- .github/workflows/pr.yml | 4 ++-- .github/workflows/release.yml | 2 +- Makefile | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 00f8782..a486b30 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: test: runs-on: ubuntu-latest container: - image: golang:1.20-bullseye + image: golang:1.21-bullseye needs: set-version env: SEMVER: ${{ needs.set-version.outputs.semVer }} @@ -42,8 +42,8 @@ jobs: fetch-depth: 1 - name: install deps run: | - apt update && apt install -y jq git \ # Chromium dependencies + apt update && apt install -y jq git \ libnss3 \ libxss1 \ libasound2 \ @@ -59,7 +59,8 @@ jobs: make REVISION=$GITHUB_SHA test - name: Publish Junit style Test Report uses: mikepenz/action-junit-report@v3 - if: always() # always run even if the previous step fails + # always run even if the previous step fails + if: always() with: report_paths: '**/.coverage/report-junit.xml' - name: Analyze with SonarCloud diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 41eb193..2cd88fb 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -25,7 +25,7 @@ jobs: pr: runs-on: ubuntu-latest container: - image: golang:1.20-bullseye + image: golang:1.21-bullseye needs: set-version env: REVISION: $GITHUB_SHA @@ -34,8 +34,8 @@ jobs: - uses: actions/checkout@v3 - name: install deps run: | - apt-get update && apt-get install -y jq git \ # Chromium dependencies + apt-get update && apt-get install -y jq git \ libnss3 \ libxss1 \ libasound2 \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4af2f28..6dd63d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: release: runs-on: ubuntu-latest container: - image: golang:1.20-bullseye + image: golang:1.21-bullseye env: FOO: Bar needs: set-version diff --git a/Makefile b/Makefile index 955cf4c..2f55ede 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ tag: tagbuildrelease: tag cross-build release -show_coverage: test +show_coverage: go tool cover -html=.coverage/out .PHONY: deps