Vendor OpenLDAP dynamic secrets (#10818)

This commit is contained in:
Michael Golowka 2021-02-02 11:41:47 -07:00 committed by GitHub
parent 4a6e00081c
commit ec18926754
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 2115 additions and 45 deletions

4
go.mod
View file

@ -95,9 +95,9 @@ require (
github.com/hashicorp/vault-plugin-secrets-gcpkms v0.7.0
github.com/hashicorp/vault-plugin-secrets-kv v0.7.0
github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.2.0
github.com/hashicorp/vault-plugin-secrets-openldap v0.3.0
github.com/hashicorp/vault-plugin-secrets-openldap v0.1.6-0.20210201204049-4f0f91977798
github.com/hashicorp/vault/api v1.0.5-0.20201001211907-38d91b749c77
github.com/hashicorp/vault/sdk v0.1.14-0.20201022214319-d87657199d4b
github.com/hashicorp/vault/sdk v0.1.14-0.20210127185906-6b455835fa8c
github.com/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4
github.com/jcmturner/gokrb5/v8 v8.0.0
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f

7
go.sum
View file

@ -338,6 +338,8 @@ github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ER
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
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-asn1-ber/asn1-ber v1.4.1 h1:qP/QDxOtmMoJVgXHCXNzDpA0+wkgYB2x5QoLMVOciyw=
github.com/go-asn1-ber/asn1-ber v1.4.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@ -347,8 +349,11 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8=
github.com/go-ldap/ldap/v3 v3.1.7/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-ldap/ldap/v3 v3.1.10 h1:7WsKqasmPThNvdl0Q5GPpbTDD/ZD98CfuawrMIuh7qQ=
github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3 h1:sfz1YppV05y4sYaW7kXZtrocU/+vimnIWt4cxAYh7+o=
github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3/go.mod h1:ZXFhGda43Z2TVbfGZefXyMJzsDHhCh0go3bZUcwTx7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
@ -665,6 +670,8 @@ github.com/hashicorp/vault-plugin-secrets-kv v0.7.0 h1:Sq5CmKWxQu+MtO6AXYM+STPHG
github.com/hashicorp/vault-plugin-secrets-kv v0.7.0/go.mod h1:B/Cybh5aVF7LNAMHwVBxY8t7r2eL0C6HVGgTyP4nKK4=
github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.2.0 h1:uTtKxt5qfwTj6PqwnwPdU0fg1lIaaoqTtauuNpI2Epc=
github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.2.0/go.mod h1:JOqn2mWJJbTp9NaC0CSCc3q5HQA99LfeSqgpC3YS+oA=
github.com/hashicorp/vault-plugin-secrets-openldap v0.1.6-0.20210201204049-4f0f91977798 h1:G3S7rF/zHfQnYZglk+WvjzBuJyjQAnP0xdGL/4i3jzM=
github.com/hashicorp/vault-plugin-secrets-openldap v0.1.6-0.20210201204049-4f0f91977798/go.mod h1:GiFI8Bxwx3+fn0A3SyVp9XdYQhm3cOgN8GzwKxyJ9So=
github.com/hashicorp/vault-plugin-secrets-openldap v0.3.0 h1:aDdWZMdr93OtwZRE3TPKJyZgY6ZTe09G7bb2GL1HeAo=
github.com/hashicorp/vault-plugin-secrets-openldap v0.3.0/go.mod h1:pRE6pJzEVTSn3reeEn6YOV73R/6PcyylwQBz33zZKds=
github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 h1:O/pT5C1Q3mVXMyuqg7yuAWUg/jMZR1/0QTzTRdNR6Uw=

View file

@ -160,6 +160,10 @@ func PrintBytes(out io.Writer, buf []byte, indent string) {
}
}
func WritePacket(out io.Writer, p *Packet) {
printPacket(out, p, 0, false)
}
func PrintPacket(p *Packet) {
printPacket(os.Stdout, p, 0, false)
}
@ -468,6 +472,22 @@ func NewBoolean(ClassType Class, TagType Type, Tag Tag, Value bool, Description
return p
}
// NewLDAPBoolean returns a RFC 4511-compliant Boolean packet
func NewLDAPBoolean(Value bool, Description string) *Packet {
intValue := int64(0)
if Value {
intValue = 255
}
p := Encode(ClassUniversal, TypePrimitive, TagBoolean, nil, Description)
p.Value = Value
p.Data.Write(encodeInteger(intValue))
return p
}
func NewInteger(ClassType Class, TagType Type, Tag Tag, Value interface{}, Description string) *Packet {
p := Encode(ClassType, TagType, Tag, nil, Description)

24
vendor/github.com/go-ldap/ldif/.gitignore generated vendored Normal file
View file

@ -0,0 +1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

22
vendor/github.com/go-ldap/ldif/.travis.yml generated vendored Normal file
View file

@ -0,0 +1,22 @@
language: go
go:
- 1.11
- 1.13
- 1.14
- tip
matrix:
fast_finish: true
allow_failures:
- go: tip
go_import_path: github.com/go-ldap/ldif
install:
- go get github.com/go-ldap/ldap/v3
- go get code.google.com/p/go.tools/cmd/cover || go get golang.org/x/tools/cmd/cover
- go get golang.org/x/lint/golint || true
- go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow
- go build -v ./...
script:
- make test
- make fmt
- make vet
- if [ "${TRAVIS_GO_VERSION}" != "1.6" ]; then make lint; fi

21
vendor/github.com/go-ldap/ldif/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 go-ldap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

41
vendor/github.com/go-ldap/ldif/Makefile generated vendored Normal file
View file

@ -0,0 +1,41 @@
.PHONY: default install build test quicktest fmt vet lint
default: fmt vet lint build quicktest
install:
go get -t -v ./...
build:
go build -v ./...
test:
go test -v -cover ./...
quicktest:
go test ./...
# Capture output and force failure when there is non-empty output
fmt:
@echo gofmt -l .
@OUTPUT=`gofmt -l . 2>&1`; \
if [ "$$OUTPUT" ]; then \
echo "gofmt must be run on the following files:"; \
echo "$$OUTPUT"; \
exit 1; \
fi
# Only run on go1.5+
vet:
go vet -vettool=$(which shadow) -atomic -bool -copylocks -nilfunc -printf -rangeloops -unreachable -unsafeptr -unusedresult .
# https://github.com/golang/lint
# go get github.com/golang/lint/golint
# Capture output and force failure when there is non-empty output
lint:
@echo golint ./...
@OUTPUT=`golint ./... 2>&1`; \
if [ "$$OUTPUT" ]; then \
echo "golint errors:"; \
echo "$$OUTPUT"; \
exit 1; \
fi

20
vendor/github.com/go-ldap/ldif/README.md generated vendored Normal file
View file

@ -0,0 +1,20 @@
# ldif
Utilities for working with ldif data. This implements most of RFC 2849.
## Change Entries
Support for moddn / modrdn changes is missing (in Unmarshal and
Marshal) - github.com/go-ldap/ldap/v3 does not support it currently
## Controls
Only simple controls without control value are supported, currently
just
Manage DSA IT - oid: 2.16.840.1.113730.3.4.2
## URLs
URL schemes in an LDIF like
jpegPhoto;binary:< file:///usr/share/photos/someone.jpg
are only supported for the "file" scheme like in the example above

57
vendor/github.com/go-ldap/ldif/apply.go generated vendored Normal file
View file

@ -0,0 +1,57 @@
package ldif
import (
"fmt"
"log"
"github.com/go-ldap/ldap/v3"
)
// Apply sends the LDIF entries to the server and does the changes as
// given by the entries.
//
// All *ldap.Entry are converted to an *ldap.AddRequest.
//
// By default, it returns on the first error. To continue with applying the
// LDIF, set the continueOnErr argument to true - in this case the errors
// are logged with log.Printf()
func (l *LDIF) Apply(conn ldap.Client, continueOnErr bool) error {
for _, entry := range l.Entries {
switch {
case entry.Entry != nil:
add := ldap.NewAddRequest(entry.Entry.DN, entry.Add.Controls)
for _, attr := range entry.Entry.Attributes {
add.Attribute(attr.Name, attr.Values)
}
entry.Add = add
fallthrough
case entry.Add != nil:
if err := conn.Add(entry.Add); err != nil {
if continueOnErr {
log.Printf("ERROR: Failed to add %s: %s", entry.Add.DN, err)
continue
}
return fmt.Errorf("failed to add %s: %s", entry.Add.DN, err)
}
case entry.Del != nil:
if err := conn.Del(entry.Del); err != nil {
if continueOnErr {
log.Printf("ERROR: Failed to delete %s: %s", entry.Del.DN, err)
continue
}
return fmt.Errorf("failed to delete %s: %s", entry.Del.DN, err)
}
case entry.Modify != nil:
if err := conn.Modify(entry.Modify); err != nil {
if continueOnErr {
log.Printf("ERROR: Failed to modify %s: %s", entry.Modify.DN, err)
continue
}
return fmt.Errorf("failed to modify %s: %s", entry.Modify.DN, err)
}
}
}
return nil
}

2
vendor/github.com/go-ldap/ldif/doc.go generated vendored Normal file
View file

@ -0,0 +1,2 @@
// Package ldif contains utilities for working with ldif data
package ldif

8
vendor/github.com/go-ldap/ldif/go.mod generated vendored Normal file
View file

@ -0,0 +1,8 @@
module github.com/go-ldap/ldif
go 1.14
require (
github.com/go-asn1-ber/asn1-ber v1.4.1 // indirect
github.com/go-ldap/ldap/v3 v3.1.7
)

5
vendor/github.com/go-ldap/ldif/go.sum generated vendored Normal file
View file

@ -0,0 +1,5 @@
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.4.1 h1:qP/QDxOtmMoJVgXHCXNzDpA0+wkgYB2x5QoLMVOciyw=
github.com/go-asn1-ber/asn1-ber v1.4.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.1.7 h1:aHjuWTgZsnxjMgqzx0JHwNqz4jBYZTcNarbPFkW1Oww=
github.com/go-ldap/ldap/v3 v3.1.7/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=

534
vendor/github.com/go-ldap/ldif/ldif.go generated vendored Normal file
View file

@ -0,0 +1,534 @@
// Package ldif contains an LDIF parser and marshaller (RFC 2849).
package ldif
import (
"bufio"
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"net/url"
"strconv"
"strings"
"github.com/go-ldap/ldap/v3"
)
// Entry is one entry in the LDIF
type Entry struct {
Entry *ldap.Entry
Add *ldap.AddRequest
Del *ldap.DelRequest
Modify *ldap.ModifyRequest
}
// The LDIF struct is used for parsing an LDIF. The Controls
// is used to tell the parser to ignore any controls found
// when parsing (default: false to ignore the controls).
// FoldWidth is used for the line lenght when marshalling.
type LDIF struct {
Entries []*Entry
Version int
changeType string
FoldWidth int
Controls bool
firstEntry bool
}
// The ParseError holds the error message and the line in the ldif
// where the error occurred.
type ParseError struct {
Line int
Message string
}
// Error implements the error interface
func (e *ParseError) Error() string {
return fmt.Sprintf("Error in line %d: %s", e.Line, e.Message)
}
var cr byte = '\x0D'
var lf byte = '\x0A'
var sep = string([]byte{cr, lf})
var comment byte = '#'
var space byte = ' '
var spaces = string(space)
// Parse wraps Unmarshal to parse an LDIF from a string
func Parse(str string) (l *LDIF, err error) {
buf := bytes.NewBuffer([]byte(str))
l = &LDIF{}
err = Unmarshal(buf, l)
return
}
// ParseWithControls wraps Unmarshal to parse an LDIF from
// a string, controls are added to change records
func ParseWithControls(str string) (l *LDIF, err error) {
buf := bytes.NewBuffer([]byte(str))
l = &LDIF{Controls: true}
err = Unmarshal(buf, l)
return
}
// Unmarshal parses the LDIF from the given io.Reader into the LDIF struct.
// The caller is responsible for closing the io.Reader if that is
// needed.
func Unmarshal(r io.Reader, l *LDIF) (err error) {
if r == nil {
return &ParseError{Line: 0, Message: "No reader present"}
}
curLine := 0
l.Version = 0
l.changeType = ""
isComment := false
reader := bufio.NewReader(r)
var lines []string
var line, nextLine string
l.firstEntry = true
for {
curLine++
nextLine, err = reader.ReadString(lf)
nextLine = strings.TrimRight(nextLine, sep)
switch err {
case nil, io.EOF:
switch len(nextLine) {
case 0:
if len(line) == 0 && err == io.EOF {
return nil
}
if len(line) == 0 && len(lines) == 0 {
continue
}
lines = append(lines, line)
entry, perr := l.parseEntry(lines)
if perr != nil {
return &ParseError{Line: curLine, Message: perr.Error()}
}
l.Entries = append(l.Entries, entry)
line = ""
lines = []string{}
if err == io.EOF {
return nil
}
default:
switch nextLine[0] {
case comment:
isComment = true
continue
case space:
if isComment {
continue
}
line += nextLine[1:]
continue
default:
isComment = false
if len(line) != 0 {
lines = append(lines, line)
}
line = nextLine
continue
}
}
default:
return &ParseError{Line: curLine, Message: err.Error()}
}
}
}
func (l *LDIF) parseEntry(lines []string) (entry *Entry, err error) {
if len(lines) == 0 {
return nil, errors.New("empty entry?")
}
if l.firstEntry && strings.HasPrefix(lines[0], "version:") {
l.firstEntry = false
line := strings.TrimLeft(lines[0][8:], spaces)
if l.Version, err = strconv.Atoi(line); err != nil {
return nil, err
}
if l.Version != 1 {
return nil, errors.New("Invalid version spec " + string(line))
}
l.Version = 1
if len(lines) == 1 {
return nil, nil
}
lines = lines[1:]
}
l.firstEntry = false
if len(lines) == 0 {
return nil, nil
}
if !strings.HasPrefix(lines[0], "dn:") {
return nil, errors.New("missing 'dn:'")
}
_, val, err := l.parseLine(lines[0])
if err != nil {
return nil, err
}
dn := val
if len(lines) == 1 {
return nil, errors.New("only a dn: line")
}
lines = lines[1:]
var controls []ldap.Control
controls, lines, err = l.parseControls(lines)
if err != nil {
return nil, err
}
if strings.HasPrefix(lines[0], "changetype:") {
_, val, err := l.parseLine(lines[0])
if err != nil {
return nil, err
}
l.changeType = val
if len(lines) > 1 {
lines = lines[1:]
}
}
switch l.changeType {
case "":
if len(controls) != 0 {
return nil, errors.New("controls found without changetype")
}
attrs, err := l.parseAttrs(lines)
if err != nil {
return nil, err
}
return &Entry{Entry: ldap.NewEntry(dn, attrs)}, nil
case "add":
attrs, err := l.parseAttrs(lines)
if err != nil {
return nil, err
}
// FIXME: controls for add - see https://github.com/go-ldap/ldap/issues/81
add := ldap.NewAddRequest(dn, controls)
for attr, vals := range attrs {
add.Attribute(attr, vals)
}
return &Entry{Add: add}, nil
case "delete":
if len(lines) > 1 {
return nil, errors.New("no attributes allowed for changetype delete")
}
return &Entry{Del: ldap.NewDelRequest(dn, controls)}, nil
case "modify":
// FIXME: controls for modify - see https://github.com/go-ldap/ldap/issues/81
mod := ldap.NewModifyRequest(dn, controls)
var op, attribute string
var values []string
if lines[len(lines)-1] != "-" {
return nil, errors.New("modify request does not close with a single dash")
}
for i := 0; i < len(lines); i++ {
if lines[i] == "-" {
switch op {
case "":
return nil, fmt.Errorf("empty operation")
case "add":
mod.Add(attribute, values)
op = ""
attribute = ""
values = nil
case "replace":
mod.Replace(attribute, values)
op = ""
attribute = ""
values = nil
case "delete":
mod.Delete(attribute, values)
op = ""
attribute = ""
values = nil
default:
return nil, fmt.Errorf("invalid operation %s in modify request", op)
}
continue
}
attr, val, err := l.parseLine(lines[i])
if err != nil {
return nil, err
}
if op == "" {
op = attr
attribute = val
} else {
if attr != attribute {
return nil, fmt.Errorf("invalid attribute %s in %s request for %s", attr, op, attribute)
}
values = append(values, val)
}
}
return &Entry{Modify: mod}, nil
case "moddn", "modrdn":
return nil, fmt.Errorf("unsupported changetype %s", l.changeType)
default:
return nil, fmt.Errorf("invalid changetype %s", l.changeType)
}
}
func (l *LDIF) parseAttrs(lines []string) (map[string][]string, error) {
attrs := make(map[string][]string)
for i := 0; i < len(lines); i++ {
attr, val, err := l.parseLine(lines[i])
if err != nil {
return nil, err
}
attrs[attr] = append(attrs[attr], val)
}
return attrs, nil
}
func (l *LDIF) parseLine(line string) (attr, val string, err error) {
off := 0
for len(line) > off && line[off] != ':' {
off++
if off >= len(line) {
err = fmt.Errorf("Missing : in line `%s`", line)
return
}
}
if off == len(line) {
err = fmt.Errorf("Missing : in the line `%s`", line)
return
}
if off > len(line)-2 {
err = errors.New("empty value")
// FIXME: this is allowed for some attributes, e.g. seeAlso
return
}
attr = line[0:off]
if err = validAttr(attr); err != nil {
attr = ""
val = ""
return
}
switch line[off+1] {
case ':':
val, err = decodeBase64(strings.TrimLeft(line[off+2:], spaces))
if err != nil {
return
}
case '<':
val, err = readURLValue(strings.TrimLeft(line[off+2:], spaces))
if err != nil {
return
}
default:
val = strings.TrimLeft(line[off+1:], spaces)
}
return
}
func (l *LDIF) parseControls(lines []string) ([]ldap.Control, []string, error) {
var controls []ldap.Control
for {
if !strings.HasPrefix(lines[0], "control:") {
break
}
if !l.Controls {
if len(lines) == 1 {
return nil, nil, errors.New("only controls found")
}
lines = lines[1:]
continue
}
_, val, err := l.parseLine(lines[0])
if err != nil {
return nil, nil, err
}
var oid, ctrlValue string
criticality := false
parts := strings.SplitN(val, " ", 3)
if err = validOID(parts[0]); err != nil {
return nil, nil, fmt.Errorf("%s is not a valid oid: %s", oid, err)
}
oid = parts[0]
if len(parts) > 1 {
switch parts[1] {
case "true":
criticality = true
if len(parts) > 2 {
parts[1] = parts[2]
parts = parts[0:2]
}
case "false":
criticality = false
if len(parts) > 2 {
parts[1] = parts[2]
parts = parts[0:2]
}
}
}
if len(parts) == 2 {
ctrlValue = parts[1]
}
if ctrlValue == "" {
switch oid {
case ldap.ControlTypeManageDsaIT:
controls = append(controls, &ldap.ControlManageDsaIT{Criticality: criticality})
default:
return nil, nil, fmt.Errorf("unsupported control found: %s", oid)
}
} else {
switch ctrlValue[0] { // where is this documented?
case ':':
if len(ctrlValue) == 1 {
return nil, nil, errors.New("missing value for base64 encoded control value")
}
ctrlValue, err = decodeBase64(strings.TrimLeft(ctrlValue[1:], spaces))
if err != nil {
return nil, nil, err
}
if ctrlValue == "" {
return nil, nil, errors.New("base64 decoded to empty value")
}
case '<':
if len(ctrlValue) == 1 {
return nil, nil, errors.New("missing value for url control value")
}
ctrlValue, err = readURLValue(strings.TrimLeft(ctrlValue[1:], spaces))
if err != nil {
return nil, nil, err
}
if ctrlValue == "" {
return nil, nil, errors.New("url resolved to an empty value")
}
}
// TODO:
// convert ctrlValue to *ber.Packet and decode with something like
// ctrl := ldap.DecodeControl()
// ... FIXME: the controls need a Decode() interface
// so we can just do a
// ctrl := ldap.ControlByOID(oid) // returns an empty &ControlSomething{}
// ctrl.Decode((*ber.Packet)(ctrlValue))
// ctrl.Criticality = criticality
// that should be usable in github.com/go-ldap/ldap/control.go also
// to decode the incoming control
// controls = append(controls, ctrl)
return nil, nil, fmt.Errorf("controls with values are not supported, oid: %s", oid)
}
if len(lines) == 1 {
return nil, nil, errors.New("only controls found")
}
lines = lines[1:]
}
return controls, lines, nil
}
func readURLValue(val string) (string, error) {
u, err := url.Parse(val)
if err != nil {
return "", fmt.Errorf("failed to parse URL: %s", err)
}
if u.Scheme != "file" {
return "", fmt.Errorf("unsupported URL scheme %s", u.Scheme)
}
data, err := ioutil.ReadFile(toPath(u))
if err != nil {
return "", fmt.Errorf("failed to read %s: %s", u.Path, err)
}
val = string(data) // FIXME: safe?
return val, nil
}
func decodeBase64(enc string) (string, error) {
dec := make([]byte, base64.StdEncoding.DecodedLen(len([]byte(enc))))
n, err := base64.StdEncoding.Decode(dec, []byte(enc))
if err != nil {
return "", err
}
return string(dec[:n]), nil
}
func validOID(oid string) error {
lastDot := true
for _, c := range oid {
switch {
case c == '.' && lastDot:
return errors.New("OID with at least 2 consecutive dots")
case c == '.':
lastDot = true
case c >= '0' && c <= '9':
lastDot = false
default:
return errors.New("Invalid character in OID")
}
}
return nil
}
func validAttr(attr string) error {
if len(attr) == 0 {
return errors.New("empty attribute name")
}
switch {
case attr[0] >= 'A' && attr[0] <= 'Z':
// A-Z
case attr[0] >= 'a' && attr[0] <= 'z':
// a-z
default:
if attr[0] >= '0' && attr[0] <= '9' {
return validOID(attr)
}
return errors.New("invalid first character in attribute")
}
for i := 1; i < len(attr); i++ {
c := attr[i]
switch {
case c >= '0' && c <= '9':
case c >= 'A' && c <= 'Z':
case c >= 'a' && c <= 'z':
case c == '-':
case c == ';':
default:
return errors.New("invalid character in attribute name")
}
}
return nil
}
// AllEntries returns all *ldap.Entries in the LDIF
func (l *LDIF) AllEntries() (entries []*ldap.Entry) {
for _, entry := range l.Entries {
if entry.Entry != nil {
entries = append(entries, entry.Entry)
}
}
return entries
}

247
vendor/github.com/go-ldap/ldif/marshal.go generated vendored Normal file
View file

@ -0,0 +1,247 @@
package ldif
import (
"encoding/base64"
"errors"
"fmt"
"io"
"github.com/go-ldap/ldap/v3"
)
var foldWidth = 76
// ErrMixed is the error, that we cannot mix change records and content
// records in one LDIF
var ErrMixed = errors.New("cannot mix change records and content records")
// Marshal returns an LDIF string from the given LDIF.
//
// The default line lenght is 76 characters. This can be changed by setting
// the fw parameter to something else than 0.
// For a fold width < 0, no folding will be done, with 0, the default is used.
func Marshal(l *LDIF) (data string, err error) {
hasEntry := false
hasChange := false
if l.Version > 0 {
data = "version: 1\n"
}
fw := l.FoldWidth
if fw == 0 {
fw = foldWidth
}
for _, e := range l.Entries {
switch {
case e.Add != nil:
hasChange = true
if hasEntry {
return "", ErrMixed
}
data += foldLine("dn: "+e.Add.DN, fw) + "\n"
data += "changetype: add\n"
for _, add := range e.Add.Attributes {
if len(add.Vals) == 0 {
return "", errors.New("changetype 'add' requires non empty value list")
}
for _, v := range add.Vals {
ev, t := encodeValue(v)
col := ": "
if t {
col = ":: "
}
data += foldLine(add.Type+col+ev, fw) + "\n"
}
}
case e.Del != nil:
hasChange = true
if hasEntry {
return "", ErrMixed
}
data += foldLine("dn: "+e.Del.DN, fw) + "\n"
data += "changetype: delete\n"
case e.Modify != nil:
hasChange = true
if hasEntry {
return "", ErrMixed
}
data += foldLine("dn: "+e.Modify.DN, fw) + "\n"
data += "changetype: modify\n"
for _, mod := range e.Modify.Changes {
switch mod.Operation {
// add operation - https://tools.ietf.org/html/rfc4511#section-4.6
case 0:
if len(mod.Modification.Vals) == 0 {
return "", errors.New("changetype 'modify', op 'add' requires non empty value list")
}
data += "add: " + mod.Modification.Type + "\n"
for _, v := range mod.Modification.Vals {
ev, t := encodeValue(v)
col := ": "
if t {
col = ":: "
}
data += foldLine(mod.Modification.Type+col+ev, fw) + "\n"
}
data += "-\n"
// delete operation - https://tools.ietf.org/html/rfc4511#section-4.6
case 1:
data += "delete: " + mod.Modification.Type + "\n"
for _, v := range mod.Modification.Vals {
ev, t := encodeValue(v)
col := ": "
if t {
col = ":: "
}
data += foldLine(mod.Modification.Type+col+ev, fw) + "\n"
}
data += "-\n"
// replace operation - https://tools.ietf.org/html/rfc4511#section-4.6
case 2:
if len(mod.Modification.Vals) == 0 {
return "", errors.New("changetype 'modify', op 'replace' requires non empty value list")
}
data += "replace: " + mod.Modification.Type + "\n"
for _, v := range mod.Modification.Vals {
ev, t := encodeValue(v)
col := ": "
if t {
col = ":: "
}
data += foldLine(mod.Modification.Type+col+ev, fw) + "\n"
}
data += "-\n"
default:
return "", fmt.Errorf("invalid type %s in modify request", mod.Modification.Type)
}
}
default:
hasEntry = true
if hasChange {
return "", ErrMixed
}
data += foldLine("dn: "+e.Entry.DN, fw) + "\n"
for _, av := range e.Entry.Attributes {
for _, v := range av.Values {
ev, t := encodeValue(v)
col := ": "
if t {
col = ":: "
}
data += foldLine(av.Name+col+ev, fw) + "\n"
}
}
}
data += "\n"
}
return data, nil
}
func encodeValue(value string) (string, bool) {
required := false
for _, r := range value {
if r < ' ' || r > '~' { // ~ = 0x7E, <DEL> = 0x7F
required = true
break
}
}
if !required {
return value, false
}
return base64.StdEncoding.EncodeToString([]byte(value)), true
}
func foldLine(line string, fw int) (folded string) {
if fw < 0 {
return line
}
if len(line) <= fw {
return line
}
folded = line[:fw] + "\n"
line = line[fw:]
for len(line) > fw-1 {
folded += " " + line[:fw-1] + "\n"
line = line[fw-1:]
}
if len(line) > 0 {
folded += " " + line
}
return
}
// Dump writes the given entries to the io.Writer.
//
// The entries argument can be *ldap.Entry or a mix of *ldap.AddRequest,
// *ldap.DelRequest, *ldap.ModifyRequest and *ldap.ModifyDNRequest or slices
// of any of those.
//
// See Marshal() for the fw argument.
func Dump(fh io.Writer, fw int, entries ...interface{}) error {
l, err := ToLDIF(entries...)
if err != nil {
return err
}
l.FoldWidth = fw
str, err := Marshal(l)
if err != nil {
return err
}
_, err = fh.Write([]byte(str))
return err
}
// ToLDIF puts the given arguments in an LDIF struct and returns it.
//
// The entries argument can be *ldap.Entry or a mix of *ldap.AddRequest,
// *ldap.DelRequest, *ldap.ModifyRequest and *ldap.ModifyDNRequest or slices
// of any of those.
func ToLDIF(entries ...interface{}) (*LDIF, error) {
l := &LDIF{}
for _, e := range entries {
switch e.(type) {
case []*ldap.Entry:
for _, en := range e.([]*ldap.Entry) {
l.Entries = append(l.Entries, &Entry{Entry: en})
}
case *ldap.Entry:
l.Entries = append(l.Entries, &Entry{Entry: e.(*ldap.Entry)})
case []*ldap.AddRequest:
for _, en := range e.([]*ldap.AddRequest) {
l.Entries = append(l.Entries, &Entry{Add: en})
}
case *ldap.AddRequest:
l.Entries = append(l.Entries, &Entry{Add: e.(*ldap.AddRequest)})
case []*ldap.DelRequest:
for _, en := range e.([]*ldap.DelRequest) {
l.Entries = append(l.Entries, &Entry{Del: en})
}
case *ldap.DelRequest:
l.Entries = append(l.Entries, &Entry{Del: e.(*ldap.DelRequest)})
case []*ldap.ModifyRequest:
for _, en := range e.([]*ldap.ModifyRequest) {
l.Entries = append(l.Entries, &Entry{Modify: en})
}
case *ldap.ModifyRequest:
l.Entries = append(l.Entries, &Entry{Modify: e.(*ldap.ModifyRequest)})
default:
return nil, fmt.Errorf("unsupported type %T", e)
}
}
return l, nil
}

9
vendor/github.com/go-ldap/ldif/path.go generated vendored Normal file
View file

@ -0,0 +1,9 @@
// +build !windows
package ldif
import "net/url"
func toPath(u *url.URL) string {
return u.Path
}

12
vendor/github.com/go-ldap/ldif/path_windows.go generated vendored Normal file
View file

@ -0,0 +1,12 @@
package ldif
import "net/url"
import "strings"
// toPath get the file path
// We use ioutil.ReadFile to read the content file.
// On windows,
// https://github.com/golang/go/blob/95a11c7381e01fdaaf34e25b82db0632081ab74e/src/net/url/url_test.go#L283-L292
func toPath(u *url.URL) string {
return strings.TrimPrefix(u.Path, "/")
}

View file

@ -36,15 +36,19 @@ func Backend(client ldapClient) *backend {
},
},
Paths: framework.PathAppend(
b.pathListRoles(),
b.pathRoles(),
b.pathCredsCreate(),
b.pathListStaticRoles(),
b.pathStaticRoles(),
b.pathStaticCredsCreate(),
b.pathRotateCredentials(),
b.pathConfig(),
b.pathDynamicRoles(),
),
InitializeFunc: b.initialize,
Secrets: []*framework.Secret{},
Secrets: []*framework.Secret{
dynamicSecretCreds(&b),
},
Clean: b.clean,
BackendType: logical.TypeLogical,
}

