From f78f5ddd734b11c2ae1cfc23489fb3d2d4ce4367 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 29 Oct 2019 09:55:06 +0100 Subject: [PATCH] Refactor --- .gitignore | 3 +- .golangci.toml | 46 +++++++-- .goreleaser.yml | 37 +++++--- .travis.yml | 21 +++-- Makefile | 48 ++++------ cmd/aws-mfa/aws-mfa.go | 9 -- cmd/root.go | 159 ------------------------------- cmd/version.go | 38 -------- docs/aws-mfa.md | 28 ++++++ docs/aws-mfa_version.md | 23 +++++ go.mod | 2 +- go.sum | 9 +- main.go | 203 ++++++++++++++++++++++++++++++++++++++++ 13 files changed, 351 insertions(+), 275 deletions(-) delete mode 100644 cmd/aws-mfa/aws-mfa.go delete mode 100644 cmd/root.go delete mode 100644 cmd/version.go create mode 100644 docs/aws-mfa.md create mode 100644 docs/aws-mfa_version.md create mode 100644 main.go diff --git a/.gitignore b/.gitignore index 6403520..2b51acb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ vendor/ .idea/ cover.out temp/ -dist/ \ No newline at end of file +dist/ +aws-mfa diff --git a/.golangci.toml b/.golangci.toml index 9b1668b..2b47bcd 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -1,16 +1,14 @@ [run] - deadline = "5m" + timeout = "5m" + skip-files = [] [linters-settings] [linters-settings.govet] check-shadowing = true - [linters-settings.golint] - min-confidence = 0.0 - [linters-settings.gocyclo] - min-complexity = 15.0 + min-complexity = 13.0 [linters-settings.maligned] suggest-new = true @@ -25,12 +23,44 @@ [linters] enable-all = true disable = [ + "maligned", "lll", "gas", "dupl", "prealloc", - "maligned", - "gochecknoinits", - "gochecknoglobals", "scopelint", + "funlen", + "godox", + "stylecheck", + "wsl", ] + +[issues] + exclude-use-default = false + max-per-linter = 0 + max-same-issues = 0 + exclude = [] + [[issues.exclude-rules]] + path = "annotations.go" + text = "`(compatibilityMapping)` is a global variable" + + [[issues.exclude-rules]] + path = ".*_test.go" + text = "`(updateExpected)` is a global variable" + + [[issues.exclude-rules]] + path = "main.go" + text = "`(Version|ShortCommit|Date)` is a global variable" + + [[issues.exclude-rules]] + path = "main.go" + text = "exported var `(Version|ShortCommit|Date)` should have comment or be unexported" + + [[issues.exclude-rules]] + path = "ingress/annotations.go" + text = "`getBoolValue` - `defaultValue` always receives `false`" + + [[issues.exclude-rules]] + path = "static/v1.go" + text = "exported type `(.+)` should have comment or be unexported" + diff --git a/.goreleaser.yml b/.goreleaser.yml index 81625eb..c9ce95a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -2,12 +2,12 @@ project_name: aws-mfa builds: - binary: aws-mfa - ldflags: - - -s -w -X github.com/mmatur/aws-mfa/cmd/version.version={{.Version}} -X github.com/mmatur/aws-mfa/cmd/version.commit={{.Commit}} -X github.com/mmatur/aws-mfa/cmd/version.date={{.Date}} + main: ./main.go + goos: - - windows - - darwin - linux + - darwin + - windows - freebsd - openbsd goarch: @@ -15,14 +15,18 @@ builds: - 386 - arm - arm64 + - ppc64le goarm: - 7 - + - 6 + - 5 ignore: - goos: darwin goarch: 386 - goos: openbsd goarch: arm + - goos: freebsd + goarch: arm changelog: sort: asc @@ -31,17 +35,20 @@ changelog: - '^docs:' - '^doc:' - '^chore:' + - '^chore(deps):' - '^test:' - '^tests:' -archive: - name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm}}v{{ .Arm }}{{ end }}' - format: tar.gz - format_overrides: - - goos: windows - format: zip - files: - - LICENSE +archives: + - id: aws-mfa + name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + format: tar.gz + format_overrides: + - goos: windows + format: zip + files: + - docs/*.md + - LICENSE -#release: -# disable: true \ No newline at end of file +checksum: + name_template: "{{ .ProjectName }}_v{{ .Version }}_checksums.txt" diff --git a/.travis.yml b/.travis.yml index 6a710ae..fa689de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,26 +1,30 @@ language: go go: - - 1.13.x + - "1.x" -sudo: false - -env: - - GO111MODULE=on +cache: + directories: + - $GOPATH/pkg/mod notifications: email: on_success: never on_failure: change +env: + - GO111MODULE=on + before_install: # Install linters and misspell - - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.21.0 + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin ${GOLANGCI_LINT_VERSION} - golangci-lint --version install: - - echo "TRAVIS_GO_VERSION=$TRAVIS_GO_VERSION" - - make dependencies + - go mod tidy + - git diff --exit-code go.mod + - git diff --exit-code go.sum + - go mod download deploy: - provider: script @@ -28,4 +32,3 @@ deploy: script: curl -sL https://git.io/goreleaser | bash on: tags: true - condition: $TRAVIS_GO_VERSION =~ ^1\.x$ diff --git a/Makefile b/Makefile index 40cfd83..683e982 100644 --- a/Makefile +++ b/Makefile @@ -1,44 +1,32 @@ -BINARY_NAME = aws-mfa -DIST_DIR = $(CURDIR)/dist -DIST_DIR_AWS_MFA = $(DIST_DIR)/$(BINARY_NAME) - -TAG_NAME := $(shell git tag -l --contains HEAD) -SHA := $(shell git rev-parse --short HEAD) -VERSION := $(if $(TAG_NAME),$(TAG_NAME),$(SHA)) -BUILD_DATE := $(shell date -u '+%Y-%m-%d_%I:%M:%S%p') +.PHONY: check clean test build package package-snapshot docs export GO111MODULE=on -GOFILES := $(shell git ls-files '*.go' | grep -v '^vendor/') - -default: clean check test build +TAG_NAME := $(shell git tag -l --contains HEAD) +SHA := $(shell git rev-parse HEAD) +VERSION := $(if $(TAG_NAME),$(TAG_NAME),$(SHA)) +DATE := $(shell date +'%Y-%m-%d %H:%M:%S') -$(DIST_DIR): - mkdir -p $(DIST_DIR) +default: check test build -dependencies: - go mod download +test: + go test -v -cover ./... clean: - rm -rf dist/ cover.out - -test: clean - go test -v -cover ./... + rm -rf dist/ -build: - CGO_ENABLED=0 go build -o ${DIST_DIR_AWS_MFA} -ldflags="-s -w \ - -X github.com/mmatur/$(BINARY_NAME)/cmd/version.version=$(VERSION) \ - -X github.com/mmatur/$(BINARY_NAME)/cmd/version.commit=$(SHA) \ - -X github.com/mmatur/$(BINARY_NAME)/cmd/version.date=$(BUILD_DATE)" \ - $(CURDIR)/cmd/$(BINARY_NAME)/*.go +build: clean + @echo Version: $(VERSION) + go build -v -ldflags '-X "main.Version=${VERSION}" -X "main.ShortCommit=${SHA}" -X "main.Date=${DATE}"' . check: golangci-lint run -fmt: - @gofmt -s -l -w $(GOFILES) +doc: + go run . doc -imports: - @goimports -w $(GOFILES) +package: + goreleaser --skip-publish --skip-validate --rm-dist -.PHONY: clean check test build dependencies fmt imports \ No newline at end of file +package-snapshot: + goreleaser --skip-publish --skip-validate --rm-dist --snapshot diff --git a/cmd/aws-mfa/aws-mfa.go b/cmd/aws-mfa/aws-mfa.go deleted file mode 100644 index b7830fb..0000000 --- a/cmd/aws-mfa/aws-mfa.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/mmatur/aws-mfa/cmd" -) - -func main() { - cmd.Execute() -} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index fd2e3d5..0000000 --- a/cmd/root.go +++ /dev/null @@ -1,159 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "os/user" - "path" - "time" - - "github.com/aws/aws-sdk-go-v2/aws/external" - "github.com/mmatur/aws-mfa/internal" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - ini "gopkg.in/ini.v1" -) - -const ( - // CredentialsFile is the default path for the AWS credentials. - credentialsFile = "/.aws/credentials" - longTermSuffix = "-long-term" -) - -var ( - credentialFile string - duration int64 - profile string - force bool - debug bool -) - -func init() { - usr, err := user.Current() - if err != nil { - log.Fatal(err) - } - - rootCmd.Flags().StringVar(&credentialFile, "credential-file", path.Join(usr.HomeDir, credentialsFile), "Credential file.") - rootCmd.Flags().Int64Var(&duration, "duration", 43200, "Duration in seconds for credentials to remain valid.") - rootCmd.Flags().StringVar(&profile, "profile", "default", "AWS profile to use.") - rootCmd.Flags().BoolVar(&force, "force", false, "Force credentials renew.") - rootCmd.Flags().BoolVar(&debug, "debug", false, "Enable debug mode.") -} - -// rootCmd represents the base command when called without any subcommands. -var rootCmd = &cobra.Command{ - Use: "aws-mfa", - Short: "AWS - MFA", - Long: "AWS - MFA", - Version: version, - Run: func(cmd *cobra.Command, args []string) { - if err := rootRun(); err != nil { - log.Fatal(err) - } - }, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - if err := rootCmd.Execute(); err != nil { - log.Println(err) - os.Exit(1) - } -} - -func rootRun() error { - cfg, err := ini.Load(credentialFile) - if err != nil { - return err - } - - if err = validate(cfg); err != nil { - return err - } - - if credentialStillValid(cfg) { - return nil - } - - err = os.Setenv(external.AWSProfileEnvVar, profile+longTermSuffix) - if err != nil { - return err - } - - awsConfig, err := external.LoadDefaultAWSConfig(external.DefaultSharedCredentialsFilename()) - if err != nil { - return err - } - - var devices []string - - if cfg.Section(profile + longTermSuffix).HasKey("aws_mfa_device") { - device := cfg.Section(profile + longTermSuffix).Key("aws_mfa_device").String() - - if debug { - fmt.Printf("MFA device %q found in %q for profile %q\n", device, credentialFile, profile+longTermSuffix) - } - - devices = append(devices, device) - } else { - devices, err = internal.ListMFADevices(awsConfig) - if err != nil { - return err - } - } - - answer, err := internal.PromptSurvey(devices) - if err != nil { - return err - } - - p, err := internal.GetSessionToken(awsConfig, duration, answer.Device, answer.Code) - if err != nil { - return err - } - - if err = updateAWSCredentials(cfg, p); err != nil { - return err - } - - fmt.Printf("Success! Credentials for profile %q valid until %s \n", profile, p.Expiration) - - return nil -} - -func credentialStillValid(cfg *ini.File) bool { - if cfg.Section(profile).HasKey("expiration") && !force { - expirationUnparsed := cfg.Section(profile).Key("expiration").String() - - expiration, err := time.Parse("2006-01-02 15:04:05", expirationUnparsed) - if err != nil { - log.Fatalf("Unable to parse %s", expirationUnparsed) - } - - secondsRemaining := expiration.Unix() - time.Now().Unix() - if secondsRemaining > 0 { - fmt.Printf("Credentials for profile %q still valid for %d seconds until %s\n", profile, secondsRemaining, expirationUnparsed) - return true - } - } - - return false -} - -func updateAWSCredentials(cfg *ini.File, p *internal.Profile) error { - if err := cfg.Section(profile).ReflectFrom(p); err != nil { - return err - } - - return cfg.SaveTo(credentialFile) -} - -func validate(cfg *ini.File) error { - if _, err := cfg.GetSection(profile + longTermSuffix); err != nil { - return fmt.Errorf("profile %s does not have long-term suffix", profile) - } - - return nil -} diff --git a/cmd/version.go b/cmd/version.go deleted file mode 100644 index e7f87f7..0000000 --- a/cmd/version.go +++ /dev/null @@ -1,38 +0,0 @@ -package cmd - -import ( - "fmt" - "runtime" - - "github.com/spf13/cobra" -) - -var ( - version = "dev" - commit = "I don't remember exactly" - date = "I don't remember exactly" -) - -// versionCmd represents the version command. -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Display version", - Run: func(cmd *cobra.Command, args []string) { - displayVersion(rootCmd.Name()) - }, -} - -func init() { - rootCmd.AddCommand(versionCmd) -} - -func displayVersion(name string) { - fmt.Printf(name+`: - version : %s - commit : %s - build date : %s - go version : %s - go compiler : %s - platform : %s/%s -`, version, commit, date, runtime.Version(), runtime.Compiler, runtime.GOOS, runtime.GOARCH) -} diff --git a/docs/aws-mfa.md b/docs/aws-mfa.md new file mode 100644 index 0000000..d212819 --- /dev/null +++ b/docs/aws-mfa.md @@ -0,0 +1,28 @@ +## aws-mfa + +AWS - MFA + +### Synopsis + +AWS - MFA + +``` +aws-mfa [flags] +``` + +### Options + +``` + --credential-file string Credential file. (default "/home/michael/.aws/credentials") + --debug Enable debug mode. + --duration int Duration in seconds for credentials to remain valid. (default 43200) + --force Force credentials renew. + -h, --help help for aws-mfa + --profile string AWS profile to use. (default "default") +``` + +### SEE ALSO + +* [aws-mfa version](aws-mfa_version.md) - Display version + +###### Auto generated by spf13/cobra on 29-Oct-2019 diff --git a/docs/aws-mfa_version.md b/docs/aws-mfa_version.md new file mode 100644 index 0000000..3e62c2a --- /dev/null +++ b/docs/aws-mfa_version.md @@ -0,0 +1,23 @@ +## aws-mfa version + +Display version + +### Synopsis + +Display version + +``` +aws-mfa version [flags] +``` + +### Options + +``` + -h, --help help for version +``` + +### SEE ALSO + +* [aws-mfa](aws-mfa.md) - AWS - MFA + +###### Auto generated by spf13/cobra on 29-Oct-2019 diff --git a/go.mod b/go.mod index 15f52b1..4912137 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.13 require ( github.com/aws/aws-sdk-go-v2 v0.15.0 - github.com/sirupsen/logrus v1.4.2 github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect github.com/spf13/cobra v0.0.5 + golang.org/x/sys v0.0.0-20190422165155-953cdadca894 // indirect gopkg.in/AlecAivazis/survey.v1 v1.8.5 gopkg.in/ini.v1 v1.49.0 ) diff --git a/go.sum b/go.sum index 9704117..95d1e34 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,7 @@ github.com/aws/aws-sdk-go-v2 v0.15.0/go.mod h1:pFLIN9LDjOEwHfruGweAXEq0XaD6uRkY8 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -28,8 +29,6 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -44,9 +43,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 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 v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= @@ -59,7 +57,6 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6 github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -86,7 +83,9 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3 google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/AlecAivazis/survey.v1 v1.8.5 h1:QoEEmn/d5BbuPIL2qvXwzJdttFFhRQFkaq+tEKb7SMI= gopkg.in/AlecAivazis/survey.v1 v1.8.5/go.mod h1:iBNOmqKz/NUbZx3bA+4hAGLRC7fSK7tgtVDT4tB22XA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.49.0 h1:MW0aLMiezbm/Ray0gJJ+nQFE2uOC9EpK2p5zPN3NqpM= gopkg.in/ini.v1 v1.49.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ae0ed1b --- /dev/null +++ b/main.go @@ -0,0 +1,203 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/user" + "path" + "runtime" + "time" + + "github.com/aws/aws-sdk-go-v2/aws/external" + "github.com/mmatur/aws-mfa/internal" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + "gopkg.in/ini.v1" +) + +var Version = "dev" +var ShortCommit = "" +var Date = "" + +type rootConfig struct { + credentialFile string + duration int64 + profile string + force bool + debug bool +} + +const ( + // CredentialsFile is the default path for the AWS credentials. + credentialsFile = "/.aws/credentials" + longTermSuffix = "-long-term" +) + +func main() { + log.SetFlags(log.Lshortfile) + + var rootCfg rootConfig + var cfg *ini.File + + rootCmd := &cobra.Command{ + Use: "aws-mfa", + Short: "AWS - MFA", + Long: "AWS - MFA", + Version: Version, + PreRunE: func(_ *cobra.Command, _ []string) error { + fmt.Printf("AWS-MFA: %s - %s - %s\n", Version, Date, ShortCommit) + + var err error + cfg, err = ini.Load(rootCfg.credentialFile) + if err != nil { + return err + } + + if err = validate(rootCfg, cfg); err != nil { + return err + } + + if credentialStillValid(rootCfg, cfg) { + return nil + } + + err = os.Setenv(external.AWSProfileEnvVar, rootCfg.profile+longTermSuffix) + if err != nil { + return err + } + + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + if err := rootRun(rootCfg, cfg); err != nil { + log.Fatal(err) + } + }, + } + + usr, err := user.Current() + if err != nil { + log.Fatal(err) + } + + rootCmd.Flags().StringVar(&rootCfg.credentialFile, "credential-file", path.Join(usr.HomeDir, credentialsFile), "Credential file.") + rootCmd.Flags().Int64Var(&rootCfg.duration, "duration", 43200, "Duration in seconds for credentials to remain valid.") + rootCmd.Flags().StringVar(&rootCfg.profile, "profile", "default", "AWS profile to use.") + rootCmd.Flags().BoolVar(&rootCfg.force, "force", false, "Force credentials renew.") + rootCmd.Flags().BoolVar(&rootCfg.debug, "debug", false, "Enable debug mode.") + + docCmd := &cobra.Command{ + Use: "doc", + Short: "Generate documentation", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + return doc.GenMarkdownTree(rootCmd, "./docs") + }, + } + + rootCmd.AddCommand(docCmd) + + versionCmd := &cobra.Command{ + Use: "version", + Short: "Display version", + Run: func(_ *cobra.Command, _ []string) { + displayVersion(rootCmd.Name()) + }, + } + + rootCmd.AddCommand(versionCmd) + + if err := rootCmd.Execute(); err != nil { + log.Println(err) + os.Exit(1) + } +} + +func displayVersion(name string) { + fmt.Printf(name+`: + version : %s + commit : %s + build date : %s + go version : %s + go compiler : %s + platform : %s/%s +`, Version, ShortCommit, Date, runtime.Version(), runtime.Compiler, runtime.GOOS, runtime.GOARCH) +} + +func rootRun(rootCfg rootConfig, cfg *ini.File) error { + awsConfig, err := external.LoadDefaultAWSConfig(external.DefaultSharedCredentialsFilename()) + if err != nil { + return err + } + + var devices []string + + if cfg.Section(rootCfg.profile + longTermSuffix).HasKey("aws_mfa_device") { + device := cfg.Section(rootCfg.profile + longTermSuffix).Key("aws_mfa_device").String() + + if rootCfg.debug { + fmt.Printf("MFA device %q found in %q for profile %q\n", device, rootCfg.credentialFile, rootCfg.profile+longTermSuffix) + } + + devices = append(devices, device) + } else { + devices, err = internal.ListMFADevices(awsConfig) + if err != nil { + return err + } + } + + answer, err := internal.PromptSurvey(devices) + if err != nil { + return err + } + + p, err := internal.GetSessionToken(awsConfig, rootCfg.duration, answer.Device, answer.Code) + if err != nil { + return err + } + + if err = updateAWSCredentials(rootCfg, cfg, p); err != nil { + return err + } + + fmt.Printf("Success! Credentials for profile %q valid until %s \n", rootCfg.profile, p.Expiration) + + return nil +} + +func credentialStillValid(rootCfg rootConfig, cfg *ini.File) bool { + if cfg.Section(rootCfg.profile).HasKey("expiration") && !rootCfg.force { + expirationUnparsed := cfg.Section(rootCfg.profile).Key("expiration").String() + + expiration, err := time.Parse("2006-01-02 15:04:05", expirationUnparsed) + if err != nil { + log.Fatalf("Unable to parse %s", expirationUnparsed) + } + + secondsRemaining := expiration.Unix() - time.Now().Unix() + if secondsRemaining > 0 { + fmt.Printf("Credentials for profile %q still valid for %d seconds until %s\n", rootCfg.profile, secondsRemaining, expirationUnparsed) + return true + } + } + + return false +} + +func updateAWSCredentials(rootCfg rootConfig, cfg *ini.File, p *internal.Profile) error { + if err := cfg.Section(rootCfg.profile).ReflectFrom(p); err != nil { + return err + } + + return cfg.SaveTo(rootCfg.credentialFile) +} + +func validate(rootCfg rootConfig, cfg *ini.File) error { + if _, err := cfg.GetSection(rootCfg.profile + longTermSuffix); err != nil { + return fmt.Errorf("profile %s does not have long-term suffix", rootCfg.profile) + } + + return nil +}