From 0816291cf48a41d73e331abb6c453e5606ee9f16 Mon Sep 17 00:00:00 2001 From: dnitsch Date: Sat, 20 Sep 2025 08:01:14 +0100 Subject: [PATCH 01/19] fix: upgrade to new cobra implementation --- .gitignore | 2 + aws-cli-auth.go | 12 +- cmd/awscliauth.go | 94 +++++++++++ cmd/{cmd_test.go => awscliauth_test.go} | 48 +++--- cmd/clear.go | 84 +++++----- cmd/root.go | 52 ------ cmd/saml.go | 201 ++++++++++++------------ cmd/specific.go | 99 ++++++------ eirctl.yaml | 82 ++++++++++ go.mod | 38 ++--- go.sum | 65 ++++---- internal/cmdutils/cmdutils_test.go | 4 +- internal/web/web.go | 13 +- internal/web/web_test.go | 11 +- 14 files changed, 486 insertions(+), 319 deletions(-) create mode 100755 cmd/awscliauth.go rename cmd/{cmd_test.go => awscliauth_test.go} (60%) delete mode 100755 cmd/root.go create mode 100644 eirctl.yaml diff --git a/.gitignore b/.gitignore index 9652f94..ed3a862 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ vendor/ .ignore* local/ +.deps/ +.cache/ \ No newline at end of file diff --git a/aws-cli-auth.go b/aws-cli-auth.go index 6801c15..abf0dc6 100755 --- a/aws-cli-auth.go +++ b/aws-cli-auth.go @@ -2,10 +2,20 @@ package main import ( "context" + "log" + "os" + "os/signal" + "syscall" "github.com/DevLabFoundry/aws-cli-auth/cmd" ) func main() { - cmd.Execute(context.Background()) + ctx, stop := signal.NotifyContext(context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM, os.Kill}...) + defer stop() + c := cmd.New() + c.WithSubCommands(cmd.SubCommands()...) + if err := c.Execute(ctx); err != nil { + log.Fatalf("\x1b[31maws-cli-auth err:\n%s\x1b[0m", err) + } } diff --git a/cmd/awscliauth.go b/cmd/awscliauth.go new file mode 100755 index 0000000..ad94223 --- /dev/null +++ b/cmd/awscliauth.go @@ -0,0 +1,94 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "path" + + "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" + "github.com/spf13/cobra" +) + +var ( + Version string = "0.0.1" + Revision string = "1111aaaa" +) + +type Root struct { + ctx context.Context + Cmd *cobra.Command + // ChannelOut io.Writer + // ChannelErr io.Writer + // viperConf *viper.Viper + rootFlags *rootCmdFlags + Datadir string +} + +type rootCmdFlags struct { + cfgSectionName string + storeInProfile bool + killHangingProcess bool + roleChain []string + verbose bool + duration int +} + +func New() *Root { + rf := &rootCmdFlags{} + r := &Root{ + rootFlags: rf, + Cmd: &cobra.Command{ + Use: "aws-cli-auth", + Short: "CLI tool for retrieving AWS temporary credentials", + Long: `CLI tool for retrieving AWS temporary credentials using SAML providers, or specified method of retrieval - i.e. force AWS_WEB_IDENTITY. +Useful in situations like CI jobs or containers where multiple env vars might be present. +Stores them under the $HOME/.aws/credentials file under a specified path or returns the crednetial_process payload for use in config`, + Version: fmt.Sprintf("%s-%s", Version, Revision), + SilenceUsage: true, + SilenceErrors: true, + }, + } + + r.Cmd.PersistentFlags().StringSliceVarP(&rf.roleChain, "role-chain", "", []string{}, "If specified it will assume the roles from the base credentials, in order they are specified in") + r.Cmd.PersistentFlags().BoolVarP(&rf.storeInProfile, "store-profile", "s", false, `By default the credentials are returned to stdout to be used by the credential_process. + Set this flag to instead store the credentials under a named profile section. You can then reference that profile name via the CLI or for use in an SDK`) + r.Cmd.PersistentFlags().StringVarP(&rf.cfgSectionName, "cfg-section", "", "", "Config section name in the default AWS credentials file. To enable priofi") + // When specifying store in profile the config section name must be provided + r.Cmd.MarkFlagsRequiredTogether("store-profile", "cfg-section") + r.Cmd.PersistentFlags().IntVarP(&rf.duration, "max-duration", "d", 900, `Override default max session duration, in seconds, of the role session [900-43200]. +NB: This cannot be higher than the 3600 as the API does not allow for AssumeRole for sessions longer than an hour`) + r.Cmd.PersistentFlags().BoolVarP(&rf.verbose, "verbose", "v", false, "Verbose output") + r.dataDirInit() + return r +} + +// SubCommands is a standalone Builder helper +// +// IF you are making your sub commands public, you can just pass them directly `WithSubCommands` +func SubCommands() []func(*Root) { + return []func(*Root){ + newSamlCmd, + newClearCmd, + newSpecificIdentityCmd, + } +} + +func (r *Root) WithSubCommands(iocFuncs ...func(rootCmd *Root)) { + for _, fn := range iocFuncs { + fn(r) + } +} + +func (r *Root) Execute(ctx context.Context) error { + return r.Cmd.ExecuteContext(ctx) +} + +func (r *Root) dataDirInit() error { + datadir := path.Join(credentialexchange.HomeDir(), fmt.Sprintf(".%s-data", credentialexchange.SELF_NAME)) + if _, err := os.Stat(datadir); err != nil { + return os.MkdirAll(datadir, 0755) + } + r.Datadir = datadir + return nil +} diff --git a/cmd/cmd_test.go b/cmd/awscliauth_test.go similarity index 60% rename from cmd/cmd_test.go rename to cmd/awscliauth_test.go index f96d39a..7cc60ca 100644 --- a/cmd/cmd_test.go +++ b/cmd/awscliauth_test.go @@ -2,13 +2,30 @@ package cmd_test import ( "bytes" + "context" + "errors" "io" "testing" "github.com/DevLabFoundry/aws-cli-auth/cmd" + "github.com/DevLabFoundry/aws-cli-auth/internal/web" ) +func cmdHelperExecutor(t *testing.T, args []string) (stdOut *bytes.Buffer, errOut *bytes.Buffer, err error) { + t.Helper() + errOut = new(bytes.Buffer) + stdOut = new(bytes.Buffer) + c := cmd.New() + c.WithSubCommands(cmd.SubCommands()...) + c.Cmd.SetArgs(args) + c.Cmd.SetErr(errOut) + c.Cmd.SetOut(stdOut) + err = c.Execute(context.Background()) + return stdOut, errOut, err +} + func Test_helpers_for_command(t *testing.T) { + ttests := map[string]struct{}{ "clear-cache": {}, "saml": {}, @@ -17,27 +34,23 @@ func Test_helpers_for_command(t *testing.T) { for name := range ttests { t.Run(name, func(t *testing.T) { cmdArgs := []string{name, "--help"} - b := new(bytes.Buffer) - o := new(bytes.Buffer) - cmd := cmd.RootCmd - cmd.SetArgs(cmdArgs) - cmd.SetErr(b) - cmd.SetOut(o) - cmd.Execute() - err, _ := io.ReadAll(b) - if len(err) > 0 { + stdOut, errOut, err := cmdHelperExecutor(t, cmdArgs) + if err != nil { + t.Fatal(err) + } + errCheck, _ := io.ReadAll(errOut) + if len(errCheck) > 0 { t.Fatal("got err, wanted nil") } - out, _ := io.ReadAll(o) - if len(out) <= 0 { + outCheck, _ := io.ReadAll(stdOut) + if len(outCheck) <= 0 { t.Fatalf("got empty, wanted a help message") } }) } } -func Test_Saml(t *testing.T) { - t.Skip() +func Test_Saml_timeout(t *testing.T) { t.Run("standard non sso should fail with incorrect saml URLs", func(t *testing.T) { cmdArgs := []string{"saml", "-p", "https://httpbin.org/anything/app123", @@ -52,13 +65,8 @@ func Test_Saml(t *testing.T) { "14400", "--reload-before", "120"} - b := new(bytes.Buffer) - o := new(bytes.Buffer) - cmd := cmd.RootCmd - cmd.SetArgs(cmdArgs) - cmd.SetErr(b) - cmd.SetOut(o) - if err := cmd.Execute(); err == nil { + _, _, err := cmdHelperExecutor(t, cmdArgs) + if err == nil && !errors.Is(err, web.ErrTimedOut) { t.Error("got nil, wanted an error") } // err, _ := io.ReadAll(b) diff --git a/cmd/clear.go b/cmd/clear.go index c75da22..c0d5fe0 100644 --- a/cmd/clear.go +++ b/cmd/clear.go @@ -10,18 +10,53 @@ import ( "github.com/spf13/cobra" ) -var ( - force bool - ClearCmd = &cobra.Command{ +type clearFlags struct { + force bool +} + +func newClearCmd(r *Root) { + flags := &clearFlags{} + + cmd := &cobra.Command{ Use: "clear-cache ", Short: "Clears any stored credentials in the OS secret store", - RunE: clear, + RunE: func(cmd *cobra.Command, args []string) error { + user, err := user.Current() + if err != nil { + return err + } + if err := samlInitConfig(); err != nil { + return err + } + secretStore, err := credentialexchange.NewSecretStore("", + fmt.Sprintf("%s-%s", credentialexchange.SELF_NAME, credentialexchange.RoleKeyConverter("")), + os.TempDir(), user.Username) + + if err != nil { + return err + } + + if flags.force { + w := &web.Web{} + if err := w.ForceKill(r.Datadir); err != nil { + return err + } + fmt.Fprint(os.Stderr, "Chromium Cache cleared") + } + + if err := secretStore.ClearAll(); err != nil { + fmt.Fprint(os.Stderr, err.Error()) + } + + if err := os.Remove(credentialexchange.ConfigIniFile("")); err != nil { + return err + } + + return nil + }, } -) -func init() { - 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. + cmd.PersistentFlags().BoolVarP(&flags.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 forcefully all chromium processes. @@ -32,37 +67,6 @@ Use with caution. If for any reason the local ini file and the secret store on your OS (keyring on GNU, keychain MacOS, windows secret store) are out of sync and the secrets cannot be retrieved by name but still exists, you might want to use CLI or GUI interface to the secret backing store on your OS and search for a secret prefixed with aws-cli-* and delete manually `) - RootCmd.AddCommand(ClearCmd) -} - -func clear(cmd *cobra.Command, args []string) error { - user, err := user.Current() - if err != nil { - return err - } - secretStore, err := credentialexchange.NewSecretStore("", - fmt.Sprintf("%s-%s", credentialexchange.SELF_NAME, credentialexchange.RoleKeyConverter("")), - os.TempDir(), user.Username) - - if err != nil { - return err - } - - if force { - w := &web.Web{} - if err := w.ForceKill(datadir); err != nil { - return err - } - fmt.Fprint(os.Stderr, "Chromium Cache cleared") - } - - if err := secretStore.ClearAll(); err != nil { - fmt.Fprint(os.Stderr, err.Error()) - } - - if err := os.Remove(credentialexchange.ConfigIniFile("")); err != nil { - return err - } - return nil + r.Cmd.AddCommand(cmd) } diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100755 index 5e9df46..0000000 --- a/cmd/root.go +++ /dev/null @@ -1,52 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "os" - - "github.com/spf13/cobra" -) - -var ( - Version string = "0.0.1" - Revision string = "1111aaaa" -) - -var ( - cfgSectionName string - storeInProfile bool - killHangingProcess bool - role string - roleChain []string - verbose bool - duration int - RootCmd = &cobra.Command{ - Use: "aws-cli-auth", - Short: "CLI tool for retrieving AWS temporary credentials", - Long: `CLI tool for retrieving AWS temporary credentials using SAML providers, or specified method of retrieval - i.e. force AWS_WEB_IDENTITY. -Useful in situations like CI jobs or containers where multiple env vars might be present. -Stores them under the $HOME/.aws/credentials file under a specified path or returns the crednetial_process payload for use in config`, - Version: fmt.Sprintf("%s-%s", Version, Revision), - } -) - -func Execute(ctx context.Context) { - if err := RootCmd.ExecuteContext(ctx); err != nil { - fmt.Errorf("cli error: %v", err) - os.Exit(1) - } - os.Exit(0) -} - -func init() { - RootCmd.PersistentFlags().StringSliceVarP(&roleChain, "role-chain", "", []string{}, "If specified it will assume the roles from the base credentials, in order they are specified in") - 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. You can then reference that profile name via the CLI or for use in an SDK`) - RootCmd.PersistentFlags().StringVarP(&cfgSectionName, "cfg-section", "", "", "Config section name in the default AWS credentials file. To enable priofi") - // When specifying store in profile the config section name must be provided - RootCmd.MarkFlagsRequiredTogether("store-profile", "cfg-section") - RootCmd.PersistentFlags().IntVarP(&duration, "max-duration", "d", 900, `Override default max session duration, in seconds, of the role session [900-43200]. -NB: This cannot be higher than the 3600 as the API does not allow for AssumeRole for sessions longer than an hour`) - RootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") -} diff --git a/cmd/saml.go b/cmd/saml.go index 8e14c68..aefcd6f 100755 --- a/cmd/saml.go +++ b/cmd/saml.go @@ -5,14 +5,13 @@ import ( "fmt" "os" "os/user" - "path" "strings" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/DevLabFoundry/aws-cli-auth/internal/cmdutils" "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" "github.com/DevLabFoundry/aws-cli-auth/internal/web" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/spf13/cobra" ) @@ -26,15 +25,12 @@ const ( SsoCredsEndpointQuery = "?account_id=%s&role_name=%s&debug=true" ) -var ( - ssoRoleAccount, ssoRoleName string -) - -var ( +type samlFlags struct { providerUrl string principalArn string acsUrl string isSso bool + role string ssoRegion string ssoRole string ssoUserEndpoint string @@ -42,124 +38,133 @@ var ( datadir string samlTimeout int32 reloadBeforeTime int - SamlCmd = &cobra.Command{ +} + +type samlCmd struct { + flags *samlFlags + ssoRoleAccount, ssoRoleName string + cmd *cobra.Command +} + +func newSamlCmd(r *Root) { + flags := &samlFlags{} + sc := &samlCmd{ + flags: flags, + } + + sc.cmd = &cobra.Command{ Use: "saml ", Short: "Get AWS credentials and out to stdout", Long: `Get AWS credentials and out to stdout through your SAML provider authentication.`, - RunE: getSaml, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + user, err := user.Current() + if err != nil { + return err + } + + if err := samlInitConfig(); err != nil { + return err + } + + allRoles := credentialexchange.MergeRoleChain(flags.role, r.rootFlags.roleChain, sc.flags.isSso) + conf := credentialexchange.CredentialConfig{ + ProviderUrl: flags.providerUrl, + PrincipalArn: flags.principalArn, + Duration: r.rootFlags.duration, + AcsUrl: flags.acsUrl, + IsSso: flags.isSso, + SsoRegion: flags.ssoRegion, + SsoRole: flags.ssoRole, + BaseConfig: credentialexchange.BaseConfig{ + StoreInProfile: r.rootFlags.storeInProfile, + Role: flags.role, + RoleChain: allRoles, + Username: user.Username, + CfgSectionName: r.rootFlags.cfgSectionName, + DoKillHangingProcess: r.rootFlags.killHangingProcess, + ReloadBeforeTime: flags.reloadBeforeTime, + }, + } + + saveRole := flags.role + if flags.isSso { + saveRole = flags.ssoRole + conf.SsoUserEndpoint = fmt.Sprintf(UserEndpoint, conf.SsoRegion) + conf.SsoCredFedEndpoint = fmt.Sprintf( + CredsEndpoint, conf.SsoRegion) + fmt.Sprintf( + SsoCredsEndpointQuery, sc.ssoRoleAccount, sc.ssoRoleName) + } + + if len(allRoles) > 0 { + saveRole = allRoles[len(allRoles)-1] + } + + secretStore, err := credentialexchange.NewSecretStore(saveRole, + fmt.Sprintf("%s-%s", credentialexchange.SELF_NAME, credentialexchange.RoleKeyConverter(saveRole)), + os.TempDir(), user.Username) + 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) + + return cmdutils.GetCredsWebUI(ctx, svc, secretStore, conf, web.NewWebConf(r.Datadir).WithTimeout(flags.samlTimeout)) + }, PreRunE: func(cmd *cobra.Command, args []string) error { - if reloadBeforeTime != 0 && reloadBeforeTime > duration { - return fmt.Errorf("reload-before: %v, must be less than duration (-d): %v", reloadBeforeTime, duration) + if flags.reloadBeforeTime != 0 && flags.reloadBeforeTime > r.rootFlags.duration { + return fmt.Errorf("reload-before: %v, must be less than duration (-d): %v", flags.reloadBeforeTime, r.rootFlags.duration) } - if len(ssoRole) > 0 { - sr := strings.Split(ssoRole, ":") + if len(flags.ssoRole) > 0 { + sr := strings.Split(flags.ssoRole, ":") if len(sr) != 2 { return fmt.Errorf("incorrectly formatted role for AWS SSO - must only be ACCOUNT:ROLE_NAME") } - ssoRoleAccount, ssoRoleName = sr[0], sr[1] + sc.ssoRoleAccount, sc.ssoRoleName = sr[0], sr[1] } return nil }, } -) -func init() { - cobra.OnInitialize(samlInitConfig) - SamlCmd.PersistentFlags().StringVarP(&providerUrl, "provider", "p", "", `Saml Entity StartSSO Url. + sc.cmd.PersistentFlags().StringVarP(&flags.providerUrl, "provider", "p", "", `Saml Entity StartSSO Url. This is the URL your Idp will make the first call to e.g.: https://company-xyz.okta.com/home/amazon_aws/12345SomeRandonId6789 `) - SamlCmd.MarkPersistentFlagRequired("provider") - SamlCmd.PersistentFlags().StringVarP(&principalArn, "principal", "", "", `Principal Arn of the SAML IdP in AWS + sc.cmd.MarkPersistentFlagRequired("provider") + sc.cmd.PersistentFlags().StringVarP(&flags.principalArn, "principal", "", "", `Principal Arn of the SAML IdP in AWS You should find it in the IAM portal e.g.: arn:aws:iam::1234567891012:saml-provider/MyCompany-Idp `) // samlCmd.MarkPersistentFlagRequired("principal") - SamlCmd.PersistentFlags().StringVarP(&role, "role", "r", "", `Set the role you want to assume when SAML or OIDC process completes`) - SamlCmd.PersistentFlags().StringVarP(&acsUrl, "acsurl", "a", "https://signin.aws.amazon.com/saml", "Override the default ACS Url, used for checkin the post of the SAMLResponse") - SamlCmd.PersistentFlags().StringVarP(&ssoUserEndpoint, "sso-user-endpoint", "", UserEndpoint, "UserEndpoint in a go style fmt.Sprintf string with a region placeholder") - SamlCmd.PersistentFlags().StringVarP(&ssoRole, "sso-role", "", "", "Sso Role name must be in this format - 12345678910:PowerUser") - SamlCmd.PersistentFlags().StringVarP(&ssoFedCredEndpoint, "sso-fed-endpoint", "", CredsEndpoint, "FederationCredEndpoint in a go style fmt.Sprintf string with a region placeholder") - SamlCmd.PersistentFlags().StringVarP(&ssoRegion, "sso-region", "", "eu-west-1", "If using SSO, you must set the region") - SamlCmd.PersistentFlags().BoolVarP(&isSso, "is-sso", "", false, `Enables the new AWS User portal login. + sc.cmd.PersistentFlags().StringVarP(&flags.role, "role", "r", "", `Set the role you want to assume when SAML or OIDC process completes`) + sc.cmd.PersistentFlags().StringVarP(&flags.acsUrl, "acsurl", "a", "https://signin.aws.amazon.com/saml", "Override the default ACS Url, used for checkin the post of the SAMLResponse") + sc.cmd.PersistentFlags().StringVarP(&flags.ssoUserEndpoint, "sso-user-endpoint", "", UserEndpoint, "UserEndpoint in a go style fmt.Sprintf string with a region placeholder") + sc.cmd.PersistentFlags().StringVarP(&flags.ssoRole, "sso-role", "", "", "Sso Role name must be in this format - 12345678910:PowerUser") + sc.cmd.PersistentFlags().StringVarP(&flags.ssoFedCredEndpoint, "sso-fed-endpoint", "", CredsEndpoint, "FederationCredEndpoint in a go style fmt.Sprintf string with a region placeholder") + sc.cmd.PersistentFlags().StringVarP(&flags.ssoRegion, "sso-region", "", "eu-west-1", "If using SSO, you must set the region") + sc.cmd.PersistentFlags().BoolVarP(&flags.isSso, "is-sso", "", false, `Enables the new AWS User portal login. If this flag is specified the --sso-role must also be specified.`) - SamlCmd.PersistentFlags().IntVarP(&reloadBeforeTime, "reload-before", "", 0, "Triggers a credentials refresh before the specified max-duration. Value provided in seconds. Should be less than the max-duration of the session") + sc.cmd.PersistentFlags().IntVarP(&flags.reloadBeforeTime, "reload-before", "", 0, "Triggers a credentials refresh before the specified max-duration. Value provided in seconds. Should be less than the max-duration of the session") // - SamlCmd.MarkFlagsMutuallyExclusive("role", "sso-role") + sc.cmd.MarkFlagsMutuallyExclusive("role", "sso-role") // samlCmd.MarkFlagsMutuallyExclusive("principal", "sso-role") // Non-SSO flow for SAML - SamlCmd.MarkFlagsRequiredTogether("principal", "role") + sc.cmd.MarkFlagsRequiredTogether("principal", "role") // SSO flow for SAML - SamlCmd.MarkFlagsRequiredTogether("is-sso", "sso-role", "sso-region") - SamlCmd.PersistentFlags().Int32VarP(&samlTimeout, "saml-timeout", "", 120, "Timeout in seconds, before the operation of waiting for a response is cancelled via the chrome driver") - RootCmd.AddCommand(SamlCmd) -} - -func getSaml(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - user, err := user.Current() - if err != nil { - return err - } - allRoles := credentialexchange.MergeRoleChain(role, roleChain, isSso) - conf := credentialexchange.CredentialConfig{ - ProviderUrl: providerUrl, - PrincipalArn: principalArn, - Duration: duration, - AcsUrl: acsUrl, - IsSso: isSso, - SsoRegion: ssoRegion, - SsoRole: ssoRole, - BaseConfig: credentialexchange.BaseConfig{ - StoreInProfile: storeInProfile, - Role: role, - RoleChain: allRoles, - Username: user.Username, - CfgSectionName: cfgSectionName, - DoKillHangingProcess: killHangingProcess, - ReloadBeforeTime: reloadBeforeTime, - }, - } + sc.cmd.MarkFlagsRequiredTogether("is-sso", "sso-role", "sso-region") + sc.cmd.PersistentFlags().Int32VarP(&flags.samlTimeout, "saml-timeout", "", 120, "Timeout in seconds, before the operation of waiting for a response is cancelled via the chrome driver") + // Add subcommand to root command + r.Cmd.AddCommand(sc.cmd) - saveRole := role - if isSso { - saveRole = ssoRole - conf.SsoUserEndpoint = fmt.Sprintf(UserEndpoint, conf.SsoRegion) - conf.SsoCredFedEndpoint = fmt.Sprintf(CredsEndpoint, conf.SsoRegion) + fmt.Sprintf(SsoCredsEndpointQuery, ssoRoleAccount, ssoRoleName) - } - - datadir := path.Join(credentialexchange.HomeDir(), fmt.Sprintf(".%s-data", credentialexchange.SELF_NAME)) - os.MkdirAll(datadir, 0755) - - if len(allRoles) > 0 { - saveRole = allRoles[len(allRoles)-1] - } - - secretStore, err := credentialexchange.NewSecretStore(saveRole, - fmt.Sprintf("%s-%s", credentialexchange.SELF_NAME, credentialexchange.RoleKeyConverter(saveRole)), - os.TempDir(), user.Username) - 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) - - return cmdutils.GetCredsWebUI(ctx, svc, secretStore, conf, web.NewWebConf(datadir).WithTimeout(samlTimeout)) } -func samlInitConfig() { +func samlInitConfig() error { 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 os.WriteFile(credentialexchange.ConfigIniFile(""), rolesInit, 0644) } + return nil } diff --git a/cmd/specific.go b/cmd/specific.go index ff38917..90e069b 100644 --- a/cmd/specific.go +++ b/cmd/specific.go @@ -4,76 +4,77 @@ import ( "fmt" "os/user" + "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sts" - "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" "github.com/spf13/cobra" ) -var ( - method string - SpecificCmd = &cobra.Command{ +type specificCmdFlags struct { + method string + role string +} + +func newSpecificIdentityCmd(r *Root) { + flags := &specificCmdFlags{} + cmd := &cobra.Command{ Use: "specific ", Short: "Initiates a specific credential provider", Long: `Initiates a specific credential provider [WEB_ID] as opposed to relying on the defaultCredentialChain provider. This is useful in CI situations where various authentication forms maybe present from AWS_ACCESS_KEY as env vars to metadata of the node. Returns the same JSON object as the call to the AWS CLI for any of the sts AssumeRole* commands`, - RunE: specific, - } -) + RunE: func(cmd *cobra.Command, args []string) error { + var awsCreds *credentialexchange.AWSCredentials + ctx := cmd.Context() -func init() { - SpecificCmd.PersistentFlags().StringVarP(&method, "method", "m", "WEB_ID", "Runs a specific credentialProvider as opposed to relying on the default chain provider fallback") - SpecificCmd.PersistentFlags().StringVarP(&role, "role", "r", "", `Set the role you want to assume when SAML or OIDC process completes`) - SpecificCmd.MarkPersistentFlagRequired("role") - RootCmd.AddCommand(SpecificCmd) -} + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return fmt.Errorf("failed to create session %s, %w", err, ErrUnableToCreateSession) + } + svc := sts.NewFromConfig(cfg) -func specific(cmd *cobra.Command, args []string) error { - var awsCreds *credentialexchange.AWSCredentials - ctx := cmd.Context() + user, err := user.Current() - cfg, err := config.LoadDefaultConfig(ctx) - if err != nil { - return fmt.Errorf("failed to create session %s, %w", err, ErrUnableToCreateSession) - } - svc := sts.NewFromConfig(cfg) + if err != nil { + return err + } - user, err := user.Current() + if flags.method != "" { + switch flags.method { + case "WEB_ID": + awsCreds, err = credentialexchange.LoginAwsWebToken(ctx, user.Name, svc) + if err != nil { + return err + } + default: + return fmt.Errorf("unsupported Method: %s", flags.method) + } + } - if err != nil { - return err - } + config := credentialexchange.CredentialConfig{ + BaseConfig: credentialexchange.BaseConfig{ + StoreInProfile: r.rootFlags.storeInProfile, + Username: user.Username, + Role: flags.role, + RoleChain: credentialexchange.MergeRoleChain(flags.role, r.rootFlags.roleChain, false), + }, + } + + conf := credentialexchange.CredentialConfig{ + Duration: r.rootFlags.duration, + } - if method != "" { - switch method { - case "WEB_ID": - awsCreds, err = credentialexchange.LoginAwsWebToken(ctx, user.Name, svc) + awsCreds, err = credentialexchange.AssumeRoleInChain(ctx, awsCreds, svc, config.BaseConfig.Username, config.BaseConfig.RoleChain, conf) if err != nil { return err } - default: - return fmt.Errorf("unsupported Method: %s", method) - } - } - config := credentialexchange.CredentialConfig{ - BaseConfig: credentialexchange.BaseConfig{ - StoreInProfile: storeInProfile, - Username: user.Username, - Role: role, - RoleChain: credentialexchange.MergeRoleChain(role, roleChain, false), + return credentialexchange.SetCredentials(awsCreds, config) }, } - conf := credentialexchange.CredentialConfig{ - Duration: duration, - } - - awsCreds, err = credentialexchange.AssumeRoleInChain(ctx, awsCreds, svc, config.BaseConfig.Username, config.BaseConfig.RoleChain, conf) - if err != nil { - return err - } - - return credentialexchange.SetCredentials(awsCreds, config) + cmd.PersistentFlags().StringVarP(&flags.method, "method", "m", "WEB_ID", "Runs a specific credentialProvider as opposed to relying on the default chain provider fallback") + cmd.PersistentFlags().StringVarP(&flags.role, "role", "r", "", `Set the role you want to assume when SAML or OIDC process completes`) + cmd.MarkPersistentFlagRequired("role") + r.Cmd.AddCommand(cmd) } diff --git a/eirctl.yaml b/eirctl.yaml new file mode 100644 index 0000000..6a18df6 --- /dev/null +++ b/eirctl.yaml @@ -0,0 +1,82 @@ +import: + - https://raw.githubusercontent.com/Ensono/eirctl/refs/heads/main/shared/build/go/eirctl.yaml + +contexts: + unit:test: + container: + name: docker.io/golang:1-trixie + container_args: + - $PWD/.cache:/root/.cache + shell: bash + shell_args: + - -c + envfile: + exclude: + - HOME + - GO + +pipelines: + unit:test:run: + - task: unit:test:prereqs + - task: unit:test + depends_on: unit:test:prereqs + +tasks: + unit:test: + context: unit:test + description: Unit test runner needs a bit of extra care in this case to ensure we have all the dependencies + command: | + apt-get update -y + apt-get install -y ca-certificates \ + fonts-liberation \ + libasound2 \ + libatk-bridge2.0-0 \ + libatk1.0-0 \ + libc6 \ + libcairo2 \ + libcups2 \ + libdbus-1-3 \ + libexpat1 \ + libfontconfig1 \ + libgbm1 \ + libgcc1 \ + libglib2.0-0 \ + libgtk-3-0 \ + libnspr4 \ + libnss3 \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libstdc++6 \ + libx11-6 \ + libx11-xcb1 \ + libxcb1 \ + libxcomposite1 \ + libxcursor1 \ + libxdamage1 \ + libxext6 \ + libxfixes3 \ + libxi6 \ + libxrandr2 \ + libxrender1 \ + libxss1 \ + libxtst6 \ + lsb-release \ + wget \ + xdg-utils + mkdir -p .coverage + export GOPATH=$PWD/.deps + go test $(go list ./... | grep -v /local/) -v -coverpkg=./... -race -mod=readonly -timeout=1m -shuffle=on -buildvcs=false -coverprofile=.coverage/out -count=1 -run=$GO_TEST_RUN_ARGS | tee .coverage/test.out + cat .coverage/test.out | go-junit-report > .coverage/report-junit.xml + gocov convert .coverage/out | gocov-xml > .coverage/report-cobertura.xml + + unit:test:prereqs: + description: Installs coverage and junit tools + context: unit:test + command: + - | + mkdir -p .coverage + export GOPATH=$PWD/.deps + go install github.com/jstemmer/go-junit-report@v0.9.1 && \ + go install github.com/axw/gocov/gocov@v1.0.0 && \ + go install github.com/AlekSi/gocov-xml@v1.0.0 + # -run ^Test_Saml_timeout$ github.com/DevLabFoundry/aws-cli-auth/cmd diff --git a/go.mod b/go.mod index 3ceaead..b151855 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/DevLabFoundry/aws-cli-auth -go 1.24.1 +go 1.24.6 require ( - github.com/aws/aws-sdk-go-v2 v1.36.3 - github.com/aws/aws-sdk-go-v2/config v1.29.12 - github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 - github.com/aws/smithy-go v1.22.3 + github.com/aws/aws-sdk-go-v2 v1.38.1 + github.com/aws/aws-sdk-go-v2/config v1.31.2 + github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 + github.com/aws/smithy-go v1.22.5 github.com/go-rod/rod v0.116.2 github.com/mitchellh/go-ps v1.0.0 github.com/spf13/cobra v1.9.1 @@ -17,27 +17,29 @@ require ( require ( al.essio.dev/pkg/shellescape v1.6.0 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.65 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.6 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 // indirect github.com/danieljoos/wincred v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/spf13/pflag v1.0.6 // indirect - github.com/ysmood/fetchup v0.3.0 // indirect + github.com/spf13/pflag v1.0.7 // indirect + github.com/ysmood/fetchup v0.5.2 // indirect github.com/ysmood/goob v0.4.0 // indirect - github.com/ysmood/got v0.40.0 // indirect + github.com/ysmood/got v0.41.0 // indirect github.com/ysmood/gson v0.7.3 // indirect github.com/ysmood/leakless v0.9.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/sys v0.31.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/sys v0.35.0 // indirect ) + +replace github.com/ysmood/fetchup => github.com/ysmood/fetchup v0.3.0 diff --git a/go.sum b/go.sum index b912322..6eaa244 100644 --- a/go.sum +++ b/go.sum @@ -1,31 +1,31 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= -github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= -github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo= -github.com/aws/aws-sdk-go-v2/config v1.29.12/go.mod h1:xse1YTjmORlb/6fhkWi8qJh3cvZi4JoVNhc+NbJt4kI= -github.com/aws/aws-sdk-go-v2/credentials v1.17.65 h1:q+nV2yYegofO/SUXruT+pn4KxkxmaQ++1B/QedcKBFM= -github.com/aws/aws-sdk-go-v2/credentials v1.17.65/go.mod h1:4zyjAuGOdikpNYiSGpsGz8hLGmUzlY8pc8r9QQ/RXYQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8= +github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2/config v1.31.2 h1:NOaSZpVGEH2Np/c1toSeW0jooNl+9ALmsUTZ8YvkJR0= +github.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes= +github.com/aws/aws-sdk-go-v2/credentials v1.18.6 h1:AmmvNEYrru7sYNJnp3pf57lGbiarX4T9qU/6AZ9SucU= +github.com/aws/aws-sdk-go-v2/credentials v1.18.6/go.mod h1:/jdQkh1iVPa01xndfECInp1v1Wnp70v3K4MvtlLGVEc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 h1:lpdMwTzmuDLkgW7086jE94HweHCqG+uOJwHf3LZs7T0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 h1:IdCLsiiIj5YJ3AFevsewURCPV+YWUlOW8JiPhoAy8vg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 h1:j7vjtr1YIssWQOMeOWRbh3z8g2oY/xPjnZH2gLY4sGw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 h1:pdgODsAhGo4dvzC3JAG5Ce0PX8kWXrTZGx+jxADD+5E= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.2/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 h1:90uX0veLKcdHVfvxhkWUQSCi5VabtwMLFutYiRke4oo= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= -github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= -github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 h1:ueB2Te0NacDMnaC+68za9jLwkjzxGWm0KB5HTUHjLTI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 h1:ve9dYBB8CfJGTFqcQ3ZLAAb/KXWgYlgu/2R2TZL2Ko0= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 h1:pd9G9HQaM6UZAZh19pYOkpKSQkyQQ9ftnl/LttQOcGI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 h1:iV1Ko4Em/lkJIsoKyGfc0nQySi+v0Udxr6Igq+y9JZc= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo= +github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= +github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= @@ -52,8 +52,9 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -62,12 +63,14 @@ 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.3.0 h1:UhYz9xnLEVn2ukSuK3KCgcznWpHMdrmbsPpllcylyu8= github.com/ysmood/fetchup v0.3.0/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A= +github.com/ysmood/fetchup v0.5.2 h1:P9w3OIA7RSNEEFvEmOiTq09IOu42C96PMyZ1MWd8TAs= +github.com/ysmood/fetchup v0.5.2/go.mod h1:yCv8s8itjsCul1LGXJ1Q+8EQnZcVjfbZ4+l1zDm4StE= 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.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= -github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= -github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/got v0.41.0 h1:XiFH311ltTSGyxjeKcNvy7dzbJjjTzn6DBgK313JHBs= +github.com/ysmood/got v0.41.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= 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= @@ -76,10 +79,10 @@ github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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= diff --git a/internal/cmdutils/cmdutils_test.go b/internal/cmdutils/cmdutils_test.go index 6f4d1ac..50f7ffd 100644 --- a/internal/cmdutils/cmdutils_test.go +++ b/internal/cmdutils/cmdutils_test.go @@ -418,7 +418,7 @@ func Test_GetSamlCreds_With(t *testing.T) { err := cmdutils.GetCredsWebUI( context.TODO(), tt.authApi(t), ss, conf, - web.NewWebConf(tempDir).WithHeadless().WithTimeout(10)) + web.NewWebConf(tempDir).WithHeadless().WithTimeout(10).WithNoSandbox()) if tt.expectErr { if err == nil { @@ -550,7 +550,7 @@ func Test_Get_SSO_Creds_with(t *testing.T) { err := cmdutils.GetCredsWebUI( context.TODO(), tt.authApi(t), ss, conf, - web.NewWebConf(tempDir).WithHeadless().WithTimeout(10)) + web.NewWebConf(tempDir).WithHeadless().WithTimeout(10).WithNoSandbox()) if tt.expectErr { if err == nil { diff --git a/internal/web/web.go b/internal/web/web.go index b6c3e0f..647f4a7 100755 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -23,9 +23,10 @@ var ( type WebConfig struct { datadir string // timeout value in seconds - timeout int32 - headless bool - leakless bool + timeout int32 + headless bool + leakless bool + noSandbox bool } func NewWebConf(datadir string) *WebConfig { @@ -46,6 +47,11 @@ func (wc *WebConfig) WithHeadless() *WebConfig { return wc } +func (wc *WebConfig) WithNoSandbox() *WebConfig { + wc.noSandbox = true + return wc +} + type Web struct { conf *WebConfig launcher *launcher.Launcher @@ -59,6 +65,7 @@ func New(conf *WebConfig) *Web { Devtools(false). Headless(conf.headless). UserDataDir(conf.datadir). + NoSandbox(conf.noSandbox). Leakless(conf.leakless) url := l.MustLaunch() diff --git a/internal/web/web_test.go b/internal/web/web_test.go index e7e1654..77c0e5c 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -96,7 +96,7 @@ func Test_WebUI_with_succesful_saml(t *testing.T) { os.RemoveAll(tempDir) }() - webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(10)) + webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(10).WithNoSandbox()) saml, err := webUi.GetSamlLogin(conf) if err != nil { t.Errorf("expected err to be got: %s", err) @@ -121,7 +121,7 @@ func Test_WebUI_timeout_and_return_error(t *testing.T) { os.RemoveAll(tempDir) }() - webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(0)) + webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(0).WithNoSandbox()) _, err := webUi.GetSamlLogin(conf) if !errors.Is(err, web.ErrTimedOut) { @@ -140,7 +140,7 @@ func Test_ClearCache(t *testing.T) { os.RemoveAll(tempDir) }() - webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(20)) + webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(20).WithNoSandbox()) if err := webUi.ForceKill(tempDir); err != nil { t.Errorf("expected , got: %s", err) @@ -148,6 +148,7 @@ func Test_ClearCache(t *testing.T) { } func mockSsoHandler(t *testing.T) http.Handler { + t.Helper() mux := http.NewServeMux() mux.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -196,7 +197,7 @@ func Test_WebUI_with_succesful_ssoLogin(t *testing.T) { os.RemoveAll(tempDir) }() - webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(10)) + webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(10).WithNoSandbox()) creds, err := webUi.GetSSOCredentials(conf) if err != nil { t.Errorf("expected err to be got: %s", err) @@ -226,7 +227,7 @@ func Test_WebUI_with_timeout_ssoLogin(t *testing.T) { os.RemoveAll(tempDir) }() - webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(0)) + webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(0).WithNoSandbox()) _, err := webUi.GetSSOCredentials(conf) if !errors.Is(err, web.ErrTimedOut) { From 225fe7290bfdaaf591a73486382204e98c190a1c Mon Sep 17 00:00:00 2001 From: dnitsch Date: Sat, 20 Sep 2025 14:57:15 +0100 Subject: [PATCH 02/19] fix: add own test container context fix: update test eirctl tasks feat: add a fallback to the chromium browser but prefer own chrome fix: remove process killer func +semver: feature +semver: FEATURE --- cmd/clear.go | 3 ++ eirctl.yaml | 63 +++++++---------------------------- go.mod | 2 +- hack/Dockerfile | 9 +++++ internal/cmdutils/cmdutils.go | 4 +-- internal/web/web.go | 62 +++++++++++++--------------------- internal/web/web_test.go | 17 ++++------ 7 files changed, 56 insertions(+), 104 deletions(-) create mode 100644 hack/Dockerfile diff --git a/cmd/clear.go b/cmd/clear.go index c0d5fe0..21ab0c7 100644 --- a/cmd/clear.go +++ b/cmd/clear.go @@ -20,6 +20,9 @@ func newClearCmd(r *Root) { cmd := &cobra.Command{ Use: "clear-cache ", Short: "Clears any stored credentials in the OS secret store", + Long: `Clears any stored credentials in the OS secret store + +NB: Occassionally you may encounter a hanging chromium processes if not using own browser binary, you should kill all the instances of the chromium PIDs`, RunE: func(cmd *cobra.Command, args []string) error { user, err := user.Current() if err != nil { diff --git a/eirctl.yaml b/eirctl.yaml index 6a18df6..10d9799 100644 --- a/eirctl.yaml +++ b/eirctl.yaml @@ -4,10 +4,9 @@ import: contexts: unit:test: container: - name: docker.io/golang:1-trixie - container_args: - - $PWD/.cache:/root/.cache - shell: bash + name: docker.io/dnitsch/aws-cli-auth-ci:0.2.0 + entrypoint: /usr/bin/env + shell: sh shell_args: - -c envfile: @@ -24,50 +23,13 @@ pipelines: tasks: unit:test: context: unit:test - description: Unit test runner needs a bit of extra care in this case to ensure we have all the dependencies + description: | + Unit test runner needs a bit of extra care in this case to ensure we have all the dependencies command: | - apt-get update -y - apt-get install -y ca-certificates \ - fonts-liberation \ - libasound2 \ - libatk-bridge2.0-0 \ - libatk1.0-0 \ - libc6 \ - libcairo2 \ - libcups2 \ - libdbus-1-3 \ - libexpat1 \ - libfontconfig1 \ - libgbm1 \ - libgcc1 \ - libglib2.0-0 \ - libgtk-3-0 \ - libnspr4 \ - libnss3 \ - libpango-1.0-0 \ - libpangocairo-1.0-0 \ - libstdc++6 \ - libx11-6 \ - libx11-xcb1 \ - libxcb1 \ - libxcomposite1 \ - libxcursor1 \ - libxdamage1 \ - libxext6 \ - libxfixes3 \ - libxi6 \ - libxrandr2 \ - libxrender1 \ - libxss1 \ - libxtst6 \ - lsb-release \ - wget \ - xdg-utils - mkdir -p .coverage - export GOPATH=$PWD/.deps - go test $(go list ./... | grep -v /local/) -v -coverpkg=./... -race -mod=readonly -timeout=1m -shuffle=on -buildvcs=false -coverprofile=.coverage/out -count=1 -run=$GO_TEST_RUN_ARGS | tee .coverage/test.out - cat .coverage/test.out | go-junit-report > .coverage/report-junit.xml - gocov convert .coverage/out | gocov-xml > .coverage/report-cobertura.xml + export GOPATH=$PWD/.deps GOBIN=$PWD/.deps/bin + CGO_ENABLED=1 go test $(go list ./... | grep -v /local/) -v -coverpkg=./... -race -mod=readonly -timeout=1m -shuffle=on -buildvcs=false -coverprofile=.coverage/out -count=1 -run=$GO_TEST_RUN_ARGS | tee .coverage/test.out + cat .coverage/test.out | .deps/bin/go-junit-report > .coverage/report-junit.xml + .deps/bin/gocov convert .coverage/out | .deps/bin/gocov-xml > .coverage/report-cobertura.xml unit:test:prereqs: description: Installs coverage and junit tools @@ -75,8 +37,7 @@ tasks: command: - | mkdir -p .coverage - export GOPATH=$PWD/.deps - go install github.com/jstemmer/go-junit-report@v0.9.1 && \ - go install github.com/axw/gocov/gocov@v1.0.0 && \ + export GOPATH=$PWD/.deps GOBIN=$PWD/.deps/bin + go install github.com/jstemmer/go-junit-report@v0.9.1 + go install github.com/axw/gocov/gocov@v1.0.0 go install github.com/AlekSi/gocov-xml@v1.0.0 - # -run ^Test_Saml_timeout$ github.com/DevLabFoundry/aws-cli-auth/cmd diff --git a/go.mod b/go.mod index b151855..f1f09bd 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/DevLabFoundry/aws-cli-auth -go 1.24.6 +go 1.25.1 require ( github.com/aws/aws-sdk-go-v2 v1.38.1 diff --git a/hack/Dockerfile b/hack/Dockerfile new file mode 100644 index 0000000..80c7e8b --- /dev/null +++ b/hack/Dockerfile @@ -0,0 +1,9 @@ +FROM docker.io/zenika/alpine-chrome:124 as chrome + +USER root + +RUN wget https://dl.google.com/go/go1.25.1.linux-arm64.tar.gz && \ + tar -C /usr/local -xzf go1.25.1.linux-arm64.tar.gz +ENV PATH=$PATH:/usr/local/go/bin + +RUN apk add build-base \ No newline at end of file diff --git a/internal/cmdutils/cmdutils.go b/internal/cmdutils/cmdutils.go index b251753..44468b1 100644 --- a/internal/cmdutils/cmdutils.go +++ b/internal/cmdutils/cmdutils.go @@ -53,7 +53,7 @@ func GetCredsWebUI(ctx context.Context, svc credentialexchange.AuthSamlApi, secr // upon successful auth from the IDP. // Once credentials are captured they are used in the role assumption process. func refreshAwsSsoCreds(ctx context.Context, conf credentialexchange.CredentialConfig, secretStore SecretStorageImpl, svc credentialexchange.AuthSamlApi, webConfig *web.WebConfig) error { - webBrowser := web.New(webConfig) + webBrowser := web.New(ctx, webConfig) capturedCreds, err := webBrowser.GetSSOCredentials(conf) if err != nil { return err @@ -65,7 +65,7 @@ func refreshAwsSsoCreds(ctx context.Context, conf credentialexchange.CredentialC func refreshSamlCreds(ctx context.Context, conf credentialexchange.CredentialConfig, secretStore SecretStorageImpl, svc credentialexchange.AuthSamlApi, webConfig *web.WebConfig) error { - webBrowser := web.New(webConfig) + webBrowser := web.New(ctx, webConfig) duration := conf.Duration diff --git a/internal/web/web.go b/internal/web/web.go index 647f4a7..dec5a97 100755 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -1,6 +1,7 @@ package web import ( + "context" "errors" "fmt" "net/http" @@ -12,7 +13,6 @@ import ( "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" - ps "github.com/mitchellh/go-ps" ) var ( @@ -23,10 +23,11 @@ var ( type WebConfig struct { datadir string // timeout value in seconds - timeout int32 - headless bool - leakless bool - noSandbox bool + timeout int32 + headless bool + leakless bool + noSandbox bool + preferChromium bool } func NewWebConf(datadir string) *WebConfig { @@ -59,10 +60,17 @@ type Web struct { } // New returns an initialised instance of Web struct -func New(conf *WebConfig) *Web { +func New(ctx context.Context, conf *WebConfig) *Web { + var l *launcher.Launcher - l := launcher.New(). - Devtools(false). + if path, found := launcher.LookPath(); (found && path != "") && !conf.preferChromium { + l = launcher.New().Bin(path) + } else { + l = launcher.New() + } + + // common set up + l.Devtools(false). Headless(conf.headless). UserDataDir(conf.datadir). NoSandbox(conf.noSandbox). @@ -70,9 +78,14 @@ func New(conf *WebConfig) *Web { url := l.MustLaunch() - browser := rod.New(). + browser, cancel := rod.New(). ControlURL(url). - MustConnect().NoDefaultDevice() + MustConnect().NoDefaultDevice().WithCancel() + go func() { + <-ctx.Done() + fmt.Println("cancelled") + cancel() + }() return &Web{ conf: conf, @@ -199,10 +212,6 @@ func (web *Web) MustClose() { func (web *Web) ForceKill(datadir string) error { errs := []error{} - - if err := checkRodProcess(); err != nil { - errs = append(errs, err) - } // once processes have been killed // we can remove the datadir if err := os.RemoveAll(datadir); err != nil { @@ -214,28 +223,3 @@ func (web *Web) ForceKill(datadir string) error { } return nil } - -// checkRodProcess gets a list running process -// kills any hanging rod browser process from any previous improprely closed sessions -func checkRodProcess() error { - pids := make([]int, 0) - ps, err := ps.Processes() - if err != nil { - return err - } - for _, v := range ps { - // grab all chromium processes - // on windows the name will be reported as `chrome.exe` - if strings.Contains(strings.ToLower(v.Executable()), "chrom") { - fmt.Fprintf(os.Stderr, "Found process: (%d) and its parent (%d)\n", v.Pid(), v.PPid()) - pids = append(pids, v.Pid()) - } - } - for _, pid := range pids { - if proc, _ := os.FindProcess(pid); proc != nil { - fmt.Fprintf(os.Stderr, "Process to be killed as part of clean up: %d\n", pid) - proc.Kill() - } - } - return nil -} diff --git a/internal/web/web_test.go b/internal/web/web_test.go index 77c0e5c..d90861f 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -1,6 +1,7 @@ package web_test import ( + "context" "errors" "fmt" "net/http" @@ -82,7 +83,6 @@ SAMLResponse=dsicisud99u2ubf92e9euhre&RelayState= } func Test_WebUI_with_succesful_saml(t *testing.T) { - t.Parallel() ts := httptest.NewServer(mockIdpHandler(t)) defer ts.Close() @@ -96,7 +96,7 @@ func Test_WebUI_with_succesful_saml(t *testing.T) { os.RemoveAll(tempDir) }() - webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(10).WithNoSandbox()) + webUi := web.New(context.TODO(), web.NewWebConf(tempDir).WithHeadless().WithTimeout(10).WithNoSandbox()) saml, err := webUi.GetSamlLogin(conf) if err != nil { t.Errorf("expected err to be got: %s", err) @@ -107,7 +107,6 @@ func Test_WebUI_with_succesful_saml(t *testing.T) { } func Test_WebUI_timeout_and_return_error(t *testing.T) { - t.Parallel() ts := httptest.NewServer(mockIdpHandler(t)) defer ts.Close() @@ -121,7 +120,7 @@ func Test_WebUI_timeout_and_return_error(t *testing.T) { os.RemoveAll(tempDir) }() - webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(0).WithNoSandbox()) + webUi := web.New(context.TODO(), web.NewWebConf(tempDir).WithHeadless().WithTimeout(0).WithNoSandbox()) _, err := webUi.GetSamlLogin(conf) if !errors.Is(err, web.ErrTimedOut) { @@ -130,8 +129,6 @@ func Test_WebUI_timeout_and_return_error(t *testing.T) { } func Test_ClearCache(t *testing.T) { - // t.Parallel() - ts := httptest.NewServer(mockIdpHandler(t)) defer ts.Close() tempDir, _ := os.MkdirTemp(os.TempDir(), "web-clear-saml-tester") @@ -140,7 +137,7 @@ func Test_ClearCache(t *testing.T) { os.RemoveAll(tempDir) }() - webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(20).WithNoSandbox()) + webUi := web.New(context.TODO(), web.NewWebConf(tempDir).WithHeadless().WithTimeout(20).WithNoSandbox()) if err := webUi.ForceKill(tempDir); err != nil { t.Errorf("expected , got: %s", err) @@ -178,7 +175,6 @@ func mockSsoHandler(t *testing.T) http.Handler { } func Test_WebUI_with_succesful_ssoLogin(t *testing.T) { - t.Parallel() ts := httptest.NewServer(mockSsoHandler(t)) defer ts.Close() @@ -197,7 +193,7 @@ func Test_WebUI_with_succesful_ssoLogin(t *testing.T) { os.RemoveAll(tempDir) }() - webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(10).WithNoSandbox()) + webUi := web.New(context.TODO(), web.NewWebConf(tempDir).WithHeadless().WithTimeout(10).WithNoSandbox()) creds, err := webUi.GetSSOCredentials(conf) if err != nil { t.Errorf("expected err to be got: %s", err) @@ -208,7 +204,6 @@ func Test_WebUI_with_succesful_ssoLogin(t *testing.T) { } func Test_WebUI_with_timeout_ssoLogin(t *testing.T) { - t.Parallel() ts := httptest.NewServer(mockSsoHandler(t)) defer ts.Close() @@ -227,7 +222,7 @@ func Test_WebUI_with_timeout_ssoLogin(t *testing.T) { os.RemoveAll(tempDir) }() - webUi := web.New(web.NewWebConf(tempDir).WithHeadless().WithTimeout(0).WithNoSandbox()) + webUi := web.New(context.TODO(), web.NewWebConf(tempDir).WithHeadless().WithTimeout(0).WithNoSandbox()) _, err := webUi.GetSSOCredentials(conf) if !errors.Is(err, web.ErrTimedOut) { From 9d499b78e2d9cac3a331dc0f25dd9fc4b8a29379 Mon Sep 17 00:00:00 2001 From: dnitsch Date: Sun, 21 Sep 2025 20:20:32 +0100 Subject: [PATCH 03/19] fix: address linters fix: add custom binary --- .github/workflows/ci.yml | 40 +++++---- .github/workflows/release.yml | 3 +- aws-cli-auth.go | 11 ++- cmd/awscliauth.go | 2 +- cmd/clear.go | 10 +-- cmd/saml.go | 2 +- cmd/specific.go | 2 +- eirctl.yaml | 85 ++++++++++++++++++- go.mod | 1 - go.sum | 4 - internal/cmdutils/cmdutils.go | 2 +- .../credentialexchange_test.go | 20 ++--- internal/credentialexchange/helper.go | 16 ++-- internal/credentialexchange/helper_test.go | 14 +-- internal/credentialexchange/secret.go | 2 +- internal/credentialexchange/secret_test.go | 6 +- internal/web/web.go | 85 ++++++++++--------- internal/web/web_test.go | 28 ++---- sonar-project.properties | 4 + 19 files changed, 208 insertions(+), 129 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f8b65e..c801451 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,47 +27,45 @@ jobs: - name: Set SemVer Version uses: gittools/actions/gitversion/execute@v1 id: gitversion + pr: runs-on: ubuntu-latest - container: - image: golang:1.24-bookworm needs: set-version env: REVISION: $GITHUB_SHA SEMVER: ${{ needs.set-version.outputs.semVer }} steps: - uses: actions/checkout@v4 - - name: install deps + + - uses: ensono/actions/eirctl-setup@v0.3.1 + with: + version: latest + isPrerelease: false + + - name: prep-git run: | - # Chromium dependencies - apt-get update && apt-get install -y jq git \ - zip unzip \ - 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 }} - - name: make test + - name: Run linters run: | - make REVISION=$GITHUB_SHA test + eirctl run vuln:check + - name: Unit Tests + run: | + eirctl run test:unit + - name: Publish Junit style Test Report uses: mikepenz/action-junit-report@v3 if: always() # always run even if the previous step fails with: report_paths: '**/report-junit.xml' - - name: Analyze with SonarCloud - # You can pin the exact commit or the version. - uses: SonarSource/sonarcloud-github-action@master + + - name: Analyze with SonarCloud + uses: SonarSource/sonarqube-scan-action@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate a token on Sonarcloud.io, add it to the secrets of this repo with the name SONAR_TOKEN (Settings > Secrets > Actions > add new repository secret) with: - args: + projectBaseDir: . + args: > -Dsonar.projectVersion=${{ needs.set-version.outputs.semVer }} - -Dsonar.go.coverage.reportPaths=/github/workspace/.coverage/out - -Dsonar.go.tests.reportPaths=/github/workspace/.coverage/report-junit.xml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e995b57..c39db9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,6 +64,7 @@ jobs: - name: release binary run: | make REVISION=$GITHUB_SHA GIT_TAG=${SEMVER} PAT=${{ secrets.GITHUB_TOKEN }} cross-build + - name: Release uses: softprops/action-gh-release@v2.2.1 with: @@ -72,4 +73,4 @@ jobs: generate_release_notes: true token: ${{ secrets.GITHUB_TOKEN }} files: ./dist/* - prerelease: false + prerelease: true diff --git a/aws-cli-auth.go b/aws-cli-auth.go index abf0dc6..7a08352 100755 --- a/aws-cli-auth.go +++ b/aws-cli-auth.go @@ -13,9 +13,18 @@ import ( func main() { ctx, stop := signal.NotifyContext(context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM, os.Kill}...) defer stop() + + go func() { + <-ctx.Done() + stop() + log.Printf("\x1b[31minterrupted: %s\x1b[0m", ctx.Err()) + os.Exit(0) + }() + c := cmd.New() c.WithSubCommands(cmd.SubCommands()...) + if err := c.Execute(ctx); err != nil { - log.Fatalf("\x1b[31maws-cli-auth err:\n%s\x1b[0m", err) + log.Fatalf("\x1b[31m%s\x1b[0m", err) } } diff --git a/cmd/awscliauth.go b/cmd/awscliauth.go index ad94223..1b58c71 100755 --- a/cmd/awscliauth.go +++ b/cmd/awscliauth.go @@ -59,7 +59,7 @@ Stores them under the $HOME/.aws/credentials file under a specified path or retu r.Cmd.PersistentFlags().IntVarP(&rf.duration, "max-duration", "d", 900, `Override default max session duration, in seconds, of the role session [900-43200]. NB: This cannot be higher than the 3600 as the API does not allow for AssumeRole for sessions longer than an hour`) r.Cmd.PersistentFlags().BoolVarP(&rf.verbose, "verbose", "v", false, "Verbose output") - r.dataDirInit() + _ = r.dataDirInit() return r } diff --git a/cmd/clear.go b/cmd/clear.go index 21ab0c7..8965c9a 100644 --- a/cmd/clear.go +++ b/cmd/clear.go @@ -6,7 +6,6 @@ import ( "os/user" "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" - "github.com/DevLabFoundry/aws-cli-auth/internal/web" "github.com/spf13/cobra" ) @@ -21,8 +20,7 @@ func newClearCmd(r *Root) { Use: "clear-cache ", Short: "Clears any stored credentials in the OS secret store", Long: `Clears any stored credentials in the OS secret store - -NB: Occassionally you may encounter a hanging chromium processes if not using own browser binary, you should kill all the instances of the chromium PIDs`, + NB: Occassionally you may encounter a hanging chromium processes, you should kill all the instances of the chromium (or if using own browser binary) PIDs`, RunE: func(cmd *cobra.Command, args []string) error { user, err := user.Current() if err != nil { @@ -40,11 +38,7 @@ NB: Occassionally you may encounter a hanging chromium processes if not using ow } if flags.force { - w := &web.Web{} - if err := w.ForceKill(r.Datadir); err != nil { - return err - } - fmt.Fprint(os.Stderr, "Chromium Cache cleared") + fmt.Fprint(os.Stderr, "delete ~/.aws-cli-auth-data/ manually") } if err := secretStore.ClearAll(); err != nil { diff --git a/cmd/saml.go b/cmd/saml.go index aefcd6f..85c5cb4 100755 --- a/cmd/saml.go +++ b/cmd/saml.go @@ -133,7 +133,7 @@ func newSamlCmd(r *Root) { sc.cmd.PersistentFlags().StringVarP(&flags.providerUrl, "provider", "p", "", `Saml Entity StartSSO Url. This is the URL your Idp will make the first call to e.g.: https://company-xyz.okta.com/home/amazon_aws/12345SomeRandonId6789 `) - sc.cmd.MarkPersistentFlagRequired("provider") + _ = sc.cmd.MarkPersistentFlagRequired("provider") sc.cmd.PersistentFlags().StringVarP(&flags.principalArn, "principal", "", "", `Principal Arn of the SAML IdP in AWS You should find it in the IAM portal e.g.: arn:aws:iam::1234567891012:saml-provider/MyCompany-Idp `) diff --git a/cmd/specific.go b/cmd/specific.go index 90e069b..6048b88 100644 --- a/cmd/specific.go +++ b/cmd/specific.go @@ -75,6 +75,6 @@ Returns the same JSON object as the call to the AWS CLI for any of the sts Assum cmd.PersistentFlags().StringVarP(&flags.method, "method", "m", "WEB_ID", "Runs a specific credentialProvider as opposed to relying on the default chain provider fallback") cmd.PersistentFlags().StringVarP(&flags.role, "role", "r", "", `Set the role you want to assume when SAML or OIDC process completes`) - cmd.MarkPersistentFlagRequired("role") + _ = cmd.MarkPersistentFlagRequired("role") r.Cmd.AddCommand(cmd) } diff --git a/eirctl.yaml b/eirctl.yaml index 10d9799..492119e 100644 --- a/eirctl.yaml +++ b/eirctl.yaml @@ -1,5 +1,5 @@ import: - - https://raw.githubusercontent.com/Ensono/eirctl/refs/heads/main/shared/build/go/eirctl.yaml + - https://raw.githubusercontent.com/Ensono/eirctl/e71dd9d66293e27e70fd0620e63a6d627579c060/shared/build/go/eirctl.yaml contexts: unit:test: @@ -13,6 +13,7 @@ contexts: exclude: - HOME - GO + - TMP pipelines: unit:test:run: @@ -20,7 +21,21 @@ pipelines: - task: unit:test depends_on: unit:test:prereqs + release: + - task: clean:dir + - task: build + depends_on: clean:dir + tasks: + tag: + command: + - | + git tag -a ${VERSION} -m "ci tag release" ${REVISION} + git push origin ${VERSION} + required: + env: + - VERSION + - REVISION unit:test: context: unit:test description: | @@ -41,3 +56,71 @@ tasks: go install github.com/jstemmer/go-junit-report@v0.9.1 go install github.com/axw/gocov/gocov@v1.0.0 go install github.com/AlekSi/gocov-xml@v1.0.0 + + clean:dir: + command: + - | + rm -rf dist/ + + build: + context: go1x + description: Builds Go binary + command: + - | + mkdir -p .deps + ldflags="-s -w -X \"github.com/{{.RepoOwner}}/{{.BinName}}/cmd.Version={{.Version}}\" -X \"github.com/{{.RepoOwner}}/{{.BinName}}/cmd.Revision={{.Revision}}\" -extldflags -static" + CGO_ENABLED=0 GOPATH=$PWD/.deps GOOS=${BUILD_GOOS} go build -mod=readonly -buildvcs=false -ldflags="$ldflags" -o dist/{{.BinName}}-${BUILD_GOOS}${BUILD_GOARCH}${BINARY_SUFFIX} . + variations: + - BUILD_GOOS: windows + BUILD_GOARCH: amd64 + BINARY_SUFFIX: "" + - BUILD_GOOS: windows + BUILD_GOARCH: "386" + BINARY_SUFFIX: "" + - BUILD_GOOS: darwin + BUILD_GOARCH: "" + BINARY_SUFFIX: "" + - BUILD_GOOS: linux + BUILD_GOARCH: "" + BINARY_SUFFIX: "" + variables: + RepoOwner: DevLabFoundry + BinName: aws-cli-auth + + build:arch: + context: go1x + description: Builds Go binary per architecture + command: + - | + mkdir -p .deps + ldflags="-s -w -X \"github.com/{{.RepoOwner}}/{{.BinName}}/cmd.Version={{.Version}}\" -X \"github.com/{{.RepoOwner}}/{{.BinName}}/cmd.Revision={{.Revision}}\" -extldflags -static" + CGO_ENABLED=0 GOPATH=$PWD/.deps GOOS=${BUILD_GOOS} GOARCH=${BUILD_GOARCH} go build -mod=readonly -buildvcs=false -ldflags="$ldflags" -o dist/{{.BinName}}-${BUILD_GOOS}-${BUILD_GOARCH}${BINARY_SUFFIX} cmd/main.go + variations: + - BUILD_GOOS: windows + BUILD_GOARCH: amd64 + BINARY_SUFFIX: .exe + - BUILD_GOOS: windows + BUILD_GOARCH: "386" + BINARY_SUFFIX: .exe + - BUILD_GOOS: windows + BUILD_GOARCH: arm64 + BINARY_SUFFIX: .exe + - BUILD_GOOS: darwin + BUILD_GOARCH: amd64 + BINARY_SUFFIX: "" + - BUILD_GOOS: darwin + BUILD_GOARCH: arm64 + BINARY_SUFFIX: "" + - BUILD_GOOS: linux + BUILD_GOARCH: arm64 + BINARY_SUFFIX: "" + - BUILD_GOOS: linux + BUILD_GOARCH: amd64 + BINARY_SUFFIX: "" + variables: + RepoOwner: DevLabFoundry + BinName: aws-cli-auth + + build:container: + description: Builds the docker image + command: docker build --build-arg Version={{.Version}} --build-arg Revision={{.Revision}} -t eirctl:{{.Version}} . diff --git a/go.mod b/go.mod index f1f09bd..2ff3338 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 github.com/aws/smithy-go v1.22.5 github.com/go-rod/rod v0.116.2 - github.com/mitchellh/go-ps v1.0.0 github.com/spf13/cobra v1.9.1 github.com/werf/lockgate v0.1.1 github.com/zalando/go-keyring v0.2.6 diff --git a/go.sum b/go.sum index 6eaa244..6c2cc4d 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/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/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/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= @@ -63,8 +61,6 @@ 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.3.0 h1:UhYz9xnLEVn2ukSuK3KCgcznWpHMdrmbsPpllcylyu8= github.com/ysmood/fetchup v0.3.0/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A= -github.com/ysmood/fetchup v0.5.2 h1:P9w3OIA7RSNEEFvEmOiTq09IOu42C96PMyZ1MWd8TAs= -github.com/ysmood/fetchup v0.5.2/go.mod h1:yCv8s8itjsCul1LGXJ1Q+8EQnZcVjfbZ4+l1zDm4StE= 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.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= diff --git a/internal/cmdutils/cmdutils.go b/internal/cmdutils/cmdutils.go index 44468b1..c05dbab 100644 --- a/internal/cmdutils/cmdutils.go +++ b/internal/cmdutils/cmdutils.go @@ -59,7 +59,7 @@ func refreshAwsSsoCreds(ctx context.Context, conf credentialexchange.CredentialC return err } awsCreds := &credentialexchange.AWSCredentials{} - awsCreds.FromRoleCredString(capturedCreds) + _, _ = awsCreds.FromRoleCredString(capturedCreds) return completeCredProcess(ctx, secretStore, svc, awsCreds, conf) } diff --git a/internal/credentialexchange/credentialexchange_test.go b/internal/credentialexchange/credentialexchange_test.go index ab65349..5ff4dc1 100644 --- a/internal/credentialexchange/credentialexchange_test.go +++ b/internal/credentialexchange/credentialexchange_test.go @@ -9,11 +9,11 @@ import ( "testing" "time" + "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" "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/DevLabFoundry/aws-cli-auth/internal/credentialexchange" ) type mockAuthApi struct { @@ -302,12 +302,12 @@ func Test_LoginAwsWebToken_with(t *testing.T) { 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") + _ = 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) + _ = os.RemoveAll(tmpDir) } }, currCred: mockSuccessCreds, @@ -325,12 +325,12 @@ func Test_LoginAwsWebToken_with(t *testing.T) { 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") + _ = 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) + _ = os.RemoveAll(tmpDir) } }, currCred: mockSuccessCreds, @@ -371,7 +371,7 @@ func Test_LoginAwsWebToken_with(t *testing.T) { // 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") + _ = os.Setenv("AWS_ROLE_ARN", "somerole") return func() { os.Clearenv() // os.RemoveAll(tmpDir) diff --git a/internal/credentialexchange/helper.go b/internal/credentialexchange/helper.go index e25f36d..b5c6f37 100644 --- a/internal/credentialexchange/helper.go +++ b/internal/credentialexchange/helper.go @@ -77,8 +77,12 @@ func storeCredentialsInProfile(creds AWSCredentials, configSection string) error awsConfPath := path.Join(basePath, "credentials") if _, err := os.Stat(basePath); os.IsNotExist(err) { - os.Mkdir(basePath, 0755) - os.WriteFile(awsConfPath, []byte(``), 0755) + if err := os.Mkdir(basePath, 0755); err != nil { + return err + } + if err := os.WriteFile(awsConfPath, []byte(``), 0755); err != nil { + return err + } } if overriddenpath, exists := os.LookupEnv("AWS_SHARED_CREDENTIALS_FILE"); exists { @@ -92,9 +96,7 @@ func storeCredentialsInProfile(creds AWSCredentials, configSection string) error cfg.Section(configSection).Key(awsAccessKeySection).SetValue(creds.AWSAccessKey) cfg.Section(configSection).Key(awsSecretKeyIdSection).SetValue(creds.AWSSecretKey) cfg.Section(configSection).Key(awsSessionTokenSection).SetValue(creds.AWSSessionToken) - cfg.SaveTo(awsConfPath) - - return nil + return cfg.SaveTo(awsConfPath) } func returnStdOutAsJson(creds AWSCredentials) error { @@ -105,7 +107,7 @@ func returnStdOutAsJson(creds AWSCredentials) error { // Errorf("Unexpected AWS credential response") return err } - fmt.Fprint(os.Stdout, string(jsonBytes)) + _, _ = fmt.Fprint(os.Stdout, string(jsonBytes)) return nil } @@ -147,7 +149,7 @@ func WriteIniSection(role string) error { return err } sct.Key("name").SetValue(role) - cfg.SaveTo(ConfigIniFile("")) + return cfg.SaveTo(ConfigIniFile("")) } return nil diff --git a/internal/credentialexchange/helper_test.go b/internal/credentialexchange/helper_test.go index ee89c88..cd38a7d 100644 --- a/internal/credentialexchange/helper_test.go +++ b/internal/credentialexchange/helper_test.go @@ -84,7 +84,7 @@ func Test_HomeDirOverwritten(t *testing.T) { return func() { for _, e := range orignalEnv { pair := strings.SplitN(e, "=", 2) - os.Setenv(pair[0], pair[1]) + _ = os.Setenv(pair[0], pair[1]) } } }, @@ -150,7 +150,7 @@ func Test_SetCredentials_with(t *testing.T) { os.Setenv("HOME", tempDir) return func() { os.Clearenv() - os.RemoveAll(tempDir) + _ = os.RemoveAll(tempDir) } }, cred: func() *credentialexchange.AWSCredentials { @@ -169,7 +169,7 @@ func Test_SetCredentials_with(t *testing.T) { os.Setenv("HOME", tempDir) return func() { os.Clearenv() - os.RemoveAll(tempDir) + _ = os.RemoveAll(tempDir) } }, cred: func() *credentialexchange.AWSCredentials { @@ -185,12 +185,12 @@ func Test_SetCredentials_with(t *testing.T) { "write using AWS_CREDENTIALS_FILE": { setup: func() func() { tempDir, _ := os.MkdirTemp(os.TempDir(), "set-creds-tester") - os.Setenv("HOME", tempDir) - os.WriteFile(path.Join(tempDir, "creds"), []byte(``), 0777) - os.Setenv("AWS_SHARED_CREDENTIALS_FILE", path.Join(tempDir, "creds")) + _ = os.Setenv("HOME", tempDir) + _ = os.WriteFile(path.Join(tempDir, "creds"), []byte(``), 0777) + _ = os.Setenv("AWS_SHARED_CREDENTIALS_FILE", path.Join(tempDir, "creds")) return func() { os.Clearenv() - os.RemoveAll(tempDir) + _ = os.RemoveAll(tempDir) } }, cred: func() *credentialexchange.AWSCredentials { diff --git a/internal/credentialexchange/secret.go b/internal/credentialexchange/secret.go index 5bfbed5..23f7b29 100644 --- a/internal/credentialexchange/secret.go +++ b/internal/credentialexchange/secret.go @@ -190,7 +190,7 @@ func (s *SecretStore) ClearAll() error { } for _, v := range cfg.Section(INI_CONF_SECTION).ChildSections() { - srvSections = append(srvSections, strings.Replace(v.Name(), fmt.Sprintf("%s.", INI_CONF_SECTION), "", -1)) + srvSections = append(srvSections, strings.ReplaceAll(v.Name(), fmt.Sprintf("%s.", INI_CONF_SECTION), "")) } for _, v := range srvSections { diff --git a/internal/credentialexchange/secret_test.go b/internal/credentialexchange/secret_test.go index 5a92162..92759ef 100644 --- a/internal/credentialexchange/secret_test.go +++ b/internal/credentialexchange/secret_test.go @@ -134,7 +134,7 @@ func Test_SecretStore_AWSCredential_(t *testing.T) { 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(` + _ = os.WriteFile(path.Join(tmpDir, fmt.Sprintf(".%s.ini", credentialexchange.SELF_NAME)), []byte(` [role] [role.roleArn] name = "arn:aws:iam::111122342343:role/DevAdmin" @@ -212,7 +212,7 @@ func Test_SaveAwsCredential_with(t *testing.T) { 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(` + _ = os.WriteFile(iniFile, []byte(` [role] [role.someotherRole] name = "arn:aws:iam::111122342343:role/DevAdmin" @@ -294,7 +294,7 @@ func Test_ClearAll_with(t *testing.T) { 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(` + _ = os.WriteFile(iniFile, []byte(` [role] [role.someotherRole] name = "arn:aws:iam::111122342343:role/DevAdmin" diff --git a/internal/web/web.go b/internal/web/web.go index dec5a97..46da832 100755 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -13,21 +13,24 @@ import ( "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" + "github.com/go-rod/rod/lib/utils" ) var ( - ErrTimedOut = errors.New("timed out waiting for input") + ErrTimedOut = errors.New("timed out waiting for input or user closed aws-cli-auth browser instance") ) -// WebConb +// WebConfig type WebConfig struct { - datadir string + // CustomChromeExecutable can point to a chromium like browser executable + // e.g. chrome, chromium, brave, edge, (any other chromium based browser) + CustomChromeExecutable string + datadir string // timeout value in seconds - timeout int32 - headless bool - leakless bool - noSandbox bool - preferChromium bool + timeout int32 + headless bool + leakless bool + noSandbox bool } func NewWebConf(datadir string) *WebConfig { @@ -57,41 +60,42 @@ type Web struct { conf *WebConfig launcher *launcher.Launcher browser *rod.Browser + ctx context.Context } // New returns an initialised instance of Web struct func New(ctx context.Context, conf *WebConfig) *Web { - var l *launcher.Launcher + l := launcher.New() - if path, found := launcher.LookPath(); (found && path != "") && !conf.preferChromium { - l = launcher.New().Bin(path) - } else { - l = launcher.New() + browserExecPath, found := conf.CustomChromeExecutable, false + // try default chrome location if custom location is not specified + if browserExecPath == "" { + if browserExecPath, found = launcher.LookPath(); browserExecPath != "" && found { + l.Bin(browserExecPath) + } } // common set up l.Devtools(false). - Headless(conf.headless). UserDataDir(conf.datadir). + Headless(conf.headless). NoSandbox(conf.noSandbox). Leakless(conf.leakless) url := l.MustLaunch() - browser, cancel := rod.New(). + browser := rod.New(). ControlURL(url). - MustConnect().NoDefaultDevice().WithCancel() - go func() { - <-ctx.Done() - fmt.Println("cancelled") - cancel() - }() + MustConnect().NoDefaultDevice() - return &Web{ + web := &Web{ conf: conf, launcher: l, browser: browser, + ctx: ctx, } + + return web } func (web *Web) WithConfig(conf *WebConfig) *Web { @@ -122,6 +126,11 @@ func (web *Web) GetSamlLogin(conf credentialexchange.CredentialConfig) (string, go router.Run() + go func() { + <-web.ctx.Done() + web.MustClose() + }() + // forever loop wait for either a successfully // extracted SAMLResponse // @@ -146,7 +155,13 @@ func (web *Web) GetSamlLogin(conf credentialexchange.CredentialConfig) (string, // GetSSOCredentials func (web *Web) GetSSOCredentials(conf credentialexchange.CredentialConfig) (string, error) { + go func() { + <-web.ctx.Done() + web.MustClose() + }() + // close browser even on error + // should cover most cases even with leakless: false defer web.MustClose() web.browser.MustPage(conf.ProviderUrl) @@ -202,24 +217,14 @@ func (web *Web) GetSSOCredentials(conf credentialexchange.CredentialConfig) (str } func (web *Web) MustClose() { - err := web.browser.Close() - if err != nil { - fmt.Fprintf(os.Stderr, "failed to close browser instance: %s", err) + if err := web.browser.Close(); err != nil { + fmt.Fprintf(os.Stderr, "failed to close browser (PID: %v)", web.launcher.PID()) } - // launcher.Kill performs the PID lookup and kills it - web.launcher.Kill() -} - -func (web *Web) ForceKill(datadir string) error { - errs := []error{} - // once processes have been killed - // we can remove the datadir - if err := os.RemoveAll(datadir); err != nil { - errs = append(errs, err) - } - - if len(errs) > 0 { - return fmt.Errorf("%v", errs[:]) + utils.Sleep(0.5) + // remove process just in case + // os.Process is cross platform safe way to remove a process + osprocess := os.Process{Pid: web.launcher.PID()} + if err := osprocess.Kill(); err != nil { + fmt.Fprintf(os.Stderr, "os/process kill err: %v", err) } - return nil } diff --git a/internal/web/web_test.go b/internal/web/web_test.go index d90861f..9384a03 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -20,7 +20,7 @@ func mockIdpHandler(t *testing.T) http.Handler { w.Header().Set("Server", "Server") w.Header().Set("X-Amzn-Requestid", "9363fdebc232c348b71c8ba5b59f9a34") // w.WriteHeader(http.StatusOK) - w.Write([]byte(` + _, _ = w.Write([]byte(` @@ -31,7 +31,7 @@ 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(` + _, _ = w.Write([]byte(`