diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..8d19e4a --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,24 @@ +name: goreleaser + +on: + push: + branches: + - "!**/*" + tags: + - "v*" + +jobs: + goreleaser: + runs-on: ubuntu-latest + env: + GO111MODULE: on + GITHUB_TOKEN: ${{ secrets.TOKEN_FOR_GITHUB }} + steps: + - uses: actions/checkout@master + - uses: actions/setup-go@master + with: + go-version: 1.13 + - uses: goreleaser/goreleaser-action@master + with: + version: latest + args: release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f15376 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.envrc +awscredswrap/awscredswrap diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..0b97798 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,53 @@ +before: + hooks: + - go mod download + +builds: + - main: ./awscredswrap/main.go + env: + - CGO_ENABLED=0 + ldflags: + -X github.com/youyo/awscredswrap/awscredswrap/cmd.Version={{ .Version }} + goos: + - darwin + - linux + goarch: + - amd64 + +archives: + - id: github release + replacements: + darwin: Darwin + linux: Linux + amd64: x86_64 + files: + - LICENSE + - README.md + - _awscredswrap +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +brews: + - + github: + owner: youyo + name: homebrew-tap + folder: Formula + commit_author: + name: goreleaserbot + email: goreleaser@carlosbecker.com + description: "awscredswrap uses temporary credentials for the specified iam role to set a shell environment variable or execute a command." + homepage: "https://github.com/youyo/awscredswrap" + install: | + bin.install "awscredswrap" + zsh_completion.install '_awscredswrap' + test: | + system "#{bin}/awscredswrap --version" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29be1e3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2019 youyo <1003ni2@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0875497 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# awscredswrap + +[![Go Report Card](https://goreportcard.com/badge/github.com/youyo/awscredswrap)](https://goreportcard.com/report/github.com/youyo/awscredswrap) +[![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](./LICENSE) + +AWS assume role credential wrapper. + +## Description + +awscredswrap uses temporary credentials for the specified iam role to set a shell environment variable or execute a command. + +## Install + +- Brew + +``` +$ brew install youyo/tap/awscredswrap +``` + +Other platforms are download from [github release page](https://github.com/youyo/awscredswrap/releases). + +## Usage + +```bash +$ awscredswrap --help +awscredswrap uses temporary credentials for the specified iam role to set a shell environment variable or execute a command. + +Usage: + awscredswrap [flags] + +Flags: + -d, --duration-seconds int The duration, in seconds, of the role session. (default 3600) + -h, --help help for awscredswrap + -m, --mfa-serial string The identification number of the MFA device that is associated with the user who is making the AssumeRole call. + -r, --role-arn string The arn of the role to assume. + -n, --role-session-name string An identifier for the assumed role session. + --version version for awscredswrap +``` + +### As command wrapper + +```console +$ awscredswrap --role-arn arn:aws:iam::00000000:role/foo -- some_command [arg1 arg2...] +``` + +### As env exporter + +When awscredswrap is executed with no arguments, awscredswrap outputs shell script to export AWS credentials environment variables. + +```console +$ awscredswrap --role-arn arn:aws:iam::00000000:role/foo +export AWS_ACCESS_KEY_ID='XXXXXXXXXXXXXXXX' +export AWS_SECRET_ACCESS_KEY='zWarBXUtMKJYnC8y4dNAf9e5HQqFTp....' +export AWS_SESSION_TOKEN='Wj3YGuSMwn8aJx4AN6TFsbtB5URKHEpVgdDkPvy7....' +export AWS_DEFAULT_REGION='us-east-1' +``` + +You can set the credentials in current shell by `eval`. + +```console +$ eval $(awscredswrap --role-arn arn:aws:iam::00000000:role/foo) +``` + +Temporary credentials has expiration time (about 1 hour). + +## License + +[MIT](LICENSE) + +## Author + +[youyo](https://github.com/youyo) diff --git a/_awscredswrap b/_awscredswrap new file mode 100644 index 0000000..bfad3ac --- /dev/null +++ b/_awscredswrap @@ -0,0 +1,11 @@ +#compdef awscredswrap + +_awscredswrap() { + _arguments -w \ + '(- *)'{-h,--help}'[show help]' \ + '(-d --duration-seconds)'{-d,--duration-seconds}'[The duration, in seconds, of the role session. (default 3600)]' \ + '(-m --mfa-serial)'{-m,--mfa-serial}'[The identification number of the MFA device that is associated with the user who is making the AssumeRole call.]' \ + '(-r --role-arn)'{-r,--role-arn}'[The arn of the role to assume.]' \ + '(-n --role-session-name)'{-n,--role-session-name}'[An identifier for the assumed role session.]' \ + '--version[version for awscredswrap]' +} diff --git a/awscredswrap.go b/awscredswrap.go new file mode 100644 index 0000000..d5fd7c8 --- /dev/null +++ b/awscredswrap.go @@ -0,0 +1,121 @@ +package awscredswrap + +import ( + "os" + "os/exec" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/session" +) + +type AwsCredsWrap struct { + Session *session.Session + Credentials credentials.Value + Region string +} + +// New +func New() *AwsCredsWrap { + sess := newSession() + awsCredsWrap := &AwsCredsWrap{ + Session: sess, + Region: *sess.Config.Region, + } + + return awsCredsWrap +} + +func (a *AwsCredsWrap) GetCredentials(roleArn, roleSessionName, mfaSerial string, durationSeconds time.Duration) (err error) { + creds := stscreds.NewCredentials(a.Session, roleArn, assumeRoleProvider(roleSessionName, mfaSerial, durationSeconds)) + + a.Credentials, err = creds.Get() + if err != nil { + return err + } + + return nil +} + +func (a *AwsCredsWrap) ExportEnvironments() []string { + s := []string{ + "export AWS_ACCESS_KEY_ID='" + a.Credentials.AccessKeyID + "'", + "export AWS_SECRET_ACCESS_KEY='" + a.Credentials.SecretAccessKey + "'", + "export AWS_SESSION_TOKEN='" + a.Credentials.SessionToken + "'", + "export AWS_DEFAULT_REGION='" + a.Region + "'", + } + + return s +} + +func (a *AwsCredsWrap) ExecuteCommand(com string, args ...string) (err error) { + a.setEnvironments() + + err = execCommand(com, args...) + + return err +} + +func newSession() (sess *session.Session) { + sess = session.Must( + session.NewSessionWithOptions( + session.Options{ + SharedConfigState: session.SharedConfigEnable, + AssumeRoleTokenProvider: stscreds.StdinTokenProvider, + }, + ), + ) + + return sess +} + +func assumeRoleProvider(roleSessionName, mfaSerial string, durationSeconds time.Duration) (f func(p *stscreds.AssumeRoleProvider)) { + f = func() (f func(p *stscreds.AssumeRoleProvider)) { + if mfaSerial != "" { + return func(p *stscreds.AssumeRoleProvider) { + p.RoleSessionName = roleSessionName + p.Duration = durationSeconds + p.SerialNumber = aws.String(mfaSerial) + p.TokenProvider = stscreds.StdinTokenProvider + } + } else { + return func(p *stscreds.AssumeRoleProvider) { + p.RoleSessionName = roleSessionName + p.Duration = durationSeconds + } + } + }() + + return f +} + +func (a *AwsCredsWrap) setEnvironments() { + os.Setenv("AWS_ACCESS_KEY_ID", a.Credentials.AccessKeyID) + os.Setenv("AWS_SECRET_ACCESS_KEY", a.Credentials.SecretAccessKey) + os.Setenv("AWS_SESSION_TOKEN", a.Credentials.SessionToken) + os.Setenv("AWS_DEFAULT_REGION", a.Region) +} + +func execCommand(com string, args ...string) (err error) { + var command *exec.Cmd + + if args != nil { + command = exec.Command(com, args...) + } else { + command = exec.Command(com) + } + + command.Stdout = os.Stdout + command.Stderr = os.Stderr + command.Stdin = os.Stdin + + if err = command.Start(); err != nil { + return err + } + + command.Wait() + + return nil +} diff --git a/awscredswrap/cmd/root.go b/awscredswrap/cmd/root.go new file mode 100644 index 0000000..d217991 --- /dev/null +++ b/awscredswrap/cmd/root.go @@ -0,0 +1,41 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/youyo/awscredswrap" +) + +var Version string + +var rootCmd = &cobra.Command{ + Use: "awscredswrap", + Short: "awscredswrap uses temporary credentials for the specified iam role to set a shell environment variable or execute a command.", + Version: Version, + PreRun: awscredswrap.PreRun, + RunE: awscredswrap.Run, + SilenceUsage: true, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + rootCmd.Flags().StringP("role-arn", "r", "", "The arn of the role to assume.") + rootCmd.Flags().StringP("role-session-name", "n", "", "An identifier for the assumed role session.") + rootCmd.Flags().IntP("duration-seconds", "d", 3600, "The duration, in seconds, of the role session.") + rootCmd.Flags().StringP("mfa-serial", "m", "", "The identification number of the MFA device that is associated with the user who is making the AssumeRole call.") + + viper.BindPFlags(rootCmd.Flags()) +} + +func initConfig() {} diff --git a/awscredswrap/main.go b/awscredswrap/main.go new file mode 100644 index 0000000..79eeb06 --- /dev/null +++ b/awscredswrap/main.go @@ -0,0 +1,28 @@ +/* +Copyright © 2019 youyo <1003ni2@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package main + +import "github.com/youyo/awscredswrap/awscredswrap/cmd" + +func main() { + cmd.Execute() +} diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..4357c4e --- /dev/null +++ b/cli.go @@ -0,0 +1,53 @@ +package awscredswrap + +import ( + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Run +func Run(cmd *cobra.Command, args []string) (err error) { + roleArn := viper.GetString("role-arn") + roleSessionName := viper.GetString("role-session-name") + durationSeconds := time.Duration(viper.GetInt("duration-seconds")) * time.Second + mfaSerial := viper.GetString("mfa-serial") + + awsCredsWrap := New() + if err := awsCredsWrap.GetCredentials(roleArn, roleSessionName, mfaSerial, durationSeconds); err != nil { + return errors.Wrap(err, "can not get credentials") + } + + switch len(args) { + case 0: + envs := awsCredsWrap.ExportEnvironments() + for _, v := range envs { + fmt.Println(v) + } + + case 1: + if err = awsCredsWrap.ExecuteCommand(args[0], nil...); err != nil { + return errors.Wrap(err, "failed to execute command") + } + + default: + if err = awsCredsWrap.ExecuteCommand(args[0], args[1:]...); err != nil { + return errors.Wrap(err, "failed to execute command") + } + + } + + return nil +} + +//PreRun +func PreRun(cmd *cobra.Command, args []string) { + roleSessionName := viper.GetString("role-session-name") + if roleSessionName == "" { + roleSessionName = "awscredswrap-session-" + time.Now().Format("20060102150405") + viper.Set("role-session-name", roleSessionName) + } +}