From ec18926754aa2d125a3d68dc7c76ad086f4671c6 Mon Sep 17 00:00:00 2001 From: Michael Golowka <72365+pcman312@users.noreply.github.com> Date: Tue, 2 Feb 2021 11:41:47 -0700 Subject: [PATCH] Vendor OpenLDAP dynamic secrets (#10818) --- go.mod | 4 +- go.sum | 7 + vendor/github.com/go-asn1-ber/asn1-ber/ber.go | 20 + vendor/github.com/go-ldap/ldif/.gitignore | 24 + vendor/github.com/go-ldap/ldif/.travis.yml | 22 + vendor/github.com/go-ldap/ldif/LICENSE | 21 + vendor/github.com/go-ldap/ldif/Makefile | 41 ++ vendor/github.com/go-ldap/ldif/README.md | 20 + vendor/github.com/go-ldap/ldif/apply.go | 57 ++ vendor/github.com/go-ldap/ldif/doc.go | 2 + vendor/github.com/go-ldap/ldif/go.mod | 8 + vendor/github.com/go-ldap/ldif/go.sum | 5 + vendor/github.com/go-ldap/ldif/ldif.go | 534 ++++++++++++++++++ vendor/github.com/go-ldap/ldif/marshal.go | 247 ++++++++ vendor/github.com/go-ldap/ldif/path.go | 9 + .../github.com/go-ldap/ldif/path_windows.go | 12 + .../vault-plugin-secrets-openldap/backend.go | 12 +- .../vault-plugin-secrets-openldap/client.go | 20 + .../client/client.go | 120 ++++ .../dynamic_role.go | 63 +++ .../vault-plugin-secrets-openldap/go.mod | 6 +- .../vault-plugin-secrets-openldap/go.sum | 25 +- .../path_dynamic_creds.go | 281 +++++++++ .../path_dynamic_roles.go | 325 +++++++++++ .../path_rotate.go | 2 +- .../{path_creds.go => path_static_creds.go} | 4 +- .../{path_roles.go => path_static_roles.go} | 36 +- .../vault-plugin-secrets-openldap/rotation.go | 6 +- .../vault/sdk/helper/template/funcs.go | 77 +++ .../vault/sdk/helper/template/template.go | 141 +++++ vendor/modules.txt | 9 +- 31 files changed, 2115 insertions(+), 45 deletions(-) create mode 100644 vendor/github.com/go-ldap/ldif/.gitignore create mode 100644 vendor/github.com/go-ldap/ldif/.travis.yml create mode 100644 vendor/github.com/go-ldap/ldif/LICENSE create mode 100644 vendor/github.com/go-ldap/ldif/Makefile create mode 100644 vendor/github.com/go-ldap/ldif/README.md create mode 100644 vendor/github.com/go-ldap/ldif/apply.go create mode 100644 vendor/github.com/go-ldap/ldif/doc.go create mode 100644 vendor/github.com/go-ldap/ldif/go.mod create mode 100644 vendor/github.com/go-ldap/ldif/go.sum create mode 100644 vendor/github.com/go-ldap/ldif/ldif.go create mode 100644 vendor/github.com/go-ldap/ldif/marshal.go create mode 100644 vendor/github.com/go-ldap/ldif/path.go create mode 100644 vendor/github.com/go-ldap/ldif/path_windows.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-openldap/dynamic_role.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_dynamic_creds.go create mode 100644 vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_dynamic_roles.go rename vendor/github.com/hashicorp/vault-plugin-secrets-openldap/{path_creds.go => path_static_creds.go} (94%) rename vendor/github.com/hashicorp/vault-plugin-secrets-openldap/{path_roles.go => path_static_roles.go} (91%) create mode 100644 vendor/github.com/hashicorp/vault/sdk/helper/template/funcs.go create mode 100644 vendor/github.com/hashicorp/vault/sdk/helper/template/template.go diff --git a/go.mod b/go.mod index 71945069e..a8a76f4fe 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index c9ea7b204..d5820ad71 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/vendor/github.com/go-asn1-ber/asn1-ber/ber.go b/vendor/github.com/go-asn1-ber/asn1-ber/ber.go index 6153f4606..1e186cb8f 100644 --- a/vendor/github.com/go-asn1-ber/asn1-ber/ber.go +++ b/vendor/github.com/go-asn1-ber/asn1-ber/ber.go @@ -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) diff --git a/vendor/github.com/go-ldap/ldif/.gitignore b/vendor/github.com/go-ldap/ldif/.gitignore new file mode 100644 index 000000000..daf913b1b --- /dev/null +++ b/vendor/github.com/go-ldap/ldif/.gitignore @@ -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 diff --git a/vendor/github.com/go-ldap/ldif/.travis.yml b/vendor/github.com/go-ldap/ldif/.travis.yml new file mode 100644 index 000000000..b3827655b --- /dev/null +++ b/vendor/github.com/go-ldap/ldif/.travis.yml @@ -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 diff --git a/vendor/github.com/go-ldap/ldif/LICENSE b/vendor/github.com/go-ldap/ldif/LICENSE new file mode 100644 index 000000000..eaf5ac905 --- /dev/null +++ b/vendor/github.com/go-ldap/ldif/LICENSE @@ -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. diff --git a/vendor/github.com/go-ldap/ldif/Makefile b/vendor/github.com/go-ldap/ldif/Makefile new file mode 100644 index 000000000..a0e671628 --- /dev/null +++ b/vendor/github.com/go-ldap/ldif/Makefile @@ -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 diff --git a/vendor/github.com/go-ldap/ldif/README.md b/vendor/github.com/go-ldap/ldif/README.md new file mode 100644 index 000000000..fdede9bd2 --- /dev/null +++ b/vendor/github.com/go-ldap/ldif/README.md @@ -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 \ No newline at end of file diff --git a/vendor/github.com/go-ldap/ldif/apply.go b/vendor/github.com/go-ldap/ldif/apply.go new file mode 100644 index 000000000..a99af3905 --- /dev/null +++ b/vendor/github.com/go-ldap/ldif/apply.go @@ -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 +} diff --git a/vendor/github.com/go-ldap/ldif/doc.go b/vendor/github.com/go-ldap/ldif/doc.go new file mode 100644 index 000000000..ea570cf97 --- /dev/null +++ b/vendor/github.com/go-ldap/ldif/doc.go @@ -0,0 +1,2 @@ +// Package ldif contains utilities for working with ldif data +package ldif diff --git a/vendor/github.com/go-ldap/ldif/go.mod b/vendor/github.com/go-ldap/ldif/go.mod new file mode 100644 index 000000000..81a86c279 --- /dev/null +++ b/vendor/github.com/go-ldap/ldif/go.mod @@ -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 +) diff --git a/vendor/github.com/go-ldap/ldif/go.sum b/vendor/github.com/go-ldap/ldif/go.sum new file mode 100644 index 000000000..14be642ad --- /dev/null +++ b/vendor/github.com/go-ldap/ldif/go.sum @@ -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= diff --git a/vendor/github.com/go-ldap/ldif/ldif.go b/vendor/github.com/go-ldap/ldif/ldif.go new file mode 100644 index 000000000..c06c54dab --- /dev/null +++ b/vendor/github.com/go-ldap/ldif/ldif.go @@ -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 +} diff --git a/vendor/github.com/go-ldap/ldif/marshal.go b/vendor/github.com/go-ldap/ldif/marshal.go new file mode 100644 index 000000000..b198226b0 --- /dev/null +++ b/vendor/github.com/go-ldap/ldif/marshal.go @@ -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, = 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 +} diff --git a/vendor/github.com/go-ldap/ldif/path.go b/vendor/github.com/go-ldap/ldif/path.go new file mode 100644 index 000000000..c735f570f --- /dev/null +++ b/vendor/github.com/go-ldap/ldif/path.go @@ -0,0 +1,9 @@ +// +build !windows + +package ldif + +import "net/url" + +func toPath(u *url.URL) string { + return u.Path +} diff --git a/vendor/github.com/go-ldap/ldif/path_windows.go b/vendor/github.com/go-ldap/ldif/path_windows.go new file mode 100644 index 000000000..1b51102d8 --- /dev/null +++ b/vendor/github.com/go-ldap/ldif/path_windows.go @@ -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, "/") +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/backend.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/backend.go index f93a57715..08365d9aa 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/backend.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/backend.go @@ -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, } diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client.go index a744e7f5f..d39462517 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client/client.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client/client.go index 8c137dcc8..bef8ad5bb 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client/client.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/client/client.go @@ -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) +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/dynamic_role.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/dynamic_role.go new file mode 100644 index 000000000..51af7e7f5 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/dynamic_role.go @@ -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)) +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.mod b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.mod index 15ecfe664..71f7d32f9 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.mod +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.mod @@ -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 ) diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.sum b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.sum index 927cad8db..1b27f9c10 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.sum +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/go.sum @@ -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= diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_dynamic_creds.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_dynamic_creds.go new file mode 100644 index 000000000..2c04e7592 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_dynamic_creds.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_dynamic_roles.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_dynamic_roles.go new file mode 100644 index 000000000..aec5d4002 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_dynamic_roles.go @@ -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 +} diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_rotate.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_rotate.go index 267f990a6..669feaee3 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_rotate.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_rotate.go @@ -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 } diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_creds.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_static_creds.go similarity index 94% rename from vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_creds.go rename to vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_static_creds.go index f6f3c4902..ca832102b 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_creds.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_static_creds.go @@ -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 } diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_roles.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_static_roles.go similarity index 91% rename from vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_roles.go rename to vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_static_roles.go index 22e8a1d4a..407753e18 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_roles.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/path_static_roles.go @@ -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 } diff --git a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/rotation.go b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/rotation.go index 3efc0df5e..3139f827f 100644 --- a/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/rotation.go +++ b/vendor/github.com/hashicorp/vault-plugin-secrets-openldap/rotation.go @@ -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 diff --git a/vendor/github.com/hashicorp/vault/sdk/helper/template/funcs.go b/vendor/github.com/hashicorp/vault/sdk/helper/template/funcs.go new file mode 100644 index 000000000..ee9927fe1 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/sdk/helper/template/funcs.go @@ -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() +} diff --git a/vendor/github.com/hashicorp/vault/sdk/helper/template/template.go b/vendor/github.com/hashicorp/vault/sdk/helper/template/template.go new file mode 100644 index 000000000..829f02d45 --- /dev/null +++ b/vendor/github.com/hashicorp/vault/sdk/helper/template/template.go @@ -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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b78b6ee58..b7aaad046 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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