From 62934349a74ae698e28b9cf9a509e5099a736b36 Mon Sep 17 00:00:00 2001 From: Andrei Burd Date: Tue, 4 Jun 2019 12:50:49 +0300 Subject: [PATCH] Feat: profiles moved from vault commands (#31) * Feat: Cache for generated tokens --- Gopkg.lock | 248 +++++++++++++- README.md | 88 +++-- command/{vault => profile}/edit_profile.go | 51 ++- command/profile/use_profile.go | 356 +++++++++++++++++++++ command/vault/find_token.go | 6 +- command/vault/use_profile.go | 89 ------ main.go | 11 +- 7 files changed, 703 insertions(+), 146 deletions(-) rename command/{vault => profile}/edit_profile.go (61%) create mode 100644 command/profile/use_profile.go delete mode 100644 command/vault/use_profile.go diff --git a/Gopkg.lock b/Gopkg.lock index 481e4e3..3238a89 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,27 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + digest = "1:a69ab3f1445ffd4815add4bd31ba05b65b3b9fec1ade5057d5d717f30e6efd6d" + name = "github.com/SermoDigital/jose" + packages = [ + ".", + "crypto", + "jws", + "jwt", + ] + pruneopts = "UT" + revision = "f6df55f235c24f236d11dbcf665249a59ac2021f" + version = "1.1" + +[[projects]] + digest = "1:c47f4964978e211c6e566596ec6246c329912ea92e9bb99c00798bb4564c5b09" + name = "github.com/armon/go-radix" + packages = ["."] + pruneopts = "UT" + revision = "1a2de0c21c94309923825da3df33a4381872c795" + version = "v1.0.0" + [[projects]] digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" name = "github.com/davecgh/go-spew" @@ -9,6 +30,31 @@ revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" version = "v1.1.1" +[[projects]] + branch = "master" + digest = "1:caf34bc06194bfba6436f1da5140fc2d8b3e9e32e7111b06488d0c73108af458" + name = "github.com/duosecurity/duo_api_golang" + packages = [ + ".", + "authapi", + ] + pruneopts = "UT" + revision = "6c680f768e746ca8563c19035adfd94a4a4101f1" + +[[projects]] + digest = "1:239c4c7fd2159585454003d9be7207167970194216193a8a210b8d29576f19c9" + name = "github.com/golang/protobuf" + packages = [ + "proto", + "ptypes", + "ptypes/any", + "ptypes/duration", + "ptypes/timestamp", + ] + pruneopts = "UT" + revision = "b5d812f8a3706043e23a9cd5babf2e5423744d30" + version = "v1.3.1" + [[projects]] branch = "master" digest = "1:4a0c6bb4805508a6287675fac876be2ac1182539ca8a32468d8128882e9d5009" @@ -17,6 +63,22 @@ pruneopts = "UT" revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a" +[[projects]] + digest = "1:8d12b0e8334cd6770e9b9b8b48c3340b9739b1469c2f448ced070b589adaa475" + name = "github.com/google/go-github" + packages = ["github"] + pruneopts = "UT" + revision = "901030391ab7f418a984389fd3f8f65e39e5c823" + version = "v25.0.4" + +[[projects]] + digest = "1:a63cff6b5d8b95638bfe300385d93b2a6d9d687734b863da8e09dc834510a690" + name = "github.com/google/go-querystring" + packages = ["query"] + pruneopts = "UT" + revision = "44c6ddd0a2342c386950e880b658017258da92fc" + version = "v1.0.0" + [[projects]] digest = "1:bfc483a051d3c7185ebeaa41b5bb67a4f76e742217bcaeab5661cc4b1320f392" name = "github.com/hashicorp/consul" @@ -41,6 +103,22 @@ revision = "e8ab9daed8d1ddd2d3c4efba338fe2eeae2e4f18" version = "v0.5.0" +[[projects]] + digest = "1:cf6b61e1b4c26b0c7526cee4a0cee6d8302b17798af4b2a56a90eedac0aef11a" + name = "github.com/hashicorp/go-hclog" + packages = ["."] + pruneopts = "UT" + revision = "5ccdce08c75b6c7b37af61159f13f6a4f5e2e928" + version = "v0.9.2" + +[[projects]] + digest = "1:6e9806a06d00f4d1f90806d7b5cfb11e35dca76503390ca6e704b05ea7051bff" + name = "github.com/hashicorp/go-immutable-radix" + packages = ["."] + pruneopts = "UT" + revision = "7dd1121b595e4e1bd6dd5caa78e0f5c454740379" + version = "v1.1.0" + [[projects]] digest = "1:f668349b83f7d779567c880550534addeca7ebadfdcf44b0b9c39be61864b4b7" name = "github.com/hashicorp/go-multierror" @@ -49,6 +127,17 @@ revision = "886a7fbe3eb1c874d46f623bfa70af45f425b3d1" version = "v1.0.0" +[[projects]] + digest = "1:5e1aece859ec4195f3d16dd3b64a0f111e186b9e95d75141465595063e3a5254" + name = "github.com/hashicorp/go-plugin" + packages = [ + ".", + "internal/plugin", + ] + pruneopts = "UT" + revision = "52e1c4730856c1438ced7597c9b5c585a7bd06a2" + version = "v1.0.0" + [[projects]] digest = "1:4112546e6964796e1c92a9ffdea8fd7ae81ffbf81eda4f946f50937e178f53da" name = "github.com/hashicorp/go-retryablehttp" @@ -73,6 +162,33 @@ pruneopts = "UT" revision = "6d291a969b86c4b633730bfc6b8b9d64c3aafed9" +[[projects]] + digest = "1:f14364057165381ea296e49f8870a9ffce2b8a95e34d6ae06c759106aaef428c" + name = "github.com/hashicorp/go-uuid" + packages = ["."] + pruneopts = "UT" + revision = "4f571afc59f3043a65f8fe6bf46d887b10a01d43" + version = "v1.0.1" + +[[projects]] + digest = "1:88e0b0baeb9072f0a4afbcf12dda615fc8be001d1802357538591155998da21b" + name = "github.com/hashicorp/go-version" + packages = ["."] + pruneopts = "UT" + revision = "ac23dc3fea5d1a983c43f6a0f6e2c13f0195d8bd" + version = "v1.2.0" + +[[projects]] + digest = "1:d15ee511aa0f56baacc1eb4c6b922fa1c03b38413b6be18166b996d82a0156ea" + name = "github.com/hashicorp/golang-lru" + packages = [ + ".", + "simplelru", + ] + pruneopts = "UT" + revision = "7087cb70de9f7a8bc0a10c375cb0d2280a8edf9c" + version = "v0.5.1" + [[projects]] digest = "1:ae22a88db2b2ed901a45665953afeb80410413edd27557e15e535b21f48a3622" name = "github.com/hashicorp/hcl" @@ -100,21 +216,49 @@ version = "v0.8.1" [[projects]] - digest = "1:0935050931cdcd69030f5649b1418f17b409b4b0f73bc882e1c346c86cb9586d" + digest = "1:66b341316d7b4fe01377f1f0d59207ac38bcc8a4e50030969eb67f7738899122" name = "github.com/hashicorp/vault" packages = [ "api", + "builtin/credential/github", + "helper/certutil", "helper/compressutil", "helper/consts", + "helper/errutil", "helper/hclutil", "helper/jsonutil", + "helper/license", + "helper/locksutil", + "helper/logging", + "helper/mfa", + "helper/mfa/duo", + "helper/mlock", "helper/parseutil", + "helper/password", + "helper/pathmanager", + "helper/pluginutil", + "helper/policyutil", + "helper/salt", "helper/strutil", + "helper/wrapping", + "logical", + "logical/framework", + "physical", + "physical/inmem", + "version", ] pruneopts = "UT" revision = "a59ffa4a0f09bbf198241fe6793a96722789b639" version = "v0.11.5" +[[projects]] + branch = "master" + digest = "1:a4826c308e84f5f161b90b54a814f0be7d112b80164b9b884698a6903ea47ab3" + name = "github.com/hashicorp/yamux" + packages = ["."] + pruneopts = "UT" + revision = "2f1d1f20f75d5404f53b9edf6b53ed5505508675" + [[projects]] digest = "1:0a69a1c0db3591fcefb47f115b224592c8dfa4368b7ba9fae509d5e16cdc95c8" name = "github.com/konsorten/go-windows-terminal-sequences" @@ -131,6 +275,14 @@ revision = "ae18d6b8b3205b561c79e8e5f69bff09736185f4" version = "v1.0.0" +[[projects]] + digest = "1:42eb1f52b84a06820cedc9baec2e710bfbda3ee6dac6cdb97f8b9a5066134ec6" + name = "github.com/mitchellh/go-testing-interface" + packages = ["."] + pruneopts = "UT" + revision = "6d0b8010fcc857872e42fc6c931227569016843c" + version = "v1.0.0" + [[projects]] digest = "1:53bc4cd4914cd7cd52139990d5170d6dc99067ae31c56530621b18b35fc30318" name = "github.com/mitchellh/mapstructure" @@ -139,6 +291,14 @@ revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe" version = "v1.1.2" +[[projects]] + digest = "1:9ec6cf1df5ad1d55cf41a43b6b1e7e118a91bade4f68ff4303379343e40c0e25" + name = "github.com/oklog/run" + packages = ["."] + pruneopts = "UT" + revision = "4dadeb3030eda0273a12382bb2348ffc7c9d1a39" + version = "v1.0.0" + [[projects]] digest = "1:e39a5ee8fcbec487f8fc68863ef95f2b025e0739b0e4aa55558a2b4cf8f0ecf0" name = "github.com/pierrec/lz4" @@ -202,17 +362,32 @@ [[projects]] branch = "master" - digest = "1:f8292d035e3f59cbcf39fecb9ca1dd24ec20ae4f8a06750609ee4b71d60d0045" + digest = "1:29fe5460430a338b64f4a0259a6c59a1e2350bbcff54fa66f906fa8d10515c4d" name = "golang.org/x/net" packages = [ + "context", + "context/ctxhttp", "http/httpguts", "http2", "http2/hpack", "idna", + "internal/timeseries", + "trace", ] pruneopts = "UT" revision = "351d144fa1fc0bd934e2408202be0c29f25e35a0" +[[projects]] + branch = "master" + digest = "1:9927d6aceb89d188e21485f42a7a254e67e6fdcf4260aba375fe18e3c300dfb4" + name = "golang.org/x/oauth2" + packages = [ + ".", + "internal", + ] + pruneopts = "UT" + revision = "950ef44c6e079baf075030377d90bf0c7e4b7b7a" + [[projects]] branch = "master" digest = "1:0c7cb58af944f9690af91474a75371eecfe54c1ce3771a8dc08924430ee785d3" @@ -255,6 +430,73 @@ pruneopts = "UT" revision = "85acf8d2951cb2a3bde7632f9ff273ef0379bcbd" +[[projects]] + digest = "1:6eb6e3b6d9fffb62958cf7f7d88dbbe1dd6839436b0802e194c590667a40412a" + name = "google.golang.org/appengine" + packages = [ + "internal", + "internal/base", + "internal/datastore", + "internal/log", + "internal/remote_api", + "internal/urlfetch", + "urlfetch", + ] + pruneopts = "UT" + revision = "4c25cacc810c02874000e4f7071286a8e96b2515" + version = "v1.6.0" + +[[projects]] + branch = "master" + digest = "1:583a0c80f5e3a9343d33aea4aead1e1afcc0043db66fdf961ddd1fe8cd3a4faf" + name = "google.golang.org/genproto" + packages = ["googleapis/rpc/status"] + pruneopts = "UT" + revision = "bb713bdc0e5239f2b68e560efbe1c701a6fe78f9" + +[[projects]] + digest = "1:64657a7d01c4377b9456d8eaf6ad31b244e3a09a9d8d5a321eb0b1d4bd16a46c" + name = "google.golang.org/grpc" + packages = [ + ".", + "balancer", + "balancer/base", + "balancer/roundrobin", + "binarylog/grpc_binarylog_v1", + "codes", + "connectivity", + "credentials", + "credentials/internal", + "encoding", + "encoding/proto", + "grpclog", + "health", + "health/grpc_health_v1", + "internal", + "internal/backoff", + "internal/balancerload", + "internal/binarylog", + "internal/channelz", + "internal/envconfig", + "internal/grpcrand", + "internal/grpcsync", + "internal/syscall", + "internal/transport", + "keepalive", + "metadata", + "naming", + "peer", + "resolver", + "resolver/dns", + "resolver/passthrough", + "stats", + "status", + "tap", + ] + pruneopts = "UT" + revision = "869adfc8d5a43efc0d05780ad109106f457f51e4" + version = "v1.21.0" + [[projects]] digest = "1:b24d38b282bacf9791408a080f606370efa3d364e4b5fd9ba0f7b87786d3b679" name = "gopkg.in/urfave/cli.v1" @@ -283,7 +525,9 @@ "github.com/hashicorp/hcl/hcl/ast", "github.com/hashicorp/hcl/hcl/printer", "github.com/hashicorp/vault/api", + "github.com/hashicorp/vault/builtin/credential/github", "github.com/hashicorp/vault/helper/parseutil", + "github.com/mitchellh/go-homedir", "github.com/mitchellh/mapstructure", "github.com/pkg/errors", "github.com/sirupsen/logrus", diff --git a/README.md b/README.md index 8257ffe..7789a8e 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ - [`--application`](#--application) - [Global Commands](#global-commands) - [`push-all`](#push-all) + - [`profile-edit`](#profile-edit) + - [`profile-use`](#profile-use) - [Consul](#consul) - [`consul-push-all`](#consul-push-all) - [`consul-push-services`](#consul-push-services) @@ -26,8 +28,6 @@ - [`vault-create-token`](#vault-create-token) - [`vault-find-token`](#vault-find-token) - [`vault-list-secrets`](#vault-list-secrets) - - [`vault-profile-edit`](#vault-profile-edit) - - [`vault-profile-use`](#vault-profile-use) - [`vault-pull-secrets`](#vault-pull-secrets) - [`vault-push-all`](#vault-push-all) - [`vault-push-auth`](#vault-push-auth) @@ -190,6 +190,58 @@ Environment Key: `APPLICATION` Push all Consul and Vault data to remote servers (same as running `vault-push-all` and `consul-push-all`) +#### `profile-edit` + +Decrypt (or create), open and encrypt the secure `HASHI_HELPER_PROFILE_FILE` (`~/.vault_profiles.pgp`) file containing your vault clusters + +File format is as described below, a simple yaml file + +```yml +--- +# Sample config (yaml) +# +# all keys are optional +# + +profile_name_1: + vault: + server: http://active.vault.service.consul:8200 + auth: + token: + unseal_token: + consul: + server: http://consul.service.consul:8500 + auth: + token: + nomad: + server: http://nomad.service.consul:4646 + auth: + token: + +profile_name_2: + vault: + server: http://active.vault.service.consul:8200 + auth: + method: github + github_token: + consul: + server: http://consul.service.consul:8500 + auth: + method: vault + creds_path: consul/creds/administrator + nomad: + server: http://nomad.service.consul:4646 + auth: + method: vault + creds_path: nomad/creds/administrator +``` + +#### `profile-use` + +Decrypt the `HASHI_HELPER_PROFILE_FILE` and output bash/zsh compatible commands to set `VAULT_ADDR`, `VAULT_TOKEN`, `ENVIRONMENT` based on the profile you selected. + +Example: `$(hashi-helper profile-use name_1)` + ### Consul #### `consul-push-all` @@ -244,38 +296,6 @@ Print a list of local from `conf.d/` (default) or remote secrets from Vault (`-- Add `--detailed` / `DETAILED` to show secret data rather than just the key names. -#### `vault-profile-edit` - -Decrypt (or create), open and encrypt the secure `VAULT_PROFILE_FILE` (`~/.vault_profiles.pgp`) file containing your vault clusters - -File format is as described below, a simple yaml file - -```yml ---- -# Sample config (yaml) -# -# all keys are optional -# - -profile_name_1: - server: http://active.vault.service.consul:8200 - consul_server: http://consul.service.consul:8500 - token: - unseal_token: - -profile_name_2: - server: http://active.vault.service.consul:8200 - consul_server: http://consul.service.consul:8500 - token: - unseal_token: -``` - -#### `vault-profile-use` - -Decrypt the `VAULT_PROFILE_FILE` and output bash/zsh compatible commands to set `VAULT_ADDR`, `VAULT_TOKEN`, `ENVIRONMENT` based on the profile you selected. - -Example: `$(hashi-helper vault-profile-use name_1)` - #### `vault-pull-secrets` NOT IMPLEMENTED YET diff --git a/command/vault/edit_profile.go b/command/profile/edit_profile.go similarity index 61% rename from command/vault/edit_profile.go rename to command/profile/edit_profile.go index 1362ae3..a38478c 100644 --- a/command/vault/edit_profile.go +++ b/command/profile/edit_profile.go @@ -1,4 +1,4 @@ -package vault +package profile import ( "io" @@ -6,14 +6,14 @@ import ( "os" "os/exec" - cli "gopkg.in/urfave/cli.v1" + "gopkg.in/urfave/cli.v1" ) // EditProfile ... func EditProfile(c *cli.Context) error { filePath := getProfileFile() - file, err := ioutil.TempFile("", "vault") + file, err := ioutil.TempFile("", "hashi_helper_profile") if err != nil { return err } @@ -25,7 +25,7 @@ func EditProfile(c *cli.Context) error { if _, err := os.Stat(filePath); err == nil { backup = true - b, err := getProfileConfig() + b, err := decryptFile(filePath) if err != nil { return err } @@ -35,10 +35,37 @@ func EditProfile(c *cli.Context) error { # Sample config (yaml) # # profile_name_1: -# server: http://:8200 # optional -# consul_server: http://:8500 # optional -# token: # optional -# unseal_token: # optional +# vault: +# server: http://active.vault.service.consul:8200 +# auth: +# token: +# unseal_token: +# consul: +# server: http://consul.service.consul:8500 +# auth: +# token: +# nomad: +# server: http://nomad.service.consul:4646 +# auth: +# token: +# +# profile_name_2: +# vault: +# server: http://active.vault.service.consul:8200 +# auth: +# method: github +# github_token: +# consul: +# server: http://consul.service.consul:8500 +# auth: +# method: vault +# creds_path: consul/creds/administrator +# nomad: +# server: http://nomad.service.consul:4646 +# auth: +# method: vault +# creds_path: nomad/creds/administrator + `) file.Write(b) } @@ -55,6 +82,7 @@ func EditProfile(c *cli.Context) error { case "code": flags = append(flags, "-w") flags = append(flags, "-n") + // More Editors should be added } // append the filename @@ -72,11 +100,8 @@ func EditProfile(c *cli.Context) error { copyFileContents(getProfileFile(), getProfileFile()+".old") } - // encrypt the file - encryptCmd := exec.Command("keybase", "pgp", "encrypt", "--infile", file.Name(), "--outfile", getProfileFile()) - encryptErr := encryptCmd.Run() - if encryptErr != nil { - return encryptErr + if err := encryptFile(file.Name(), getProfileFile()); err != nil { + return err } return nil diff --git a/command/profile/use_profile.go b/command/profile/use_profile.go new file mode 100644 index 0000000..dd104e7 --- /dev/null +++ b/command/profile/use_profile.go @@ -0,0 +1,356 @@ +package profile + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/hashicorp/vault/api" + vault "github.com/hashicorp/vault/api" + vgh "github.com/hashicorp/vault/builtin/credential/github" + "github.com/mitchellh/go-homedir" + "gopkg.in/urfave/cli.v1" + "gopkg.in/yaml.v2" +) + +type profiles map[string]profileStruct + +type profileStruct struct { + Vault vaultCreds `yaml:"vault"` + Consul consulCreds `yaml:"consul"` + Nomad nomadCreds `yaml:"nomad"` +} + +type InternalTokenHelper struct { + tokenPath string + profileName string +} + +type authConfig struct { + Method string `yaml:"method"` + CredsPath string `yaml:"creds_path"` + Token string `yaml:"token"` + ExpireTime string `yaml:"expire_time"` //used internal for cache files only + UnsealToken string `yaml:"unseal_token"` + GithubToken string `yaml:"github_token"` + GithubMount string `yaml:"mount"` +} + +type vaultCreds struct { + Auth authConfig `yaml:"auth"` + Server string `yaml:"server"` +} + +type consulCreds struct { + Server string `yaml:"server"` + Auth authConfig `yaml:"auth"` +} + +type nomadCreds struct { + Auth authConfig `yaml:"auth"` + Server string `yaml:"server"` +} + +// UseProfile ... +func UseProfile(c *cli.Context) error { + + var printingBuffer bytes.Buffer + + + if !c.Args().Present() { + return fmt.Errorf("Please provide a profile name as first argument") + } + + name := c.Args().First() + if name == "" { + return fmt.Errorf("Missing profile name") + } + + // parsing profiles file + var parsedProfiles, profilesCache profiles + dat, err := decryptFile(getProfileFile()) + if err != nil { + return err + } + if err := yaml.Unmarshal(dat, &parsedProfiles); err != nil { + return err + } + + profile, ok := parsedProfiles[name] + if !ok { + return fmt.Errorf("No profile with the name '%s' was found", name) + } + + cacheDat, err := decryptFile(getCacheFile()) + + if cacheDat != nil { + if err := yaml.Unmarshal(cacheDat, &profilesCache); err != nil { + return err + } + } else { + profilesCache = make(profiles) + } + + profileCache := profilesCache[name] + + // Creating Vault Client for checking creds + v, err := api.NewClient(vault.DefaultConfig()) + // setting Vault timeout to 5 seconds + v.SetClientTimeout(time.Second * 5) + + if profile.Vault.Server != "" { + v.SetAddress(profile.Vault.Server) + printingBuffer.WriteString(fmt.Sprintf("export VAULT_ADDR=%s\n", profile.Vault.Server)) + } + + if profile.Vault.Auth.Token != "" { + printingBuffer.WriteString(fmt.Sprintf("export VAULT_TOKEN=%s\n", profile.Vault.Auth.Token)) + } + + if profile.Vault.Auth.UnsealToken != "" { + printingBuffer.WriteString(fmt.Sprintf("export VAULT_UNSEAL_KEY=%s\n", profile.Vault.Auth.UnsealToken)) + } + + if profile.Vault.Auth.Method != "" { + switch profile.Vault.Auth.Method { + case "github": + if profileCache.Vault.Auth.Token != "" { + if profileCache.Vault.Auth.ExpireTime != "" { + et, err := time.Parse(time.RFC3339Nano, profileCache.Vault.Auth.ExpireTime) + if err != nil { // if expire time can't be parsed as time assume cache is bad and relogin + profileCache.Vault, err = vaultLoginGitHub(profile.Vault, v) + if err != nil { + return fmt.Errorf("can't login to Vault using github for profile %s - %s", name, err) + } + } else if et.Before(time.Now()) { // relogin if token is expired + profileCache.Vault, err = vaultLoginGitHub(profile.Vault, v) + if err != nil { + return fmt.Errorf("can't login to Vault using github for profile %s - %s", name, err) + } + } + } + } else { // token is absent cache - login + profileCache.Vault, err = vaultLoginGitHub(profile.Vault, v) + if err != nil { + return fmt.Errorf("can't login to Vault using github for profile %s - %s", name, err) + } + } + v.SetToken(profileCache.Vault.Auth.Token) + printingBuffer.WriteString(fmt.Sprintf("export VAULT_TOKEN=%s\n", profileCache.Vault.Auth.Token)) + // TODO: More Auth methods to be added here + } + } + + if profile.Consul.Server != "" { + printingBuffer.WriteString(fmt.Sprintf("export CONSUL_HTTP_ADDR=%s\n", profile.Consul.Server)) + } + + if profile.Consul.Auth.Token != "" { + printingBuffer.WriteString(fmt.Sprintf("export CONSUL_HTTP_TOKEN=%s\n", profile.Consul.Auth.Token)) + } + + if profile.Consul.Auth.Method == "vault" { + if profileCache.Consul.Auth.Token != "" { + if profileCache.Consul.Auth.ExpireTime != "" { + et, err := time.Parse(time.RFC3339Nano, profileCache.Consul.Auth.ExpireTime) + if err != nil { // if expire time can't be parsed as time assume cache is bad and relogin + profileCache.Consul, err = vaultGetConsulCreds(profile.Consul, v) + if err != nil { + return fmt.Errorf("error reading Consul creds for profile %s", name) + } + } else if et.Before(time.Now()) { // relogin if token is expired + profileCache.Consul, err = vaultGetConsulCreds(profile.Consul, v) + if err != nil { + return fmt.Errorf("error reading Consul creds for profile %s", name) + } + } + } + } else { // token is absent cache - login + profileCache.Nomad, err = vaultGetNomadCreds(profile.Nomad, v) + if err != nil { + return fmt.Errorf("error reading Consul creds for profile %s", name) + } + } + printingBuffer.WriteString(fmt.Sprintf("export CONSUL_HTTP_TOKEN=%s\n", profileCache.Consul.Auth.Token)) + } + + if profile.Nomad.Server != "" { + printingBuffer.WriteString(fmt.Sprintf("export NOMAD_ADDR=%s\n", profile.Nomad.Server)) + } + + if profile.Nomad.Auth.Token != "" { + printingBuffer.WriteString(fmt.Sprintf("export NOMAD_TOKEN=%s\n", profile.Nomad.Auth.Token)) + } + + if profile.Nomad.Auth.Method == "vault" { + if profileCache.Nomad.Auth.Token != "" { + if profileCache.Nomad.Auth.ExpireTime != "" { + et, err := time.Parse(time.RFC3339Nano, profileCache.Nomad.Auth.ExpireTime) + if err != nil { // if expire time can't be parsed as time assume cache is bad and relogin + profileCache.Nomad, err = vaultGetNomadCreds(profile.Nomad, v) + if err != nil { + return fmt.Errorf("error reading Nomad creds for profile %s", name) + } + } else if et.Before(time.Now()) { // relogin if token is expired + profileCache.Nomad, err = vaultGetNomadCreds(profile.Nomad, v) + if err != nil { + return fmt.Errorf("error reading Nomad creds for profile %s", name) + } + } + } + } else { // token is absent cache - login + profileCache.Nomad, err = vaultGetNomadCreds(profile.Nomad, v) + if err != nil { + return fmt.Errorf("error reading Nomad creds for profile %s", name) + } + } + printingBuffer.WriteString(fmt.Sprintf("export NOMAD_TOKEN=%s\n", profileCache.Nomad.Auth.Token)) + } + + profilesCache[name] = profileCache + + // Create a file for cache update + cacheTempFile, err := ioutil.TempFile("", "hashi_helper_cache") + if err != nil { + return fmt.Errorf("can't create temp file for cache generation %s", cacheTempFile.Name()) + } + + yamlReadableCache, err := yaml.Marshal(&profilesCache) + if err != nil { + return fmt.Errorf("cache generation failed for profile %s", name) + } + + // Write to the file + if err := ioutil.WriteFile(cacheTempFile.Name(), yamlReadableCache, 600); err != nil { + return fmt.Errorf("can't write to cache temp file %s", cacheTempFile.Name()) + } + cacheTempFile.Close() + + defer os.Remove(cacheTempFile.Name()) + + encryptFile(cacheTempFile.Name(), getCacheFile()) + + // printing everything that happend + fmt.Printf("%s", printingBuffer.String()) + + return nil +} + +func vaultGetNomadCreds(n nomadCreds, vc *vault.Client) (nomadCreds, error) { + r, err := readFromVault(vc, n.Auth.CredsPath) + if err != nil { + return n, err + } + + n.Auth.Token = r.Data["secret_id"].(string) + n.Auth.ExpireTime = time.Now().Add(time.Second * time.Duration(r.LeaseDuration)).Format(time.RFC3339Nano) + + return n, err +} + +func vaultGetConsulCreds(c consulCreds, vc *vault.Client) (consulCreds, error) { + r, err := readFromVault(vc, c.Auth.CredsPath) + if err != nil { + return c, err + } + + c.Auth.Token = r.Data["secret_id"].(string) + c.Auth.ExpireTime = time.Now().Add(time.Second * time.Duration(r.LeaseDuration)).Format(time.RFC3339Nano) + + return c, err +} + +func vaultLoginGitHub(v vaultCreds, vc *vault.Client) (vaultCreds, error) { + m := make(map[string]string) + if v.Auth.GithubMount != "" { + m["mount"] = v.Auth.GithubMount + } + if v.Auth.GithubToken == "" { + return v, fmt.Errorf("github_token should be provided when using GitHub Vault auth method") + } else { + m["token"] = v.Auth.GithubToken + h := vgh.CLIHandler{} + secret, err := h.Auth(vc, m) + if err != nil { + return v, err + } + + v.Auth.Token = secret.Auth.ClientToken + v.Auth.ExpireTime = time.Now().Add(time.Second * time.Duration(secret.Auth.LeaseDuration)).Format(time.RFC3339Nano) + + return v, nil + } +} + +func readFromVault(v *vault.Client, path string) (*vault.Secret, error) { + + creds, err := v.Logical().Read(path) + if err != nil { + return nil, err + } + + return creds, nil +} + +func getCacheFile() string { + path := os.Getenv("HASHI_HELPER_CACHE_FILE") + if path == "" { + homePath, err := homedir.Dir() + if err != nil { + panic(fmt.Sprintf("error getting user's home directory: %v", err)) + } + path = filepath.Join(homePath, "/.hashi_helper_cache.pgp") + } + return path +} + +func decryptFile(filePath string) ([]byte, error) { + + if _, err := os.Stat(filePath); err != nil { + return nil, err + } + cmd := exec.Command("keybase", "pgp", "decrypt", "--infile", filePath) + + var stdout bytes.Buffer + cmd.Stdout = &stdout + + var stderr bytes.Buffer + cmd.Stderr = &stderr + + fmt.Fprintf(os.Stdout, "# Decrypting %s using keybase \n", filePath) + err := cmd.Run() + if err != nil { + return nil, fmt.Errorf("Failed to run keybase gpg decrypt: %s - %s", err, stderr.String()) + } + + return stdout.Bytes(), nil +} + +func encryptFile(inFile, outFile string) error { + + encryptCmd := exec.Command("keybase", "pgp", "encrypt", "--infile", inFile, "--outfile", outFile) + + encryptErr := encryptCmd.Run() + if encryptErr != nil { + return encryptErr + } + + return nil + +} + +func getProfileFile() string { + path := os.Getenv("HASHI_HELPER_PROFILE_FILE") + if path == "" { + homePath, err := homedir.Dir() + if err != nil { + panic(fmt.Sprintf("error getting user's home directory: %v", err)) + } + path = filepath.Join(homePath, "/.hashi_helper_profiles.pgp") + } + return path +} diff --git a/command/vault/find_token.go b/command/vault/find_token.go index 25854a4..91d6b56 100644 --- a/command/vault/find_token.go +++ b/command/vault/find_token.go @@ -84,10 +84,10 @@ func FindToken(c *cli.Context) error { log.Infof(" accessor : %s", token.Data["accessor"]) if token.Data["meta"] != nil { - for k, v := range token.Data["meta"].(map[string]interface{}) { - log.Infof(" meta[%s] %s: %v", k, strings.Repeat(" ", max(0, 12-len(k))), v) - } + for k, v := range token.Data["meta"].(map[string]interface{}) { + log.Infof(" meta[%s] %s: %v", k, strings.Repeat(" ", max(0, 12-len(k))), v) } + } log.Info("") diff --git a/command/vault/use_profile.go b/command/vault/use_profile.go deleted file mode 100644 index db9765b..0000000 --- a/command/vault/use_profile.go +++ /dev/null @@ -1,89 +0,0 @@ -package vault - -import ( - "bytes" - "fmt" - "os" - "os/exec" - - cli "gopkg.in/urfave/cli.v1" - yaml "gopkg.in/yaml.v2" -) - -type profiles map[string]profile -type profile struct { - Token string `yaml:"token"` - UnsealToken string `yaml:"unseal_token"` - Server string `yaml:"server"` - ConsulServer string `yaml:"consul_server"` -} - -// UseProfile ... -func UseProfile(c *cli.Context) error { - if !c.Args().Present() { - return fmt.Errorf("Please provide a profile name as first argument") - } - - name := c.Args().First() - if name == "" { - return fmt.Errorf("Missing profile name") - } - - var profiles profiles - dat, err := getProfileConfig() - if err != nil { - return err - } - if err := yaml.Unmarshal(dat, &profiles); err != nil { - return err - } - - profile, ok := profiles[name] - if !ok { - return fmt.Errorf("No profile with the name '%s' was found", name) - } - - if profile.Server != "" { - fmt.Printf("export VAULT_ADDR=%s\n", profile.Server) - } - - if profile.ConsulServer != "" { - fmt.Printf("export CONSUL_HTTP_ADDR=%s\n", profile.ConsulServer) - } - - if profile.Token != "" { - fmt.Printf("export VAULT_TOKEN=%s\n", profile.Token) - } - - if profile.UnsealToken != "" { - fmt.Printf("export VAULT_UNSEAL_KEY=%s\n", profile.UnsealToken) - } - - return nil -} - -func getProfileConfig() ([]byte, error) { - cmd := exec.Command("keybase", "pgp", "decrypt", "--infile", getProfileFile()) - - var stdout bytes.Buffer - cmd.Stdout = &stdout - - var stderr bytes.Buffer - cmd.Stderr = &stderr - - fmt.Fprintf(os.Stderr, "# Starting keybase decrypt of %s\n", getProfileFile()) - err := cmd.Run() - if err != nil { - return nil, fmt.Errorf("Failed to run keybase gpg decrypt: %s - %s", err, stderr.String()) - } - - return stdout.Bytes(), nil -} - -func getProfileFile() string { - path := os.Getenv("VAULT_PROFILE_FILE") - if path == "" { - path = os.Getenv("HOME") + "/.vault_profiles.pgp" - } - return path -} diff --git a/main.go b/main.go index bc37bc3..d6a1ac6 100644 --- a/main.go +++ b/main.go @@ -7,9 +7,10 @@ import ( allCommand "github.com/seatgeek/hashi-helper/command" consulCommand "github.com/seatgeek/hashi-helper/command/consul" + profileCommand "github.com/seatgeek/hashi-helper/command/profile" vaultCommand "github.com/seatgeek/hashi-helper/command/vault" log "github.com/sirupsen/logrus" - cli "gopkg.in/urfave/cli.v1" + "gopkg.in/urfave/cli.v1" ) var ( @@ -77,17 +78,17 @@ func main() { }, }, { - Name: "vault-profile-use", + Name: "profile-use", Usage: "Change your current vault env profile", Action: func(c *cli.Context) error { - return vaultCommand.UseProfile(c) + return profileCommand.UseProfile(c) }, }, { - Name: "vault-profile-edit", + Name: "profile-edit", Usage: "Edit your current vault env profile", Action: func(c *cli.Context) error { - return vaultCommand.EditProfile(c) + return profileCommand.EditProfile(c) }, }, {