diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index dd84ea7..f24e313 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,38 +1,11 @@ --- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - +name: PR --- -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. +**Describe the PR** **Screenshots** If applicable, add screenshots to help explain your problem. -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - **Additional context** Add any other context about the problem here. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f8b65e..aaff147 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,9 +6,15 @@ on: push: branches: [ master, main ] +permissions: + contents: write + statuses: write + checks: write + pull-requests: write + jobs: set-version: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 container: image: mcr.microsoft.com/dotnet/sdk:6.0 outputs: @@ -27,47 +33,50 @@ jobs: - name: Set SemVer Version uses: gittools/actions/gitversion/execute@v1 id: gitversion + pr: - runs-on: ubuntu-latest - container: - image: golang:1.24-bookworm + runs-on: ubuntu-22.04 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: | + echo 'eirctl run vuln:check' + + - name: Unit Tests run: | - 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 + eirctl run unit:test:run + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: success() || failure() 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 + report_paths: '.coverage/report-junit.xml' + commit: ${{ github.sha }} + fail_on_failure: true + check_name: aws-cli-auth Unit Tests + + - 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) + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 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..226f37d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,12 +39,9 @@ jobs: run: | echo "REVISION -> $GITHUB_SHA" echo "VERSION -> $GITVERSION_SEMVER" + release: runs-on: ubuntu-latest - container: - image: golang:1.24-bookworm - env: - FOO: Bar needs: set-version env: SEMVER: ${{ needs.set-version.outputs.semVer }} @@ -52,18 +49,25 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 1 - - name: install deps + + - uses: ensono/actions/eirctl-setup@v0.3.1 + with: + version: latest + isPrerelease: false + + - name: git-deps run: | - apt-get update && apt-get install jq git -y git config --global --add safe.directory "$GITHUB_WORKSPACE" git config user.email ${{ github.actor }}-ci@gha.org git config user.name ${{ github.actor }} - name: release library run: | - make GIT_TAG=${SEMVER} REVISION=$GITHUB_SHA tag - - name: release binary + VERSION=${SEMVER} REVISION=$GITHUB_SHA eirctl run tag + + - name: Build binary run: | - make REVISION=$GITHUB_SHA GIT_TAG=${SEMVER} PAT=${{ secrets.GITHUB_TOKEN }} cross-build + eirctl run pipeline bin:release --set Version=${SEMVER} --set Revision=$GITHUB_SHA + - name: Release uses: softprops/action-gh-release@v2.2.1 with: @@ -72,4 +76,4 @@ jobs: generate_release_notes: true token: ${{ secrets.GITHUB_TOKEN }} files: ./dist/* - prerelease: false + prerelease: true 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/README.md b/README.md index a14bf36..879c06f 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ This tool deals with IdP logins via SAML, both into an AWS account directly or v If, however, you need to support a non standard user journeys enforced by your IdP i.e. a sub company selection within your organization login portal, or a selection screen for different MFA providers - PingID or RSA HardToken etc.... you cannot reliably automate the flow or it would have to be too specific. -As such this approach uses [go-rod](https://github.com/go-rod/rod) library to uniformly allow the user to complete any and all auth steps and selections in a managed browser session up to the point of where the SAMLResponse is to be sent to AWS ACS service `https://signin.aws.amazon.com/saml`. +As such this approach uses [go-rod](https://github.com/go-rod/rod) library to uniformly allow the user to complete any and all auth steps and selections in a managed browser session up to the point of where the SAMLResponse is to be sent to AWS ACS service `https://signin.aws.amazon.com/saml`. Capturing this via hijack request and posting to AWS STS service to exchange this for the temporary credentials. diff --git a/aws-cli-auth.go b/aws-cli-auth.go index 6801c15..fa6577e 100755 --- a/aws-cli-auth.go +++ b/aws-cli-auth.go @@ -2,10 +2,29 @@ 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() + + 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[31m%s\x1b[0m", err) + } } diff --git a/cmd/awscliauth.go b/cmd/awscliauth.go new file mode 100755 index 0000000..59a890d --- /dev/null +++ b/cmd/awscliauth.go @@ -0,0 +1,93 @@ +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 { + 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..8965c9a 100644 --- a/cmd/clear.go +++ b/cmd/clear.go @@ -6,22 +6,54 @@ import ( "os/user" "github.com/DevLabFoundry/aws-cli-auth/internal/credentialexchange" - "github.com/DevLabFoundry/aws-cli-auth/internal/web" "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, + Long: `Clears any stored credentials in the OS secret store + 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 { + 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 { + fmt.Fprint(os.Stderr, "delete ~/.aws-cli-auth-data/ manually") + } + + if err := secretStore.ClearAll(); err != nil { + fmt.Fprint(os.Stderr, err.Error()) + } + + if err := os.Remove(credentialexchange.ConfigIniFile("")); err != nil { + return err + } + + 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 +64,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..ffe6266 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,140 +25,153 @@ const ( SsoCredsEndpointQuery = "?account_id=%s&role_name=%s&debug=true" ) -var ( +type samlFlags struct { + providerUrl string + principalArn string + acsUrl string + isSso bool + role string + ssoRegion string + ssoRole string + ssoUserEndpoint string + ssoFedCredEndpoint string + customExecutablePath string + samlTimeout int32 + reloadBeforeTime int +} + +type samlCmd struct { + flags *samlFlags ssoRoleAccount, ssoRoleName string -) + cmd *cobra.Command +} -var ( - providerUrl string - principalArn string - acsUrl string - isSso bool - ssoRegion string - ssoRole string - ssoUserEndpoint string - ssoFedCredEndpoint string - datadir string - samlTimeout int32 - reloadBeforeTime int - SamlCmd = &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) + webConfig := web.NewWebConf(r.Datadir).WithTimeout(flags.samlTimeout) + webConfig.CustomChromeExecutable = flags.customExecutablePath + return cmdutils.GetCredsWebUI(ctx, svc, secretStore, conf, webConfig) + }, PreRunE: func(cmd *cobra.Command, args []string) error { - if 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().StringVarP(&flags.customExecutablePath, "executable-path", "", "", `Custom path to an executable + +This needs to be a chromium like executable - e.g. Chrome, Chromium, Brave, Edge. + +You can find out the path by opening your browser and typing in chrome|brave|edge://version +`) + sc.cmd.PersistentFlags().BoolVarP(&flags.isSso, "is-sso", "", false, `Enables the new AWS User portal login. 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..6048b88 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/docs/usage.md b/docs/usage.md index 8780a52..5d67342 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -34,16 +34,40 @@ Usage: aws-cli-auth saml [flags] Flags: - -a, --acsurl string Override the default ACS Url, used for checkin the post of the SAMLResponse (default "https://signin.aws.amazon.com/saml") - -h, --help help for saml - -d, --max-duration int Override default max session duration, in seconds, of the role session [900-43200] (default 900) - --principal string Principal Arn of the SAML IdP in AWS - -p, --provider string Saml Entity StartSSO Url + -a, --acsurl string Override the default ACS Url, used for checkin the post of the SAMLResponse (default "https://signin.aws.amazon.com/saml") + --executable-path string Custom path to an executable + + This needs to be a chromium like executable - e.g. Chrome, Chromium, Brave, Edge. + + You can find out the path by opening your browser and typing in chrome|brave|edge://version + + -h, --help help for saml + --is-sso Enables the new AWS User portal login. + If this flag is specified the --sso-role must also be specified. + --principal string 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 + + -p, --provider string 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 + + --reload-before int Triggers a credentials refresh before the specified max-duration. Value provided in seconds. Should be less than the max-duration of the session + -r, --role string Set the role you want to assume when SAML or OIDC process completes + --saml-timeout int32 Timeout in seconds, before the operation of waiting for a response is cancelled via the chrome driver (default 120) + --sso-fed-endpoint string FederationCredEndpoint in a go style fmt.Sprintf string with a region placeholder (default "https://portal.sso.%s.amazonaws.com/federation/credentials/") + --sso-region string If using SSO, you must set the region (default "eu-west-1") + --sso-role string Sso Role name must be in this format - 12345678910:PowerUser + --sso-user-endpoint string UserEndpoint in a go style fmt.Sprintf string with a region placeholder (default "https://portal.sso.%s.amazonaws.com/user") Global Flags: - --cfg-section string config section name in the yaml config file - -r, --role string Set the role you want to assume when SAML or OIDC process completes - -s, --store-profile By default the credentials are returned to stdout to be used by the credential_process + --cfg-section string Config section name in the default AWS credentials file. To enable priofi + -d, --max-duration int 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 (default 900) + --role-chain strings If specified it will assume the roles from the base credentials, in order they are specified in + -s, --store-profile 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 + -v, --verbose Verbose output ``` Example: diff --git a/eirctl.yaml b/eirctl.yaml new file mode 100644 index 0000000..ccd45ae --- /dev/null +++ b/eirctl.yaml @@ -0,0 +1,138 @@ +import: + - https://raw.githubusercontent.com/Ensono/eirctl/e71dd9d66293e27e70fd0620e63a6d627579c060/shared/build/go/eirctl.yaml + +contexts: + unit:test: + container: + name: ghcr.io/devlabfoundry/aws-cli-auth-ci:0.3.0 + entrypoint: /usr/bin/env + shell: sh + shell_args: + - -c + envfile: + exclude: + - HOME + - GO + +pipelines: + build: + - task: build:unix + - task: build:win + depends_on: build:unix + + unit:test:run: + - task: unit:test:prereqs + - task: unit:test + depends_on: unit:test:prereqs + + bin:release: + - task: clean:dir + - pipeline: 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: | + Unit test runner needs a bit of extra care in this case to ensure we have all the dependencies + command: | + export GOPATH=$PWD/.deps GOBIN=$PWD/.deps/bin + CGO_ENABLED=1 go test $(go list ./... | grep -v /local/) -v -coverpkg=./... -race -mod=readonly -timeout=1m -shuffle=on -buildvcs=false -coverprofile=.coverage/out -count=1 -run=$GO_TEST_RUN_ARGS | tee .coverage/test.out + 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 + context: unit:test + command: + - | + mkdir -p .coverage + 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 + + clean:dir: + command: + - | + rm -rf dist/ + + build:win: + 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} GOARCH=${BUILD_GOARCH} go build -mod=readonly -buildvcs=false -ldflags="$ldflags" -o dist/{{.BinName}}-${BUILD_GOOS}${BUILD_GOARCH} . + variations: + - BUILD_GOOS: windows + BUILD_GOARCH: amd64 + - BUILD_GOOS: windows + BUILD_GOARCH: "386" + variables: + RepoOwner: DevLabFoundry + BinName: aws-cli-auth + + build:unix: + 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} . + variations: + - BUILD_GOOS: darwin + - BUILD_GOOS: linux + 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 3ceaead..2ff3338 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,13 @@ module github.com/DevLabFoundry/aws-cli-auth -go 1.24.1 +go 1.25.1 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 github.com/werf/lockgate v0.1.1 github.com/zalando/go-keyring v0.2.6 @@ -17,27 +16,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..6c2cc4d 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= @@ -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= @@ -52,8 +50,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= @@ -66,8 +65,8 @@ 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 +75,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.go b/internal/cmdutils/cmdutils.go index b251753..c05dbab 100644 --- a/internal/cmdutils/cmdutils.go +++ b/internal/cmdutils/cmdutils.go @@ -53,19 +53,19 @@ 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 } awsCreds := &credentialexchange.AWSCredentials{} - awsCreds.FromRoleCredString(capturedCreds) + _, _ = awsCreds.FromRoleCredString(capturedCreds) return completeCredProcess(ctx, secretStore, svc, awsCreds, conf) } 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/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/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 b6c3e0f..35ac0f9 100755 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -1,6 +1,7 @@ package web import ( + "context" "errors" "fmt" "net/http" @@ -12,20 +13,24 @@ 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" + "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 + timeout int32 + headless bool + leakless bool + noSandbox bool } func NewWebConf(datadir string) *WebConfig { @@ -46,19 +51,35 @@ 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 browser *rod.Browser + ctx context.Context } // New returns an initialised instance of Web struct -func New(conf *WebConfig) *Web { +func New(ctx context.Context, conf *WebConfig) *Web { + 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) + } + } - l := launcher.New(). - Devtools(false). - Headless(conf.headless). + // common set up + l.Devtools(false). UserDataDir(conf.datadir). + Headless(conf.headless). + NoSandbox(conf.noSandbox). Leakless(conf.leakless) url := l.MustLaunch() @@ -67,11 +88,14 @@ func New(conf *WebConfig) *Web { ControlURL(url). MustConnect().NoDefaultDevice() - return &Web{ + web := &Web{ conf: conf, launcher: l, browser: browser, + ctx: ctx, } + + return web } func (web *Web) WithConfig(conf *WebConfig) *Web { @@ -102,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 // @@ -126,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) @@ -182,53 +217,11 @@ 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) - } - // launcher.Kill performs the PID lookup and kills it - web.launcher.Kill() -} - -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 { - errs = append(errs, err) - } - - if len(errs) > 0 { - return fmt.Errorf("%v", errs[:]) - } - 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 + // swallows errors here - until a structured logger + _ = web.browser.Close() + 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()} + _ = osprocess.Kill() } diff --git a/internal/web/web_test.go b/internal/web/web_test.go index e7e1654..9384a03 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" @@ -19,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(` @@ -30,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(`