Add OpenLDAP Secret Plugin (#8360)

* Add openldap secret plugin

* go mod vendor

* Revert to go-ldap 3.1.3

* go mod vendor
This commit is contained in:
Jason O'Donnell 2020-02-15 13:21:07 -05:00 committed by GitHub
parent 9dd18d8487
commit dd9f25a118
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 2485 additions and 0 deletions

View File

@ -380,6 +380,7 @@ func TestPredict_Plugins(t *testing.T) {
"oci",
"oidc",
"okta",
"openldap",
"pcf", // Deprecated.
"pki",
"postgresql",

1
go.mod
View File

@ -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

3
go.sum
View File

@ -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=

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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/
```

View File

@ -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.
`

View File

@ -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)
}

View File

@ -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())
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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=

View File

@ -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.
`

View File

@ -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.
`

View File

@ -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.
`

View File

@ -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.
`

View File

@ -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
}

3
vendor/modules.txt vendored
View File

@ -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