View file

@ -3,13 +3,19 @@ package openldap
import (
"fmt"
"github.com/go-ldap/ldap/v3"
"github.com/go-ldap/ldif"
"github.com/hashicorp/vault-plugin-secrets-openldap/client"
)
type ldapClient interface {
Add(conf *client.Config, req *ldap.AddRequest) error
Get(conf *client.Config, dn string) (*client.Entry, error)
Del(conf *client.Config, req *ldap.DelRequest) error
UpdatePassword(conf *client.Config, dn string, newPassword string) error
UpdateRootPassword(conf *client.Config, newPassword string) error
Execute(conf *client.Config, entries []*ldif.Entry, continueOnError bool) (err error)
}
func NewClient() *Client {
@ -18,6 +24,8 @@ func NewClient() *Client {
}
}
var _ ldapClient = (*Client)(nil)
type Client struct {
ldap client.Client
}
@ -58,3 +66,15 @@ func (c *Client) UpdateRootPassword(conf *client.Config, newPassword string) err
return c.ldap.UpdatePassword(conf, conf.BindDN, newValues, filters)
}
func (c *Client) Add(conf *client.Config, req *ldap.AddRequest) error {
return c.ldap.Add(conf, req)
}
func (c *Client) Del(conf *client.Config, req *ldap.DelRequest) error {
return c.ldap.Del(conf, req)
}
func (c *Client) Execute(conf *client.Config, entries []*ldif.Entry, continueOnError bool) (err error) {
return c.ldap.Execute(conf, entries, continueOnError)
}

