diff --git a/command/base_predict_test.go b/command/base_predict_test.go index 45093d09c..731a02bd3 100644 --- a/command/base_predict_test.go +++ b/command/base_predict_test.go @@ -380,6 +380,7 @@ func TestPredict_Plugins(t *testing.T) { "oci", "oidc", "okta", + "openldap", "pcf", // Deprecated. "pki", "postgresql", diff --git a/go.mod b/go.mod index 79040d6ac..51b0ffb55 100644 --- a/go.mod +++ b/go.mod @@ -87,6 +87,7 @@ require ( github.com/hashicorp/vault-plugin-secrets-gcpkms v0.5.2-0.20190814210149-315cdbf5de6e github.com/hashicorp/vault-plugin-secrets-kv v0.5.2-0.20191017213228-e8cf7060a4d0 github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.0.0-20200124190647-0026e6bed4fb + github.com/hashicorp/vault-plugin-secrets-openldap v0.0.0-20200215165936-237ad8919d2c github.com/hashicorp/vault/api v1.0.5-0.20200214222743-c39f5634b39f github.com/hashicorp/vault/sdk v0.1.14-0.20200214222719-7a3b716487a5 github.com/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4 diff --git a/go.sum b/go.sum index 23d545e0a..aafe0be53 100644 --- a/go.sum +++ b/go.sum @@ -436,6 +436,8 @@ github.com/hashicorp/vault-plugin-secrets-kv v0.5.2-0.20191017213228-e8cf7060a4d github.com/hashicorp/vault-plugin-secrets-kv v0.5.2-0.20191017213228-e8cf7060a4d0/go.mod h1:H0VKQagsJoK9o2qpULMgbspuWVnFe3G4S/K7f0Dr8qY= github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.0.0-20200124190647-0026e6bed4fb h1:mKgSR5EimaRgFzdV1ld5+k1l+zzpJ3x7T+eTHNxxKuo= github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.0.0-20200124190647-0026e6bed4fb/go.mod h1:O+csRy1tVsA/LEvBUgRNM3GUml6TFnVBld+HRciLAdg= +github.com/hashicorp/vault-plugin-secrets-openldap v0.0.0-20200215165936-237ad8919d2c h1:QkOEpku9tJjgE9Y4w5FfzaUIrO1QvnJvEt0SPx58zAE= +github.com/hashicorp/vault-plugin-secrets-openldap v0.0.0-20200215165936-237ad8919d2c/go.mod h1:oOUQK0t8vnW7qHXMlg7jcCi8z7THVkeqpUl00nOSvZA= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -821,6 +823,7 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/helper/builtinplugins/registry.go b/helper/builtinplugins/registry.go index 9bf7719bd..d1591eaf7 100644 --- a/helper/builtinplugins/registry.go +++ b/helper/builtinplugins/registry.go @@ -47,6 +47,7 @@ import ( logicalMssql "github.com/hashicorp/vault/builtin/logical/mssql" logicalMysql "github.com/hashicorp/vault/builtin/logical/mysql" logicalNomad "github.com/hashicorp/vault/builtin/logical/nomad" + logicalOpenLDAP "github.com/hashicorp/vault-plugin-secrets-openldap" logicalPki "github.com/hashicorp/vault/builtin/logical/pki" logicalPostgres "github.com/hashicorp/vault/builtin/logical/postgresql" logicalRabbit "github.com/hashicorp/vault/builtin/logical/rabbitmq" @@ -122,6 +123,7 @@ func newRegistry() *registry { "mssql": logicalMssql.Factory, // Deprecated "mysql": logicalMysql.Factory, // Deprecated "nomad": logicalNomad.Factory, + "openldap": logicalOpenLDAP.Factory, "pki": logicalPki.Factory, "postgresql": logicalPostgres.Factory, // Deprecated "rabbitmq": logicalRabbit.Factory, diff --git a/scripts/gen_openapi.sh b/scripts/gen_openapi.sh index be9ae5116..b40457f0a 100755 --- a/scripts/gen_openapi.sh +++ b/scripts/gen_openapi.sh @@ -59,6 +59,7 @@ vault secrets enable mongodb vault secrets enable mssql vault secrets enable mysql vault secrets enable nomad +vault secrets enable openldap vault secrets enable pki vault secrets enable postgresql vault secrets enable rabbitmq diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/.gitignore b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/.gitignore new file mode 100644 index 000000000..e618b7898 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/.gitignore @@ -0,0 +1,85 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test +.cover + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +# Other dirs +/bin/ +/pkg/ + +# Vault-specific +example.hcl +example.vault.d + +# Ruby +website/vendor +website/.bundle +website/build + +# Vagrant +.vagrant/ +Vagrantfile + +# Configs +*.hcl + +.DS_Store +.idea +.vscode + +dist/* + +tags + +# Editor backups +*~ +*.sw[a-z] + +# IntelliJ IDEA project files +.idea +*.ipr +*.iml + +# compiled output +ui/dist +ui/tmp + +# dependencies +ui/node_modules +ui/bower_components + +# misc +ui/.DS_Store +ui/.sass-cache +ui/connect.lock +ui/coverage/* +ui/libpeerconnection.log +ui/npm-debug.log +ui/testem.log +tmp/ + +scripts/custom.sh + +# binary +bin/vault-plugin-auth-openldap diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/LICENSE b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/LICENSE new file mode 100644 index 000000000..a612ad981 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/Makefile b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/Makefile new file mode 100644 index 000000000..d209670e9 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/Makefile @@ -0,0 +1,51 @@ +TOOL?=vault-plugin-secrets-openldap +TEST?=$$(go list ./... | grep -v /vendor/ | grep -v teamcity) +VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -printf -rangeloops -shift -structtags -unsafeptr +EXTERNAL_TOOLS=\ + github.com/mitchellh/gox \ + github.com/golang/dep/cmd/dep +BUILD_TAGS?=${TOOL} +GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor) + +# bin generates the releaseable binaries for this plugin +bin: fmtcheck generate + @CGO_ENABLED=0 BUILD_TAGS='$(BUILD_TAGS)' sh -c "'$(CURDIR)/scripts/build.sh'" + +default: dev + +# dev creates binaries for testing Vault locally. These are put +# into ./bin/ as well as $GOPATH/bin. +dev: fmtcheck generate + @CGO_ENABLED=0 BUILD_TAGS='$(BUILD_TAGS)' VAULT_DEV_BUILD=1 sh -c "'$(CURDIR)/scripts/build.sh'" + +# test runs the unit tests and vets the code +test: fmtcheck generate + CGO_ENABLED=0 VAULT_TOKEN= VAULT_ACC= go test -v -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -count=1 -timeout=20m -parallel=4 + +testcompile: fmtcheck generate + @for pkg in $(TEST) ; do \ + go test -v -c -tags='$(BUILD_TAGS)' $$pkg -parallel=4 ; \ + done + +# generate runs `go generate` to build the dynamically generated +# source files. +generate: + go generate $(go list ./... | grep -v /vendor/) + +# bootstrap the build by downloading additional tools +bootstrap: + @for tool in $(EXTERNAL_TOOLS) ; do \ + echo "Installing/Updating $$tool" ; \ + go get -u $$tool; \ + done + +fmtcheck: + @sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'" + +fmt: + gofmt -w $(GOFMT_FILES) + +proto: + protoc *.proto --go_out=plugins=grpc:. + +.PHONY: bin default generate test vet bootstrap fmt fmtcheck diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/README.md b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/README.md new file mode 100644 index 000000000..e90efa951 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/README.md @@ -0,0 +1,34 @@ +# Vault Plugin: OpenLDAP Secrets Backend + +This is a standalone backend plugin for use with [Hashicorp Vault](https://www.github.com/hashicorp/vault). +This plugin provides OpenLDAP functionality to Vault. + +**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). + +## Quick Links +- Vault Website: https://www.vaultproject.io +- OpenLDAP Docs: https://www.vaultproject.io/docs/secrets/openldap/index.html +- Main Project Github: https://www.github.com/hashicorp/vault + +## Getting Started + +This is a [Vault plugin](https://www.vaultproject.io/docs/internals/plugins.html) +and is meant to work with Vault. This guide assumes you have already installed Vault +and have a basic understanding of how Vault works. + +Otherwise, first read this guide on how to [get started with Vault](https://www.vaultproject.io/intro/getting-started/install.html). + +To learn specifically about how plugins work, see documentation on [Vault plugins](https://www.vaultproject.io/docs/internals/plugins.html). + +## Usage + +Please see [documentation for the plugin](https://www.vaultproject.io/docs/secrets/openldap/index.html) +on the Vault website. + +This plugin is currently built into Vault and by default is accessed +at `openldap`. To enable this in a running Vault server: + +```sh +$ vault secrets enable openldap +Success! Enabled the openldap secrets engine at: openldap/ +``` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/backend.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/backend.go new file mode 100644 index 000000000..f93a57715 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/backend.go @@ -0,0 +1,116 @@ +package openldap + +import ( + "context" + "strings" + "sync" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/locksutil" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/sdk/queue" +) + +func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { + ldapClient := NewClient() + b := Backend(ldapClient) + if err := b.Setup(ctx, conf); err != nil { + return nil, err + } + + return b, nil +} + +func Backend(client ldapClient) *backend { + var b backend + b.Backend = &framework.Backend{ + Help: strings.TrimSpace(backendHelp), + + PathsSpecial: &logical.Paths{ + LocalStorage: []string{ + framework.WALPrefix, + }, + SealWrapStorage: []string{ + "config", + "static-role/*", + }, + }, + Paths: framework.PathAppend( + b.pathListRoles(), + b.pathRoles(), + b.pathCredsCreate(), + b.pathRotateCredentials(), + b.pathConfig(), + ), + InitializeFunc: b.initialize, + + Secrets: []*framework.Secret{}, + Clean: b.clean, + BackendType: logical.TypeLogical, + } + b.client = client + b.roleLocks = locksutil.CreateLocks() + b.credRotationQueue = queue.New() + + return &b +} + +func (b *backend) initialize(ctx context.Context, initRequest *logical.InitializationRequest) error { + // Create a context with a cancel method for processing any WAL entries and + // populating the queue + ictx, cancel := context.WithCancel(context.Background()) + b.cancelQueue = cancel + + // Load queue and kickoff new periodic ticker + go b.initQueue(ictx, initRequest) + + return nil +} + +func (b *backend) clean(ctx context.Context) { + b.invalidateQueue() +} + +// invalidateQueue cancels any background queue loading and destroys the queue. +func (b *backend) invalidateQueue() { + b.Lock() + defer b.Unlock() + + if b.cancelQueue != nil { + b.cancelQueue() + } + b.credRotationQueue = nil +} + +type backend struct { + *framework.Backend + sync.RWMutex + // CredRotationQueue is an in-memory priority queue used to track Static Roles + // that require periodic rotation. Backends will have a PriorityQueue + // initialized on setup, but only backends that are mounted by a primary + // server or mounted as a local mount will perform the rotations. + // + // cancelQueue is used to remove the priority queue and terminate the + // background ticker. + credRotationQueue *queue.PriorityQueue + cancelQueue context.CancelFunc + + // roleLocks is used to lock modifications to roles in the queue, to ensure + // concurrent requests are not modifying the same role and possibly causing + // issues with the priority queue. + roleLocks []*locksutil.LockEntry + client ldapClient +} + +const backendHelp = ` +The OpenLDAP backend supports managing existing LDAP entry passwords by providing: + + * end points to add entries + * manual rotation of entry passwords + * auto rotation of entry passwords + +The OpenLDAP secret engine is limited to OpenLDAP and does not support any other +implementations of LDAP. + +After mounting this secret backend, configure it using the "openldap/config" path. +` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client.go new file mode 100644 index 000000000..320118e4e --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client.go @@ -0,0 +1,57 @@ +package openldap + +import ( + "fmt" + + "github.com/hashicorp/vault-plugin-secrets-openldap/client" +) + +type ldapClient interface { + Get(conf *client.Config, dn string) (*client.Entry, error) + UpdatePassword(conf *client.Config, dn string, newPassword string) error + UpdateRootPassword(conf *client.Config, newPassword string) error +} + +func NewClient() *Client { + return &Client{ + ldap: client.New(), + } +} + +type Client struct { + ldap client.Client +} + +func (c *Client) Get(conf *client.Config, dn string) (*client.Entry, error) { + filters := map[*client.Field][]string{ + client.FieldRegistry.ObjectClass: {"*"}, + } + + entries, err := c.ldap.Search(conf, dn, filters) + if err != nil { + return nil, err + } + + if len(entries) == 0 { + return nil, fmt.Errorf("unable to find entry %s in openldap", dn) + } + if len(entries) > 1 { + return nil, fmt.Errorf("expected one matching entry, but received %d", len(entries)) + } + return entries[0], nil +} + +func (c *Client) UpdatePassword(conf *client.Config, dn string, newPassword string) error { + filters := map[*client.Field][]string{ + client.FieldRegistry.ObjectClass: {"*"}, + } + return c.ldap.UpdatePassword(conf, dn, filters, newPassword) +} + +func (c *Client) UpdateRootPassword(conf *client.Config, newPassword string) error { + filters := map[*client.Field][]string{ + client.FieldRegistry.ObjectClass: {"*"}, + } + + return c.ldap.UpdatePassword(conf, conf.BindDN, filters, newPassword) +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client/client.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client/client.go new file mode 100644 index 000000000..20adb7e70 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client/client.go @@ -0,0 +1,147 @@ +package client + +import ( + "errors" + "fmt" + "math" + "strings" + "time" + + "github.com/go-ldap/ldap/v3" + "github.com/hashicorp/vault/sdk/helper/ldaputil" +) + +type Config struct { + *ldaputil.ConfigEntry + LastBindPassword string `json:"last_bind_password"` + LastBindPasswordRotation time.Time `json:"last_bind_password_rotation"` +} + +func New() Client { + return Client{ + ldap: &ldaputil.Client{ + LDAP: ldaputil.NewLDAP(), + }, + } +} + +type Client struct { + ldap *ldaputil.Client +} + +func (c *Client) Search(cfg *Config, baseDN string, filters map[*Field][]string) ([]*Entry, error) { + req := &ldap.SearchRequest{ + BaseDN: baseDN, + Scope: ldap.ScopeBaseObject, + Filter: toString(filters), + SizeLimit: math.MaxInt32, + } + + conn, err := c.ldap.DialLDAP(cfg.ConfigEntry) + if err != nil { + return nil, err + } + defer conn.Close() + + if err := bind(cfg, conn); err != nil { + return nil, err + } + + result, err := conn.Search(req) + if err != nil { + return nil, err + } + + entries := make([]*Entry, len(result.Entries)) + for i, rawEntry := range result.Entries { + entries[i] = NewEntry(rawEntry) + } + return entries, nil +} + +func (c *Client) UpdateEntry(cfg *Config, baseDN string, filters map[*Field][]string, newValues map[*Field][]string) error { + entries, err := c.Search(cfg, baseDN, filters) + if err != nil { + return err + } + if len(entries) != 1 { + return fmt.Errorf("expected one matching entry, but received %d", len(entries)) + } + + modifyReq := &ldap.ModifyRequest{ + DN: entries[0].DN, + } + + for field, vals := range newValues { + modifyReq.Replace(field.String(), vals) + } + + conn, err := c.ldap.DialLDAP(cfg.ConfigEntry) + if err != nil { + return err + } + defer conn.Close() + + if err := bind(cfg, conn); err != nil { + return err + } + + return conn.Modify(modifyReq) +} + +// UpdatePassword uses a Modify call under the hood instead of LDAP change password function. +// This allows AD and OpenLDAP secret engines to use the same api without changes to +// the interface. +func (c *Client) UpdatePassword(cfg *Config, baseDN string, filters map[*Field][]string, newPassword string) error { + newValues := map[*Field][]string{ + FieldRegistry.UserPassword: {newPassword}, + } + return c.UpdateEntry(cfg, baseDN, filters, newValues) +} + +// Ex. "(cn=Ellen Jones)" +func toString(filters map[*Field][]string) string { + var fieldEquals []string + for f, values := range filters { + for _, v := range values { + fieldEquals = append(fieldEquals, fmt.Sprintf("%s=%s", f, v)) + } + } + result := strings.Join(fieldEquals, ",") + return "(" + result + ")" +} + +func bind(cfg *Config, conn ldaputil.Connection) error { + if cfg.BindPassword == "" { + return errors.New("unable to bind due to lack of configured password") + } + + if cfg.BindDN == "" { + return errors.New("must provide binddn") + } + + origErr := conn.Bind(cfg.BindDN, cfg.BindPassword) + if origErr == nil { + return nil + } + if !shouldTryLastPwd(cfg.LastBindPassword, cfg.LastBindPasswordRotation) { + return origErr + } + + return conn.Bind(cfg.BindDN, cfg.LastBindPassword) +} + +// shouldTryLastPwd determines if we should try a previous password. +// LDAP can return a variety of errors when a password is invalid. +// Rather than attempting to catalogue these errors across multiple versions of +// OpenLDAP, we simply try the last password if it's been less than a set amount of +// time since a rotation occurred. +func shouldTryLastPwd(lastPwd string, lastBindPasswordRotation time.Time) bool { + if lastPwd == "" { + return false + } + if lastBindPasswordRotation.IsZero() { + return false + } + return lastBindPasswordRotation.Add(10 * time.Minute).After(time.Now()) +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client/entry.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client/entry.go new file mode 100644 index 000000000..c74ce5bff --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client/entry.go @@ -0,0 +1,41 @@ +package client + +import ( + "strings" + + "github.com/go-ldap/ldap/v3" +) + +// Entry is an LDAP-specific construct +// to make knowing and grabbing fields more convenient, +// while retaining all original information. +func NewEntry(ldapEntry *ldap.Entry) *Entry { + fieldMap := make(map[string][]string) + for _, attribute := range ldapEntry.Attributes { + field := FieldRegistry.Parse(attribute.Name) + if field == nil { + // This field simply isn't in the registry, no big deal. + continue + } + fieldMap[field.String()] = attribute.Values + } + return &Entry{fieldMap: fieldMap, Entry: ldapEntry} +} + +type Entry struct { + *ldap.Entry + fieldMap map[string][]string +} + +func (e *Entry) Get(field *Field) ([]string, bool) { + values, found := e.fieldMap[field.String()] + return values, found +} + +func (e *Entry) GetJoined(field *Field) (string, bool) { + values, found := e.Get(field) + if !found { + return "", false + } + return strings.Join(values, ","), true +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client/fieldregistry.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client/fieldregistry.go new file mode 100644 index 000000000..14fad9e17 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client/fieldregistry.go @@ -0,0 +1,88 @@ +package client + +import ( + "reflect" +) + +// FieldRegistry is designed to look and feel +// like an enum from another language like Python. +// +// Example: Accessing constants +// +// FieldRegistry.AccountExpires +// FieldRegistry.BadPasswordCount +// +// Example: Utility methods +// +// FieldRegistry.List() +// FieldRegistry.Parse("givenName") +// +var FieldRegistry = newFieldRegistry() + +// newFieldRegistry iterates through all the fields in the registry, +// pulls their ldap strings, and sets up each field to contain its ldap string +func newFieldRegistry() *fieldRegistry { + reg := &fieldRegistry{} + vOfReg := reflect.ValueOf(reg) + + registryFields := vOfReg.Elem() + for i := 0; i < registryFields.NumField(); i++ { + + if registryFields.Field(i).Kind() == reflect.Ptr { + + field := registryFields.Type().Field(i) + ldapString := field.Tag.Get("ldap") + ldapField := &Field{ldapString} + vOfLDAPField := reflect.ValueOf(ldapField) + + registryFields.FieldByName(field.Name).Set(vOfLDAPField) + + reg.fieldList = append(reg.fieldList, ldapField) + } + } + return reg +} + +// fieldRegistry isn't currently intended to be an exhaustive list - +// there are more fields in LDAP because schema can be defined by the user. +// Here are some of the more common fields. +type fieldRegistry struct { + CommonName *Field `ldap:"cn"` + DisplayName *Field `ldap:"displayName"` + DistinguishedName *Field `ldap:"distinguishedName"` + DomainComponent *Field `ldap:"dc"` + DomainName *Field `ldap:"dn"` + Name *Field `ldap:"name"` + ObjectCategory *Field `ldap:"objectCategory"` + ObjectClass *Field `ldap:"objectClass"` + ObjectGUID *Field `ldap:"objectGUID"` + ObjectSID *Field `ldap:"objectSid"` + OrganizationalUnit *Field `ldap:"ou"` + PasswordLastSet *Field `ldap:"passwordLastSet"` + UnicodePassword *Field `ldap:"unicodePwd"` + UserPassword *Field `ldap:"userPassword"` + UserPrincipalName *Field `ldap:"userPrincipalName"` + + fieldList []*Field +} + +func (r *fieldRegistry) List() []*Field { + return r.fieldList +} + +func (r *fieldRegistry) Parse(s string) *Field { + for _, f := range r.List() { + if f.String() == s { + return f + } + } + return nil +} + +type Field struct { + str string +} + +func (f *Field) String() string { + return f.str +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.mod b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.mod new file mode 100644 index 000000000..c5ea49e11 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.mod @@ -0,0 +1,17 @@ +module github.com/hashicorp/vault-plugin-secrets-openldap + +go 1.13 + +require ( + github.com/go-ldap/ldap/v3 v3.1.3 + github.com/hashicorp/errwrap v1.0.0 + github.com/hashicorp/go-hclog v0.12.0 + github.com/hashicorp/go-multierror v1.0.0 + github.com/hashicorp/go-uuid v1.0.2 // indirect + github.com/hashicorp/go-version v1.2.0 // indirect + github.com/hashicorp/vault/api v1.0.5-0.20200117231345-460d63e36490 + github.com/hashicorp/vault/sdk v0.1.14-0.20200117231345-460d63e36490 + github.com/stretchr/testify v1.4.0 // indirect + golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect + golang.org/x/text v0.3.2 // indirect +) diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.sum b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.sum new file mode 100644 index 000000000..677ce1d94 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.sum @@ -0,0 +1,183 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/armon/go-metrics v0.3.0 h1:B7AQgHi8QSEi4uHu7Sbsga+IJDU+CENgjxoo81vDUqU= +github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck= +github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-ldap/ldap/v3 v3.1.3 h1:RIgdpHXJpsUqUK5WXwKyVsESrGFqo5BRWPk3RR4/ogQ= +github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8= +github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31 h1:28FVBuwkwowZMjbA7M0wXsI6t3PYulRTMio3SO+eKCM= +github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.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 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.12.0 h1:d4QkX8FRTYaKaCZBoXYY8zJX2BXjWxurN/GA2tkrmZM= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE= +github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.6.2 h1:bHM2aVXwBtBJWxHtkSrWuI4umABCUczs52eiUS9nSiw= +github.com/hashicorp/go-retryablehttp v0.6.2/go.mod h1:gEx6HMUGxYYhJScX7W1Il64m6cc2C1mDaW3NQ9sY1FY= +github.com/hashicorp/go-rootcerts v1.0.1 h1:DMo4fmknnz0E0evoNYnV48RjWndOsmd6OW+09R3cEP8= +github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +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-0.20191001231223-f32f5fe8d6a8/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +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.0.5-0.20200117231345-460d63e36490 h1:jzQDZKCJV74KjQS3Ag3Aunjd9aUP1whRaNZzXqcEHs4= +github.com/hashicorp/vault/api v1.0.5-0.20200117231345-460d63e36490/go.mod h1:Uf8LaHyrYsgVgHzO2tMZKhqRGlL3UJ6XaSwW2EA1Iqo= +github.com/hashicorp/vault/sdk v0.1.14-0.20191108161836-82f2b5571044/go.mod h1:PcekaFGiPJyHnFy+NZhP6ll650zEw51Ag7g/YEa+EOU= +github.com/hashicorp/vault/sdk v0.1.14-0.20200117231345-460d63e36490 h1:ZrfYu5Cdi1RXvqFsu8ssK2gRsI+QQ76NtPx4PqByfEM= +github.com/hashicorp/vault/sdk v0.1.14-0.20200117231345-460d63e36490/go.mod h1:PcekaFGiPJyHnFy+NZhP6ll650zEw51Ag7g/YEa+EOU= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +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= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.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= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +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-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107 h1:xtNn7qFlagY2mQNFHMSRPjT2RkOV4OXM7P5TVy9xATo= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_config.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_config.go new file mode 100644 index 000000000..411d66810 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_config.go @@ -0,0 +1,153 @@ +package openldap + +import ( + "context" + "errors" + + "github.com/hashicorp/vault-plugin-secrets-openldap/client" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/ldaputil" + "github.com/hashicorp/vault/sdk/logical" +) + +const ( + configPath = "config" + defaultPasswordLength = 64 + defaultTLSVersion = "tls12" +) + +func readConfig(ctx context.Context, storage logical.Storage) (*config, error) { + entry, err := storage.Get(ctx, configPath) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + config := &config{} + if err := entry.DecodeJSON(config); err != nil { + return nil, err + } + return config, nil +} + +func (b *backend) pathConfig() []*framework.Path { + return []*framework.Path{ + { + Pattern: configPath, + Fields: b.configFields(), + Operations: map[logical.Operation]framework.OperationHandler{ + logical.CreateOperation: &framework.PathOperation{ + Callback: b.configCreateUpdateOperation, + }, + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.configCreateUpdateOperation, + }, + logical.ReadOperation: &framework.PathOperation{ + Callback: b.configReadOperation, + }, + logical.DeleteOperation: &framework.PathOperation{ + Callback: b.configDeleteOperation, + }, + }, + HelpSynopsis: configHelpSynopsis, + HelpDescription: configHelpDescription, + }, + } +} + +func (b *backend) configFields() map[string]*framework.FieldSchema { + fields := ldaputil.ConfigFields() + fields["ttl"] = &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Description: "The default password time-to-live.", + } + fields["max_ttl"] = &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Description: "The maximum password time-to-live.", + } + fields["length"] = &framework.FieldSchema{ + Type: framework.TypeInt, + Default: defaultPasswordLength, + Description: "The desired length of passwords that Vault generates.", + } + return fields +} + +func (b *backend) configCreateUpdateOperation(ctx context.Context, req *logical.Request, fieldData *framework.FieldData) (*logical.Response, error) { + // Build and validate the ldap conf. + ldapConf, err := ldaputil.NewConfigEntry(nil, fieldData) + if err != nil { + return nil, err + } + + if err := ldapConf.Validate(); err != nil { + return nil, err + } + + length := fieldData.Get("length").(int) + url := fieldData.Get("url").(string) + + if url == "" { + return nil, errors.New("url is required") + } + + config := &config{ + LDAP: &client.Config{ + ConfigEntry: ldapConf, + }, + PasswordLength: length, + } + + entry, err := logical.StorageEntryJSON(configPath, config) + if err != nil { + return nil, err + } + if err := req.Storage.Put(ctx, entry); err != nil { + return nil, err + } + + // Respond with a 204. + return nil, nil +} + +func (b *backend) configReadOperation(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { + config, err := readConfig(ctx, req.Storage) + if err != nil { + return nil, err + } + if config == nil { + return nil, nil + } + + // "password" is intentionally not returned by this endpoint + configMap := config.LDAP.Map() + delete(configMap, "bindpass") + configMap["length"] = config.PasswordLength + + resp := &logical.Response{ + Data: configMap, + } + return resp, nil +} + +func (b *backend) configDeleteOperation(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { + if err := req.Storage.Delete(ctx, configPath); err != nil { + return nil, err + } + return nil, nil +} + +type config struct { + LDAP *client.Config + PasswordLength int `json:"length"` +} + +const configHelpSynopsis = ` +Configure the OpenLDAP secret engine plugin. +` + +const configHelpDescription = ` +This path configures the OpenLDAP secret engine plugin. See the documentation for the plugin specified +for a full list of accepted connection details. +` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_creds.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_creds.go new file mode 100644 index 000000000..f6f3c4902 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_creds.go @@ -0,0 +1,64 @@ +package openldap + +import ( + "context" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" +) + +const staticCredPath = "static-cred/" + +func (b *backend) pathCredsCreate() []*framework.Path { + return []*framework.Path{ + { + Pattern: staticCredPath + framework.GenericNameRegex("name"), + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeLowerCaseString, + Description: "Name of the static role.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: b.pathStaticCredsRead, + }, + }, + HelpSynopsis: pathStaticCredsReadHelpSyn, + HelpDescription: pathStaticCredsReadHelpDesc, + }, + } +} + +func (b *backend) pathStaticCredsRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + name := data.Get("name").(string) + + role, err := b.StaticRole(ctx, req.Storage, name) + if err != nil { + return nil, err + } + if role == nil { + return logical.ErrorResponse("unknown role: %s", name), nil + } + + return &logical.Response{ + Data: map[string]interface{}{ + "dn": role.StaticAccount.DN, + "username": role.StaticAccount.Username, + "password": role.StaticAccount.Password, + "ttl": role.StaticAccount.PasswordTTL().Seconds(), + "rotation_period": role.StaticAccount.RotationPeriod.Seconds(), + "last_vault_rotation": role.StaticAccount.LastVaultRotation, + }, + }, nil +} + +const pathStaticCredsReadHelpSyn = ` +Request LDAP credentials for a certain static role. These credentials are +rotated periodically.` + +const pathStaticCredsReadHelpDesc = ` +This path reads LDAP credentials for a certain static role. The LDAPs +credentials are rotated periodically according to their configuration, and will +return the same password until they are rotated. +` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_roles.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_roles.go new file mode 100644 index 000000000..3961a1501 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_roles.go @@ -0,0 +1,370 @@ +package openldap + +import ( + "context" + "time" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/locksutil" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/sdk/queue" +) + +const ( + staticRolePath = "static-role/" +) + +func (b *backend) pathListRoles() []*framework.Path { + return []*framework.Path{ + { + Pattern: staticRolePath + "?$", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ListOperation: &framework.PathOperation{ + Callback: b.pathRoleList, + }, + }, + HelpSynopsis: staticRolesListHelpSynopsis, + HelpDescription: staticRolesListHelpDescription, + }, + } +} + +func (b *backend) pathRoles() []*framework.Path { + return []*framework.Path{ + { + Pattern: staticRolePath + framework.GenericNameRegex("name"), + Fields: fieldsForType(staticRolePath), + ExistenceCheck: b.pathStaticRoleExistenceCheck, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.pathStaticRoleCreateUpdate, + }, + logical.CreateOperation: &framework.PathOperation{ + Callback: b.pathStaticRoleCreateUpdate, + }, + logical.ReadOperation: &framework.PathOperation{ + Callback: b.pathStaticRoleRead, + }, + logical.DeleteOperation: &framework.PathOperation{ + Callback: b.pathStaticRoleDelete, + }, + }, + HelpSynopsis: staticRoleHelpSynopsis, + HelpDescription: staticRoleHelpDescription, + }, + } +} + +// fieldsForType returns a map of string/FieldSchema items for the given role +// type. The purpose is to keep the shared fields between dynamic and static +// roles consistent, and allow for each type to override or provide their own +// specific fields +func fieldsForType(roleType string) map[string]*framework.FieldSchema { + fields := map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeLowerCaseString, + Description: "Name of the role", + }, + "username": { + Type: framework.TypeString, + Description: "The username/logon name for the entry with which this role will be associated.", + }, + "dn": { + Type: framework.TypeString, + Description: "The distinguished name of the entry to manage.", + }, + "ttl": { + Type: framework.TypeDurationSecond, + Description: "The time-to-live for the password.", + }, + } + + // Get the fields that are specific to the type of role, and add them to the + // common fields. In the future we can add additional for dynamic roles. + var typeFields map[string]*framework.FieldSchema + switch roleType { + case staticRolePath: + typeFields = staticFields() + } + + for k, v := range typeFields { + fields[k] = v + } + + return fields +} + +// staticFields returns a map of key and field schema items that are specific +// only to static roles +func staticFields() map[string]*framework.FieldSchema { + fields := map[string]*framework.FieldSchema{ + "rotation_period": { + Type: framework.TypeDurationSecond, + Description: "Period for automatic credential rotation of the given entry.", + }, + } + return fields +} + +func (b *backend) pathStaticRoleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { + role, err := b.StaticRole(ctx, req.Storage, data.Get("name").(string)) + if err != nil { + return false, err + } + return role != nil, nil +} + +func (b *backend) pathStaticRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + name := data.Get("name").(string) + + // Grab the exclusive lock + lock := locksutil.LockForKey(b.roleLocks, name) + lock.Lock() + defer lock.Unlock() + + //TODO: Add retry logic + + // Remove the item from the queue + _, err := b.popFromRotationQueueByKey(name) + if err != nil { + return nil, err + } + + role, err := b.StaticRole(ctx, req.Storage, name) + if err != nil { + return nil, err + } + if role == nil { + return nil, nil + } + + err = req.Storage.Delete(ctx, staticRolePath+name) + if err != nil { + return nil, err + } + + return nil, nil +} + +func (b *backend) pathStaticRoleRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + role, err := b.StaticRole(ctx, req.Storage, d.Get("name").(string)) + if err != nil { + return nil, err + } + if role == nil { + return nil, nil + } + + data := map[string]interface{}{ + "dn": role.StaticAccount.DN, + "username": role.StaticAccount.Username, + } + + data["rotation_period"] = role.StaticAccount.RotationPeriod.Seconds() + if !role.StaticAccount.LastVaultRotation.IsZero() { + data["last_vault_rotation"] = role.StaticAccount.LastVaultRotation + } + + return &logical.Response{Data: data}, nil +} + +func (b *backend) pathStaticRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + name := data.Get("name").(string) + + // Grab the exclusive lock as well potentially pop and re-push the queue item + // for this role + lock := locksutil.LockForKey(b.roleLocks, name) + lock.Lock() + defer lock.Unlock() + + role, err := b.StaticRole(ctx, req.Storage, data.Get("name").(string)) + if err != nil { + return nil, err + } + + if role == nil { + role = &roleEntry{ + StaticAccount: &staticAccount{}, + } + } + + dn := data.Get("dn").(string) + if dn == "" { + return logical.ErrorResponse("dn is a required field to manage a static account"), nil + } + role.StaticAccount.DN = dn + + username := data.Get("username").(string) + if username == "" { + return logical.ErrorResponse("username is a required field to manage a static account"), nil + } + role.StaticAccount.Username = username + + rotationPeriodSecondsRaw, ok := data.GetOk("rotation_period") + if !ok { + return logical.ErrorResponse("rotation_period is required for static accounts"), nil + } + rotationPeriodSeconds := rotationPeriodSecondsRaw.(int) + if rotationPeriodSeconds < queueTickSeconds { + // If rotation frequency is specified the value + // must be at least that of the constant queueTickSeconds (5 seconds at + // time of writing), otherwise we wont be able to rotate in time + return logical.ErrorResponse("rotation_period must be %d seconds or more", queueTickSeconds), nil + } + role.StaticAccount.RotationPeriod = time.Duration(rotationPeriodSeconds) * time.Second + + // lvr represents the role's LastVaultRotation + lvr := role.StaticAccount.LastVaultRotation + + // Only call setStaticAccountPassword if we're creating the role for the + // first time + switch req.Operation { + case logical.CreateOperation: + // setStaticAccountPassword calls Storage.Put and saves the role to storage + resp, err := b.setStaticAccountPassword(ctx, req.Storage, &setStaticAccountInput{ + RoleName: name, + Role: role, + }) + if err != nil { + return nil, err + } + // guard against RotationTime not being set or zero-value + lvr = resp.RotationTime + case logical.UpdateOperation: + // store updated Role + entry, err := logical.StorageEntryJSON(staticRolePath+name, role) + if err != nil { + return nil, err + } + if err := req.Storage.Put(ctx, entry); err != nil { + return nil, err + } + + // In case this is an update, remove any previous version of the item from + // the queue + + //TODO: Add retry logic + _, err = b.popFromRotationQueueByKey(name) + if err != nil { + return nil, err + } + } + + // Add their rotation to the queue + if err := b.pushItem(&queue.Item{ + Key: name, + Priority: lvr.Add(role.StaticAccount.RotationPeriod).Unix(), + }); err != nil { + return nil, err + } + + return nil, nil +} + +type roleEntry struct { + StaticAccount *staticAccount `json:"static_account" mapstructure:"static_account"` +} + +type staticAccount struct { + // DN to create or assume management for static accounts + DN string `json:"dn"` + + // Username to create or assume management for static accounts + Username string `json:"username"` + + // Password is the current password for static accounts. As an input, this is + // used/required when trying to assume management of an existing static + // account. Return this on credential request if it exists. + Password string `json:"password"` + + // LastVaultRotation represents the last time Vault rotated the password + LastVaultRotation time.Time `json:"last_vault_rotation"` + + // RotationPeriod is number in seconds between each rotation, effectively a + // "time to live". This value is compared to the LastVaultRotation to + // determine if a password needs to be rotated + RotationPeriod time.Duration `json:"rotation_period"` +} + +// NextRotationTime calculates the next rotation by adding the Rotation Period +// to the last known vault rotation +func (s *staticAccount) NextRotationTime() time.Time { + return s.LastVaultRotation.Add(s.RotationPeriod) +} + +// PasswordTTL calculates the approximate time remaining until the password is +// no longer valid. This is approximate because the periodic rotation is only +// checked approximately every 5 seconds, and each rotation can take a small +// amount of time to process. This can result in a negative TTL time while the +// rotation function processes the Static Role and performs the rotation. If the +// TTL is negative, zero is returned. Users should not trust passwords with a +// Zero TTL, as they are likely in the process of being rotated and will quickly +// be invalidated. +func (s *staticAccount) PasswordTTL() time.Duration { + next := s.NextRotationTime() + ttl := next.Sub(time.Now()).Round(time.Second) + if ttl < 0 { + ttl = time.Duration(0) + } + return ttl +} + +func (b *backend) pathRoleList(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { + path := staticRolePath + entries, err := req.Storage.List(ctx, path) + if err != nil { + return nil, err + } + + return logical.ListResponse(entries), nil +} + +func (b *backend) StaticRole(ctx context.Context, s logical.Storage, roleName string) (*roleEntry, error) { + return b.roleAtPath(ctx, s, roleName, staticRolePath) +} + +func (b *backend) roleAtPath(ctx context.Context, s logical.Storage, roleName string, pathPrefix string) (*roleEntry, error) { + entry, err := s.Get(ctx, pathPrefix+roleName) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result roleEntry + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + + return &result, nil +} + +const staticRoleHelpSynopsis = ` +Manage the static roles that can be created with this backend. +` + +const staticRoleHelpDescription = ` +This path lets you manage the static roles that can be created with this +backend. Static Roles are associated with a single LDAP entry, and manage the +password based on a rotation period, automatically rotating the password. + +The "dn" parameter is required and configures the domain name to use when managing +the existing entry. + +The "username" parameter is required and configures the username for the LDAP entry. +This is helpful to provide a usable name when domain name (DN) isn't used directly for +authentication. + + +The "rotation_period' parameter is required and configures how often, in seconds, the credentials should be +automatically rotated by Vault. The minimum is 5 seconds (5s). +` + +const staticRolesListHelpDescription = ` +List all the static roles being managed by Vault. +` + +const staticRolesListHelpSynopsis = ` +This path lists all the static roles Vault is currently managing in OpenLDAP. +` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_rotate.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_rotate.go new file mode 100644 index 000000000..6c9b45713 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_rotate.go @@ -0,0 +1,197 @@ +package openldap + +import ( + "context" + "errors" + "fmt" + "math" + "time" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/base62" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/sdk/queue" +) + +const ( + rotateRootPath = "rotate-root" + rotateRolePath = "rotate-role/" +) + +func (b *backend) pathRotateCredentials() []*framework.Path { + return []*framework.Path{ + { + Pattern: rotateRootPath, + Fields: map[string]*framework.FieldSchema{}, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.pathRotateCredentialsUpdate, + }, + logical.CreateOperation: &framework.PathOperation{ + Callback: b.pathRotateCredentialsUpdate, + }, + }, + HelpSynopsis: pathRotateCredentialsUpdateHelpSyn, + HelpDescription: pathRotateCredentialsUpdateHelpDesc, + }, + { + Pattern: rotateRolePath + framework.GenericNameRegex("name"), + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Name of the static role", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.pathRotateRoleCredentialsUpdate, + }, + logical.CreateOperation: &framework.PathOperation{ + Callback: b.pathRotateRoleCredentialsUpdate, + }, + }, + HelpSynopsis: pathRotateRoleCredentialsUpdateHelpSyn, + HelpDescription: pathRotateRoleCredentialsUpdateHelpDesc, + }, + } +} + +func (b *backend) pathRotateCredentialsUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + config, err := readConfig(ctx, req.Storage) + if err != nil { + return nil, err + } + if config == nil { + return nil, errors.New("the config is currently unset") + } + + newPassword, err := GeneratePassword(config.PasswordLength) + if err != nil { + return nil, err + } + oldPassword := config.LDAP.BindPassword + + // Take out the backend lock since we are swapping out the connection + b.Lock() + defer b.Unlock() + + // Update the password remotely. + if err := b.client.UpdateRootPassword(config.LDAP, newPassword); err != nil { + return nil, err + } + config.LDAP.BindPassword = newPassword + + // Update the password locally. + if pwdStoringErr := storePassword(ctx, req.Storage, config); pwdStoringErr != nil { + // We were unable to store the new password locally. We can't continue in this state because we won't be able + // to roll any passwords, including our own to get back into a state of working. So, we need to roll back to + // the last password we successfully got into storage. + if rollbackErr := b.rollBackPassword(ctx, config, oldPassword); rollbackErr != nil { + return nil, fmt.Errorf(`unable to store new password due to %s and unable to return to previous password +due to %s, configure a new binddn and bindpass to restore openldap function`, pwdStoringErr, rollbackErr) + } + return nil, fmt.Errorf("unable to update password due to storage err: %s", pwdStoringErr) + } + + // Respond with a 204. + return nil, nil +} +func (b *backend) pathRotateRoleCredentialsUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + name := data.Get("name").(string) + if name == "" { + return logical.ErrorResponse("empty role name attribute given"), nil + } + + role, err := b.StaticRole(ctx, req.Storage, name) + if err != nil { + return nil, err + } + if role == nil { + return logical.ErrorResponse("role doesn't exist: %s", name), nil + } + + // In create/update of static accounts, we only care if the operation + // err'd , and this call does not return credentials + item, err := b.popFromRotationQueueByKey(name) + if err != nil { + item = &queue.Item{ + Key: name, + } + } + + resp, err := b.setStaticAccountPassword(ctx, req.Storage, &setStaticAccountInput{ + RoleName: name, + Role: role, + }) + if err != nil { + b.Logger().Warn("unable to rotate credentials in rotate-role", "error", err) + // Update the priority to re-try this rotation and re-add the item to + // the queue + item.Priority = time.Now().Add(10 * time.Second).Unix() + + // Preserve the WALID if it was returned + if resp.WALID != "" { + item.Value = resp.WALID + } + } else { + item.Priority = resp.RotationTime.Add(role.StaticAccount.RotationPeriod).Unix() + } + + // Add their rotation to the queue + if err := b.pushItem(item); err != nil { + return nil, err + } + + // We're not returning creds here because we do not know if its been processed + // by the queue. + return nil, nil +} + +// rollBackPassword uses naive exponential backoff to retry updating to an old password, +// because LDAP may still be propagating the previous password change. +func (b *backend) rollBackPassword(ctx context.Context, config *config, oldPassword string) error { + var err error + for i := 0; i < 10; i++ { + waitSeconds := math.Pow(float64(i), 2) + timer := time.NewTimer(time.Duration(waitSeconds) * time.Second) + select { + case <-timer.C: + case <-ctx.Done(): + // Outer environment is closing. + return fmt.Errorf("unable to roll back password because enclosing environment is shutting down") + } + if err = b.client.UpdateRootPassword(config.LDAP, oldPassword); err == nil { + // Success. + return nil + } + } + // Failure after looping. + return err +} + +func storePassword(ctx context.Context, s logical.Storage, config *config) error { + entry, err := logical.StorageEntryJSON(configPath, config) + if err != nil { + return err + } + return s.Put(ctx, entry) +} + +func GeneratePassword(length int) (string, error) { + return base62.Random(length) +} + +const pathRotateCredentialsUpdateHelpSyn = ` +Request to rotate the root credentials Vault uses for the OpenLDAP administrator account. +` + +const pathRotateCredentialsUpdateHelpDesc = ` +This path attempts to rotate the root credentials of the administrator account (binddn) used by Vault to manage OpenLDAP. +` + +const pathRotateRoleCredentialsUpdateHelpSyn = ` +Request to rotate the credentials for a static user account. +` +const pathRotateRoleCredentialsUpdateHelpDesc = ` +This path attempts to rotate the credentials for the given OpenLDAP static user account. +` diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/rotation.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/rotation.go new file mode 100644 index 000000000..49dca3fcf --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/rotation.go @@ -0,0 +1,498 @@ +package openldap + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/hashicorp/errwrap" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/consts" + "github.com/hashicorp/vault/sdk/helper/locksutil" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/sdk/queue" +) + +const ( + // Interval to check the queue for items needing rotation + queueTickSeconds = 5 + queueTickInterval = queueTickSeconds * time.Second + + // WAL storage key used for static account rotations + staticWALKey = "staticRotationKey" +) + +// populateQueue loads the priority queue with existing static accounts. This +// occurs at initialization, after any WAL entries of failed or interrupted +// rotations have been processed. It lists the roles from storage and searches +// for any that have an associated static account, then adds them to the +// priority queue for rotations. +func (b *backend) populateQueue(ctx context.Context, s logical.Storage) { + log := b.Logger() + log.Info("populating role rotation queue") + + // Build map of role name / wal entries + walMap, err := b.loadStaticWALs(ctx, s) + if err != nil { + log.Warn("unable to load rotation WALs", "error", err) + } + + roles, err := s.List(ctx, staticRolePath) + if err != nil { + log.Warn("unable to list role for enqueueing", "error", err) + return + } + + for _, roleName := range roles { + select { + case <-ctx.Done(): + log.Info("rotation queue restore cancelled") + return + default: + } + + role, err := b.StaticRole(ctx, s, roleName) + if err != nil { + log.Warn("unable to read static role", "error", err, "role", roleName) + continue + } + + item := queue.Item{ + Key: roleName, + Priority: role.StaticAccount.LastVaultRotation.Add(role.StaticAccount.RotationPeriod).Unix(), + } + + // Check if role name is in map + walEntry := walMap[roleName] + if walEntry != nil { + // Check walEntry last vault time + if !walEntry.LastVaultRotation.IsZero() && walEntry.LastVaultRotation.Before(role.StaticAccount.LastVaultRotation) { + // WAL's last vault rotation record is older than the role's data, so + // delete and move on + if err := framework.DeleteWAL(ctx, s, walEntry.walID); err != nil { + log.Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID) + } + } else { + log.Info("adjusting priority for Role") + item.Value = walEntry.walID + item.Priority = time.Now().Unix() + } + } + + if err := b.pushItem(&item); err != nil { + log.Warn("unable to enqueue item", "error", err, "role", roleName) + } + } +} + +// runTicker kicks off a periodic ticker that invoke the automatic credential +// rotation method at a determined interval. The default interval is 5 seconds. +func (b *backend) runTicker(ctx context.Context, s logical.Storage) { + b.Logger().Info("starting periodic ticker") + tick := time.NewTicker(queueTickInterval) + defer tick.Stop() + for { + select { + case <-tick.C: + b.rotateCredentials(ctx, s) + + case <-ctx.Done(): + b.Logger().Info("stopping periodic ticker") + return + } + } +} + +// setCredentialsWAL is used to store information in a WAL that can retry a +// credential setting or rotation in the event of partial failure. +type setCredentialsWAL struct { + NewPassword string `json:"new_password"` + OldPassword string `json:"old_password"` + RoleName string `json:"role_name"` + Username string `json:"username"` + DN string `json:"dn"` + + LastVaultRotation time.Time `json:"last_vault_rotation"` + + walID string +} + +// rotateCredentials sets a new password for a static account. This method is +// invoked in the runTicker method, which is in it's own go-routine, and invoked +// periodically (approximately every 5 seconds). +// +// This method loops through the priority queue, popping the highest priority +// item until it encounters the first item that does not yet need rotation, +// based on the current time. +func (b *backend) rotateCredentials(ctx context.Context, s logical.Storage) { + for b.rotateCredential(ctx, s) { + } +} + +func (b *backend) rotateCredential(ctx context.Context, s logical.Storage) bool { + // Quit rotating credentials if shutdown has started + select { + case <-ctx.Done(): + return false + default: + } + item, err := b.popFromRotationQueue() + if err != nil { + if err != queue.ErrEmpty { + b.Logger().Error("error popping item from queue", "err", err) + } + return false + } + + // Guard against possible nil item + if item == nil { + return false + } + + // Grab the exclusive lock for this Role, to make sure we don't incur and + // writes during the rotation process + lock := locksutil.LockForKey(b.roleLocks, item.Key) + lock.Lock() + defer lock.Unlock() + + // Validate the role still exists + role, err := b.StaticRole(ctx, s, item.Key) + if err != nil { + b.Logger().Error("unable to load role", "role", item.Key, "error", err) + item.Priority = time.Now().Add(10 * time.Second).Unix() + if err := b.pushItem(item); err != nil { + b.Logger().Error("unable to push item on to queue", "error", err) + } + return true + } + if role == nil { + b.Logger().Warn("role not found", "role", item.Key, "error", err) + return true + } + + // If "now" is less than the Item priority, then this item does not need to + // be rotated + if time.Now().Unix() < item.Priority { + if err := b.pushItem(item); err != nil { + b.Logger().Error("unable to push item on to queue", "error", err) + } + // Break out of the for loop + return false + } + + input := &setStaticAccountInput{ + RoleName: item.Key, + Role: role, + } + + // If there is a WAL entry related to this Role, the corresponding WAL ID + // should be stored in the Item's Value field. + if walID, ok := item.Value.(string); ok { + walEntry, err := b.findStaticWAL(ctx, s, walID) + if err != nil { + b.Logger().Error("error finding static WAL", "error", err) + item.Priority = time.Now().Add(10 * time.Second).Unix() + if err := b.pushItem(item); err != nil { + b.Logger().Error("unable to push item on to queue", "error", err) + } + } + if walEntry != nil && walEntry.NewPassword != "" { + input.Password = walEntry.NewPassword + input.WALID = walID + } + } + + resp, err := b.setStaticAccountPassword(ctx, s, input) + if err != nil { + b.Logger().Error("unable to rotate credentials in periodic function", "error", err) + // Increment the priority enough so that the next call to this method + // likely will not attempt to rotate it, as a back-off of sorts + item.Priority = time.Now().Add(10 * time.Second).Unix() + + // Preserve the WALID if it was returned + if resp != nil && resp.WALID != "" { + item.Value = resp.WALID + } + + if err := b.pushItem(item); err != nil { + b.Logger().Error("unable to push item on to queue", "error", err) + } + // Go to next item + return true + } + + lvr := resp.RotationTime + if lvr.IsZero() { + lvr = time.Now() + } + + // Update priority and push updated Item to the queue + nextRotation := lvr.Add(role.StaticAccount.RotationPeriod) + item.Priority = nextRotation.Unix() + if err := b.pushItem(item); err != nil { + b.Logger().Warn("unable to push item on to queue", "error", err) + } + return true +} + +// findStaticWAL loads a WAL entry by ID. If found, only return the WAL if it +// is of type staticWALKey, otherwise return nil +func (b *backend) findStaticWAL(ctx context.Context, s logical.Storage, id string) (*setCredentialsWAL, error) { + wal, err := framework.GetWAL(ctx, s, id) + if err != nil { + return nil, err + } + + if wal == nil || wal.Kind != staticWALKey { + return nil, nil + } + + data := wal.Data.(map[string]interface{}) + walEntry := setCredentialsWAL{ + walID: id, + NewPassword: data["new_password"].(string), + OldPassword: data["old_password"].(string), + RoleName: data["role_name"].(string), + Username: data["username"].(string), + DN: data["dn"].(string), + } + lvr, err := time.Parse(time.RFC3339, data["last_vault_rotation"].(string)) + if err != nil { + return nil, err + } + walEntry.LastVaultRotation = lvr + + return &walEntry, nil +} + +type setStaticAccountInput struct { + RoleName string + Role *roleEntry + Password string + CreateUser bool + WALID string +} + +type setStaticAccountOutput struct { + RotationTime time.Time + Password string + // Optional return field, in the event WAL was created and not destroyed + // during the operation + WALID string +} + +// setStaticAccountPassword sets the password for a static account associated with a +// Role. This method does many things: +// - verifies role exists and is in the allowed roles list +// - loads an existing WAL entry if WALID input is given, otherwise creates a +// new WAL entry +// - gets a database connection +// - accepts an input password, otherwise generates a new one via gRPC to the +// database plugin +// - sets new password for the static account +// - uses WAL for ensuring passwords are not lost if storage to Vault fails +// +// This method does not perform any operations on the priority queue. Those +// tasks must be handled outside of this method. +func (b *backend) setStaticAccountPassword(ctx context.Context, s logical.Storage, input *setStaticAccountInput) (*setStaticAccountOutput, error) { + var merr error + if input == nil || input.Role == nil || input.RoleName == "" { + return nil, errors.New("input was empty when attempting to set credentials for static account") + } + // Re-use WAL ID if present, otherwise PUT a new WAL + output := &setStaticAccountOutput{WALID: input.WALID} + + config, err := readConfig(ctx, s) + if err != nil { + return nil, err + } + if config == nil { + return nil, errors.New("the config is currently unset") + } + + newPassword, err := GeneratePassword(config.PasswordLength) + if err != nil { + return nil, err + } + + oldPassword := input.Role.StaticAccount.Password + + // Take out the backend lock since we are swapping out the connection + b.Lock() + defer b.Unlock() + + if output.WALID == "" { + output.WALID, err = framework.PutWAL(ctx, s, staticWALKey, &setCredentialsWAL{ + RoleName: input.RoleName, + Username: input.Role.StaticAccount.Username, + DN: input.Role.StaticAccount.DN, + NewPassword: newPassword, + OldPassword: oldPassword, + LastVaultRotation: input.Role.StaticAccount.LastVaultRotation, + }) + if err != nil { + return output, errwrap.Wrapf("error writing WAL entry: {{err}}", err) + } + } + + // Update the password remotely. + if err := b.client.UpdatePassword(config.LDAP, input.Role.StaticAccount.DN, newPassword); err != nil { + return nil, err + } + + // Update the password locally. + if pwdStoringErr := storePassword(ctx, s, config); pwdStoringErr != nil { + // We were unable to store the new password locally. We can't continue in this state because we won't be able + // to roll any passwords, including our own to get back into a state of working. So, we need to roll back to + // the last password we successfully got into storage. + if rollbackErr := b.rollBackPassword(ctx, config, oldPassword); rollbackErr != nil { + return nil, fmt.Errorf(`unable to store new password due to %s and unable to return to previous password due +to %s, configure a new binddn and bindpass to restore openldap function`, pwdStoringErr, rollbackErr) + } + return nil, fmt.Errorf("unable to update password due to storage err: %s", pwdStoringErr) + } + + // Store updated role information + // lvr is the known LastVaultRotation + lvr := time.Now() + input.Role.StaticAccount.LastVaultRotation = lvr + input.Role.StaticAccount.Password = newPassword + output.RotationTime = lvr + + entry, err := logical.StorageEntryJSON(staticRolePath+input.RoleName, input.Role) + if err != nil { + return output, err + } + if err := s.Put(ctx, entry); err != nil { + return output, err + } + + // Cleanup WAL after successfully rotating and pushing new item on to queue + if err := framework.DeleteWAL(ctx, s, output.WALID); err != nil { + merr = multierror.Append(merr, err) + return output, merr + } + + // The WAL has been deleted, return new setStaticAccountOutput without it + return &setStaticAccountOutput{RotationTime: lvr}, merr +} + +// initQueue preforms the necessary checks and initializations needed to preform +// automatic credential rotation for roles associated with static accounts. This +// method verifies if a queue is needed (primary server or local mount), and if +// so initializes the queue and launches a go-routine to periodically invoke a +// method to preform the rotations. +// +// initQueue is invoked by the Factory method in a go-routine. The Factory does +// not wait for success or failure of it's tasks before continuing. This is to +// avoid blocking the mount process while loading and evaluating existing roles, +// etc. +func (b *backend) initQueue(ctx context.Context, conf *logical.InitializationRequest) { + // Verify this mount is on the primary server, or is a local mount. If not, do + // not create a queue or launch a ticker. Both processing the WAL list and + // populating the queue are done sequentially and before launching a + // go-routine to run the periodic ticker. + replicationState := b.System().ReplicationState() + if (b.System().LocalMount() || !replicationState.HasState(consts.ReplicationPerformanceSecondary)) && + !replicationState.HasState(consts.ReplicationDRSecondary) && + !replicationState.HasState(consts.ReplicationPerformanceStandby) { + b.Logger().Info("initializing database rotation queue") + + // Load roles and populate queue with static accounts + b.populateQueue(ctx, conf.Storage) + + // Launch ticker + go b.runTicker(ctx, conf.Storage) + } +} + +// loadStaticWALs reads WAL entries and returns a map of roles and their +// setCredentialsWAL, if found. +func (b *backend) loadStaticWALs(ctx context.Context, s logical.Storage) (map[string]*setCredentialsWAL, error) { + keys, err := framework.ListWAL(ctx, s) + if err != nil { + return nil, err + } + if len(keys) == 0 { + b.Logger().Debug("no WAL entries found") + return nil, nil + } + + walMap := make(map[string]*setCredentialsWAL) + // Loop through WAL keys and process any rotation ones + for _, walID := range keys { + walEntry, err := b.findStaticWAL(ctx, s, walID) + if err != nil { + b.Logger().Error("error loading static WAL", "id", walID, "error", err) + continue + } + if walEntry == nil { + continue + } + + // Verify the static role still exists + roleName := walEntry.RoleName + role, err := b.StaticRole(ctx, s, roleName) + if err != nil { + b.Logger().Warn("unable to read static role", "error", err, "role", roleName) + continue + } + if role == nil || role.StaticAccount == nil { + if err := framework.DeleteWAL(ctx, s, walEntry.walID); err != nil { + b.Logger().Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID) + } + continue + } + + walEntry.walID = walID + walMap[walEntry.RoleName] = walEntry + } + return walMap, nil +} + +// pushItem wraps the internal queue's Push call, to make sure a queue is +// actually available. This is needed because both runTicker and initQueue +// operate in go-routines, and could be accessing the queue concurrently +func (b *backend) pushItem(item *queue.Item) error { + b.RLock() + defer b.RUnlock() + + if b.credRotationQueue != nil { + return b.credRotationQueue.Push(item) + } + + b.Logger().Warn("no queue found during push item") + return nil +} + +// popFromRotationQueue wraps the internal queue's Pop call, to make sure a queue is +// actually available. This is needed because both runTicker and initQueue +// operate in go-routines, and could be accessing the queue concurrently +func (b *backend) popFromRotationQueue() (*queue.Item, error) { + b.RLock() + defer b.RUnlock() + if b.credRotationQueue != nil { + return b.credRotationQueue.Pop() + } + return nil, queue.ErrEmpty +} + +// popFromRotationQueueByKey wraps the internal queue's PopByKey call, to make sure a queue is +// actually available. This is needed because both runTicker and initQueue +// operate in go-routines, and could be accessing the queue concurrently +func (b *backend) popFromRotationQueueByKey(name string) (*queue.Item, error) { + b.RLock() + defer b.RUnlock() + if b.credRotationQueue != nil { + item, err := b.credRotationQueue.PopByKey(name) + if err != nil { + return nil, err + } + if item != nil { + return item, nil + } + } + return nil, queue.ErrEmpty +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 52f918bc2..a20933e14 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -425,6 +425,9 @@ github.com/hashicorp/vault-plugin-secrets-gcpkms github.com/hashicorp/vault-plugin-secrets-kv # github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.0.0-20200124190647-0026e6bed4fb github.com/hashicorp/vault-plugin-secrets-mongodbatlas +# github.com/hashicorp/vault-plugin-secrets-openldap v0.0.0-20200215165936-237ad8919d2c +github.com/hashicorp/vault-plugin-secrets-openldap +github.com/hashicorp/vault-plugin-secrets-openldap/client # github.com/hashicorp/vault/api v1.0.5-0.20200214222743-c39f5634b39f => ./api github.com/hashicorp/vault/api # github.com/hashicorp/vault/sdk v0.1.14-0.20200214222719-7a3b716487a5 => ./sdk