From 9ebb9a60218df2ed9c0a549913c52d9ccf8de2e0 Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Tue, 16 Aug 2022 16:46:33 -0400 Subject: [PATCH 01/17] + added initial elasticache redis implementation --- README.md | 130 ++++++++-------- .../main.go | 27 ++++ cmd/vault-plugin-scaffolding/main.go | 30 ---- go.mod | 25 +--- go.sum | 52 ++----- internal/plugin/plugin.go | 50 +++++++ internal/plugin/redisElastiCacheClient.go | 140 ++++++++++++++++++ .../plugin/redisElastiCacheClient_test.go | 50 +++++++ 8 files changed, 353 insertions(+), 151 deletions(-) create mode 100644 cmd/vault-plugin-database-redis-elasticache/main.go delete mode 100644 cmd/vault-plugin-scaffolding/main.go create mode 100644 internal/plugin/plugin.go create mode 100644 internal/plugin/redisElastiCacheClient.go create mode 100644 internal/plugin/redisElastiCacheClient_test.go diff --git a/README.md b/README.md index b3724b1..4639ff6 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,21 @@ -# Vault Plugin Scaffolding +# Vault Plugin Database Redis ElastiCache -This is a standalone backend plugin for use with [Hashicorp +This is a standalone [Database Plugin](https://www.vaultproject.io/docs/secrets/databases) for use with [Hashicorp Vault](https://www.github.com/hashicorp/vault). -[//]: <> (Include a general statement about this plugin) +This plugin supports exclusively AWS ElastiCache for Redis. [Redis Enterprise](https://github.com/RedisLabs/vault-plugin-database-redis-enterprise) +and [Redis Open Source](https://github.com/fhitchen/vault-plugin-database-redis) use different plugins. Please note: We take Vault's security and our users' trust very seriously. If you believe you have found a security issue in Vault, please responsibly disclose by contacting us at [security@hashicorp.com](mailto:security@hashicorp.com). -## Using this Template Repository - -_Note: Remove this instruction sub-heading once you've created a repository from this template_ - -This repository is a template for a Vault secret engine and auth method plugins. -It is intended as a starting point for creating Vault plugins, containing: - -- Changelog, readme, Makefile, pull request template -- Scripts for internal tooling -- Jira sync and basic testing GitHub actions -- A base `main.go` for compiling the plugin - -There's some minimal GitHub Secrets setup required in order to get the Jira sync -GH action working. Install the `gh` [CLI](https://cli.github.com/manual/) and -perform the following commands to set secrets for this repository. - -```sh -gh secret set JIRA_SYNC_BASE_URL -gh secret set JIRA_SYNC_USER_EMAIL -gh secret set JIRA_SYNC_API_TOKEN -``` - - -This template repository does not include a Mozilla Public License 2.0 `LICENSE` -since plugins created this way can be internal to hashicorp and for Vault -Enterprise consumption. To add a license, follow [these GitHub -instructions](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-license-to-a-repository), -or obtain one from one of our public Vault plugins. - -Please see the [GitHub template repository -documentation](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template) -for how to create a new repository from this template on GitHub. - -Things _not_ handled by this template repository: -- Repository settings, such as branch protection rules -- Memberships and permissions -- GitHub secrets for this repository - -Please see the [Repository Configuration Page](https://hashicorp.atlassian.net/wiki/spaces/VAULT/pages/2103476333/Repository+Configuration) -for the setting proper repository configuration values. ## Quick Links - [Vault Website](https://www.vaultproject.io) -- [Vault Project GitHub](https://www.github.com/hashicorp/vault) +- [Plugin System](https://www.vaultproject.io/docs/plugins) -[//]: <> (Include any other quick links relevant to your plugin) ## Getting Started @@ -67,14 +27,16 @@ Otherwise, first read this guide on how to [get started with Vault](https://www.vaultproject.io/intro/getting-started/install.html). -## Usage +## Development -[//]: <> (Provide usage instructions and/or links to this plugin) +If you wish to work on this plugin, you'll first need +[Go](https://www.golang.org) installed on your machine (version 1.17+ recommended) -## Developing +Make sure Go is properly installed, including setting up a [GOPATH](https://golang.org/doc/code.html#GOPATH). -If you wish to work on this plugin, you'll first need -[Go](https://www.golang.org) installed on your machine. +To run the tests locally you will need to have write permissions to an [ElastiCache for Redis](https://aws.amazon.com/elasticache/redis/) instance. + +## Building If you're developing for the first time, run `make bootstrap` to install the necessary tools. Bootstrap will also update repository name references if that @@ -92,6 +54,10 @@ mode will only generate the binary for your platform and is faster: $ make dev ``` +## Tests + +### Testing Manually + Put the plugin binary into a location of your choice. This directory will be specified as the [`plugin_directory`](https://www.vaultproject.io/docs/configuration#plugin_directory) in the Vault config used to start the server. @@ -112,25 +78,67 @@ $ vault server -dev -config=path/to/config.hcl ... Once the server is started, register the plugin in the Vault server's [plugin catalog](https://www.vaultproject.io/docs/plugins/plugin-architecture#plugin-catalog): ```sh -$ SHA256=$(openssl dgst -sha256 $GOPATH/vault-plugin-secrets-myplugin | cut -d ' ' -f2) -$ vault plugin register \ - -sha256=$SHA256 \ - -command="vault-plugin-secrets-myplugin" \ - secrets myplugin +$ SHA256=$(openssl dgst -sha256 $GOPATH/vault-plugin-database-redis-elasticache | cut -d ' ' -f2) +$ vault write sys/plugins/catalog/database/vault-plugin-database-redis-elasticache \ + command=vault-plugin-database-redis-elasticache \ + sha256=$SHA256 ... -Success! Data written to: sys/plugins/catalog/myplugin +Success! Data written to: sys/plugins/catalog/database/vault-plugin-database-redis-elasticache ``` -Enable the secrets engine to use this plugin: +Enable the database engine to use this plugin: ```sh -$ vault secrets enable myplugin +$ vault secrets enable database ... -Successfully enabled 'plugin' at 'myplugin'! +Success! Enabled the database secrets engine at: database/ ``` -### Tests +Once the database engine is enabled you can configure an ElastiCache instance: + +```sh +$ vault write database/config/redis-mydb \ + plugin_name="vault-plugin-database-redis-elasticache" \ + username=$USERNAME \ + password=$PASSWORD \ + url=$URL \ + region=$REGION +... + +Success! Data written to: database/config/redis-mydb +``` + +Configure a role: + +```sh +$ vault write database/roles/redis-myrole \ + db_name="redis-mydb" \ + creation_statements=$CREATION_STATEMENTS \ + default_ttl=$DEFAULT_TTL \ + max_ttl=$MAX_TTL +... + +Success! Data written to: database/roles/redis-myrole +``` + +And generate your first set of dynamic credentials: + +```sh +$ vault read database/creds/redis-myrole +... + +Key Value +--- ----- +lease_id database/creds/redis-myrole/ID +lease_duration Xm +lease_renewable true +password PASSWORD +username v_token_redis-myrole_ID_EPOCH +``` + + +### Automated Tests To run the tests, invoke `make test`: @@ -143,5 +151,3 @@ You can also specify a `TESTARGS` variable to filter tests like so: ```sh $ make test TESTARGS='-run=TestConfig' ``` - -[//]: <> (Specify any other test instructions such as acceptance/integration tests) diff --git a/cmd/vault-plugin-database-redis-elasticache/main.go b/cmd/vault-plugin-database-redis-elasticache/main.go new file mode 100644 index 0000000..bd49749 --- /dev/null +++ b/cmd/vault-plugin-database-redis-elasticache/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + "os" + + "github.com/hashicorp/vault-plugin-database-redis-elasticache/internal/plugin" + + "github.com/hashicorp/vault/sdk/database/dbplugin/v5" +) + +func main() { + if err := Run(); err != nil { + log.Println(err) + os.Exit(1) + } +} + +// Run starts serving the plugin +func Run() error { + db, err := plugin.New() + if err != nil { + return err + } + dbplugin.Serve(db.(dbplugin.Database)) + return nil +} diff --git a/cmd/vault-plugin-scaffolding/main.go b/cmd/vault-plugin-scaffolding/main.go deleted file mode 100644 index f3149f4..0000000 --- a/cmd/vault-plugin-scaffolding/main.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import ( - "os" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/sdk/plugin" -) - -func main() { - apiClientMeta := &api.PluginAPIClientMeta{} - flags := apiClientMeta.FlagSet() - flags.Parse(os.Args[1:]) - - tlsConfig := apiClientMeta.GetTLSConfig() - tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) - - err := plugin.Serve(&plugin.ServeOpts{ - // TODO: Add the plugin's Factory function here, e.g.: - // BackendFactoryFunc: vault-plugin-scaffolding.Factory, - TLSProviderFunc: tlsProviderFunc, - }) - if err != nil { - logger := hclog.New(&hclog.LoggerOptions{}) - - logger.Error("plugin shutting down", "error", err) - os.Exit(1) - } -} diff --git a/go.mod b/go.mod index a1ad0f1..dbd87e9 100644 --- a/go.mod +++ b/go.mod @@ -1,57 +1,42 @@ -module github.com/hashicorp/vault-plugin-scaffolding +module github.com/hashicorp/vault-plugin-database-redis-elasticache go 1.17 require ( + github.com/aws/aws-sdk-go v1.44.76 github.com/hashicorp/go-hclog v1.2.1 - github.com/hashicorp/vault/api v1.7.1 github.com/hashicorp/vault/sdk v0.5.0 ) require ( github.com/armon/go-metrics v0.3.9 // indirect - github.com/armon/go-radix v1.0.0 // indirect - github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/fatih/color v1.13.0 // indirect github.com/frankban/quicktest v1.14.2 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.5.7 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.4.3 // indirect - github.com/hashicorp/go-retryablehttp v0.6.6 // indirect - github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 // indirect github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect - github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect - github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/hashicorp/go-version v1.2.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mitchellh/copystructure v1.0.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/oklog/run v1.0.0 // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect - github.com/ryanuber/go-glob v1.0.0 // indirect - go.uber.org/atomic v1.9.0 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect - golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect google.golang.org/grpc v1.41.0 // indirect google.golang.org/protobuf v1.26.0 // indirect - gopkg.in/square/go-jose.v2 v2.5.1 // indirect ) diff --git a/go.sum b/go.sum index 7d27254..1dbe093 100644 --- a/go.sum +++ b/go.sum @@ -10,14 +10,13 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/armon/go-metrics v0.3.9 h1:O2sNqxBdvq8Eq5xmzljcYzAORli6RWCvEym4cJf9m18= github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.44.76 h1:5e8yGO/XeNYKckOjpBKUd5wStf0So3CrQIiOMCVLpOI= +github.com/aws/aws-sdk-go v1.44.76/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= -github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= @@ -36,7 +35,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch/v5 v5.5.0 h1:bAmFiUJ+o0o2B4OiTFeE3MqCOtyo+jjPP9iZ0VRxYUc= github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= @@ -44,7 +42,6 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= -github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns= github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -55,7 +52,6 @@ github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9p github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -93,10 +89,6 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw= @@ -104,31 +96,22 @@ github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-kms-wrapping/entropy v0.1.0 h1:xuTi5ZwjimfpvpL09jDE71smCBRpnF5xfo871BSX4gs= github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= -github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 h1:6KMBnfEv0/kLAz0O76sliN5mXbCDcLfs2kP7ssP7+DQ= github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.5/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= -github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= -github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= @@ -138,10 +121,7 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/vault/api v1.7.1 h1:uUpxcZO3XV1Sb96dEtT+tZlSpV7U/zEi0NoksM7lU5M= -github.com/hashicorp/vault/api v1.7.1/go.mod h1:TlKWwxZySuDARVFz/H0sf6rgWddIlX4t4DO9baT2nXc= github.com/hashicorp/vault/sdk v0.5.0 h1:EED7p0OCU3OY5SAqJwSANofY1YKMytm+jDHDQ2EzGVQ= github.com/hashicorp/vault/sdk v0.5.0/go.mod h1:UJZHlfwj7qUJG8g22CuxUgkdJouFrBNvBHCyx8XAPdo= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= @@ -149,6 +129,10 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -156,7 +140,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -177,10 +160,7 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= @@ -188,7 +168,6 @@ github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUb github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -204,7 +183,6 @@ github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -226,7 +204,6 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -242,7 +219,6 @@ github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8 github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -265,8 +241,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -288,20 +264,18 @@ golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -345,13 +319,13 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go new file mode 100644 index 0000000..5cea433 --- /dev/null +++ b/internal/plugin/plugin.go @@ -0,0 +1,50 @@ +package plugin + +import ( + "os" + + "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/database/dbplugin/v5" +) + +// Verify interface is implemented +var _ dbplugin.Database = (*redisElastiCacheDB)(nil) + +type redisElastiCacheDB struct { + logger hclog.Logger + config config + client *elasticache.ElastiCache +} + +type config struct { + Username string `mapstructure:"username,omitempty"` + Password string `mapstructure:"password,omitempty"` + Url string `mapstructure:"url,omitempty"` + Region string `mapstructure:"region,omitempty"` +} + +func New() (dbplugin.Database, error) { + logger := hclog.New(&hclog.LoggerOptions{ + Level: hclog.Trace, + Output: os.Stderr, + JSONFormat: true, + }) + + db := &redisElastiCacheDB{ + logger: logger, + } + + return wrapWithSanitizerMiddleware(db), nil +} + +func wrapWithSanitizerMiddleware(db *redisElastiCacheDB) dbplugin.Database { + return dbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.secretValuesToMask) +} + +func (r *redisElastiCacheDB) secretValuesToMask() map[string]string { + return map[string]string{ + r.config.Password: "[password]", + r.config.Username: "[username]", + } +} diff --git a/internal/plugin/redisElastiCacheClient.go b/internal/plugin/redisElastiCacheClient.go new file mode 100644 index 0000000..151a9a0 --- /dev/null +++ b/internal/plugin/redisElastiCacheClient.go @@ -0,0 +1,140 @@ +package plugin + +import ( + "context" + "fmt" + "regexp" + "strings" + "unicode" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/hashicorp/vault/sdk/database/dbplugin/v5" + "github.com/hashicorp/vault/sdk/database/helper/credsutil" + "github.com/mitchellh/mapstructure" +) + +var ( + noPasswordRequired = false + engine = "Redis" + nonAlphanumericRegex = regexp.MustCompile("[^a-zA-Z\\d]+") +) + +func (r *redisElastiCacheDB) Initialize(_ context.Context, req dbplugin.InitializeRequest) (dbplugin.InitializeResponse, error) { + r.logger.Debug("initializing AWS ElastiCache Redis client") + + if err := mapstructure.WeakDecode(req.Config, &r.config); err != nil { + return dbplugin.InitializeResponse{}, err + } + + sess := session.Must(session.NewSession(&aws.Config{ + Region: aws.String(r.config.Region), + Credentials: credentials.NewStaticCredentials(r.config.Username, r.config.Password, ""), + })) + r.client = elasticache.New(sess) + + if req.VerifyConnection { + r.logger.Debug("Verifying connection to instance", "url", r.config.Url) + + _, err := r.client.DescribeUsers(nil) + if err != nil { + return dbplugin.InitializeResponse{}, fmt.Errorf("unable to connect to ElastiCache Redis endpoint: %w", err) + } + } + + return dbplugin.InitializeResponse{ + Config: req.Config, + }, nil +} + +func (r *redisElastiCacheDB) Type() (string, error) { + return "redisElastiCache", nil +} + +func (r *redisElastiCacheDB) Close() error { + return nil +} + +func (r *redisElastiCacheDB) NewUser(_ context.Context, req dbplugin.NewUserRequest) (dbplugin.NewUserResponse, error) { + r.logger.Debug("creating new AWS ElastiCache Redis user", "role", req.UsernameConfig.RoleName) + + // Format: v_{displayName}_{roleName}_{ID[20]}_{epoch[11]} + // Length limits set so unique identifiers are not truncated + username, err := credsutil.GenerateUsername( + credsutil.DisplayName(req.UsernameConfig.DisplayName, 5), + credsutil.RoleName(req.UsernameConfig.RoleName, 39), + credsutil.MaxLength(80), + ) + if err != nil { + return dbplugin.NewUserResponse{}, fmt.Errorf("unable to generate username: %w", err) + } + + accessString := strings.Join(req.Statements.Commands[:], " ") + + userId := generateUserId(username) + + output, err := r.client.CreateUser(&elasticache.CreateUserInput{ + AccessString: &accessString, + Engine: &engine, + NoPasswordRequired: &noPasswordRequired, + Passwords: []*string{&req.Password}, + Tags: []*elasticache.Tag{}, + UserId: &userId, + UserName: &username, + }) + if err != nil { + return dbplugin.NewUserResponse{}, fmt.Errorf("unable to create new user: %w", err) + } + + return dbplugin.NewUserResponse{Username: *output.UserName}, nil +} + +func (r *redisElastiCacheDB) UpdateUser(_ context.Context, req dbplugin.UpdateUserRequest) (dbplugin.UpdateUserResponse, error) { + r.logger.Debug("updating AWS ElastiCache Redis user", "username", req.Username) + + userId := generateUserId(req.Username) + + _, err := r.client.ModifyUser(&elasticache.ModifyUserInput{ + UserId: &userId, + Passwords: []*string{&req.Password.NewPassword}, + }) + if err != nil { + return dbplugin.UpdateUserResponse{}, fmt.Errorf("unable to update user: %w", err) + } + + return dbplugin.UpdateUserResponse{}, nil +} + +func (r *redisElastiCacheDB) DeleteUser(_ context.Context, req dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) { + r.logger.Debug("deleting AWS ElastiCache Redis user", "username", req.Username) + + userId := generateUserId(req.Username) + + _, err := r.client.DeleteUser(&elasticache.DeleteUserInput{ + UserId: &userId, + }) + if err != nil { + return dbplugin.DeleteUserResponse{}, fmt.Errorf("unable to delete user: %w", err) + } + + return dbplugin.DeleteUserResponse{}, nil +} + +// The ID can have up to 40 characters, and must begin with a letter. +// It should not end with a hyphen or contain two consecutive hyphens. +// Valid characters: A-Z, a-z, 0-9, and -(hyphen). +func generateUserId(username string) string { + userId := nonAlphanumericRegex.ReplaceAllString(username, "") + + if len(userId) > 40 { + userId = userId[len(userId)-40:] + } + + if unicode.IsNumber(rune(userId[0])) { + userId = string(rune('A'-17+userId[0])) + userId[1:] + } + + return userId +} diff --git a/internal/plugin/redisElastiCacheClient_test.go b/internal/plugin/redisElastiCacheClient_test.go new file mode 100644 index 0000000..79575a5 --- /dev/null +++ b/internal/plugin/redisElastiCacheClient_test.go @@ -0,0 +1,50 @@ +package plugin + +import ( + "testing" +) + +func Test_generateUserId(t *testing.T) { + type args struct { + username string + } + + tests := []struct { + name string + args args + want string + }{ + { + name: "compliant username", + args: args{username: "isrole1234eEvyH4mEPcCIT4tCvE131660656371"}, + want: "isrole1234eEvyH4mEPcCIT4tCvE131660656371", + }, + { + name: "short username", + args: args{username: "abcd"}, + want: "abcd", + }, + { + name: "username too long", + args: args{username: "vtokenredisrole1234eEvyH4mEPcCIT4tCvE131660656371"}, + want: "isrole1234eEvyH4mEPcCIT4tCvE131660656371", + }, + { + name: "username with non-alphanumeric characters", + args: args{username: "v_token_redis-role!/$}"}, + want: "vtokenredisrole", + }, + { + name: "username starting with a number", + args: args{username: "1bcd"}, + want: "abcd", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := generateUserId(tt.args.username); got != tt.want { + t.Errorf("generateUserId() = %v, want %v", got, tt.want) + } + }) + } +} From 7a03f332b25cd45b201d1de10c8b537336ed9cc9 Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Sat, 20 Aug 2022 12:42:49 -0400 Subject: [PATCH 02/17] + added acceptance tests --- Makefile | 11 +- README.md | 60 ++- bootstrap/terraform/elasticache.tf | 78 ++++ .../main.go | 8 +- internal/plugin/plugin.go | 2 +- .../plugin/redisElastiCacheClient_test.go | 415 ++++++++++++++++++ 6 files changed, 562 insertions(+), 12 deletions(-) create mode 100644 bootstrap/terraform/elasticache.tf diff --git a/Makefile b/Makefile index 13a5261..561cb33 100644 --- a/Makefile +++ b/Makefile @@ -36,4 +36,13 @@ fmtcheck: .PHONY: fmt fmt: - gofumpt -l -w . \ No newline at end of file + gofumpt -l -w . + +.PHONY: setup-env +setup-env: + cd bootstrap/terraform && terraform init && terraform apply -auto-approve + + +.PHONY: teardown-env +teardown-env: + cd bootstrap/terraform && terraform init && terraform destroy -auto-approve \ No newline at end of file diff --git a/README.md b/README.md index 4639ff6..47dcda7 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ If you wish to work on this plugin, you'll first need Make sure Go is properly installed, including setting up a [GOPATH](https://golang.org/doc/code.html#GOPATH). -To run the tests locally you will need to have write permissions to an [ElastiCache for Redis](https://aws.amazon.com/elasticache/redis/) instance. +To run the tests locally you will need to have write permissions to an [ElastiCache for Redis](https://aws.amazon.com/elasticache/redis/) instance. +A small Terraform project is included to provision one for you if needed. More details in the [Environment Set Up](#environment-set-up) section. ## Building @@ -56,6 +57,35 @@ $ make dev ## Tests +### Environment Set Up + +To test the plugin, you need access to an Elasticache for Redis Cluster. +A Terraform project is included for convenience to initialize a new cluster if needed. +If not already available, you can install Terraform by using [this documentation](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). + +The setup script tries to find and use available AWS credentials from the environment. You can configure AWS credentials using [this documentation](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). +Or if you prefer you can edit the provider defined ./bootstrap/terraform/elasticache.tf with your desired set of credentials. + +Note that resources created via the Terraform project cost a small amount of money per hour. + +To set up the test cluster: + +```hcl +$ make set-up-env +... +Apply complete! Resources: 4 added, 0 changed, 0 destroyed. +``` + +### Environment Teardown + +The test cluster created via the set-up-env command can be destroyed using the teardown-env command. + +```hcl +$ make teardown-env +... +Destroy complete! Resources: 4 destroyed. +``` + ### Testing Manually Put the plugin binary into a location of your choice. This directory @@ -114,9 +144,9 @@ Configure a role: ```sh $ vault write database/roles/redis-myrole \ db_name="redis-mydb" \ - creation_statements=$CREATION_STATEMENTS \ - default_ttl=$DEFAULT_TTL \ - max_ttl=$MAX_TTL + creation_statements="on ~* +@all" \ + default_ttl=5m \ + max_ttl=15m ... Success! Data written to: database/roles/redis-myrole @@ -151,3 +181,25 @@ You can also specify a `TESTARGS` variable to filter tests like so: ```sh $ make test TESTARGS='-run=TestConfig' ``` + +### Acceptance Tests + +The majority of tests must communicate with an existing ElastiCache instance. See the [Environment Set Up](#environment-set-up) section for instructions on how to prepare a test cluster. + +Some environment variables are required to run tests expecting to communicate with an ElastiCache cluster. +The username and password should be valid IAM access key and secret key with read and write access to the ElastiCache cluster used for testing. The URL should be the complete configuration endpoint including the port, for example: `vault-plugin-elasticache-test.id.xxx.use1.cache.amazonaws.com:6379`. + +```sh +$ export TEST_ELASTICACHE_USERNAME="AWS ACCESS KEY ID" +$ export TEST_ELASTICACHE_PASSWORD="AWS SECRET ACCESS KEY" +$ export TEST_ELASTICACHE_URL="vault-plugin-elasticache-test.id.xxx.use1.cache.amazonaws.com:6379" +$ export TEST_ELASTICACHE_REGION="us-east-1" + +$ make test +``` + +You can also specify a `TESTARGS` variable to filter tests like so: + +```sh +$ make test TESTARGS='-run=TestConfig' +``` \ No newline at end of file diff --git a/bootstrap/terraform/elasticache.tf b/bootstrap/terraform/elasticache.tf new file mode 100644 index 0000000..ac020bd --- /dev/null +++ b/bootstrap/terraform/elasticache.tf @@ -0,0 +1,78 @@ +provider "aws" { + // Credentials and configuration derived from the environment + // Uncomment if you wish to configure the provider explicitly + + // access_key = "" + // secret_key = "" + // region = "" +} + +resource "aws_elasticache_cluster" "vault_plugin_elasticache_test" { + cluster_id = "vault-plugin-elasticache-test" + engine = "redis" + engine_version = "6.2" + node_type = "cache.t4g.micro" + num_cache_nodes = 1 + parameter_group_name = "default.redis6.x" + + tags = { + "description" : "vault elasticache plugin generated test cluster" + } +} + +resource "aws_iam_user" "vault_plugin_elasticache_test" { + name = "vault-plugin-elasticache-user-test" + + tags = { + "description" : "vault elasticache plugin generated test user" + } +} + +resource "aws_iam_access_key" "vault_plugin_elasticache_test" { + user = aws_iam_user.vault_plugin_elasticache_test.name +} + +resource "aws_iam_user_policy" "vault_plugin_elasticache_test" { + name = "vault-plugin-elasticache-policy-test" + user = aws_iam_user.vault_plugin_elasticache_test.name + + policy = data.aws_iam_policy_document.vault_plugin_elasticache_test.json +} + +data "aws_iam_policy_document" "vault_plugin_elasticache_test" { + statement { + actions = [ + "elasticache:DescribeUsers", + "elasticache:CreateUser", + "elasticache:ModifyUser", + "elasticache:DeleteUser", + ] + resources = ["arn:aws:elasticache:*:*:user:*"] + } +} + +// export TEST_ELASTICACHE_USERNAME=${username} +output "username" { + value = aws_iam_access_key.vault_plugin_elasticache_test.id +} + +// export TEST_ELASTICACHE_PASSWORD=${password} +// Use `terraform output password` to access the value +output "password" { + sensitive = true + value = aws_iam_access_key.vault_plugin_elasticache_test.secret +} + +// export TEST_ELASTICACHE_URL=${url} +output "url" { + value = format( + "%s:%s", + aws_elasticache_cluster.vault_plugin_elasticache_test.cache_nodes[0].address, + aws_elasticache_cluster.vault_plugin_elasticache_test.port) +} + +// export TEST_ELASTICACHE_REGION=${region} +data "aws_region" "current" {} +output "region" { + value = data.aws_region.current.name +} diff --git a/cmd/vault-plugin-database-redis-elasticache/main.go b/cmd/vault-plugin-database-redis-elasticache/main.go index bd49749..ad8aaef 100644 --- a/cmd/vault-plugin-database-redis-elasticache/main.go +++ b/cmd/vault-plugin-database-redis-elasticache/main.go @@ -5,7 +5,6 @@ import ( "os" "github.com/hashicorp/vault-plugin-database-redis-elasticache/internal/plugin" - "github.com/hashicorp/vault/sdk/database/dbplugin/v5" ) @@ -18,10 +17,7 @@ func main() { // Run starts serving the plugin func Run() error { - db, err := plugin.New() - if err != nil { - return err - } - dbplugin.Serve(db.(dbplugin.Database)) + dbplugin.ServeMultiplex(plugin.New) + return nil } diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 5cea433..70cf631 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -24,7 +24,7 @@ type config struct { Region string `mapstructure:"region,omitempty"` } -func New() (dbplugin.Database, error) { +func New() (interface{}, error) { logger := hclog.New(&hclog.LoggerOptions{ Level: hclog.Trace, Output: os.Stderr, diff --git a/internal/plugin/redisElastiCacheClient_test.go b/internal/plugin/redisElastiCacheClient_test.go index 79575a5..92180d2 100644 --- a/internal/plugin/redisElastiCacheClient_test.go +++ b/internal/plugin/redisElastiCacheClient_test.go @@ -1,9 +1,424 @@ package plugin import ( + "context" + "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/database/dbplugin/v5" + "os" + "reflect" + "strings" "testing" + "time" ) +type fields struct { + logger hclog.Logger + config config + client *elasticache.ElastiCache +} + +type args struct { + ctx context.Context + req interface{} +} + +type testCases []struct { + name string + fields fields + args args + want interface{} + wantErr bool +} + +func skipIfEnvIsUnset(t *testing.T, config config) { + if config.Username == "" || config.Password == "" || config.Url == "" || config.Region == "" { + t.Skip("Skipping acceptance tests because required environment variables are not configured") + } +} + +func setUpEnvironment() (fields, map[string]interface{}, redisElastiCacheDB) { + username := os.Getenv("TEST_ELASTICACHE_USERNAME") + password := os.Getenv("TEST_ELASTICACHE_PASSWORD") + url := os.Getenv("TEST_ELASTICACHE_URL") + region := os.Getenv("TEST_ELASTICACHE_REGION") + + f := fields{ + logger: hclog.New(&hclog.LoggerOptions{ + Level: hclog.Trace, + Output: os.Stderr, + JSONFormat: true, + }), + config: config{ + Username: username, + Password: password, + Url: url, + Region: region, + }, + client: nil, + } + + c := map[string]interface{}{ + "username": username, + "password": password, + "url": url, + "region": region, + } + + r := redisElastiCacheDB{ + logger: f.logger, + config: f.config, + client: f.client, + } + + return f, c, r +} + +func setUpClient(t *testing.T, r *redisElastiCacheDB, config map[string]interface{}) { + _, err := r.Initialize(nil, dbplugin.InitializeRequest{ + Config: config, + VerifyConnection: true, + }) + + if err != nil { + t.Errorf("unable to pre initialize redis client for test cases: %v", err) + } +} + +func setUpTestUser(t *testing.T, r *redisElastiCacheDB) string { + user, err := r.NewUser(nil, dbplugin.NewUserRequest{ + UsernameConfig: dbplugin.UsernameMetadata{ + DisplayName: "display", + RoleName: "role", + }, + Statements: dbplugin.Statements{ + Commands: []string{"on ~test* -@all +@read"}, + }, + Password: "abcdefghijklmnopqrstuvwxyz", + }) + + if err != nil { + t.Errorf("unable to provision test user for test cases: %v", err) + } + + return user.Username +} + +func teardownTestUser(t *testing.T, r redisElastiCacheDB, username string) { + if username == "" { + return + } + + // Creating or Modifying users cannot be deleted until they return to Active status + for i := 0; i < 20; i++ { + _, err := r.DeleteUser(nil, dbplugin.DeleteUserRequest{ + Username: username, + } + + if err == nil { + break + } else { + t.Logf("unable to clean test user '%s' due to: %v; retrying", username, err) + } + + time.Sleep(3 * time.Second) + } +} + +func Test_redisElastiCacheDB_Initialize(t *testing.T) { + f, c, r := setUpEnvironment() + skipIfEnvIsUnset(t, f.config) + + tests := testCases{ + { + name: "initialize and verify connection succeeds", + fields: f, + args: args{ + req: dbplugin.InitializeRequest{ + Config: c, + VerifyConnection: true, + }, + }, + want: dbplugin.InitializeResponse{ + Config: c, + }, + }, + { + name: "initialize with invalid config fails", + fields: f, + args: args{ + req: dbplugin.InitializeRequest{ + Config: map[string]interface{}{ + "username": "wrong", + "password": "wrong", + "url": "wrong", + "region": "wrong", + }, + VerifyConnection: true, + }, + }, + want: dbplugin.InitializeResponse{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &r + got, err := r.Initialize(tt.args.ctx, tt.args.req.(dbplugin.InitializeRequest)) + if (err != nil) != tt.wantErr { + t.Errorf("Initialize() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Initialize() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_redisElastiCacheDB_NewUser(t *testing.T) { + f, c, r := setUpEnvironment() + + skipIfEnvIsUnset(t, f.config) + + setUpClient(t, &r, c) + + tests := testCases{ + { + name: "create new valid user succeeds", + fields: f, + args: args{ + ctx: context.Background(), + req: dbplugin.NewUserRequest{ + UsernameConfig: dbplugin.UsernameMetadata{ + DisplayName: "display", + RoleName: "role", + }, + Statements: dbplugin.Statements{ + Commands: []string{"on ~test* -@all +@read"}, + }, + Password: "abcdefghijklmnopqrstuvwxyz", + }, + }, + want: dbplugin.NewUserResponse{ + Username: "v_displ_role_", + }, + }, + { + name: "create new valid user from multiple commands", + fields: f, + args: args{ + ctx: context.Background(), + req: dbplugin.NewUserRequest{ + UsernameConfig: dbplugin.UsernameMetadata{ + DisplayName: "display", + RoleName: "role", + }, + Statements: dbplugin.Statements{ + Commands: []string{"on", "~test*", "-@all", "+@read"}, + }, + Password: "abcdefghijklmnopqrstuvwxyz", + }, + }, + want: dbplugin.NewUserResponse{ + Username: "v_displ_role_", + }, + }, + { + name: "create user truncates username", + fields: f, + args: args{ + ctx: context.Background(), + req: dbplugin.NewUserRequest{ + UsernameConfig: dbplugin.UsernameMetadata{ + DisplayName: "iAmSupeExtremelyLongThisWillHaveToBeTruncated", + RoleName: "iAmEvenLongerTheApiWillDefinitelyRejectUsIfWeArePassedAsIsWithoutAnyModifications", + }, + Statements: dbplugin.Statements{ + Commands: []string{"on ~test* -@all +@read"}, + }, + Password: "abcdefghijklmnopqrstuvwxyz", + }, + }, + want: dbplugin.NewUserResponse{ + Username: "v_iAmSu_iAmEvenLongerTheApiWillDefinitelyRejec", + }, + }, + { + name: "create user with invalid password fails", + fields: f, + args: args{ + ctx: context.Background(), + req: dbplugin.NewUserRequest{ + UsernameConfig: dbplugin.UsernameMetadata{ + DisplayName: "display", + RoleName: "role", + }, + Statements: dbplugin.Statements{ + Commands: []string{"+@all"}, + }, + Password: "too short", + }, + }, + want: dbplugin.NewUserResponse{}, + wantErr: true, + }, + { + name: "create user with invalid statements fails", + fields: f, + args: args{ + ctx: context.Background(), + req: dbplugin.NewUserRequest{ + UsernameConfig: dbplugin.UsernameMetadata{ + DisplayName: "display", + RoleName: "role", + }, + Statements: dbplugin.Statements{ + Commands: []string{"+@invalid"}, + }, + Password: "abcdefghijklmnopqrstuvwxyz", + }, + }, + want: dbplugin.NewUserResponse{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := r.NewUser(tt.args.ctx, tt.args.req.(dbplugin.NewUserRequest)) + if (err != nil) != tt.wantErr { + t.Errorf("NewUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !strings.HasPrefix(got.Username, tt.want.(dbplugin.NewUserResponse).Username) { + t.Errorf("NewUser() got = %v, want %v", got, tt.want) + } + + teardownTestUser(t, r, got.Username) + }) + } +} + +func Test_redisElastiCacheDB_UpdateUser(t *testing.T) { + f, c, r := setUpEnvironment() + + skipIfEnvIsUnset(t, f.config) + + setUpClient(t, &r, c) + username := setUpTestUser(t, &r) + defer teardownTestUser(t, r, username) + + tests := testCases{ + { + name: "update password of existing user succeeds", + fields: f, + args: args{ + ctx: context.Background(), + req: dbplugin.UpdateUserRequest{ + Username: username, + CredentialType: 0, + Password: &dbplugin.ChangePassword{ + NewPassword: "abcdefghijklmnopqrstuvwxyz1", + }, + }, + }, + want: dbplugin.UpdateUserResponse{}, + }, + { + name: "update password of non-existing user fails", + fields: f, + args: args{ + ctx: context.Background(), + req: dbplugin.UpdateUserRequest{ + Username: "I do not exist", + CredentialType: 0, + Password: &dbplugin.ChangePassword{ + NewPassword: "abcdefghijklmnopqrstuvwxyz1", + }, + }, + }, + want: dbplugin.UpdateUserResponse{}, + wantErr: true, + }, + { + name: "update to invalid password fails", + fields: f, + args: args{ + ctx: context.Background(), + req: dbplugin.UpdateUserRequest{ + Username: username, + CredentialType: 0, + Password: &dbplugin.ChangePassword{ + NewPassword: "too short", + }, + }, + }, + want: dbplugin.UpdateUserResponse{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := r.UpdateUser(tt.args.ctx, tt.args.req.(dbplugin.UpdateUserRequest)) + if (err != nil) != tt.wantErr { + t.Errorf("UpdateUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("UpdateUser() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_redisElastiCacheDB_DeleteUser(t *testing.T) { + f, c, r := setUpEnvironment() + + skipIfEnvIsUnset(t, f.config) + + setUpClient(t, &r, c) + username := setUpTestUser(t, &r) + + tests := testCases{ + { + name: "delete existing user succeeds", + fields: f, + args: args{ + ctx: context.Background(), + req: dbplugin.DeleteUserRequest{ + Username: username, + }, + }, + want: dbplugin.DeleteUserResponse{}, + wantErr: false, + }, + { + name: "delete non-existing user fails", + fields: f, + args: args{ + ctx: context.Background(), + req: dbplugin.DeleteUserRequest{ + Username: "I do not exist", + }, + }, + want: dbplugin.DeleteUserResponse{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := r.DeleteUser(tt.args.ctx, tt.args.req.(dbplugin.DeleteUserRequest)) + if (err != nil) != tt.wantErr { + t.Errorf("DeleteUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("DeleteUser() got = %v, want %v", got, tt.want) + } + }) + } +} + func Test_generateUserId(t *testing.T) { type args struct { username string From 110ab6683b8305e6794029a3bc66ec13f0464dff Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Mon, 22 Aug 2022 11:31:58 -0400 Subject: [PATCH 03/17] * go mod tidy --- go.mod | 4 ++-- go.sum | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 4c76703..4f66668 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,10 @@ module github.com/hashicorp/vault-plugin-database-redis-elasticache go 1.17 require ( + github.com/aws/aws-sdk-go v1.44.81 github.com/hashicorp/go-hclog v1.2.2 - github.com/hashicorp/vault/api v1.7.2 github.com/hashicorp/vault/sdk v0.5.3 + github.com/mitchellh/mapstructure v1.5.0 ) require ( @@ -28,7 +29,6 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/oklog/run v1.0.0 // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect diff --git a/go.sum b/go.sum index 7e9413d..23e627f 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/armon/go-metrics v0.3.9 h1:O2sNqxBdvq8Eq5xmzljcYzAORli6RWCvEym4cJf9m1 github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aws/aws-sdk-go v1.44.76 h1:5e8yGO/XeNYKckOjpBKUd5wStf0So3CrQIiOMCVLpOI= -github.com/aws/aws-sdk-go v1.44.76/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.81 h1:C8oBZ+a+ka0qk3Q24MohQIFq0tkbO8IAu5tfpAMKVWE= +github.com/aws/aws-sdk-go v1.44.81/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -107,7 +107,6 @@ github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PU github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= @@ -123,9 +122,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/vault/api v1.7.2 h1:kawHE7s/4xwrdKbkmwQi0wYaIeUhk5ueek7ljuezCVQ= -github.com/hashicorp/vault/api v1.7.2/go.mod h1:xbfA+1AvxFseDzxxdWaL0uO99n1+tndus4GCrtouy0M= -github.com/hashicorp/vault/sdk v0.5.1/go.mod h1:DoGraE9kKGNcVgPmTuX357Fm6WAx1Okvde8Vp3dPDoU= github.com/hashicorp/vault/sdk v0.5.3 h1:PWY8sq/9pRrK9vUIy75qCH2Jd8oeENAgkaa/qbhzFrs= github.com/hashicorp/vault/sdk v0.5.3/go.mod h1:DoGraE9kKGNcVgPmTuX357Fm6WAx1Okvde8Vp3dPDoU= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= From 0b39bfd21a781e2498d9171160cfc042c46dd960 Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Mon, 22 Aug 2022 11:34:32 -0400 Subject: [PATCH 04/17] * fix tests --- internal/plugin/redisElastiCacheClient_test.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/plugin/redisElastiCacheClient_test.go b/internal/plugin/redisElastiCacheClient_test.go index 92180d2..86234dd 100644 --- a/internal/plugin/redisElastiCacheClient_test.go +++ b/internal/plugin/redisElastiCacheClient_test.go @@ -2,14 +2,15 @@ package plugin import ( "context" - "github.com/aws/aws-sdk-go/service/elasticache" - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "os" "reflect" "strings" "testing" "time" + + "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/database/dbplugin/v5" ) type fields struct { @@ -79,7 +80,6 @@ func setUpClient(t *testing.T, r *redisElastiCacheDB, config map[string]interfac Config: config, VerifyConnection: true, }) - if err != nil { t.Errorf("unable to pre initialize redis client for test cases: %v", err) } @@ -96,7 +96,6 @@ func setUpTestUser(t *testing.T, r *redisElastiCacheDB) string { }, Password: "abcdefghijklmnopqrstuvwxyz", }) - if err != nil { t.Errorf("unable to provision test user for test cases: %v", err) } @@ -113,7 +112,7 @@ func teardownTestUser(t *testing.T, r redisElastiCacheDB, username string) { for i := 0; i < 20; i++ { _, err := r.DeleteUser(nil, dbplugin.DeleteUserRequest{ Username: username, - } + }) if err == nil { break From ecb43aa87c9e46d031024a578c36e24c040d1b76 Mon Sep 17 00:00:00 2001 From: Max Coulombe <109547106+maxcoulombe@users.noreply.github.com> Date: Mon, 22 Aug 2022 11:35:37 -0400 Subject: [PATCH 05/17] Vault 7721 ElastiCache Acceptance Tests (#3) + added acceptance tests * converted to multiplex version --- .github/workflows/jira.yaml | 72 +++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/jira.yaml diff --git a/.github/workflows/jira.yaml b/.github/workflows/jira.yaml new file mode 100644 index 0000000..877b3c3 --- /dev/null +++ b/.github/workflows/jira.yaml @@ -0,0 +1,72 @@ +on: + issues: + types: [opened, closed, deleted, reopened] + pull_request_target: + types: [opened, closed, reopened] + issue_comment: # Also triggers when commenting on a PR from the conversation view + types: [created] + +name: Jira Sync + +jobs: + sync: + runs-on: ubuntu-latest + name: Jira sync + steps: + - name: Login + uses: atlassian/gajira-login@v2.0.0 + env: + JIRA_BASE_URL: ${{ secrets.JIRA_SYNC_BASE_URL }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_SYNC_USER_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_SYNC_API_TOKEN }} + + - name: Preprocess + if: github.event.action == 'opened' || github.event.action == 'created' + id: preprocess + run: | + if [[ "${{ github.event_name }}" == "pull_request_target" ]]; then + echo "::set-output name=type::PR" + else + echo "::set-output name=type::ISS" + fi + + - name: Create ticket + if: github.event.action == 'opened' + uses: tomhjp/gh-action-jira-create@v0.2.0 + with: + project: VAULT + issuetype: "GH Issue" + summary: "${{ github.event.repository.name }} [${{ steps.preprocess.outputs.type }} #${{ github.event.issue.number || github.event.pull_request.number }}]: ${{ github.event.issue.title || github.event.pull_request.title }}" + description: "${{ github.event.issue.body || github.event.pull_request.body }}\n\n_Created from GitHub Action for ${{ github.event.issue.html_url || github.event.pull_request.html_url }} from ${{ github.actor }}_" + # customfield_10089 is Issue Link custom field + # customfield_10091 is team custom field + extraFields: '{"fixVersions": [{"name": "TBD"}], "customfield_10091": ["ecosystem"], "customfield_10089": "${{ github.event.issue.html_url || github.event.pull_request.html_url }}"}' + + - name: Search + if: github.event.action != 'opened' + id: search + uses: tomhjp/gh-action-jira-search@v0.2.1 + with: + # cf[10089] is Issue Link custom field + jql: 'project = "VAULT" and cf[10089]="${{ github.event.issue.html_url || github.event.pull_request.html_url }}"' + + - name: Sync comment + if: github.event.action == 'created' && steps.search.outputs.issue + uses: tomhjp/gh-action-jira-comment@v0.2.0 + with: + issue: ${{ steps.search.outputs.issue }} + comment: "${{ github.actor }} ${{ github.event.review.state || 'commented' }}:\n\n${{ github.event.comment.body || github.event.review.body }}\n\n${{ github.event.comment.html_url || github.event.review.html_url }}" + + - name: Close ticket + if: (github.event.action == 'closed' || github.event.action == 'deleted') && steps.search.outputs.issue + uses: atlassian/gajira-transition@v2.0.1 + with: + issue: ${{ steps.search.outputs.issue }} + transition: Close + + - name: Reopen ticket + if: github.event.action == 'reopened' && steps.search.outputs.issue + uses: atlassian/gajira-transition@v2.0.1 + with: + issue: ${{ steps.search.outputs.issue }} + transition: "Pending Triage" \ No newline at end of file From bced1df76f013b61d57fc02c0eec52ec8901dddd Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Mon, 22 Aug 2022 11:39:28 -0400 Subject: [PATCH 06/17] * update jira action --- .github/workflows/jira.yaml | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/jira.yaml b/.github/workflows/jira.yaml index 877b3c3..27baf44 100644 --- a/.github/workflows/jira.yaml +++ b/.github/workflows/jira.yaml @@ -13,6 +13,21 @@ jobs: runs-on: ubuntu-latest name: Jira sync steps: + - name: Check if community user + if: github.event.action == 'opened' + id: vault-team-role + run: | + TEAM=vault + ROLE="$(hub api orgs/hashicorp/teams/${TEAM}/memberships/${{ github.actor }} | jq -r '.role | select(.!=null)')" + if [[ -n ${ROLE} ]]; then + echo "Actor ${{ github.actor }} is a ${TEAM} team member, skipping ticket creation" + else + echo "Actor ${{ github.actor }} is not a ${TEAM} team member" + fi + echo "::set-output name=role::${ROLE}" + env: + GITHUB_TOKEN: ${{ secrets.JIRA_SYNC_GITHUB_TOKEN }} + - name: Login uses: atlassian/gajira-login@v2.0.0 env: @@ -31,7 +46,7 @@ jobs: fi - name: Create ticket - if: github.event.action == 'opened' + if: github.event.action == 'opened' && !steps.vault-team-role.outputs.role uses: tomhjp/gh-action-jira-create@v0.2.0 with: project: VAULT @@ -48,7 +63,7 @@ jobs: uses: tomhjp/gh-action-jira-search@v0.2.1 with: # cf[10089] is Issue Link custom field - jql: 'project = "VAULT" and cf[10089]="${{ github.event.issue.html_url || github.event.pull_request.html_url }}"' + jql: 'project = "VAULT" and issuetype = "GH Issue" and cf[10089]="${{ github.event.issue.html_url || github.event.pull_request.html_url }}"' - name: Sync comment if: github.event.action == 'created' && steps.search.outputs.issue @@ -62,11 +77,11 @@ jobs: uses: atlassian/gajira-transition@v2.0.1 with: issue: ${{ steps.search.outputs.issue }} - transition: Close + transition: Done - name: Reopen ticket if: github.event.action == 'reopened' && steps.search.outputs.issue uses: atlassian/gajira-transition@v2.0.1 with: issue: ${{ steps.search.outputs.issue }} - transition: "Pending Triage" \ No newline at end of file + transition: "To Do" From 28c410c67438734799ed9ae33664bf437fd6f229 Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Tue, 23 Aug 2022 11:48:51 -0400 Subject: [PATCH 07/17] * review refactoring --- README.md | 4 +-- internal/plugin/plugin.go | 17 --------- ...eClient.go => redis_elasticache_client.go} | 35 ++++++++++++++----- ...st.go => redis_elasticache_client_test.go} | 0 4 files changed, 28 insertions(+), 28 deletions(-) rename internal/plugin/{redisElastiCacheClient.go => redis_elasticache_client.go} (84%) rename internal/plugin/{redisElastiCacheClient_test.go => redis_elasticache_client_test.go} (100%) diff --git a/README.md b/README.md index 47dcda7..5dbda01 100644 --- a/README.md +++ b/README.md @@ -71,14 +71,14 @@ Note that resources created via the Terraform project cost a small amount of mon To set up the test cluster: ```hcl -$ make set-up-env +$ make setup-env ... Apply complete! Resources: 4 added, 0 changed, 0 destroyed. ``` ### Environment Teardown -The test cluster created via the set-up-env command can be destroyed using the teardown-env command. +The test cluster created via the setup-env command can be destroyed using the teardown-env command. ```hcl $ make teardown-env diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 70cf631..f9254c6 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -3,27 +3,10 @@ package plugin import ( "os" - "github.com/aws/aws-sdk-go/service/elasticache" "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/sdk/database/dbplugin/v5" ) -// Verify interface is implemented -var _ dbplugin.Database = (*redisElastiCacheDB)(nil) - -type redisElastiCacheDB struct { - logger hclog.Logger - config config - client *elasticache.ElastiCache -} - -type config struct { - Username string `mapstructure:"username,omitempty"` - Password string `mapstructure:"password,omitempty"` - Url string `mapstructure:"url,omitempty"` - Region string `mapstructure:"region,omitempty"` -} - func New() (interface{}, error) { logger := hclog.New(&hclog.LoggerOptions{ Level: hclog.Trace, diff --git a/internal/plugin/redisElastiCacheClient.go b/internal/plugin/redis_elasticache_client.go similarity index 84% rename from internal/plugin/redisElastiCacheClient.go rename to internal/plugin/redis_elasticache_client.go index 151a9a0..45e96ef 100644 --- a/internal/plugin/redisElastiCacheClient.go +++ b/internal/plugin/redis_elasticache_client.go @@ -7,6 +7,8 @@ import ( "strings" "unicode" + "github.com/hashicorp/go-hclog" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" @@ -16,11 +18,23 @@ import ( "github.com/mitchellh/mapstructure" ) -var ( - noPasswordRequired = false - engine = "Redis" - nonAlphanumericRegex = regexp.MustCompile("[^a-zA-Z\\d]+") -) +var nonAlphanumericRegex = regexp.MustCompile("[^a-zA-Z\\d]+") + +// Verify interface is implemented +var _ dbplugin.Database = (*redisElastiCacheDB)(nil) + +type redisElastiCacheDB struct { + logger hclog.Logger + config config + client *elasticache.ElastiCache +} + +type config struct { + Username string `mapstructure:"username,omitempty"` + Password string `mapstructure:"password,omitempty"` + Url string `mapstructure:"url,omitempty"` + Region string `mapstructure:"region,omitempty"` +} func (r *redisElastiCacheDB) Initialize(_ context.Context, req dbplugin.InitializeRequest) (dbplugin.InitializeResponse, error) { r.logger.Debug("initializing AWS ElastiCache Redis client") @@ -29,10 +43,13 @@ func (r *redisElastiCacheDB) Initialize(_ context.Context, req dbplugin.Initiali return dbplugin.InitializeResponse{}, err } - sess := session.Must(session.NewSession(&aws.Config{ + sess, err := session.NewSession(&aws.Config{ Region: aws.String(r.config.Region), Credentials: credentials.NewStaticCredentials(r.config.Username, r.config.Password, ""), - })) + }) + if err != nil { + return dbplugin.InitializeResponse{}, fmt.Errorf("unable to initialize AWS session: %w", err) + } r.client = elasticache.New(sess) if req.VerifyConnection { @@ -77,8 +94,8 @@ func (r *redisElastiCacheDB) NewUser(_ context.Context, req dbplugin.NewUserRequ output, err := r.client.CreateUser(&elasticache.CreateUserInput{ AccessString: &accessString, - Engine: &engine, - NoPasswordRequired: &noPasswordRequired, + Engine: aws.String("Redis"), + NoPasswordRequired: aws.Bool(false), Passwords: []*string{&req.Password}, Tags: []*elasticache.Tag{}, UserId: &userId, diff --git a/internal/plugin/redisElastiCacheClient_test.go b/internal/plugin/redis_elasticache_client_test.go similarity index 100% rename from internal/plugin/redisElastiCacheClient_test.go rename to internal/plugin/redis_elasticache_client_test.go From e84347d37e399ef7663fcf45e9c8eb8eeba30de6 Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Wed, 24 Aug 2022 22:57:13 -0400 Subject: [PATCH 08/17] * refactored package * changed creation_statements format --- Makefile | 2 +- README.md | 10 +- bootstrap/terraform/elasticache.tf | 49 +++++++-- .../main.go | 4 +- internal/plugin/plugin.go => plugin.go | 2 +- ...e_client.go => redis_elasticache_client.go | 75 +++++++++---- ...est.go => redis_elasticache_client_test.go | 102 +++++++++++++----- 7 files changed, 176 insertions(+), 68 deletions(-) rename internal/plugin/plugin.go => plugin.go (96%) rename internal/plugin/redis_elasticache_client.go => redis_elasticache_client.go (70%) rename internal/plugin/redis_elasticache_client_test.go => redis_elasticache_client_test.go (82%) diff --git a/Makefile b/Makefile index 561cb33..d21f014 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ fmtcheck: .PHONY: fmt fmt: - gofumpt -l -w . + gofumpt -l -w . && cd bootstrap/terraform && terraform fmt .PHONY: setup-env setup-env: diff --git a/README.md b/README.md index 5dbda01..fad4a43 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Note that resources created via the Terraform project cost a small amount of mon To set up the test cluster: -```hcl +```sh $ make setup-env ... Apply complete! Resources: 4 added, 0 changed, 0 destroyed. @@ -80,7 +80,7 @@ Apply complete! Resources: 4 added, 0 changed, 0 destroyed. The test cluster created via the setup-env command can be destroyed using the teardown-env command. -```hcl +```sh $ make teardown-env ... Destroy complete! Resources: 4 destroyed. @@ -109,9 +109,7 @@ Once the server is started, register the plugin in the Vault server's [plugin ca ```sh $ SHA256=$(openssl dgst -sha256 $GOPATH/vault-plugin-database-redis-elasticache | cut -d ' ' -f2) -$ vault write sys/plugins/catalog/database/vault-plugin-database-redis-elasticache \ - command=vault-plugin-database-redis-elasticache \ - sha256=$SHA256 +$ vault plugin register -sha256=$SHA256 database vault-plugin-database-redis-elasticache ... Success! Data written to: sys/plugins/catalog/database/vault-plugin-database-redis-elasticache ``` @@ -144,7 +142,7 @@ Configure a role: ```sh $ vault write database/roles/redis-myrole \ db_name="redis-mydb" \ - creation_statements="on ~* +@all" \ + creation_statements='["~*", "+@read"]' \ default_ttl=5m \ max_ttl=15m ... diff --git a/bootstrap/terraform/elasticache.tf b/bootstrap/terraform/elasticache.tf index ac020bd..0e6b819 100644 --- a/bootstrap/terraform/elasticache.tf +++ b/bootstrap/terraform/elasticache.tf @@ -7,13 +7,15 @@ provider "aws" { // region = "" } -resource "aws_elasticache_cluster" "vault_plugin_elasticache_test" { - cluster_id = "vault-plugin-elasticache-test" - engine = "redis" - engine_version = "6.2" - node_type = "cache.t4g.micro" - num_cache_nodes = 1 - parameter_group_name = "default.redis6.x" +resource "aws_elasticache_replication_group" "vault_plugin_elasticache_test" { + replication_group_id = "vault-plugin-elasticache-test" + description = "vault elasticache plugin generated test cluster" + engine = "redis" + engine_version = "6.2" + node_type = "cache.t4g.micro" + num_cache_clusters = 1 + parameter_group_name = "default.redis6.x" + transit_encryption_enabled = true tags = { "description" : "vault elasticache plugin generated test cluster" @@ -47,7 +49,32 @@ data "aws_iam_policy_document" "vault_plugin_elasticache_test" { "elasticache:ModifyUser", "elasticache:DeleteUser", ] - resources = ["arn:aws:elasticache:*:*:user:*"] + resources = [ + "arn:aws:elasticache:*:*:user:*", + ] + } + + statement { + actions = [ + "elasticache:DescribeUserGroups", + "elasticache:CreateUserGroup", + "elasticache:ModifyUserGroup", + "elasticache:DeleteUserGroup", + "elasticache:ModifyReplicationGroup", + ] + resources = [ + "arn:aws:elasticache:*:*:usergroup:*", + ] + } + + statement { + actions = [ + "elasticache:DescribeReplicationGroups", + "elasticache:ModifyReplicationGroup", + ] + resources = [ + "arn:aws:elasticache:*:*:replicationgroup:*", + ] } } @@ -60,15 +87,15 @@ output "username" { // Use `terraform output password` to access the value output "password" { sensitive = true - value = aws_iam_access_key.vault_plugin_elasticache_test.secret + value = aws_iam_access_key.vault_plugin_elasticache_test.secret } // export TEST_ELASTICACHE_URL=${url} output "url" { value = format( "%s:%s", - aws_elasticache_cluster.vault_plugin_elasticache_test.cache_nodes[0].address, - aws_elasticache_cluster.vault_plugin_elasticache_test.port) + aws_elasticache_replication_group.vault_plugin_elasticache_test.primary_endpoint_address, + aws_elasticache_replication_group.vault_plugin_elasticache_test.port) } // export TEST_ELASTICACHE_REGION=${region} diff --git a/cmd/vault-plugin-database-redis-elasticache/main.go b/cmd/vault-plugin-database-redis-elasticache/main.go index ad8aaef..512eac2 100644 --- a/cmd/vault-plugin-database-redis-elasticache/main.go +++ b/cmd/vault-plugin-database-redis-elasticache/main.go @@ -4,7 +4,7 @@ import ( "log" "os" - "github.com/hashicorp/vault-plugin-database-redis-elasticache/internal/plugin" + "github.com/hashicorp/vault-plugin-database-redis-elasticache" "github.com/hashicorp/vault/sdk/database/dbplugin/v5" ) @@ -17,7 +17,7 @@ func main() { // Run starts serving the plugin func Run() error { - dbplugin.ServeMultiplex(plugin.New) + dbplugin.ServeMultiplex(rediselasticache.New) return nil } diff --git a/internal/plugin/plugin.go b/plugin.go similarity index 96% rename from internal/plugin/plugin.go rename to plugin.go index f9254c6..0708f40 100644 --- a/internal/plugin/plugin.go +++ b/plugin.go @@ -1,4 +1,4 @@ -package plugin +package rediselasticache import ( "os" diff --git a/internal/plugin/redis_elasticache_client.go b/redis_elasticache_client.go similarity index 70% rename from internal/plugin/redis_elasticache_client.go rename to redis_elasticache_client.go index 45e96ef..59ec8c4 100644 --- a/internal/plugin/redis_elasticache_client.go +++ b/redis_elasticache_client.go @@ -1,24 +1,27 @@ -package plugin +package rediselasticache import ( "context" + "encoding/json" "fmt" "regexp" "strings" "unicode" - "github.com/hashicorp/go-hclog" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/sdk/database/dbplugin/v5" "github.com/hashicorp/vault/sdk/database/helper/credsutil" "github.com/mitchellh/mapstructure" ) -var nonAlphanumericRegex = regexp.MustCompile("[^a-zA-Z\\d]+") +var ( + nonAlphanumericHyphenRegex = regexp.MustCompile("[^a-zA-Z\\d-]+") + doubleHyphenRegex = regexp.MustCompile("-{2,}") +) // Verify interface is implemented var _ dbplugin.Database = (*redisElastiCacheDB)(nil) @@ -88,18 +91,21 @@ func (r *redisElastiCacheDB) NewUser(_ context.Context, req dbplugin.NewUserRequ return dbplugin.NewUserResponse{}, fmt.Errorf("unable to generate username: %w", err) } - accessString := strings.Join(req.Statements.Commands[:], " ") + accessString, err := parseCreationCommands(req.Statements.Commands) + if err != nil { + return dbplugin.NewUserResponse{}, fmt.Errorf("unable to parse acess string: %w", err) + } - userId := generateUserId(username) + userId := normaliseId(username) output, err := r.client.CreateUser(&elasticache.CreateUserInput{ - AccessString: &accessString, + AccessString: aws.String(accessString), Engine: aws.String("Redis"), NoPasswordRequired: aws.Bool(false), Passwords: []*string{&req.Password}, Tags: []*elasticache.Tag{}, - UserId: &userId, - UserName: &username, + UserId: aws.String(userId), + UserName: aws.String(username), }) if err != nil { return dbplugin.NewUserResponse{}, fmt.Errorf("unable to create new user: %w", err) @@ -111,7 +117,7 @@ func (r *redisElastiCacheDB) NewUser(_ context.Context, req dbplugin.NewUserRequ func (r *redisElastiCacheDB) UpdateUser(_ context.Context, req dbplugin.UpdateUserRequest) (dbplugin.UpdateUserResponse, error) { r.logger.Debug("updating AWS ElastiCache Redis user", "username", req.Username) - userId := generateUserId(req.Username) + userId := normaliseId(req.Username) _, err := r.client.ModifyUser(&elasticache.ModifyUserInput{ UserId: &userId, @@ -127,7 +133,7 @@ func (r *redisElastiCacheDB) UpdateUser(_ context.Context, req dbplugin.UpdateUs func (r *redisElastiCacheDB) DeleteUser(_ context.Context, req dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) { r.logger.Debug("deleting AWS ElastiCache Redis user", "username", req.Username) - userId := generateUserId(req.Username) + userId := normaliseId(req.Username) _, err := r.client.DeleteUser(&elasticache.DeleteUserInput{ UserId: &userId, @@ -139,19 +145,50 @@ func (r *redisElastiCacheDB) DeleteUser(_ context.Context, req dbplugin.DeleteUs return dbplugin.DeleteUserResponse{}, nil } -// The ID can have up to 40 characters, and must begin with a letter. +func parseCreationCommands(commands []string) (string, error) { + if len(commands) == 0 { + return "on ~* +@read", nil + } + + accessString := "" + for _, command := range commands { + var rules []string + err := json.Unmarshal([]byte(command), &rules) + if err != nil { + return "", err + } + + accessString += strings.Join(rules, " ") + accessString += " " + } + + if !(strings.HasPrefix(accessString, "on ") || strings.Contains(accessString, " on ") || strings.HasSuffix(accessString, " on")) { + accessString = "on " + accessString + } + + accessString = strings.TrimSpace(accessString) + + return accessString, nil +} + +// All Elasticache IDs can have up to 40 characters, and must begin with a letter. // It should not end with a hyphen or contain two consecutive hyphens. // Valid characters: A-Z, a-z, 0-9, and -(hyphen). -func generateUserId(username string) string { - userId := nonAlphanumericRegex.ReplaceAllString(username, "") +func normaliseId(raw string) string { + normalized := nonAlphanumericHyphenRegex.ReplaceAllString(raw, "") + normalized = doubleHyphenRegex.ReplaceAllString(normalized, "") + + if len(normalized) > 40 { + normalized = normalized[len(normalized)-40:] + } - if len(userId) > 40 { - userId = userId[len(userId)-40:] + if unicode.IsNumber(rune(normalized[0])) { + normalized = string(rune('A'-17+normalized[0])) + normalized[1:] } - if unicode.IsNumber(rune(userId[0])) { - userId = string(rune('A'-17+userId[0])) + userId[1:] + if strings.HasSuffix(normalized, "-") { + normalized = normalized[:len(normalized)-1] + "x" } - return userId + return normalized } diff --git a/internal/plugin/redis_elasticache_client_test.go b/redis_elasticache_client_test.go similarity index 82% rename from internal/plugin/redis_elasticache_client_test.go rename to redis_elasticache_client_test.go index 86234dd..7fd5252 100644 --- a/internal/plugin/redis_elasticache_client_test.go +++ b/redis_elasticache_client_test.go @@ -1,4 +1,4 @@ -package plugin +package rediselasticache import ( "context" @@ -92,7 +92,7 @@ func setUpTestUser(t *testing.T, r *redisElastiCacheDB) string { RoleName: "role", }, Statements: dbplugin.Statements{ - Commands: []string{"on ~test* -@all +@read"}, + Commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, }, Password: "abcdefghijklmnopqrstuvwxyz", }) @@ -194,27 +194,7 @@ func Test_redisElastiCacheDB_NewUser(t *testing.T) { RoleName: "role", }, Statements: dbplugin.Statements{ - Commands: []string{"on ~test* -@all +@read"}, - }, - Password: "abcdefghijklmnopqrstuvwxyz", - }, - }, - want: dbplugin.NewUserResponse{ - Username: "v_displ_role_", - }, - }, - { - name: "create new valid user from multiple commands", - fields: f, - args: args{ - ctx: context.Background(), - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "display", - RoleName: "role", - }, - Statements: dbplugin.Statements{ - Commands: []string{"on", "~test*", "-@all", "+@read"}, + Commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, }, Password: "abcdefghijklmnopqrstuvwxyz", }, @@ -234,7 +214,7 @@ func Test_redisElastiCacheDB_NewUser(t *testing.T) { RoleName: "iAmEvenLongerTheApiWillDefinitelyRejectUsIfWeArePassedAsIsWithoutAnyModifications", }, Statements: dbplugin.Statements{ - Commands: []string{"on ~test* -@all +@read"}, + Commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, }, Password: "abcdefghijklmnopqrstuvwxyz", }, @@ -254,7 +234,7 @@ func Test_redisElastiCacheDB_NewUser(t *testing.T) { RoleName: "role", }, Statements: dbplugin.Statements{ - Commands: []string{"+@all"}, + Commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, }, Password: "too short", }, @@ -418,7 +398,73 @@ func Test_redisElastiCacheDB_DeleteUser(t *testing.T) { } } -func Test_generateUserId(t *testing.T) { +func Test_parseCreationCommands(t *testing.T) { + type testCases []struct { + name string + commands []string + want string + wantErr bool + } + + tests := testCases{ + { + name: "empty command returns read-only user", + commands: []string{}, + want: "on ~* +@read", + }, + { + name: "single command with multiple rules parses correctly", + commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, + want: "on ~test* -@all +@read", + }, + { + name: "multiple commands with multiple rules parses correctly", + commands: []string{"[\"~test*\"]", "[\"-@all\", \"+@read\"]"}, + want: "on ~test* -@all +@read", + }, + { + name: "'on' is added if missing for convenience", + commands: []string{"[\"~test*\"]"}, + want: "on ~test*", + }, + { + name: "'on' is ignored if passed at the beginning", + commands: []string{"[\"on\", \"~test*\"]"}, + want: "on ~test*", + }, + { + name: "'on' is ignored if passed explicitly within the rules", + commands: []string{"[\"~test*\", \"on\"]", "[\"+@read\"]"}, + want: "~test* on +@read", + }, + { + name: "'on' is ignored if passed explicitly at the end", + commands: []string{"[\"~test*\", \"on\"]"}, + want: "~test* on", + }, + { + name: "parsing invalid command format fails", + commands: []string{"{\"command:\", \"on ~* +@read\"}"}, + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseCreationCommands(tt.commands) + if (err != nil) != tt.wantErr { + t.Errorf("parseCreationCommands() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("parseCreationCommands() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_normaliseId(t *testing.T) { type args struct { username string } @@ -446,7 +492,7 @@ func Test_generateUserId(t *testing.T) { { name: "username with non-alphanumeric characters", args: args{username: "v_token_redis-role!/$}"}, - want: "vtokenredisrole", + want: "vtokenredis-role", }, { name: "username starting with a number", @@ -456,7 +502,7 @@ func Test_generateUserId(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := generateUserId(tt.args.username); got != tt.want { + if got := normaliseId(tt.args.username); got != tt.want { t.Errorf("generateUserId() = %v, want %v", got, tt.want) } }) From bdc53751d61946aeb4f65d9d2d377ea9c186c6b8 Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Wed, 24 Aug 2022 23:02:51 -0400 Subject: [PATCH 09/17] * typo --- redis_elasticache_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redis_elasticache_client.go b/redis_elasticache_client.go index 59ec8c4..6a863ac 100644 --- a/redis_elasticache_client.go +++ b/redis_elasticache_client.go @@ -93,7 +93,7 @@ func (r *redisElastiCacheDB) NewUser(_ context.Context, req dbplugin.NewUserRequ accessString, err := parseCreationCommands(req.Statements.Commands) if err != nil { - return dbplugin.NewUserResponse{}, fmt.Errorf("unable to parse acess string: %w", err) + return dbplugin.NewUserResponse{}, fmt.Errorf("unable to parse access string: %w", err) } userId := normaliseId(username) From 78aef98d5f3ee1cec87960862ca8f1c7f4be0973 Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Wed, 24 Aug 2022 23:11:58 -0400 Subject: [PATCH 10/17] * forbid off users --- redis_elasticache_client.go | 11 +++++++++-- redis_elasticache_client_test.go | 11 +++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/redis_elasticache_client.go b/redis_elasticache_client.go index 6a863ac..7227dce 100644 --- a/redis_elasticache_client.go +++ b/redis_elasticache_client.go @@ -3,6 +3,7 @@ package rediselasticache import ( "context" "encoding/json" + "errors" "fmt" "regexp" "strings" @@ -158,8 +159,14 @@ func parseCreationCommands(commands []string) (string, error) { return "", err } - accessString += strings.Join(rules, " ") - accessString += " " + if len(rules) > 0 { + accessString += strings.Join(rules, " ") + accessString += " " + } + } + + if strings.HasPrefix(accessString, "off ") || strings.Contains(accessString, " off ") || strings.HasSuffix(accessString, " off") { + return "", errors.New("creation of disabled or 'off' users is forbidden") } if !(strings.HasPrefix(accessString, "on ") || strings.Contains(accessString, " on ") || strings.HasSuffix(accessString, " on")) { diff --git a/redis_elasticache_client_test.go b/redis_elasticache_client_test.go index 7fd5252..26d51cb 100644 --- a/redis_elasticache_client_test.go +++ b/redis_elasticache_client_test.go @@ -422,6 +422,11 @@ func Test_parseCreationCommands(t *testing.T) { commands: []string{"[\"~test*\"]", "[\"-@all\", \"+@read\"]"}, want: "on ~test* -@all +@read", }, + { + name: "empty commands are tolerated", + commands: []string{"[\"~test*\"]", "[]", "[\"-@all\", \"+@read\"]"}, + want: "on ~test* -@all +@read", + }, { name: "'on' is added if missing for convenience", commands: []string{"[\"~test*\"]"}, @@ -448,6 +453,12 @@ func Test_parseCreationCommands(t *testing.T) { want: "", wantErr: true, }, + { + name: "creation of disabled users is forbidden", + commands: []string{"[\"~test*\", \"off\"]", "[\"+@read\"]"}, + want: "", + wantErr: true, + }, } for _, tt := range tests { From 681e2214a35e0e925d60db8e4c48f63b01c6f357 Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Fri, 26 Aug 2022 10:05:51 -0400 Subject: [PATCH 11/17] * supporting user groups --- redis_elasticache_client.go | 122 +++++++++++++++++++++++++++---- redis_elasticache_client_test.go | 29 ++------ 2 files changed, 117 insertions(+), 34 deletions(-) diff --git a/redis_elasticache_client.go b/redis_elasticache_client.go index 7227dce..7fe498f 100644 --- a/redis_elasticache_client.go +++ b/redis_elasticache_client.go @@ -7,9 +7,11 @@ import ( "fmt" "regexp" "strings" + "time" "unicode" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/elasticache" @@ -111,6 +113,50 @@ func (r *redisElastiCacheDB) NewUser(_ context.Context, req dbplugin.NewUserRequ if err != nil { return dbplugin.NewUserResponse{}, fmt.Errorf("unable to create new user: %w", err) } + r.waitForUserActiveState(userId) + + clusterId := extractClusterId(r.config.Url) + + _, err = r.client.DescribeUserGroups(&elasticache.DescribeUserGroupsInput{ + UserGroupId: aws.String(clusterId), + }) + if err != nil && err.(awserr.Error).Code() == "UserGroupNotFound" { + r.logger.Debug("bootstrapping vault user group for cluster", "cluster id", clusterId) + _, err = r.client.CreateUserGroup(&elasticache.CreateUserGroupInput{ + Engine: aws.String("Redis"), + Tags: []*elasticache.Tag{}, + UserGroupId: aws.String(clusterId), + UserIds: []*string{ + aws.String("default"), // User groups must contain a user with the username default + aws.String(userId), + }, + }) + if r.waitForGroupActiveState(clusterId) { + _, err = r.client.ModifyReplicationGroup(&elasticache.ModifyReplicationGroupInput{ + ReplicationGroupId: aws.String(clusterId), + UserGroupIdsToAdd: []*string{aws.String(clusterId)}, + }) + } else { + err = fmt.Errorf("unable to update replication group %s, newly created user group never turned active", clusterId) + } + } else { + if r.waitForGroupActiveState(clusterId) { + _, err = r.client.ModifyUserGroup(&elasticache.ModifyUserGroupInput{ + UserGroupId: aws.String(clusterId), + UserIdsToAdd: []*string{aws.String(userId)}, + }) + } else { + err = fmt.Errorf("unable to update user group %s, user group never turned active", clusterId) + } + } + + if err != nil { + r.logger.Debug("encountered error while configuring newly created user, attempting to clean up", "user id", userId) + _, _ = r.DeleteUser(nil, dbplugin.DeleteUserRequest{ + Username: userId, + }) + return dbplugin.NewUserResponse{}, fmt.Errorf("unable to configure newly created user %s: %w", userId, err) + } return dbplugin.NewUserResponse{Username: *output.UserName}, nil } @@ -120,15 +166,19 @@ func (r *redisElastiCacheDB) UpdateUser(_ context.Context, req dbplugin.UpdateUs userId := normaliseId(req.Username) - _, err := r.client.ModifyUser(&elasticache.ModifyUserInput{ - UserId: &userId, - Passwords: []*string{&req.Password.NewPassword}, - }) - if err != nil { - return dbplugin.UpdateUserResponse{}, fmt.Errorf("unable to update user: %w", err) - } + if r.waitForUserActiveState(userId) { + _, err := r.client.ModifyUser(&elasticache.ModifyUserInput{ + UserId: &userId, + Passwords: []*string{&req.Password.NewPassword}, + }) + if err != nil { + return dbplugin.UpdateUserResponse{}, fmt.Errorf("unable to update user %s: %w", userId, err) + } - return dbplugin.UpdateUserResponse{}, nil + return dbplugin.UpdateUserResponse{}, nil + } else { + return dbplugin.UpdateUserResponse{}, fmt.Errorf("unable to update user %s, user never turned active", userId) + } } func (r *redisElastiCacheDB) DeleteUser(_ context.Context, req dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) { @@ -136,14 +186,53 @@ func (r *redisElastiCacheDB) DeleteUser(_ context.Context, req dbplugin.DeleteUs userId := normaliseId(req.Username) - _, err := r.client.DeleteUser(&elasticache.DeleteUserInput{ - UserId: &userId, + out, err := r.client.DescribeUsers(&elasticache.DescribeUsersInput{UserId: aws.String(userId)}) + if (err != nil && err.(awserr.Error).Code() == "UserNotFound") || (out != nil && len(out.Users) == 1 && *out.Users[0].Status == "deleting") { + r.logger.Debug("user does not exist or is being deleted, considering deletion successful", "user id", userId) + return dbplugin.DeleteUserResponse{}, nil + } + + if r.waitForUserActiveState(userId) { + _, err = r.client.DeleteUser(&elasticache.DeleteUserInput{ + UserId: &userId, + }) + } else { + err = fmt.Errorf("unable to delete user %s, user never turned active", userId) + } + + return dbplugin.DeleteUserResponse{}, err +} + +func (r *redisElastiCacheDB) waitForGroupActiveState(userGroupId string) bool { + return retry(userGroupId, func(s string) bool { + out, err := r.client.DescribeUserGroups(&elasticache.DescribeUserGroupsInput{ + UserGroupId: aws.String(userGroupId), + }) + + return err == nil && len(out.UserGroups) == 1 && *out.UserGroups[0].Status == "active" }) - if err != nil { - return dbplugin.DeleteUserResponse{}, fmt.Errorf("unable to delete user: %w", err) +} + +func (r *redisElastiCacheDB) waitForUserActiveState(userId string) bool { + return retry(userId, func(s string) bool { + out, err := r.client.DescribeUsers(&elasticache.DescribeUsersInput{ + UserId: aws.String(userId), + }) + + return err == nil && len(out.Users) == 1 && *out.Users[0].Status == "active" + }) +} + +func retry(s string, f func(string) bool) bool { + for i := 0; i < 50; i++ { + if f(s) { + return true + } else { + time.Sleep(3 * time.Second) + } } - return dbplugin.DeleteUserResponse{}, nil + return false } func parseCreationCommands(commands []string) (string, error) { @@ -178,6 +267,13 @@ func parseCreationCommands(commands []string) (string, error) { return accessString, nil } +// Elasticache URLs are always in the form of prefix.cluster-id.dns-suffix:port and cluster ids nor prefix cannot contain "." characters +func extractClusterId(url string) string { + _, after, _ := strings.Cut(url, ".") + before, _, _ := strings.Cut(after, ".") + return normaliseId(before) +} + // All Elasticache IDs can have up to 40 characters, and must begin with a letter. // It should not end with a hyphen or contain two consecutive hyphens. // Valid characters: A-Z, a-z, 0-9, and -(hyphen). diff --git a/redis_elasticache_client_test.go b/redis_elasticache_client_test.go index 26d51cb..14e22b5 100644 --- a/redis_elasticache_client_test.go +++ b/redis_elasticache_client_test.go @@ -6,7 +6,6 @@ import ( "reflect" "strings" "testing" - "time" "github.com/aws/aws-sdk-go/service/elasticache" "github.com/hashicorp/go-hclog" @@ -103,25 +102,14 @@ func setUpTestUser(t *testing.T, r *redisElastiCacheDB) string { return user.Username } -func teardownTestUser(t *testing.T, r redisElastiCacheDB, username string) { +func teardownTestUser(r redisElastiCacheDB, username string) { if username == "" { return } - // Creating or Modifying users cannot be deleted until they return to Active status - for i := 0; i < 20; i++ { - _, err := r.DeleteUser(nil, dbplugin.DeleteUserRequest{ - Username: username, - }) - - if err == nil { - break - } else { - t.Logf("unable to clean test user '%s' due to: %v; retrying", username, err) - } - - time.Sleep(3 * time.Second) - } + _, _ = r.DeleteUser(nil, dbplugin.DeleteUserRequest{ + Username: username, + }) } func Test_redisElastiCacheDB_Initialize(t *testing.T) { @@ -273,7 +261,7 @@ func Test_redisElastiCacheDB_NewUser(t *testing.T) { t.Errorf("NewUser() got = %v, want %v", got, tt.want) } - teardownTestUser(t, r, got.Username) + teardownTestUser(r, got.Username) }) } } @@ -285,7 +273,7 @@ func Test_redisElastiCacheDB_UpdateUser(t *testing.T) { setUpClient(t, &r, c) username := setUpTestUser(t, &r) - defer teardownTestUser(t, r, username) + defer teardownTestUser(r, username) tests := testCases{ { @@ -372,7 +360,7 @@ func Test_redisElastiCacheDB_DeleteUser(t *testing.T) { wantErr: false, }, { - name: "delete non-existing user fails", + name: "delete non-existing user is lenient", fields: f, args: args{ ctx: context.Background(), @@ -380,8 +368,7 @@ func Test_redisElastiCacheDB_DeleteUser(t *testing.T) { Username: "I do not exist", }, }, - want: dbplugin.DeleteUserResponse{}, - wantErr: true, + want: dbplugin.DeleteUserResponse{}, }, } for _, tt := range tests { From 65e105964a7b9b72405f67da783e9fc81d280c6e Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Fri, 26 Aug 2022 11:49:01 -0400 Subject: [PATCH 12/17] * refactor --- redis_elasticache_client.go | 146 ++++++++++++++++++++++++------------ 1 file changed, 96 insertions(+), 50 deletions(-) diff --git a/redis_elasticache_client.go b/redis_elasticache_client.go index 7fe498f..1778cc0 100644 --- a/redis_elasticache_client.go +++ b/redis_elasticache_client.go @@ -115,43 +115,8 @@ func (r *redisElastiCacheDB) NewUser(_ context.Context, req dbplugin.NewUserRequ } r.waitForUserActiveState(userId) - clusterId := extractClusterId(r.config.Url) - - _, err = r.client.DescribeUserGroups(&elasticache.DescribeUserGroupsInput{ - UserGroupId: aws.String(clusterId), - }) - if err != nil && err.(awserr.Error).Code() == "UserGroupNotFound" { - r.logger.Debug("bootstrapping vault user group for cluster", "cluster id", clusterId) - _, err = r.client.CreateUserGroup(&elasticache.CreateUserGroupInput{ - Engine: aws.String("Redis"), - Tags: []*elasticache.Tag{}, - UserGroupId: aws.String(clusterId), - UserIds: []*string{ - aws.String("default"), // User groups must contain a user with the username default - aws.String(userId), - }, - }) - if r.waitForGroupActiveState(clusterId) { - _, err = r.client.ModifyReplicationGroup(&elasticache.ModifyReplicationGroupInput{ - ReplicationGroupId: aws.String(clusterId), - UserGroupIdsToAdd: []*string{aws.String(clusterId)}, - }) - } else { - err = fmt.Errorf("unable to update replication group %s, newly created user group never turned active", clusterId) - } - } else { - if r.waitForGroupActiveState(clusterId) { - _, err = r.client.ModifyUserGroup(&elasticache.ModifyUserGroupInput{ - UserGroupId: aws.String(clusterId), - UserIdsToAdd: []*string{aws.String(userId)}, - }) - } else { - err = fmt.Errorf("unable to update user group %s, user group never turned active", clusterId) - } - } - - if err != nil { - r.logger.Debug("encountered error while configuring newly created user, attempting to clean up", "user id", userId) + if err = r.associateUserWithReplicationGroup(userId); err != nil { + r.logger.Debug("encountered error while associating newly created user, attempting to clean up", "user id", userId) _, _ = r.DeleteUser(nil, dbplugin.DeleteUserRequest{ Username: userId, }) @@ -203,29 +168,117 @@ func (r *redisElastiCacheDB) DeleteUser(_ context.Context, req dbplugin.DeleteUs return dbplugin.DeleteUserResponse{}, err } +// If replication group already has an associated user group, we use it +// If not and a default user group already exists, we associate it +// If not and a default group does not exist, we create and associate it +func (r *redisElastiCacheDB) associateUserWithReplicationGroup(userId string) error { + replicationGroupId := r.extractReplicationGroupId() + userGroupId, err := r.extractUserGroupId(replicationGroupId) + if err != nil { + return fmt.Errorf("unable to determine if replication group %s already has an associated user group", replicationGroupId) + } + + if userGroupId != "" { + if r.waitForGroupActiveState(userGroupId) { + _, err = r.client.ModifyUserGroup(&elasticache.ModifyUserGroupInput{ + UserGroupId: aws.String(userGroupId), + UserIdsToAdd: []*string{aws.String(userId)}, + }) + } else { + err = fmt.Errorf("unable to update user group %s, user group never turned active", replicationGroupId) + } + } else { + r.logger.Debug("configuring user group for replication group", "replication group id", replicationGroupId) + + _, err = r.client.DescribeUserGroups(&elasticache.DescribeUserGroupsInput{ + UserGroupId: aws.String(replicationGroupId), + }) + if err != nil && err.(awserr.Error).Code() == "UserGroupNotFound" { + _, err = r.client.CreateUserGroup(&elasticache.CreateUserGroupInput{ + Engine: aws.String("Redis"), + Tags: []*elasticache.Tag{}, + UserGroupId: aws.String(replicationGroupId), + UserIds: []*string{ + aws.String("default"), // User groups must contain a user with the username default + aws.String(userId), + }, + }) + } + + if r.waitForGroupActiveState(replicationGroupId) { + _, err = r.client.ModifyReplicationGroup(&elasticache.ModifyReplicationGroupInput{ + ReplicationGroupId: aws.String(replicationGroupId), + UserGroupIdsToAdd: []*string{aws.String(replicationGroupId)}, + }) + } else { + err = fmt.Errorf("unable to update replication group %s, newly created user group never turned active", replicationGroupId) + } + } + + return err +} + +// Replication groups can have none or up to 1 associated user group at any given time +func (r *redisElastiCacheDB) extractUserGroupId(replicationGroupId string) (string, error) { + out, err := r.client.DescribeReplicationGroups(&elasticache.DescribeReplicationGroupsInput{ + ReplicationGroupId: aws.String(replicationGroupId), + }) + + userGroupId := "" + if err == nil && len(out.ReplicationGroups) == 1 && len(out.ReplicationGroups[0].UserGroupIds) == 1 { + userGroupId = *out.ReplicationGroups[0].UserGroupIds[0] + } + + return userGroupId, err +} + +// Elasticache URLs are always in the form of prefix.cluster-id.dns-suffix:port and cluster ids nor prefix cannot contain "." characters +func (r *redisElastiCacheDB) extractReplicationGroupId() string { + _, after, found := strings.Cut(r.config.Url, ".") + if found { + before, _, found := strings.Cut(after, ".") + if found { + return normaliseId(before) + } + } + + return "" +} + func (r *redisElastiCacheDB) waitForGroupActiveState(userGroupId string) bool { - return retry(userGroupId, func(s string) bool { + return retry(userGroupId, func(s string) (bool, error) { out, err := r.client.DescribeUserGroups(&elasticache.DescribeUserGroupsInput{ UserGroupId: aws.String(userGroupId), }) - return err == nil && len(out.UserGroups) == 1 && *out.UserGroups[0].Status == "active" + if err != nil { + return false, err + } else { + return len(out.UserGroups) == 1 && *out.UserGroups[0].Status == "active", nil + } }) } func (r *redisElastiCacheDB) waitForUserActiveState(userId string) bool { - return retry(userId, func(s string) bool { + return retry(userId, func(s string) (bool, error) { out, err := r.client.DescribeUsers(&elasticache.DescribeUsersInput{ UserId: aws.String(userId), }) - return err == nil && len(out.Users) == 1 && *out.Users[0].Status == "active" + if err != nil { + return false, err + } else { + return len(out.Users) == 1 && *out.Users[0].Status == "active", nil + } }) } -func retry(s string, f func(string) bool) bool { +func retry(s string, f func(string) (bool, error)) bool { for i := 0; i < 50; i++ { - if f(s) { + ok, err := f(s) + if err != nil { + return false + } else if ok { return true } else { time.Sleep(3 * time.Second) @@ -267,13 +320,6 @@ func parseCreationCommands(commands []string) (string, error) { return accessString, nil } -// Elasticache URLs are always in the form of prefix.cluster-id.dns-suffix:port and cluster ids nor prefix cannot contain "." characters -func extractClusterId(url string) string { - _, after, _ := strings.Cut(url, ".") - before, _, _ := strings.Cut(after, ".") - return normaliseId(before) -} - // All Elasticache IDs can have up to 40 characters, and must begin with a letter. // It should not end with a hyphen or contain two consecutive hyphens. // Valid characters: A-Z, a-z, 0-9, and -(hyphen). From 2f3a01ef3f5256d4373a6183d418e9655fbc7959 Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Tue, 6 Sep 2022 12:05:10 -0400 Subject: [PATCH 13/17] * switched to static-role support only --- bootstrap/terraform/elasticache.tf | 26 ++- go.mod | 6 +- go.sum | 15 +- redis_elasticache_client.go | 290 ++++--------------------- redis_elasticache_client_test.go | 325 +---------------------------- 5 files changed, 92 insertions(+), 570 deletions(-) diff --git a/bootstrap/terraform/elasticache.tf b/bootstrap/terraform/elasticache.tf index 0e6b819..14e7f08 100644 --- a/bootstrap/terraform/elasticache.tf +++ b/bootstrap/terraform/elasticache.tf @@ -7,21 +7,40 @@ provider "aws" { // region = "" } +resource "random_password" "vault_plugin_elasticache_test" { + length = 16 +} + resource "aws_elasticache_replication_group" "vault_plugin_elasticache_test" { replication_group_id = "vault-plugin-elasticache-test" description = "vault elasticache plugin generated test cluster" - engine = "redis" + engine = "REDIS" engine_version = "6.2" node_type = "cache.t4g.micro" num_cache_clusters = 1 parameter_group_name = "default.redis6.x" transit_encryption_enabled = true + user_group_ids = [aws_elasticache_user_group.vault_plugin_elasticache_test.id] tags = { "description" : "vault elasticache plugin generated test cluster" } } +resource "aws_elasticache_user_group" "vault_plugin_elasticache_test" { + engine = "REDIS" + user_group_id = "vault-test-user-group" + user_ids = ["default", aws_elasticache_user.vault_plugin_elasticache_test.user_id] +} + +resource "aws_elasticache_user" "vault_plugin_elasticache_test" { + user_id = "vault-test" + user_name = "vault-test" + access_string = "on ~* +@all" + engine = "REDIS" + passwords = [random_password.vault_plugin_elasticache_test.result] +} + resource "aws_iam_user" "vault_plugin_elasticache_test" { name = "vault-plugin-elasticache-user-test" @@ -103,3 +122,8 @@ data "aws_region" "current" {} output "region" { value = data.aws_region.current.name } + +// export TEST_ELASTICACHE_USER=${user} +output "user" { + value = aws_elasticache_user.vault_plugin_elasticache_test.user_name +} diff --git a/go.mod b/go.mod index 4f66668..5622095 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.17 require ( github.com/aws/aws-sdk-go v1.44.81 github.com/hashicorp/go-hclog v1.2.2 + github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 github.com/hashicorp/vault/sdk v0.5.3 github.com/mitchellh/mapstructure v1.5.0 ) @@ -17,9 +18,10 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.5.7 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.4.3 // indirect - github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 // indirect github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/hashicorp/go-version v1.2.0 // indirect @@ -31,6 +33,7 @@ require ( github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/oklog/run v1.0.0 // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect @@ -39,4 +42,5 @@ require ( google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect google.golang.org/grpc v1.41.0 // indirect google.golang.org/protobuf v1.26.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 23e627f..79ac99b 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,7 @@ github.com/armon/go-metrics v0.3.9 h1:O2sNqxBdvq8Eq5xmzljcYzAORli6RWCvEym4cJf9m1 github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.44.81 h1:C8oBZ+a+ka0qk3Q24MohQIFq0tkbO8IAu5tfpAMKVWE= github.com/aws/aws-sdk-go v1.44.81/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -51,6 +52,7 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -89,6 +91,8 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.2.2 h1:ihRI7YFwcZdiSD7SIenIhHfQH3OuDvWerAUBZbeQS3M= @@ -98,11 +102,13 @@ github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJ github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 h1:6KMBnfEv0/kLAz0O76sliN5mXbCDcLfs2kP7ssP7+DQ= +github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 h1:W9WN8p6moV1fjKLkeqEgkAMu5rauy9QeYDAmIaPuuiA= +github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6/go.mod h1:MpCPSPGLDILGb4JMm94/mMi3YysIqsXzGCzkEZjcjXg= github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= @@ -129,6 +135,7 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -183,6 +190,7 @@ github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -239,6 +247,7 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= @@ -263,6 +272,7 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -324,8 +334,9 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/redis_elasticache_client.go b/redis_elasticache_client.go index 1778cc0..0c0338f 100644 --- a/redis_elasticache_client.go +++ b/redis_elasticache_client.go @@ -2,30 +2,18 @@ package rediselasticache import ( "context" - "encoding/json" - "errors" "fmt" - "regexp" - "strings" "time" - "unicode" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/elasticache" "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-secure-stdlib/awsutil" "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - "github.com/hashicorp/vault/sdk/database/helper/credsutil" "github.com/mitchellh/mapstructure" ) -var ( - nonAlphanumericHyphenRegex = regexp.MustCompile("[^a-zA-Z\\d-]+") - doubleHyphenRegex = regexp.MustCompile("-{2,}") -) - // Verify interface is implemented var _ dbplugin.Database = (*redisElastiCacheDB)(nil) @@ -49,9 +37,19 @@ func (r *redisElastiCacheDB) Initialize(_ context.Context, req dbplugin.Initiali return dbplugin.InitializeResponse{}, err } + creds, err := awsutil.RetrieveCreds(r.config.Username, r.config.Password, "", r.logger) + if err != nil { + return dbplugin.InitializeResponse{}, fmt.Errorf("unable to rerieve AWS credentials from provider chain: %w", err) + } + + region, err := awsutil.GetRegion(r.config.Region) + if err != nil { + return dbplugin.InitializeResponse{}, fmt.Errorf("unable to determine AWS region from config nor context: %w", err) + } + sess, err := session.NewSession(&aws.Config{ - Region: aws.String(r.config.Region), - Credentials: credentials.NewStaticCredentials(r.config.Username, r.config.Password, ""), + Region: aws.String(region), + Credentials: creds, }) if err != nil { return dbplugin.InitializeResponse{}, fmt.Errorf("unable to initialize AWS session: %w", err) @@ -80,264 +78,52 @@ func (r *redisElastiCacheDB) Close() error { return nil } -func (r *redisElastiCacheDB) NewUser(_ context.Context, req dbplugin.NewUserRequest) (dbplugin.NewUserResponse, error) { - r.logger.Debug("creating new AWS ElastiCache Redis user", "role", req.UsernameConfig.RoleName) - - // Format: v_{displayName}_{roleName}_{ID[20]}_{epoch[11]} - // Length limits set so unique identifiers are not truncated - username, err := credsutil.GenerateUsername( - credsutil.DisplayName(req.UsernameConfig.DisplayName, 5), - credsutil.RoleName(req.UsernameConfig.RoleName, 39), - credsutil.MaxLength(80), - ) - if err != nil { - return dbplugin.NewUserResponse{}, fmt.Errorf("unable to generate username: %w", err) - } - - accessString, err := parseCreationCommands(req.Statements.Commands) - if err != nil { - return dbplugin.NewUserResponse{}, fmt.Errorf("unable to parse access string: %w", err) - } - - userId := normaliseId(username) - - output, err := r.client.CreateUser(&elasticache.CreateUserInput{ - AccessString: aws.String(accessString), - Engine: aws.String("Redis"), - NoPasswordRequired: aws.Bool(false), - Passwords: []*string{&req.Password}, - Tags: []*elasticache.Tag{}, - UserId: aws.String(userId), - UserName: aws.String(username), - }) - if err != nil { - return dbplugin.NewUserResponse{}, fmt.Errorf("unable to create new user: %w", err) - } - r.waitForUserActiveState(userId) - - if err = r.associateUserWithReplicationGroup(userId); err != nil { - r.logger.Debug("encountered error while associating newly created user, attempting to clean up", "user id", userId) - _, _ = r.DeleteUser(nil, dbplugin.DeleteUserRequest{ - Username: userId, - }) - return dbplugin.NewUserResponse{}, fmt.Errorf("unable to configure newly created user %s: %w", userId, err) - } +func (r *redisElastiCacheDB) NewUser(_ context.Context, _ dbplugin.NewUserRequest) (dbplugin.NewUserResponse, error) { + return dbplugin.NewUserResponse{}, fmt.Errorf("user creation not supported") +} - return dbplugin.NewUserResponse{Username: *output.UserName}, nil +func (r *redisElastiCacheDB) DeleteUser(_ context.Context, _ dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) { + return dbplugin.DeleteUserResponse{}, fmt.Errorf("user deletion not supported") } func (r *redisElastiCacheDB) UpdateUser(_ context.Context, req dbplugin.UpdateUserRequest) (dbplugin.UpdateUserResponse, error) { r.logger.Debug("updating AWS ElastiCache Redis user", "username", req.Username) - userId := normaliseId(req.Username) - - if r.waitForUserActiveState(userId) { - _, err := r.client.ModifyUser(&elasticache.ModifyUserInput{ - UserId: &userId, - Passwords: []*string{&req.Password.NewPassword}, - }) - if err != nil { - return dbplugin.UpdateUserResponse{}, fmt.Errorf("unable to update user %s: %w", userId, err) - } - - return dbplugin.UpdateUserResponse{}, nil - } else { - return dbplugin.UpdateUserResponse{}, fmt.Errorf("unable to update user %s, user never turned active", userId) - } -} - -func (r *redisElastiCacheDB) DeleteUser(_ context.Context, req dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) { - r.logger.Debug("deleting AWS ElastiCache Redis user", "username", req.Username) - - userId := normaliseId(req.Username) - - out, err := r.client.DescribeUsers(&elasticache.DescribeUsersInput{UserId: aws.String(userId)}) - if (err != nil && err.(awserr.Error).Code() == "UserNotFound") || (out != nil && len(out.Users) == 1 && *out.Users[0].Status == "deleting") { - r.logger.Debug("user does not exist or is being deleted, considering deletion successful", "user id", userId) - return dbplugin.DeleteUserResponse{}, nil - } - - if r.waitForUserActiveState(userId) { - _, err = r.client.DeleteUser(&elasticache.DeleteUserInput{ - UserId: &userId, - }) - } else { - err = fmt.Errorf("unable to delete user %s, user never turned active", userId) - } - - return dbplugin.DeleteUserResponse{}, err -} - -// If replication group already has an associated user group, we use it -// If not and a default user group already exists, we associate it -// If not and a default group does not exist, we create and associate it -func (r *redisElastiCacheDB) associateUserWithReplicationGroup(userId string) error { - replicationGroupId := r.extractReplicationGroupId() - userGroupId, err := r.extractUserGroupId(replicationGroupId) + out, err := r.client.DescribeUsers(&elasticache.DescribeUsersInput{ + UserId: aws.String(req.Username), + }) if err != nil { - return fmt.Errorf("unable to determine if replication group %s already has an associated user group", replicationGroupId) + return dbplugin.UpdateUserResponse{}, fmt.Errorf("unable to get user %s: %w", req.Username, err) } - - if userGroupId != "" { - if r.waitForGroupActiveState(userGroupId) { - _, err = r.client.ModifyUserGroup(&elasticache.ModifyUserGroupInput{ - UserGroupId: aws.String(userGroupId), - UserIdsToAdd: []*string{aws.String(userId)}, - }) - } else { - err = fmt.Errorf("unable to update user group %s, user group never turned active", replicationGroupId) - } - } else { - r.logger.Debug("configuring user group for replication group", "replication group id", replicationGroupId) - - _, err = r.client.DescribeUserGroups(&elasticache.DescribeUserGroupsInput{ - UserGroupId: aws.String(replicationGroupId), - }) - if err != nil && err.(awserr.Error).Code() == "UserGroupNotFound" { - _, err = r.client.CreateUserGroup(&elasticache.CreateUserGroupInput{ - Engine: aws.String("Redis"), - Tags: []*elasticache.Tag{}, - UserGroupId: aws.String(replicationGroupId), - UserIds: []*string{ - aws.String("default"), // User groups must contain a user with the username default - aws.String(userId), - }, - }) - } - - if r.waitForGroupActiveState(replicationGroupId) { - _, err = r.client.ModifyReplicationGroup(&elasticache.ModifyReplicationGroupInput{ - ReplicationGroupId: aws.String(replicationGroupId), - UserGroupIdsToAdd: []*string{aws.String(replicationGroupId)}, - }) - } else { - err = fmt.Errorf("unable to update replication group %s, newly created user group never turned active", replicationGroupId) - } + if len(out.Users) == 1 && *out.Users[0].Status != "active" { + return dbplugin.UpdateUserResponse{}, fmt.Errorf("user %s cannot be updated because it is not in the 'active' state", req.Username) } - return err -} - -// Replication groups can have none or up to 1 associated user group at any given time -func (r *redisElastiCacheDB) extractUserGroupId(replicationGroupId string) (string, error) { - out, err := r.client.DescribeReplicationGroups(&elasticache.DescribeReplicationGroupsInput{ - ReplicationGroupId: aws.String(replicationGroupId), + _, err = r.client.ModifyUser(&elasticache.ModifyUserInput{ + UserId: &req.Username, + Passwords: []*string{&req.Password.NewPassword}, }) - - userGroupId := "" - if err == nil && len(out.ReplicationGroups) == 1 && len(out.ReplicationGroups[0].UserGroupIds) == 1 { - userGroupId = *out.ReplicationGroups[0].UserGroupIds[0] - } - - return userGroupId, err -} - -// Elasticache URLs are always in the form of prefix.cluster-id.dns-suffix:port and cluster ids nor prefix cannot contain "." characters -func (r *redisElastiCacheDB) extractReplicationGroupId() string { - _, after, found := strings.Cut(r.config.Url, ".") - if found { - before, _, found := strings.Cut(after, ".") - if found { - return normaliseId(before) - } + if err != nil { + return dbplugin.UpdateUserResponse{}, fmt.Errorf("unable to update user %s: %w", req.Username, err) } - return "" -} - -func (r *redisElastiCacheDB) waitForGroupActiveState(userGroupId string) bool { - return retry(userGroupId, func(s string) (bool, error) { - out, err := r.client.DescribeUserGroups(&elasticache.DescribeUserGroupsInput{ - UserGroupId: aws.String(userGroupId), - }) - - if err != nil { - return false, err - } else { - return len(out.UserGroups) == 1 && *out.UserGroups[0].Status == "active", nil - } - }) + return dbplugin.UpdateUserResponse{}, nil } func (r *redisElastiCacheDB) waitForUserActiveState(userId string) bool { - return retry(userId, func(s string) (bool, error) { - out, err := r.client.DescribeUsers(&elasticache.DescribeUsersInput{ + isActive := false + + for i := 0; i <= 50; i++ { + user, err := r.client.DescribeUsers(&elasticache.DescribeUsersInput{ UserId: aws.String(userId), }) - - if err != nil { - return false, err - } else { - return len(out.Users) == 1 && *out.Users[0].Status == "active", nil - } - }) -} - -func retry(s string, f func(string) (bool, error)) bool { - for i := 0; i < 50; i++ { - ok, err := f(s) - if err != nil { - return false - } else if ok { - return true + if err != nil && len(user.Users) == 1 && *user.Users[0].Status == "active" { + isActive = true + break } else { time.Sleep(3 * time.Second) } } - return false -} - -func parseCreationCommands(commands []string) (string, error) { - if len(commands) == 0 { - return "on ~* +@read", nil - } - - accessString := "" - for _, command := range commands { - var rules []string - err := json.Unmarshal([]byte(command), &rules) - if err != nil { - return "", err - } - - if len(rules) > 0 { - accessString += strings.Join(rules, " ") - accessString += " " - } - } - - if strings.HasPrefix(accessString, "off ") || strings.Contains(accessString, " off ") || strings.HasSuffix(accessString, " off") { - return "", errors.New("creation of disabled or 'off' users is forbidden") - } - - if !(strings.HasPrefix(accessString, "on ") || strings.Contains(accessString, " on ") || strings.HasSuffix(accessString, " on")) { - accessString = "on " + accessString - } - - accessString = strings.TrimSpace(accessString) - - return accessString, nil -} - -// All Elasticache IDs can have up to 40 characters, and must begin with a letter. -// It should not end with a hyphen or contain two consecutive hyphens. -// Valid characters: A-Z, a-z, 0-9, and -(hyphen). -func normaliseId(raw string) string { - normalized := nonAlphanumericHyphenRegex.ReplaceAllString(raw, "") - normalized = doubleHyphenRegex.ReplaceAllString(normalized, "") - - if len(normalized) > 40 { - normalized = normalized[len(normalized)-40:] - } - - if unicode.IsNumber(rune(normalized[0])) { - normalized = string(rune('A'-17+normalized[0])) + normalized[1:] - } - - if strings.HasSuffix(normalized, "-") { - normalized = normalized[:len(normalized)-1] + "x" - } - - return normalized + return isActive } diff --git a/redis_elasticache_client_test.go b/redis_elasticache_client_test.go index 14e22b5..5cc9828 100644 --- a/redis_elasticache_client_test.go +++ b/redis_elasticache_client_test.go @@ -4,7 +4,6 @@ import ( "context" "os" "reflect" - "strings" "testing" "github.com/aws/aws-sdk-go/service/elasticache" @@ -31,17 +30,18 @@ type testCases []struct { wantErr bool } -func skipIfEnvIsUnset(t *testing.T, config config) { - if config.Username == "" || config.Password == "" || config.Url == "" || config.Region == "" { +func skipIfEnvIsUnset(t *testing.T, config config, username string) { + if config.Username == "" || config.Password == "" || config.Url == "" || config.Region == "" || username == "" { t.Skip("Skipping acceptance tests because required environment variables are not configured") } } -func setUpEnvironment() (fields, map[string]interface{}, redisElastiCacheDB) { +func setUpEnvironment() (fields, map[string]interface{}, redisElastiCacheDB, string) { username := os.Getenv("TEST_ELASTICACHE_USERNAME") password := os.Getenv("TEST_ELASTICACHE_PASSWORD") url := os.Getenv("TEST_ELASTICACHE_URL") region := os.Getenv("TEST_ELASTICACHE_REGION") + user := os.Getenv("TEST_ELASTICACHE_USER") f := fields{ logger: hclog.New(&hclog.LoggerOptions{ @@ -71,7 +71,7 @@ func setUpEnvironment() (fields, map[string]interface{}, redisElastiCacheDB) { client: f.client, } - return f, c, r + return f, c, r, user } func setUpClient(t *testing.T, r *redisElastiCacheDB, config map[string]interface{}) { @@ -84,37 +84,9 @@ func setUpClient(t *testing.T, r *redisElastiCacheDB, config map[string]interfac } } -func setUpTestUser(t *testing.T, r *redisElastiCacheDB) string { - user, err := r.NewUser(nil, dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "display", - RoleName: "role", - }, - Statements: dbplugin.Statements{ - Commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, - }, - Password: "abcdefghijklmnopqrstuvwxyz", - }) - if err != nil { - t.Errorf("unable to provision test user for test cases: %v", err) - } - - return user.Username -} - -func teardownTestUser(r redisElastiCacheDB, username string) { - if username == "" { - return - } - - _, _ = r.DeleteUser(nil, dbplugin.DeleteUserRequest{ - Username: username, - }) -} - func Test_redisElastiCacheDB_Initialize(t *testing.T) { - f, c, r := setUpEnvironment() - skipIfEnvIsUnset(t, f.config) + f, c, r, u := setUpEnvironment() + skipIfEnvIsUnset(t, f.config, u) tests := testCases{ { @@ -163,117 +135,11 @@ func Test_redisElastiCacheDB_Initialize(t *testing.T) { } } -func Test_redisElastiCacheDB_NewUser(t *testing.T) { - f, c, r := setUpEnvironment() - - skipIfEnvIsUnset(t, f.config) - - setUpClient(t, &r, c) - - tests := testCases{ - { - name: "create new valid user succeeds", - fields: f, - args: args{ - ctx: context.Background(), - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "display", - RoleName: "role", - }, - Statements: dbplugin.Statements{ - Commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, - }, - Password: "abcdefghijklmnopqrstuvwxyz", - }, - }, - want: dbplugin.NewUserResponse{ - Username: "v_displ_role_", - }, - }, - { - name: "create user truncates username", - fields: f, - args: args{ - ctx: context.Background(), - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "iAmSupeExtremelyLongThisWillHaveToBeTruncated", - RoleName: "iAmEvenLongerTheApiWillDefinitelyRejectUsIfWeArePassedAsIsWithoutAnyModifications", - }, - Statements: dbplugin.Statements{ - Commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, - }, - Password: "abcdefghijklmnopqrstuvwxyz", - }, - }, - want: dbplugin.NewUserResponse{ - Username: "v_iAmSu_iAmEvenLongerTheApiWillDefinitelyRejec", - }, - }, - { - name: "create user with invalid password fails", - fields: f, - args: args{ - ctx: context.Background(), - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "display", - RoleName: "role", - }, - Statements: dbplugin.Statements{ - Commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, - }, - Password: "too short", - }, - }, - want: dbplugin.NewUserResponse{}, - wantErr: true, - }, - { - name: "create user with invalid statements fails", - fields: f, - args: args{ - ctx: context.Background(), - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "display", - RoleName: "role", - }, - Statements: dbplugin.Statements{ - Commands: []string{"+@invalid"}, - }, - Password: "abcdefghijklmnopqrstuvwxyz", - }, - }, - want: dbplugin.NewUserResponse{}, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := r.NewUser(tt.args.ctx, tt.args.req.(dbplugin.NewUserRequest)) - if (err != nil) != tt.wantErr { - t.Errorf("NewUser() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !strings.HasPrefix(got.Username, tt.want.(dbplugin.NewUserResponse).Username) { - t.Errorf("NewUser() got = %v, want %v", got, tt.want) - } - - teardownTestUser(r, got.Username) - }) - } -} - func Test_redisElastiCacheDB_UpdateUser(t *testing.T) { - f, c, r := setUpEnvironment() - - skipIfEnvIsUnset(t, f.config) + f, c, r, u := setUpEnvironment() + skipIfEnvIsUnset(t, f.config, u) setUpClient(t, &r, c) - username := setUpTestUser(t, &r) - defer teardownTestUser(r, username) tests := testCases{ { @@ -282,7 +148,7 @@ func Test_redisElastiCacheDB_UpdateUser(t *testing.T) { args: args{ ctx: context.Background(), req: dbplugin.UpdateUserRequest{ - Username: username, + Username: u, CredentialType: 0, Password: &dbplugin.ChangePassword{ NewPassword: "abcdefghijklmnopqrstuvwxyz1", @@ -313,7 +179,7 @@ func Test_redisElastiCacheDB_UpdateUser(t *testing.T) { args: args{ ctx: context.Background(), req: dbplugin.UpdateUserRequest{ - Username: username, + Username: u, CredentialType: 0, Password: &dbplugin.ChangePassword{ NewPassword: "too short", @@ -337,172 +203,3 @@ func Test_redisElastiCacheDB_UpdateUser(t *testing.T) { }) } } - -func Test_redisElastiCacheDB_DeleteUser(t *testing.T) { - f, c, r := setUpEnvironment() - - skipIfEnvIsUnset(t, f.config) - - setUpClient(t, &r, c) - username := setUpTestUser(t, &r) - - tests := testCases{ - { - name: "delete existing user succeeds", - fields: f, - args: args{ - ctx: context.Background(), - req: dbplugin.DeleteUserRequest{ - Username: username, - }, - }, - want: dbplugin.DeleteUserResponse{}, - wantErr: false, - }, - { - name: "delete non-existing user is lenient", - fields: f, - args: args{ - ctx: context.Background(), - req: dbplugin.DeleteUserRequest{ - Username: "I do not exist", - }, - }, - want: dbplugin.DeleteUserResponse{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := r.DeleteUser(tt.args.ctx, tt.args.req.(dbplugin.DeleteUserRequest)) - if (err != nil) != tt.wantErr { - t.Errorf("DeleteUser() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("DeleteUser() got = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_parseCreationCommands(t *testing.T) { - type testCases []struct { - name string - commands []string - want string - wantErr bool - } - - tests := testCases{ - { - name: "empty command returns read-only user", - commands: []string{}, - want: "on ~* +@read", - }, - { - name: "single command with multiple rules parses correctly", - commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, - want: "on ~test* -@all +@read", - }, - { - name: "multiple commands with multiple rules parses correctly", - commands: []string{"[\"~test*\"]", "[\"-@all\", \"+@read\"]"}, - want: "on ~test* -@all +@read", - }, - { - name: "empty commands are tolerated", - commands: []string{"[\"~test*\"]", "[]", "[\"-@all\", \"+@read\"]"}, - want: "on ~test* -@all +@read", - }, - { - name: "'on' is added if missing for convenience", - commands: []string{"[\"~test*\"]"}, - want: "on ~test*", - }, - { - name: "'on' is ignored if passed at the beginning", - commands: []string{"[\"on\", \"~test*\"]"}, - want: "on ~test*", - }, - { - name: "'on' is ignored if passed explicitly within the rules", - commands: []string{"[\"~test*\", \"on\"]", "[\"+@read\"]"}, - want: "~test* on +@read", - }, - { - name: "'on' is ignored if passed explicitly at the end", - commands: []string{"[\"~test*\", \"on\"]"}, - want: "~test* on", - }, - { - name: "parsing invalid command format fails", - commands: []string{"{\"command:\", \"on ~* +@read\"}"}, - want: "", - wantErr: true, - }, - { - name: "creation of disabled users is forbidden", - commands: []string{"[\"~test*\", \"off\"]", "[\"+@read\"]"}, - want: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseCreationCommands(tt.commands) - if (err != nil) != tt.wantErr { - t.Errorf("parseCreationCommands() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("parseCreationCommands() got = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_normaliseId(t *testing.T) { - type args struct { - username string - } - - tests := []struct { - name string - args args - want string - }{ - { - name: "compliant username", - args: args{username: "isrole1234eEvyH4mEPcCIT4tCvE131660656371"}, - want: "isrole1234eEvyH4mEPcCIT4tCvE131660656371", - }, - { - name: "short username", - args: args{username: "abcd"}, - want: "abcd", - }, - { - name: "username too long", - args: args{username: "vtokenredisrole1234eEvyH4mEPcCIT4tCvE131660656371"}, - want: "isrole1234eEvyH4mEPcCIT4tCvE131660656371", - }, - { - name: "username with non-alphanumeric characters", - args: args{username: "v_token_redis-role!/$}"}, - want: "vtokenredis-role", - }, - { - name: "username starting with a number", - args: args{username: "1bcd"}, - want: "abcd", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := normaliseId(tt.args.username); got != tt.want { - t.Errorf("generateUserId() = %v, want %v", got, tt.want) - } - }) - } -} From 23a967c71dbe7b87a39ed80ba1b82d5a8831028f Mon Sep 17 00:00:00 2001 From: Max Coulombe <109547106+maxcoulombe@users.noreply.github.com> Date: Tue, 6 Sep 2022 12:07:10 -0400 Subject: [PATCH 14/17] Vault 7916 user group support (#10) * supporting user groups * switched to static-role support only --- bootstrap/terraform/elasticache.tf | 26 ++- go.mod | 6 +- go.sum | 15 +- redis_elasticache_client.go | 158 ++++---------- redis_elasticache_client_test.go | 338 +---------------------------- 5 files changed, 97 insertions(+), 446 deletions(-) diff --git a/bootstrap/terraform/elasticache.tf b/bootstrap/terraform/elasticache.tf index 0e6b819..14e7f08 100644 --- a/bootstrap/terraform/elasticache.tf +++ b/bootstrap/terraform/elasticache.tf @@ -7,21 +7,40 @@ provider "aws" { // region = "" } +resource "random_password" "vault_plugin_elasticache_test" { + length = 16 +} + resource "aws_elasticache_replication_group" "vault_plugin_elasticache_test" { replication_group_id = "vault-plugin-elasticache-test" description = "vault elasticache plugin generated test cluster" - engine = "redis" + engine = "REDIS" engine_version = "6.2" node_type = "cache.t4g.micro" num_cache_clusters = 1 parameter_group_name = "default.redis6.x" transit_encryption_enabled = true + user_group_ids = [aws_elasticache_user_group.vault_plugin_elasticache_test.id] tags = { "description" : "vault elasticache plugin generated test cluster" } } +resource "aws_elasticache_user_group" "vault_plugin_elasticache_test" { + engine = "REDIS" + user_group_id = "vault-test-user-group" + user_ids = ["default", aws_elasticache_user.vault_plugin_elasticache_test.user_id] +} + +resource "aws_elasticache_user" "vault_plugin_elasticache_test" { + user_id = "vault-test" + user_name = "vault-test" + access_string = "on ~* +@all" + engine = "REDIS" + passwords = [random_password.vault_plugin_elasticache_test.result] +} + resource "aws_iam_user" "vault_plugin_elasticache_test" { name = "vault-plugin-elasticache-user-test" @@ -103,3 +122,8 @@ data "aws_region" "current" {} output "region" { value = data.aws_region.current.name } + +// export TEST_ELASTICACHE_USER=${user} +output "user" { + value = aws_elasticache_user.vault_plugin_elasticache_test.user_name +} diff --git a/go.mod b/go.mod index 4f66668..5622095 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.17 require ( github.com/aws/aws-sdk-go v1.44.81 github.com/hashicorp/go-hclog v1.2.2 + github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 github.com/hashicorp/vault/sdk v0.5.3 github.com/mitchellh/mapstructure v1.5.0 ) @@ -17,9 +18,10 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.5.7 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.4.3 // indirect - github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 // indirect github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/hashicorp/go-version v1.2.0 // indirect @@ -31,6 +33,7 @@ require ( github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/oklog/run v1.0.0 // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect @@ -39,4 +42,5 @@ require ( google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect google.golang.org/grpc v1.41.0 // indirect google.golang.org/protobuf v1.26.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 23e627f..79ac99b 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,7 @@ github.com/armon/go-metrics v0.3.9 h1:O2sNqxBdvq8Eq5xmzljcYzAORli6RWCvEym4cJf9m1 github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.44.81 h1:C8oBZ+a+ka0qk3Q24MohQIFq0tkbO8IAu5tfpAMKVWE= github.com/aws/aws-sdk-go v1.44.81/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -51,6 +52,7 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -89,6 +91,8 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.2.2 h1:ihRI7YFwcZdiSD7SIenIhHfQH3OuDvWerAUBZbeQS3M= @@ -98,11 +102,13 @@ github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJ github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 h1:6KMBnfEv0/kLAz0O76sliN5mXbCDcLfs2kP7ssP7+DQ= +github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 h1:W9WN8p6moV1fjKLkeqEgkAMu5rauy9QeYDAmIaPuuiA= +github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6/go.mod h1:MpCPSPGLDILGb4JMm94/mMi3YysIqsXzGCzkEZjcjXg= github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= @@ -129,6 +135,7 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -183,6 +190,7 @@ github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -239,6 +247,7 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= @@ -263,6 +272,7 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -324,8 +334,9 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/redis_elasticache_client.go b/redis_elasticache_client.go index 7227dce..0c0338f 100644 --- a/redis_elasticache_client.go +++ b/redis_elasticache_client.go @@ -2,28 +2,18 @@ package rediselasticache import ( "context" - "encoding/json" - "errors" "fmt" - "regexp" - "strings" - "unicode" + "time" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/elasticache" "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-secure-stdlib/awsutil" "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - "github.com/hashicorp/vault/sdk/database/helper/credsutil" "github.com/mitchellh/mapstructure" ) -var ( - nonAlphanumericHyphenRegex = regexp.MustCompile("[^a-zA-Z\\d-]+") - doubleHyphenRegex = regexp.MustCompile("-{2,}") -) - // Verify interface is implemented var _ dbplugin.Database = (*redisElastiCacheDB)(nil) @@ -47,9 +37,19 @@ func (r *redisElastiCacheDB) Initialize(_ context.Context, req dbplugin.Initiali return dbplugin.InitializeResponse{}, err } + creds, err := awsutil.RetrieveCreds(r.config.Username, r.config.Password, "", r.logger) + if err != nil { + return dbplugin.InitializeResponse{}, fmt.Errorf("unable to rerieve AWS credentials from provider chain: %w", err) + } + + region, err := awsutil.GetRegion(r.config.Region) + if err != nil { + return dbplugin.InitializeResponse{}, fmt.Errorf("unable to determine AWS region from config nor context: %w", err) + } + sess, err := session.NewSession(&aws.Config{ - Region: aws.String(r.config.Region), - Credentials: credentials.NewStaticCredentials(r.config.Username, r.config.Password, ""), + Region: aws.String(region), + Credentials: creds, }) if err != nil { return dbplugin.InitializeResponse{}, fmt.Errorf("unable to initialize AWS session: %w", err) @@ -78,124 +78,52 @@ func (r *redisElastiCacheDB) Close() error { return nil } -func (r *redisElastiCacheDB) NewUser(_ context.Context, req dbplugin.NewUserRequest) (dbplugin.NewUserResponse, error) { - r.logger.Debug("creating new AWS ElastiCache Redis user", "role", req.UsernameConfig.RoleName) - - // Format: v_{displayName}_{roleName}_{ID[20]}_{epoch[11]} - // Length limits set so unique identifiers are not truncated - username, err := credsutil.GenerateUsername( - credsutil.DisplayName(req.UsernameConfig.DisplayName, 5), - credsutil.RoleName(req.UsernameConfig.RoleName, 39), - credsutil.MaxLength(80), - ) - if err != nil { - return dbplugin.NewUserResponse{}, fmt.Errorf("unable to generate username: %w", err) - } - - accessString, err := parseCreationCommands(req.Statements.Commands) - if err != nil { - return dbplugin.NewUserResponse{}, fmt.Errorf("unable to parse access string: %w", err) - } - - userId := normaliseId(username) - - output, err := r.client.CreateUser(&elasticache.CreateUserInput{ - AccessString: aws.String(accessString), - Engine: aws.String("Redis"), - NoPasswordRequired: aws.Bool(false), - Passwords: []*string{&req.Password}, - Tags: []*elasticache.Tag{}, - UserId: aws.String(userId), - UserName: aws.String(username), - }) - if err != nil { - return dbplugin.NewUserResponse{}, fmt.Errorf("unable to create new user: %w", err) - } +func (r *redisElastiCacheDB) NewUser(_ context.Context, _ dbplugin.NewUserRequest) (dbplugin.NewUserResponse, error) { + return dbplugin.NewUserResponse{}, fmt.Errorf("user creation not supported") +} - return dbplugin.NewUserResponse{Username: *output.UserName}, nil +func (r *redisElastiCacheDB) DeleteUser(_ context.Context, _ dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) { + return dbplugin.DeleteUserResponse{}, fmt.Errorf("user deletion not supported") } func (r *redisElastiCacheDB) UpdateUser(_ context.Context, req dbplugin.UpdateUserRequest) (dbplugin.UpdateUserResponse, error) { r.logger.Debug("updating AWS ElastiCache Redis user", "username", req.Username) - userId := normaliseId(req.Username) - - _, err := r.client.ModifyUser(&elasticache.ModifyUserInput{ - UserId: &userId, - Passwords: []*string{&req.Password.NewPassword}, + out, err := r.client.DescribeUsers(&elasticache.DescribeUsersInput{ + UserId: aws.String(req.Username), }) if err != nil { - return dbplugin.UpdateUserResponse{}, fmt.Errorf("unable to update user: %w", err) + return dbplugin.UpdateUserResponse{}, fmt.Errorf("unable to get user %s: %w", req.Username, err) + } + if len(out.Users) == 1 && *out.Users[0].Status != "active" { + return dbplugin.UpdateUserResponse{}, fmt.Errorf("user %s cannot be updated because it is not in the 'active' state", req.Username) } - return dbplugin.UpdateUserResponse{}, nil -} - -func (r *redisElastiCacheDB) DeleteUser(_ context.Context, req dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) { - r.logger.Debug("deleting AWS ElastiCache Redis user", "username", req.Username) - - userId := normaliseId(req.Username) - - _, err := r.client.DeleteUser(&elasticache.DeleteUserInput{ - UserId: &userId, + _, err = r.client.ModifyUser(&elasticache.ModifyUserInput{ + UserId: &req.Username, + Passwords: []*string{&req.Password.NewPassword}, }) if err != nil { - return dbplugin.DeleteUserResponse{}, fmt.Errorf("unable to delete user: %w", err) + return dbplugin.UpdateUserResponse{}, fmt.Errorf("unable to update user %s: %w", req.Username, err) } - return dbplugin.DeleteUserResponse{}, nil + return dbplugin.UpdateUserResponse{}, nil } -func parseCreationCommands(commands []string) (string, error) { - if len(commands) == 0 { - return "on ~* +@read", nil - } - - accessString := "" - for _, command := range commands { - var rules []string - err := json.Unmarshal([]byte(command), &rules) - if err != nil { - return "", err - } - - if len(rules) > 0 { - accessString += strings.Join(rules, " ") - accessString += " " +func (r *redisElastiCacheDB) waitForUserActiveState(userId string) bool { + isActive := false + + for i := 0; i <= 50; i++ { + user, err := r.client.DescribeUsers(&elasticache.DescribeUsersInput{ + UserId: aws.String(userId), + }) + if err != nil && len(user.Users) == 1 && *user.Users[0].Status == "active" { + isActive = true + break + } else { + time.Sleep(3 * time.Second) } } - if strings.HasPrefix(accessString, "off ") || strings.Contains(accessString, " off ") || strings.HasSuffix(accessString, " off") { - return "", errors.New("creation of disabled or 'off' users is forbidden") - } - - if !(strings.HasPrefix(accessString, "on ") || strings.Contains(accessString, " on ") || strings.HasSuffix(accessString, " on")) { - accessString = "on " + accessString - } - - accessString = strings.TrimSpace(accessString) - - return accessString, nil -} - -// All Elasticache IDs can have up to 40 characters, and must begin with a letter. -// It should not end with a hyphen or contain two consecutive hyphens. -// Valid characters: A-Z, a-z, 0-9, and -(hyphen). -func normaliseId(raw string) string { - normalized := nonAlphanumericHyphenRegex.ReplaceAllString(raw, "") - normalized = doubleHyphenRegex.ReplaceAllString(normalized, "") - - if len(normalized) > 40 { - normalized = normalized[len(normalized)-40:] - } - - if unicode.IsNumber(rune(normalized[0])) { - normalized = string(rune('A'-17+normalized[0])) + normalized[1:] - } - - if strings.HasSuffix(normalized, "-") { - normalized = normalized[:len(normalized)-1] + "x" - } - - return normalized + return isActive } diff --git a/redis_elasticache_client_test.go b/redis_elasticache_client_test.go index 26d51cb..5cc9828 100644 --- a/redis_elasticache_client_test.go +++ b/redis_elasticache_client_test.go @@ -4,9 +4,7 @@ import ( "context" "os" "reflect" - "strings" "testing" - "time" "github.com/aws/aws-sdk-go/service/elasticache" "github.com/hashicorp/go-hclog" @@ -32,17 +30,18 @@ type testCases []struct { wantErr bool } -func skipIfEnvIsUnset(t *testing.T, config config) { - if config.Username == "" || config.Password == "" || config.Url == "" || config.Region == "" { +func skipIfEnvIsUnset(t *testing.T, config config, username string) { + if config.Username == "" || config.Password == "" || config.Url == "" || config.Region == "" || username == "" { t.Skip("Skipping acceptance tests because required environment variables are not configured") } } -func setUpEnvironment() (fields, map[string]interface{}, redisElastiCacheDB) { +func setUpEnvironment() (fields, map[string]interface{}, redisElastiCacheDB, string) { username := os.Getenv("TEST_ELASTICACHE_USERNAME") password := os.Getenv("TEST_ELASTICACHE_PASSWORD") url := os.Getenv("TEST_ELASTICACHE_URL") region := os.Getenv("TEST_ELASTICACHE_REGION") + user := os.Getenv("TEST_ELASTICACHE_USER") f := fields{ logger: hclog.New(&hclog.LoggerOptions{ @@ -72,7 +71,7 @@ func setUpEnvironment() (fields, map[string]interface{}, redisElastiCacheDB) { client: f.client, } - return f, c, r + return f, c, r, user } func setUpClient(t *testing.T, r *redisElastiCacheDB, config map[string]interface{}) { @@ -85,48 +84,9 @@ func setUpClient(t *testing.T, r *redisElastiCacheDB, config map[string]interfac } } -func setUpTestUser(t *testing.T, r *redisElastiCacheDB) string { - user, err := r.NewUser(nil, dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "display", - RoleName: "role", - }, - Statements: dbplugin.Statements{ - Commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, - }, - Password: "abcdefghijklmnopqrstuvwxyz", - }) - if err != nil { - t.Errorf("unable to provision test user for test cases: %v", err) - } - - return user.Username -} - -func teardownTestUser(t *testing.T, r redisElastiCacheDB, username string) { - if username == "" { - return - } - - // Creating or Modifying users cannot be deleted until they return to Active status - for i := 0; i < 20; i++ { - _, err := r.DeleteUser(nil, dbplugin.DeleteUserRequest{ - Username: username, - }) - - if err == nil { - break - } else { - t.Logf("unable to clean test user '%s' due to: %v; retrying", username, err) - } - - time.Sleep(3 * time.Second) - } -} - func Test_redisElastiCacheDB_Initialize(t *testing.T) { - f, c, r := setUpEnvironment() - skipIfEnvIsUnset(t, f.config) + f, c, r, u := setUpEnvironment() + skipIfEnvIsUnset(t, f.config, u) tests := testCases{ { @@ -175,117 +135,11 @@ func Test_redisElastiCacheDB_Initialize(t *testing.T) { } } -func Test_redisElastiCacheDB_NewUser(t *testing.T) { - f, c, r := setUpEnvironment() - - skipIfEnvIsUnset(t, f.config) - - setUpClient(t, &r, c) - - tests := testCases{ - { - name: "create new valid user succeeds", - fields: f, - args: args{ - ctx: context.Background(), - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "display", - RoleName: "role", - }, - Statements: dbplugin.Statements{ - Commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, - }, - Password: "abcdefghijklmnopqrstuvwxyz", - }, - }, - want: dbplugin.NewUserResponse{ - Username: "v_displ_role_", - }, - }, - { - name: "create user truncates username", - fields: f, - args: args{ - ctx: context.Background(), - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "iAmSupeExtremelyLongThisWillHaveToBeTruncated", - RoleName: "iAmEvenLongerTheApiWillDefinitelyRejectUsIfWeArePassedAsIsWithoutAnyModifications", - }, - Statements: dbplugin.Statements{ - Commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, - }, - Password: "abcdefghijklmnopqrstuvwxyz", - }, - }, - want: dbplugin.NewUserResponse{ - Username: "v_iAmSu_iAmEvenLongerTheApiWillDefinitelyRejec", - }, - }, - { - name: "create user with invalid password fails", - fields: f, - args: args{ - ctx: context.Background(), - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "display", - RoleName: "role", - }, - Statements: dbplugin.Statements{ - Commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, - }, - Password: "too short", - }, - }, - want: dbplugin.NewUserResponse{}, - wantErr: true, - }, - { - name: "create user with invalid statements fails", - fields: f, - args: args{ - ctx: context.Background(), - req: dbplugin.NewUserRequest{ - UsernameConfig: dbplugin.UsernameMetadata{ - DisplayName: "display", - RoleName: "role", - }, - Statements: dbplugin.Statements{ - Commands: []string{"+@invalid"}, - }, - Password: "abcdefghijklmnopqrstuvwxyz", - }, - }, - want: dbplugin.NewUserResponse{}, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := r.NewUser(tt.args.ctx, tt.args.req.(dbplugin.NewUserRequest)) - if (err != nil) != tt.wantErr { - t.Errorf("NewUser() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !strings.HasPrefix(got.Username, tt.want.(dbplugin.NewUserResponse).Username) { - t.Errorf("NewUser() got = %v, want %v", got, tt.want) - } - - teardownTestUser(t, r, got.Username) - }) - } -} - func Test_redisElastiCacheDB_UpdateUser(t *testing.T) { - f, c, r := setUpEnvironment() - - skipIfEnvIsUnset(t, f.config) + f, c, r, u := setUpEnvironment() + skipIfEnvIsUnset(t, f.config, u) setUpClient(t, &r, c) - username := setUpTestUser(t, &r) - defer teardownTestUser(t, r, username) tests := testCases{ { @@ -294,7 +148,7 @@ func Test_redisElastiCacheDB_UpdateUser(t *testing.T) { args: args{ ctx: context.Background(), req: dbplugin.UpdateUserRequest{ - Username: username, + Username: u, CredentialType: 0, Password: &dbplugin.ChangePassword{ NewPassword: "abcdefghijklmnopqrstuvwxyz1", @@ -325,7 +179,7 @@ func Test_redisElastiCacheDB_UpdateUser(t *testing.T) { args: args{ ctx: context.Background(), req: dbplugin.UpdateUserRequest{ - Username: username, + Username: u, CredentialType: 0, Password: &dbplugin.ChangePassword{ NewPassword: "too short", @@ -349,173 +203,3 @@ func Test_redisElastiCacheDB_UpdateUser(t *testing.T) { }) } } - -func Test_redisElastiCacheDB_DeleteUser(t *testing.T) { - f, c, r := setUpEnvironment() - - skipIfEnvIsUnset(t, f.config) - - setUpClient(t, &r, c) - username := setUpTestUser(t, &r) - - tests := testCases{ - { - name: "delete existing user succeeds", - fields: f, - args: args{ - ctx: context.Background(), - req: dbplugin.DeleteUserRequest{ - Username: username, - }, - }, - want: dbplugin.DeleteUserResponse{}, - wantErr: false, - }, - { - name: "delete non-existing user fails", - fields: f, - args: args{ - ctx: context.Background(), - req: dbplugin.DeleteUserRequest{ - Username: "I do not exist", - }, - }, - want: dbplugin.DeleteUserResponse{}, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := r.DeleteUser(tt.args.ctx, tt.args.req.(dbplugin.DeleteUserRequest)) - if (err != nil) != tt.wantErr { - t.Errorf("DeleteUser() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("DeleteUser() got = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_parseCreationCommands(t *testing.T) { - type testCases []struct { - name string - commands []string - want string - wantErr bool - } - - tests := testCases{ - { - name: "empty command returns read-only user", - commands: []string{}, - want: "on ~* +@read", - }, - { - name: "single command with multiple rules parses correctly", - commands: []string{"[\"~test*\",\"-@all\",\"+@read\"]"}, - want: "on ~test* -@all +@read", - }, - { - name: "multiple commands with multiple rules parses correctly", - commands: []string{"[\"~test*\"]", "[\"-@all\", \"+@read\"]"}, - want: "on ~test* -@all +@read", - }, - { - name: "empty commands are tolerated", - commands: []string{"[\"~test*\"]", "[]", "[\"-@all\", \"+@read\"]"}, - want: "on ~test* -@all +@read", - }, - { - name: "'on' is added if missing for convenience", - commands: []string{"[\"~test*\"]"}, - want: "on ~test*", - }, - { - name: "'on' is ignored if passed at the beginning", - commands: []string{"[\"on\", \"~test*\"]"}, - want: "on ~test*", - }, - { - name: "'on' is ignored if passed explicitly within the rules", - commands: []string{"[\"~test*\", \"on\"]", "[\"+@read\"]"}, - want: "~test* on +@read", - }, - { - name: "'on' is ignored if passed explicitly at the end", - commands: []string{"[\"~test*\", \"on\"]"}, - want: "~test* on", - }, - { - name: "parsing invalid command format fails", - commands: []string{"{\"command:\", \"on ~* +@read\"}"}, - want: "", - wantErr: true, - }, - { - name: "creation of disabled users is forbidden", - commands: []string{"[\"~test*\", \"off\"]", "[\"+@read\"]"}, - want: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseCreationCommands(tt.commands) - if (err != nil) != tt.wantErr { - t.Errorf("parseCreationCommands() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("parseCreationCommands() got = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_normaliseId(t *testing.T) { - type args struct { - username string - } - - tests := []struct { - name string - args args - want string - }{ - { - name: "compliant username", - args: args{username: "isrole1234eEvyH4mEPcCIT4tCvE131660656371"}, - want: "isrole1234eEvyH4mEPcCIT4tCvE131660656371", - }, - { - name: "short username", - args: args{username: "abcd"}, - want: "abcd", - }, - { - name: "username too long", - args: args{username: "vtokenredisrole1234eEvyH4mEPcCIT4tCvE131660656371"}, - want: "isrole1234eEvyH4mEPcCIT4tCvE131660656371", - }, - { - name: "username with non-alphanumeric characters", - args: args{username: "v_token_redis-role!/$}"}, - want: "vtokenredis-role", - }, - { - name: "username starting with a number", - args: args{username: "1bcd"}, - want: "abcd", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := normaliseId(tt.args.username); got != tt.want { - t.Errorf("generateUserId() = %v, want %v", got, tt.want) - } - }) - } -} From 0efd1937d8002157c569371be3d678e636a19351 Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Tue, 6 Sep 2022 12:17:29 -0400 Subject: [PATCH 15/17] * change readme for static creds only --- README.md | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fad4a43..54a15c5 100644 --- a/README.md +++ b/README.md @@ -137,32 +137,29 @@ $ vault write database/config/redis-mydb \ Success! Data written to: database/config/redis-mydb ``` -Configure a role: +Configure a static role: ```sh -$ vault write database/roles/redis-myrole \ +$ vault write database/static-roles/redis-myrole \ db_name="redis-mydb" \ - creation_statements='["~*", "+@read"]' \ - default_ttl=5m \ - max_ttl=15m + username="my-elasticache-username" \ + rotation_period=5m ... Success! Data written to: database/roles/redis-myrole ``` -And generate your first set of dynamic credentials: +Retrieve your first set of static credentials: ```sh -$ vault read database/creds/redis-myrole -... - -Key Value ---- ----- -lease_id database/creds/redis-myrole/ID -lease_duration Xm -lease_renewable true -password PASSWORD -username v_token_redis-myrole_ID_EPOCH +$ vault read database/static-creds/redis-myrole +Key Value +--- ----- +last_vault_rotation 2022-09-06T12:15:33.958413491-04:00 +password PASSWORD +rotation_period 5m +ttl 4m55s +username my-elasticache-username ``` From 385357c1ddff4ca8b8ddc17dc9c1fcd4072b8bee Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Tue, 6 Sep 2022 13:27:01 -0400 Subject: [PATCH 16/17] * clean-up --- README.md | 1 + bootstrap/terraform/elasticache.tf | 27 +-------------------------- redis_elasticache_client.go | 20 -------------------- 3 files changed, 2 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 54a15c5..427903a 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ $ export TEST_ELASTICACHE_USERNAME="AWS ACCESS KEY ID" $ export TEST_ELASTICACHE_PASSWORD="AWS SECRET ACCESS KEY" $ export TEST_ELASTICACHE_URL="vault-plugin-elasticache-test.id.xxx.use1.cache.amazonaws.com:6379" $ export TEST_ELASTICACHE_REGION="us-east-1" +$ export TEST_ELASTICACHE_USER="vault-test" $ make test ``` diff --git a/bootstrap/terraform/elasticache.tf b/bootstrap/terraform/elasticache.tf index 14e7f08..c585515 100644 --- a/bootstrap/terraform/elasticache.tf +++ b/bootstrap/terraform/elasticache.tf @@ -64,35 +64,10 @@ data "aws_iam_policy_document" "vault_plugin_elasticache_test" { statement { actions = [ "elasticache:DescribeUsers", - "elasticache:CreateUser", "elasticache:ModifyUser", - "elasticache:DeleteUser", ] resources = [ - "arn:aws:elasticache:*:*:user:*", - ] - } - - statement { - actions = [ - "elasticache:DescribeUserGroups", - "elasticache:CreateUserGroup", - "elasticache:ModifyUserGroup", - "elasticache:DeleteUserGroup", - "elasticache:ModifyReplicationGroup", - ] - resources = [ - "arn:aws:elasticache:*:*:usergroup:*", - ] - } - - statement { - actions = [ - "elasticache:DescribeReplicationGroups", - "elasticache:ModifyReplicationGroup", - ] - resources = [ - "arn:aws:elasticache:*:*:replicationgroup:*", + "arn:aws:elasticache:*.*:user:*", ] } } diff --git a/redis_elasticache_client.go b/redis_elasticache_client.go index 0c0338f..5035a5c 100644 --- a/redis_elasticache_client.go +++ b/redis_elasticache_client.go @@ -3,8 +3,6 @@ package rediselasticache import ( "context" "fmt" - "time" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/elasticache" @@ -109,21 +107,3 @@ func (r *redisElastiCacheDB) UpdateUser(_ context.Context, req dbplugin.UpdateUs return dbplugin.UpdateUserResponse{}, nil } - -func (r *redisElastiCacheDB) waitForUserActiveState(userId string) bool { - isActive := false - - for i := 0; i <= 50; i++ { - user, err := r.client.DescribeUsers(&elasticache.DescribeUsersInput{ - UserId: aws.String(userId), - }) - if err != nil && len(user.Users) == 1 && *user.Users[0].Status == "active" { - isActive = true - break - } else { - time.Sleep(3 * time.Second) - } - } - - return isActive -} From 575ab78e93f02709de10678e8fedfc30bd63f2a5 Mon Sep 17 00:00:00 2001 From: maxcoulombe Date: Tue, 6 Sep 2022 13:29:48 -0400 Subject: [PATCH 17/17] * fmt --- redis_elasticache_client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/redis_elasticache_client.go b/redis_elasticache_client.go index 5035a5c..823b8a1 100644 --- a/redis_elasticache_client.go +++ b/redis_elasticache_client.go @@ -3,6 +3,7 @@ package rediselasticache import ( "context" "fmt" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/elasticache"