View file

@ -8,6 +8,8 @@ import (
"time"
"github.com/go-ldap/ldap/v3"
"github.com/go-ldap/ldif"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/sdk/helper/ldaputil"
)
@ -143,3 +145,121 @@ func shouldTryLastPwd(lastPwd string, lastBindPasswordRotation time.Time) bool {
}
return lastBindPasswordRotation.Add(10 * time.Minute).After(time.Now())
}
func (c *Client) Add(cfg *Config, req *ldap.AddRequest) error {
if req == nil {
return fmt.Errorf("invalid request: request is nil")
}
if req.DN == "" {
return fmt.Errorf("invalid request: DN is empty")
}
conn, err := c.ldap.DialLDAP(cfg.ConfigEntry)
if err != nil {
return err
}
defer conn.Close()
if err := bind(cfg, conn); err != nil {
return err
}
err = conn.Add(req)
if err != nil {
return err
}
return nil
}
func (c *Client) Del(cfg *Config, req *ldap.DelRequest) error {
if req == nil {
return fmt.Errorf("invalid request: request is nil")
}
if req.DN == "" {
return fmt.Errorf("invalid request: DN is empty")
}
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.Del(req)
}
func (c *Client) Execute(cfg *Config, entries []*ldif.Entry, continueOnFailure bool) (err error) {
if len(entries) == 0 {
return nil
}
conn, err := c.ldap.DialLDAP(cfg.ConfigEntry)
if err != nil {
return err
}
defer conn.Close()
if err := bind(cfg, conn); err != nil {
return err
}
merr := new(multierror.Error)
for _, entry := range entries {
if entry == nil {
// Skip entries that are nil since they don't indicate an error in execution. Since these entries
// are usually coming from an ldif parse, this should generally not happen so it's mainly to
// protect against developers from screwing up and creating a panic due to a nil reference
continue
}
var err error
switch {
case entry.Entry != nil:
addReq := coerceToAddRequest(entry.Entry)
err = errorf("failed to run AddRequest: %w", conn.Add(addReq))
case entry.Add != nil:
err = errorf("failed to run AddRequest: %w", conn.Add(entry.Add))
case entry.Modify != nil:
err = errorf("failed to run ModifyRequest: %w", conn.Modify(entry.Modify))
case entry.Del != nil:
err = errorf("failed to run DelRequest: %w", conn.Del(entry.Del))
default:
err = fmt.Errorf("unrecognized or missing LDIF command")
}
if err != nil {
if continueOnFailure {
merr = multierror.Append(merr, err)
} else {
return err
}
}
}
return merr.ErrorOrNil()
}
func coerceToAddRequest(entry *ldap.Entry) *ldap.AddRequest {
attributes := make([]ldap.Attribute, 0, len(entry.Attributes))
for _, entryAttribute := range entry.Attributes {
attribute := ldap.Attribute{
Type: entryAttribute.Name,
Vals: entryAttribute.Values,
}
attributes = append(attributes, attribute)
}
addReq := &ldap.AddRequest{
DN: entry.DN,
Attributes: attributes,
Controls: nil,
}
return addReq
}
func errorf(format string, wrappedErr error) error {
if wrappedErr == nil {
return nil
}
return fmt.Errorf(format, wrappedErr)
}

View file

@ -0,0 +1,63 @@
package openldap
import (
"context"
"fmt"
"path"
"time"
"github.com/hashicorp/vault/sdk/logical"
)
type dynamicRole struct {
// required fields
Name string `json:"name" mapstructure:"name"`
CreationLDIF string `json:"creation_ldif" mapstructure:"creation_ldif"`
DeletionLDIF string `json:"deletion_ldif" mapstructure:"deletion_ldif"`
// optional fields
RollbackLDIF string `json:"rollback_ldif" mapstructure:"rollback_ldif,omitempty"`
UsernameTemplate string `json:"username_template,omitempty" mapstructure:"username_template,omitempty"`
DefaultTTL time.Duration `json:"default_ttl,omitempty" mapstructure:"default_ttl,omitempty"`
MaxTTL time.Duration `json:"max_ttl,omitempty" mapstructure:"max_ttl,omitempty"`
}
func retrieveDynamicRole(ctx context.Context, s logical.Storage, roleName string) (*dynamicRole, error) {
entry, err := s.Get(ctx, path.Join(dynamicRolePath, roleName))
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
result := new(dynamicRole)
if err := entry.DecodeJSON(result); err != nil {
return nil, err
}
return result, nil
}
func storeDynamicRole(ctx context.Context, s logical.Storage, role *dynamicRole) error {
if role.Name == "" {
return fmt.Errorf("missing role name")
}
entry, err := logical.StorageEntryJSON(path.Join(dynamicRolePath, role.Name), role)
if err != nil {
return fmt.Errorf("unable to marshal storage entry: %w", err)
}
err = s.Put(ctx, entry)
if err != nil {
return fmt.Errorf("failed to store dynamic role: %w", err)
}
return nil
}
func deleteDynamicRole(ctx context.Context, s logical.Storage, roleName string) error {
if roleName == "" {
return fmt.Errorf("missing role name")
}
return s.Delete(ctx, path.Join(dynamicRolePath, roleName))
}

View file

@ -4,11 +4,15 @@ go 1.13
require (
github.com/go-ldap/ldap/v3 v3.1.10
github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3
github.com/hashicorp/errwrap v1.0.0
github.com/hashicorp/go-hclog v0.14.1
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/vault/api v1.0.5-0.20200826195146-c03009a7e370
github.com/hashicorp/vault/sdk v0.1.14-0.20200826195146-c03009a7e370
github.com/hashicorp/vault/sdk v0.1.14-0.20210127185906-6b455835fa8c
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/mitchellh/mapstructure v1.3.2
github.com/stretchr/testify v1.5.1
golang.org/x/text v0.3.2
)

View file

@ -10,6 +10,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs=
github.com/armon/go-metrics v0.3.3 h1:a9F4rlj7EWWrbj7BYw8J8+x+ZZkJeqzNyRk8hdPF+ro=
github.com/armon/go-metrics v0.3.3/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
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=
@ -60,12 +61,16 @@ github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
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-asn1-ber/asn1-ber v1.4.1 h1:qP/QDxOtmMoJVgXHCXNzDpA0+wkgYB2x5QoLMVOciyw=
github.com/go-asn1-ber/asn1-ber v1.4.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
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-ldap/ldap/v3 v3.1.7/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-ldap/ldap/v3 v3.1.10 h1:7WsKqasmPThNvdl0Q5GPpbTDD/ZD98CfuawrMIuh7qQ=
github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3 h1:sfz1YppV05y4sYaW7kXZtrocU/+vimnIWt4cxAYh7+o=
github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3/go.mod h1:ZXFhGda43Z2TVbfGZefXyMJzsDHhCh0go3bZUcwTx7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@ -99,6 +104,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
@ -108,7 +115,6 @@ github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVo
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.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-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
@ -151,10 +157,9 @@ github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f/go.mod h1:eu
github.com/hashicorp/vault/api v1.0.5-0.20200826195146-c03009a7e370 h1:Q0Nx5sfSDTDr1WjQhV8DCZC8e47EiRittzuhXq4ES2U=
github.com/hashicorp/vault/api v1.0.5-0.20200826195146-c03009a7e370/go.mod h1:R3Umvhlxi2TN7Ex2hzOowyeNb+SfbVWI973N+ctaFMk=
github.com/hashicorp/vault/sdk v0.1.14-0.20200519221530-14615acda45f/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 h1:e1ok06zGrWJW91rzRroyl5nRNqraaBe4d5hiKcVZuHM=
github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10=
github.com/hashicorp/vault/sdk v0.1.14-0.20200826195146-c03009a7e370 h1:gw9lQAYoyNb9IKxuvtbLg9E6ehD+4NwdrShZmwAIw8A=
github.com/hashicorp/vault/sdk v0.1.14-0.20200826195146-c03009a7e370/go.mod h1:+S2qzS1Tex9JgbHxb/Jv7CdZyKydxqg09G/qVvyVmUc=
github.com/hashicorp/vault/sdk v0.1.14-0.20210127185906-6b455835fa8c h1:CSvbHEivYEK8njYzPB1Wn972h4U0z+xMGFZnTdVK+s4=
github.com/hashicorp/vault/sdk v0.1.14-0.20210127185906-6b455835fa8c/go.mod h1:cAGI4nVnEfAyMeqt9oB+Mase8DNn3qA/LDNHURiwssY=
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=
@ -173,6 +178,8 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -222,8 +229,8 @@ github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
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/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@ -261,6 +268,7 @@ github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKv
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/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/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@ -275,7 +283,6 @@ golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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/crypto v0.0.0-20190418165655-df01cb2cc480 h1:O5YqonU5IWby+w98jVUG9h7zlCWCcH4RHyPVReBmhzk=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -370,8 +377,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View file

@ -0,0 +1,281 @@
package openldap
import (
"context"
"fmt"
"time"
"github.com/go-ldap/ldif"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault-plugin-secrets-openldap/client"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/template"
"github.com/hashicorp/vault/sdk/logical"
"github.com/mitchellh/mapstructure"
"golang.org/x/text/encoding/unicode"
)
func (b *backend) pathDynamicCredsRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName := data.Get("name").(string)
// Get the role and LDAP configs
dRole, err := retrieveDynamicRole(ctx, req.Storage, roleName)
if err != nil {
return nil, fmt.Errorf("failed to retrieve dynamic role: %w", err)
}
if dRole == nil {
return nil, nil
}
config, err := readConfig(ctx, req.Storage)
if err != nil {
return nil, err
}
if config == nil {
return nil, fmt.Errorf("missing OpenLDAP configuration")
}
// Generate dynamic data
username, err := generateUsername(req, dRole)
if err != nil {
return nil, fmt.Errorf("failed to generate username: %w", err)
}
password, err := b.GeneratePassword(ctx, config)
if err != nil {
return nil, err
}
// Apply the template & execute
now := time.Now()
exp := now.Add(dRole.DefaultTTL)
templateData := dynamicTemplateData{
Username: username,
Password: password,
DisplayName: req.DisplayName,
RoleName: roleName,
IssueTime: now.Format(time.RFC3339),
IssueTimeSeconds: now.Unix(),
ExpirationTime: exp.Format(time.RFC3339),
ExpirationTimeSeconds: exp.Unix(),
}
dns, err := b.executeLDIF(config.LDAP, dRole.CreationLDIF, templateData, false)
if err != nil {
// Creation failed, attempt a rollback if one is specified
if dRole.RollbackLDIF == "" {
return nil, fmt.Errorf("failed to create user: %w", err)
}
merr := multierror.Append(fmt.Errorf("failed to create user: %w", err))
_, err = b.executeLDIF(config.LDAP, dRole.RollbackLDIF, templateData, true)
if err != nil {
merr = multierror.Append(fmt.Errorf("failed to roll back user creation: %w", err))
}
return nil, merr
}
respData := map[string]interface{}{
"username": username,
"password": password,
"distinguished_names": dns,
}
internal := map[string]interface{}{
"name": roleName,
// Including the deletion_ldif in the event that the role is deleted while
// leases are active otherwise leases will fail to revoke
"deletion_ldif": dRole.DeletionLDIF,
"template_data": templateData,
}
resp := b.Secret(secretCredsType).Response(respData, internal)
resp.Secret.TTL = dRole.DefaultTTL
resp.Secret.MaxTTL = dRole.MaxTTL
return resp, nil
}
// executeLDIF applies the template data against the LDIF template & executes the LDIF statements against the LDAP
// server. If more than one statement is specified within the LDIF string, this will result in multiple operations
// against the LDAP server. This is due to the fact that LDAP does not have transactions, nor any other form of
// atomic operations across multiple LDIF entries. If `continueOnError` is false, this will exit immediately upon
// any error occurring. If true, this will attempt to execute all of the specified LDIF statements and returns an error
// upon completion if any occurred.
func (b *backend) executeLDIF(config *client.Config, ldifTemplate string, templateData dynamicTemplateData, continueOnError bool) (dns []string, err error) {
rawLDIF, err := applyTemplate(ldifTemplate, templateData)
if err != nil {
return nil, fmt.Errorf("failed to apply template: %w", err)
}
// Parse the raw LDIF & run it against the LDAP client
entries, err := ldif.Parse(rawLDIF)
if err != nil {
return nil, fmt.Errorf("failed to parse generated LDIF: %w", err)
}
err = b.client.Execute(config, entries.Entries, continueOnError)
if err != nil {
return nil, fmt.Errorf("failed to execute statements: %w", err)
}
dns = getDNs(entries.Entries)
return dns, nil
}
func getDNs(entries []*ldif.Entry) []string {
dns := make([]string, 0, len(entries))
for _, entry := range entries {
if entry == nil {
continue
}
switch {
case entry.Entry != nil:
dns = append(dns, entry.Entry.DN)
case entry.Add != nil:
dns = append(dns, entry.Add.DN)
case entry.Modify != nil:
dns = append(dns, entry.Modify.DN)
case entry.Del != nil:
dns = append(dns, entry.Del.DN)
}
}
return dns
}
func (b *backend) secretCredsRenew() framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// Retrieve the role to ensure it still exists. If it doesn't, this will reject the renewal request.
roleNameRaw, ok := req.Secret.InternalData["name"]
if !ok {
return nil, fmt.Errorf("missing role name")
}
roleName := roleNameRaw.(string)
dRole, err := retrieveDynamicRole(ctx, req.Storage, roleName)
if err != nil {
return nil, fmt.Errorf("failed to retrieve dynamic role: %w", err)
}
if dRole == nil {
return nil, fmt.Errorf("unable to renew: role does not exist")
}
// Update the default TTL & MaxTTL to the latest from the role in the event the role definition has changed
secret := req.Secret
secret.TTL = dRole.DefaultTTL
secret.MaxTTL = dRole.MaxTTL
resp := &logical.Response{
Secret: req.Secret,
}
return resp, nil
}
}
func (b *backend) secretCredsRevoke() framework.OperationFunc {
return func(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, fmt.Errorf("missing OpenLDAP configuration")
}
deletionTemplate, err := getString(req.Secret.InternalData, "deletion_ldif")
if err != nil {
return nil, fmt.Errorf("broken internal data: unable to retrieve deletion_ldif: %w", err)
}
if deletionTemplate == "" {
return nil, fmt.Errorf("broken internal data: missing deletion_ldif")
}
var templateData dynamicTemplateData
rawTemplateData := req.Secret.InternalData["template_data"]
switch td := rawTemplateData.(type) {
case dynamicTemplateData:
templateData = td
case map[string]interface{}:
err := mapstructure.WeakDecode(td, &templateData)
if err != nil {
return nil, fmt.Errorf("unable to decode internal data: %w", err)
}
default:
return nil, fmt.Errorf("unable to revoke OpenLDAP dynamic credentials: unrecognized internal data type: %T", td)
}
_, err = b.executeLDIF(config.LDAP, deletionTemplate, templateData, true)
return nil, err
}
}
type usernameTemplateData struct {
DisplayName string
RoleName string
}
const defaultUsernameTemplate = "v_{{.DisplayName}}_{{.RoleName}}_{{random 10}}_{{unix_time}}"
func generateUsername(req *logical.Request, role *dynamicRole) (string, error) {
usernameTemplate := role.UsernameTemplate
if role.UsernameTemplate == "" {
usernameTemplate = defaultUsernameTemplate
}
tmpl, err := template.NewTemplate(
template.Template(usernameTemplate),
)
if err != nil {
return "", err
}
usernameData := usernameTemplateData{
DisplayName: req.DisplayName,
RoleName: role.Name,
}
return tmpl.Generate(usernameData)
}
type dynamicTemplateData struct {
Username string
Password string
DisplayName string
RoleName string
IssueTime string
IssueTimeSeconds int64
ExpirationTime string
ExpirationTimeSeconds int64
}
func applyTemplate(rawTemplate string, data dynamicTemplateData) (string, error) {
tmpl, err := template.NewTemplate(
template.Template(rawTemplate),
template.Function("utf16le", encodeUTF16LE),
)
if err != nil {
return "", fmt.Errorf("failed to parse template: %w", err)
}
str, err := tmpl.Generate(data)
if err != nil {
return "", fmt.Errorf("failed to execute template: %w", err)
}
return str, nil
}
func encodeUTF16LE(str string) (string, error) {
enc := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
return enc.String(str)
}
func getString(m map[string]interface{}, key string) (string, error) {
if m == nil {
return "", fmt.Errorf("nil map")
}
val, exists := m[key]
if !exists {
return "", nil
}
str, ok := val.(string)
if !ok {
return "", fmt.Errorf("key %s has %T value, not string", key, val)
}
return str, nil
}

View file

@ -0,0 +1,325 @@
package openldap
import (
"context"
"encoding/base64"
"fmt"
"path"
"time"
"github.com/go-ldap/ldif"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/parseutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/mitchellh/mapstructure"
)
const (
secretCredsType = "creds"
dynamicRolePath = "role/"
dynamicCredPath = "cred/"
)
func (b *backend) pathDynamicRoles() []*framework.Path {
return []*framework.Path{
// POST/GET/DELETE role/:name
{
Pattern: path.Join(dynamicRolePath, framework.GenericNameRegex("name")),
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeLowerCaseString,
Description: "Name of the role (lowercase)",
Required: true,
},
"creation_ldif": {
Type: framework.TypeString,
Description: "LDIF string used to create new entities within OpenLDAP. This LDIF can be templated.",
Required: true,
},
"deletion_ldif": {
Type: framework.TypeString,
Description: "LDIF string used to delete entities created within OpenLDAP. This LDIF can be templated.",
Required: true,
},
"rollback_ldif": {
Type: framework.TypeString,
Description: "LDIF string used to rollback changes in the event of a failure to create credentials. This LDIF can be templated.",
},
"username_template": {
Type: framework.TypeString,
Description: "The template used to create a username",
},
"default_ttl": {
Type: framework.TypeDurationSecond,
Description: "Default TTL for dynamic credentials",
},
"max_ttl": {
Type: framework.TypeDurationSecond,
Description: "Max TTL a dynamic credential can be extended to",
},
},
ExistenceCheck: b.pathDynamicRoleExistenceCheck,
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathDynamicRoleCreateUpdate,
},
logical.CreateOperation: &framework.PathOperation{
Callback: b.pathDynamicRoleCreateUpdate,
},
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathDynamicRoleRead,
},
logical.DeleteOperation: &framework.PathOperation{
Callback: b.pathDynamicRoleDelete,
},
},
HelpSynopsis: staticRoleHelpSynopsis,
HelpDescription: staticRoleHelpDescription,
},
// LIST role
{
Pattern: dynamicRolePath + "?$",
Operations: map[logical.Operation]framework.OperationHandler{
logical.ListOperation: &framework.PathOperation{
Callback: b.pathDynamicRoleList,
},
},
HelpSynopsis: "List all the dynamic roles Vault is currently managing in OpenLDAP.",
HelpDescription: "List all the dynamic roles being managed by Vault.",
},
// GET credentials
{
Pattern: path.Join(dynamicCredPath, framework.MatchAllRegex("name")),
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeLowerCaseString,
Description: "Name of the dynamic role.",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathDynamicCredsRead,
},
},
HelpSynopsis: "Request LDAP credentials for a dynamic role. These credentials are " +
"created within OpenLDAP when querying this endpoint.",
HelpDescription: "This path requests new LDAP credentials for a certain dynamic role. " +
"The credentials are created within OpenLDAP based on the creation_ldif specified " +
"within the dynamic role configuration.",
},
}
}
func dynamicSecretCreds(b *backend) *framework.Secret {
return &framework.Secret{
Type: secretCredsType,
Fields: map[string]*framework.FieldSchema{
"username": {
Type: framework.TypeString,
Description: "Username of the generated account",
},
"password": {
Type: framework.TypeString,
Description: "Password to access the generated account",
},
"distinguished_names": {
Type: framework.TypeStringSlice,
Description: "List of the distinguished names (DN) created. Each name in this list corresponds to" +
"each action taken within the creation_ldif statements. This does not de-duplicate entries, " +
"so this will have one entry for each LDIF statement within creation_ldif.",
},
},
Renew: b.secretCredsRenew(),
Revoke: b.secretCredsRevoke(),
}
}
func (b *backend) pathDynamicRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
rawData := data.Raw
err := convertToDuration(rawData, "default_ttl", "max_ttl")
if err != nil {
return nil, fmt.Errorf("failed to convert TTLs to duration: %w", err)
}
roleName := data.Get("name").(string)
dRole, err := retrieveDynamicRole(ctx, req.Storage, roleName)
if err != nil {
return nil, fmt.Errorf("unable to look for existing role: %w", err)
}
if dRole == nil {
if req.Operation == logical.UpdateOperation {
return nil, fmt.Errorf("unable to update role: role does not exist")
}
dRole = &dynamicRole{}
}
err = mapstructure.WeakDecode(rawData, dRole)
if err != nil {
return nil, fmt.Errorf("failed to decode request: %w", err)
}
dRole.CreationLDIF = decodeBase64(dRole.CreationLDIF)
dRole.RollbackLDIF = decodeBase64(dRole.RollbackLDIF)
dRole.DeletionLDIF = decodeBase64(dRole.DeletionLDIF)
err = validateDynamicRole(dRole)
if err != nil {
return nil, err
}
err = storeDynamicRole(ctx, req.Storage, dRole)
if err != nil {
return nil, fmt.Errorf("failed to save dynamic role: %w", err)
}
return nil, nil
}
func validateDynamicRole(dRole *dynamicRole) error {
if dRole.CreationLDIF == "" {
return fmt.Errorf("missing creation_ldif")
}
if dRole.DeletionLDIF == "" {
return fmt.Errorf("missing deletion_ldif")
}
err := assertValidLDIFTemplate(dRole.CreationLDIF)
if err != nil {
return fmt.Errorf("invalid creation_ldif: %w", err)
}
err = assertValidLDIFTemplate(dRole.DeletionLDIF)
if err != nil {
return fmt.Errorf("invalid deletion_ldif: %w", err)
}
if dRole.RollbackLDIF != "" {
err = assertValidLDIFTemplate(dRole.RollbackLDIF)
if err != nil {
return fmt.Errorf("invalid rollback_ldif: %w", err)
}
}
return nil
}
// convertToDuration all keys in the data map into time.Duration objects. Keys not found in the map will be ignored
func convertToDuration(data map[string]interface{}, keys ...string) error {
merr := new(multierror.Error)
for _, key := range keys {
val, exists := data[key]
if !exists {
continue
}
dur, err := parseutil.ParseDurationSecond(val)
if err != nil {
merr = multierror.Append(merr, fmt.Errorf("invalid duration %s: %w", key, err))
continue
}
data[key] = dur
}
return merr.ErrorOrNil()
}
// decodeBase64 attempts to base64 decode the provided string. If the string is not base64 encoded, this
// returns the original string.
// This is equivalent to "if string is base64 encoded, decode it and return, otherwise return the original string"
func decodeBase64(str string) string {
if str == "" {
return ""
}
decoded, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return str
}
return string(decoded)
}
func assertValidLDIFTemplate(rawTemplate string) error {
// Test the template to ensure there aren't any errors in the template syntax
now := time.Now()
exp := now.Add(24 * time.Hour)
testTemplateData := dynamicTemplateData{
Username: "testuser",
Password: "testpass",
DisplayName: "testdisplayname",
RoleName: "testrolename",
IssueTime: now.Format(time.RFC3339),
IssueTimeSeconds: now.Unix(),
ExpirationTime: exp.Format(time.RFC3339),
ExpirationTimeSeconds: exp.Unix(),
}
testLDIF, err := applyTemplate(rawTemplate, testTemplateData)
if err != nil {
return fmt.Errorf("invalid template: %w", err)
}
// Test the LDIF to ensure there aren't any errors in the syntax
entries, err := ldif.Parse(testLDIF)
if err != nil {
return fmt.Errorf("LDIF is invalid: %w", err)
}
if len(entries.Entries) == 0 {
return fmt.Errorf("must specify at least one LDIF entry")
}
return nil
}
func (b *backend) pathDynamicRoleRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName := data.Get("name").(string)
dRole, err := retrieveDynamicRole(ctx, req.Storage, roleName)
if err != nil {
return nil, fmt.Errorf("failed to retrieve dynamic role: %w", err)
}
if dRole == nil {
return nil, nil
}
resp := &logical.Response{
Data: map[string]interface{}{
"creation_ldif": dRole.CreationLDIF,
"deletion_ldif": dRole.DeletionLDIF,
"rollback_ldif": dRole.RollbackLDIF,
"username_template": dRole.UsernameTemplate,
"default_ttl": dRole.DefaultTTL.Seconds(),
"max_ttl": dRole.MaxTTL.Seconds(),
},
}
return resp, nil
}
func (b *backend) pathDynamicRoleList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roles, err := req.Storage.List(ctx, dynamicRolePath)
if err != nil {
return nil, fmt.Errorf("failed to list roles: %w", err)
}
return logical.ListResponse(roles), nil
}
func (b *backend) pathDynamicRoleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
roleName := data.Get("name").(string)
role, err := retrieveDynamicRole(ctx, req.Storage, roleName)
if err != nil {
return false, fmt.Errorf("error finding role: %w", err)
}
return role != nil, nil
}
func (b *backend) pathDynamicRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName := data.Get("name").(string)
err := deleteDynamicRole(ctx, req.Storage, roleName)
if err != nil {
return nil, fmt.Errorf("failed to delete role: %w", err)
}
return nil, nil
}

View file

@ -116,7 +116,7 @@ func (b *backend) pathRotateRoleCredentialsUpdate(ctx context.Context, req *logi
return logical.ErrorResponse("empty role name attribute given"), nil
}
role, err := b.StaticRole(ctx, req.Storage, name)
role, err := b.staticRole(ctx, req.Storage, name)
if err != nil {
return nil, err
}

View file

@ -9,7 +9,7 @@ import (
const staticCredPath = "static-cred/"
func (b *backend) pathCredsCreate() []*framework.Path {
func (b *backend) pathStaticCredsCreate() []*framework.Path {
return []*framework.Path{
{
Pattern: staticCredPath + framework.GenericNameRegex("name"),
@ -33,7 +33,7 @@ func (b *backend) pathCredsCreate() []*framework.Path {
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)
role, err := b.staticRole(ctx, req.Storage, name)
if err != nil {
return nil, err
}

View file

@ -2,6 +2,8 @@ package openldap
import (
"context"
"fmt"
"path"
"time"
"github.com/hashicorp/vault/sdk/framework"
@ -14,13 +16,13 @@ const (
staticRolePath = "static-role/"
)
func (b *backend) pathListRoles() []*framework.Path {
func (b *backend) pathListStaticRoles() []*framework.Path {
return []*framework.Path{
{
Pattern: staticRolePath + "?$",
Pattern: path.Join(staticRolePath, framework.OptionalParamRegex("prefix")),
Operations: map[logical.Operation]framework.OperationHandler{
logical.ListOperation: &framework.PathOperation{
Callback: b.pathRoleList,
Callback: b.pathStaticRoleList,
},
},
HelpSynopsis: staticRolesListHelpSynopsis,
@ -29,7 +31,7 @@ func (b *backend) pathListRoles() []*framework.Path {
}
}
func (b *backend) pathRoles() []*framework.Path {
func (b *backend) pathStaticRoles() []*framework.Path {
return []*framework.Path{
{
Pattern: staticRolePath + framework.GenericNameRegex("name"),
@ -109,7 +111,7 @@ func staticFields() map[string]*framework.FieldSchema {
}
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))
role, err := b.staticRole(ctx, req.Storage, data.Get("name").(string))
if err != nil {
return false, err
}
@ -132,7 +134,7 @@ func (b *backend) pathStaticRoleDelete(ctx context.Context, req *logical.Request
return nil, err
}
role, err := b.StaticRole(ctx, req.Storage, name)
role, err := b.staticRole(ctx, req.Storage, name)
if err != nil {
return nil, err
}
@ -149,7 +151,7 @@ func (b *backend) pathStaticRoleDelete(ctx context.Context, req *logical.Request
}
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))
role, err := b.staticRole(ctx, req.Storage, d.Get("name").(string))
if err != nil {
return nil, err
}
@ -179,7 +181,7 @@ func (b *backend) pathStaticRoleCreateUpdate(ctx context.Context, req *logical.R
lock.Lock()
defer lock.Unlock()
role, err := b.StaticRole(ctx, req.Storage, data.Get("name").(string))
role, err := b.staticRole(ctx, req.Storage, data.Get("name").(string))
if err != nil {
return nil, err
}
@ -311,22 +313,16 @@ func (s *staticAccount) PasswordTTL() time.Duration {
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)
func (b *backend) pathStaticRoleList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roles, err := req.Storage.List(ctx, staticRolePath)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to list roles: %w", err)
}
return logical.ListResponse(entries), nil
return logical.ListResponse(roles), 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)
func (b *backend) staticRole(ctx context.Context, s logical.Storage, roleName string) (*roleEntry, error) {
entry, err := s.Get(ctx, staticRolePath+roleName)
if err != nil {
return nil, err
}

View file

@ -54,7 +54,7 @@ func (b *backend) populateQueue(ctx context.Context, s logical.Storage) {
default:
}
role, err := b.StaticRole(ctx, s, roleName)
role, err := b.staticRole(ctx, s, roleName)
if err != nil {
log.Warn("unable to read static role", "error", err, "role", roleName)
continue
@ -159,7 +159,7 @@ func (b *backend) rotateCredential(ctx context.Context, s logical.Storage) bool
defer lock.Unlock()
// Validate the role still exists
role, err := b.StaticRole(ctx, s, item.Key)
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()
@ -457,7 +457,7 @@ func (b *backend) loadStaticWALs(ctx context.Context, s logical.Storage) (map[st
// Verify the static role still exists
roleName := walEntry.RoleName
role, err := b.StaticRole(ctx, s, roleName)
role, err := b.staticRole(ctx, s, roleName)
if err != nil {
b.Logger().Warn("unable to read static role", "error", err, "role", roleName)
continue

View file

@ -0,0 +1,77 @@
package template
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"strconv"
"strings"
"time"
UUID "github.com/hashicorp/go-uuid"
)
func unixTime() string {
return strconv.FormatInt(time.Now().Unix(), 10)
}
func unixTimeMillis() string {
return strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
}
func timestamp(format string) string {
return time.Now().Format(format)
}
func truncate(maxLen int, str string) (string, error) {
if maxLen <= 0 {
return "", fmt.Errorf("max length must be > 0 but was %d", maxLen)
}
if len(str) > maxLen {
return str[:maxLen], nil
}
return str, nil
}
const (
sha256HashLen = 8
)
func truncateSHA256(maxLen int, str string) (string, error) {
if maxLen <= 8 {
return "", fmt.Errorf("max length must be > 8 but was %d", maxLen)
}
if len(str) <= maxLen {
return str, nil
}
truncIndex := maxLen - sha256HashLen
hash := hashSHA256(str[truncIndex:])
result := fmt.Sprintf("%s%s", str[:truncIndex], hash[:sha256HashLen])
return result, nil
}
func hashSHA256(str string) string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(str)))
}
func encodeBase64(str string) string {
return base64.StdEncoding.EncodeToString([]byte(str))
}
func uppercase(str string) string {
return strings.ToUpper(str)
}
func lowercase(str string) string {
return strings.ToLower(str)
}
func replace(find string, replace string, str string) string {
return strings.ReplaceAll(str, find, replace)
}
func uuid() (string, error) {
return UUID.GenerateUUID()
}

View file

@ -0,0 +1,141 @@
package template
import (
"fmt"
"strings"
"text/template"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/sdk/helper/base62"
)
type Opt func(*StringTemplate) error
func Template(rawTemplate string) Opt {
return func(up *StringTemplate) error {
up.rawTemplate = rawTemplate
return nil
}
}
// Function allows the user to specify functions for use in the template. If the name provided is a function that
// already exists in the function map, this will override the previously specified function.
func Function(name string, f interface{}) Opt {
return func(up *StringTemplate) error {
if name == "" {
return fmt.Errorf("missing function name")
}
if f == nil {
return fmt.Errorf("missing function")
}
up.funcMap[name] = f
return nil
}
}
// StringTemplate creates strings based on the provided template.
// This uses the go templating language, so anything that adheres to that language will function in this struct.
// There are several custom functions available for use in the template:
// - random
// - Randomly generated characters. This uses the charset specified in RandomCharset. Must include a length.
// Example: {{ rand 20 }}
// - truncate
// - Truncates the previous value to the specified length. Must include a maximum length.
// Example: {{ .DisplayName | truncate 10 }}
// - truncate_sha256
// - Truncates the previous value to the specified length. If the original length is greater than the length
// specified, the remaining characters will be sha256 hashed and appended to the end. The hash will be only the first 8 characters The maximum length will
// be no longer than the length specified.
// Example: {{ .DisplayName | truncate_sha256 30 }}
// - uppercase
// - Uppercases the previous value.
// Example: {{ .RoleName | uppercase }}
// - lowercase
// - Lowercases the previous value.
// Example: {{ .DisplayName | lowercase }}
// - replace
// - Performs a string find & replace
// Example: {{ .DisplayName | replace - _ }}
// - sha256
// - SHA256 hashes the previous value.
// Example: {{ .DisplayName | sha256 }}
// - base64
// - base64 encodes the previous value.
// Example: {{ .DisplayName | base64 }}
// - unix_time
// - Provides the current unix time in seconds.
// Example: {{ unix_time }}
// - unix_time_millis
// - Provides the current unix time in milliseconds.
// Example: {{ unix_time_millis }}
// - timestamp
// - Provides the current time. Must include a standard Go format string
// - uuid
// - Generates a UUID
// Example: {{ uuid }}
type StringTemplate struct {
rawTemplate string
tmpl *template.Template
funcMap template.FuncMap
}
// NewTemplate creates a StringTemplate. No arguments are required
// as this has reasonable defaults for all values.
// The default template is specified in the DefaultTemplate constant.
func NewTemplate(opts ...Opt) (up StringTemplate, err error) {
up = StringTemplate{
funcMap: map[string]interface{}{
"random": base62.Random,
"truncate": truncate,
"truncate_sha256": truncateSHA256,
"uppercase": uppercase,
"lowercase": lowercase,
"replace": replace,
"sha256": hashSHA256,
"base64": encodeBase64,
"unix_time": unixTime,
"unix_time_millis": unixTimeMillis,
"timestamp": timestamp,
"uuid": uuid,
},
}
merr := &multierror.Error{}
for _, opt := range opts {
merr = multierror.Append(merr, opt(&up))
}
err = merr.ErrorOrNil()
if err != nil {
return up, err
}
if up.rawTemplate == "" {
return StringTemplate{}, fmt.Errorf("missing template")
}
tmpl, err := template.New("template").
Funcs(up.funcMap).
Parse(up.rawTemplate)
if err != nil {
return StringTemplate{}, fmt.Errorf("unable to parse template: %w", err)
}
up.tmpl = tmpl
return up, nil
}
// Generate based on the provided template
func (up StringTemplate) Generate(data interface{}) (string, error) {
if up.tmpl == nil || up.rawTemplate == "" {
return "", fmt.Errorf("failed to generate: template not initialized")
}
str := &strings.Builder{}
err := up.tmpl.Execute(str, data)
if err != nil {
return "", fmt.Errorf("unable to apply template: %w", err)
}
return str.String(), nil
}

9
vendor/modules.txt vendored
View file

@ -333,12 +333,14 @@ github.com/gammazero/deque
github.com/gammazero/workerpool
# github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
github.com/ghodss/yaml
# github.com/go-asn1-ber/asn1-ber v1.3.1
# github.com/go-asn1-ber/asn1-ber v1.4.1
github.com/go-asn1-ber/asn1-ber
# github.com/go-errors/errors v1.0.1
github.com/go-errors/errors
# github.com/go-ldap/ldap/v3 v3.1.10
github.com/go-ldap/ldap/v3
# github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3
github.com/go-ldap/ldif
# github.com/go-ole/go-ole v1.2.4
github.com/go-ole/go-ole
github.com/go-ole/go-ole/oleutil
@ -580,12 +582,12 @@ github.com/hashicorp/vault-plugin-secrets-gcpkms
github.com/hashicorp/vault-plugin-secrets-kv
# github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.2.0
github.com/hashicorp/vault-plugin-secrets-mongodbatlas
# github.com/hashicorp/vault-plugin-secrets-openldap v0.3.0
# github.com/hashicorp/vault-plugin-secrets-openldap v0.1.6-0.20210201204049-4f0f91977798
github.com/hashicorp/vault-plugin-secrets-openldap
github.com/hashicorp/vault-plugin-secrets-openldap/client
# github.com/hashicorp/vault/api v1.0.5-0.20201001211907-38d91b749c77 => ./api
github.com/hashicorp/vault/api
# github.com/hashicorp/vault/sdk v0.1.14-0.20201022214319-d87657199d4b => ./sdk
# github.com/hashicorp/vault/sdk v0.1.14-0.20210127185906-6b455835fa8c => ./sdk
github.com/hashicorp/vault/sdk/database/dbplugin
github.com/hashicorp/vault/sdk/database/dbplugin/v5
github.com/hashicorp/vault/sdk/database/dbplugin/v5/proto
@ -622,6 +624,7 @@ github.com/hashicorp/vault/sdk/helper/pointerutil
github.com/hashicorp/vault/sdk/helper/policyutil
github.com/hashicorp/vault/sdk/helper/salt
github.com/hashicorp/vault/sdk/helper/strutil
github.com/hashicorp/vault/sdk/helper/template
github.com/hashicorp/vault/sdk/helper/tlsutil
github.com/hashicorp/vault/sdk/helper/tokenutil
github.com/hashicorp/vault/sdk/helper/useragent