open-vault/logical/framework/backend.go

420 lines
10 KiB
Go
Raw Normal View History

2015-03-15 23:39:49 +00:00
package framework
2015-03-14 04:11:19 +00:00
import (
2015-03-19 18:41:41 +00:00
"fmt"
"io/ioutil"
"log"
"regexp"
"sort"
2015-04-04 04:10:54 +00:00
"strings"
"sync"
"time"
2015-03-18 00:15:23 +00:00
"github.com/hashicorp/go-multierror"
2015-03-15 21:57:19 +00:00
"github.com/hashicorp/vault/logical"
2015-03-14 04:11:19 +00:00
)
2015-03-15 21:57:19 +00:00
// Backend is an implementation of logical.Backend that allows
2015-03-14 04:11:19 +00:00
// the implementer to code a backend using a much more programmer-friendly
// framework that handles a lot of the routing and validation for you.
//
2015-03-15 21:57:19 +00:00
// This is recommended over implementing logical.Backend directly.
2015-03-14 04:11:19 +00:00
type Backend struct {
2015-04-04 03:36:47 +00:00
// Help is the help text that is shown when a help request is made
// on the root of this resource. The root help is special since we
// show all the paths that can be requested.
Help string
// Paths are the various routes that the backend responds to.
// This cannot be modified after construction (i.e. dynamically changing
// paths, including adding or removing, is not allowed once the
// backend is in use).
//
2015-03-31 00:46:18 +00:00
// PathsSpecial is the list of path patterns that denote the
// paths above that require special privileges. These can't be
2015-03-16 00:35:59 +00:00
// regular expressions, it is either exact match or prefix match.
// For prefix match, append '*' as a suffix.
2015-03-31 00:46:18 +00:00
Paths []*Path
PathsSpecial *logical.Paths
2015-03-16 00:35:59 +00:00
// Secrets is the list of secret types that this backend can
// return. It is used to automatically generate proper responses,
// and ease specifying callbacks for revocation, renewal, etc.
Secrets []*Secret
2015-03-18 00:15:23 +00:00
// Rollback is called when a WAL entry (see wal.go) has to be rolled
// back. It is called with the data from the entry.
//
// RollbackMinAge is the minimum age of a WAL entry before it is attempted
// to be rolled back. This should be longer than the maximum time it takes
// to successfully create a secret.
Rollback RollbackFunc
RollbackMinAge time.Duration
2015-03-18 00:15:23 +00:00
// AuthRenew is the callback to call when a RenewRequest for an
// authentication comes in. By default, renewal won't be allowed.
// See the built-in AuthRenew helpers in lease.go for common callbacks.
AuthRenew OperationFunc
logger *log.Logger
once sync.Once
pathsRe []*regexp.Regexp
2015-03-14 04:11:19 +00:00
}
// OperationFunc is the callback called for an operation on a path.
vault: clean up VaultID duplications, make secret responses clearer /cc @armon - This is a reasonably major refactor that I think cleans up a lot of the logic with secrets in responses. The reason for the refactor is that while implementing Renew/Revoke in logical/framework I found the existing API to be really awkward to work with. Primarily, we needed a way to send down internal data for Vault core to store since not all the data you need to revoke a key is always sent down to the user (for example the user than AWS key belongs to). At first, I was doing this manually in logical/framework with req.Storage, but this is going to be such a common event that I think its something core should assist with. Additionally, I think the added context for secrets will be useful in the future when we have a Vault API for returning orphaned out keys: we can also return the internal data that might help an operator. So this leads me to this refactor. I've removed most of the fields in `logical.Response` and replaced it with a single `*Secret` pointer. If this is non-nil, then the response represents a secret. The Secret struct encapsulates all the lease info and such. It also has some fields on it that are only populated at _request_ time for Revoke/Renew operations. There is precedent for this sort of behavior in the Go stdlib where http.Request/http.Response have fields that differ based on client/server. I copied this style. All core unit tests pass. The APIs fail for obvious reasons but I'll fix that up in the next commit.
2015-03-19 22:11:42 +00:00
type OperationFunc func(*logical.Request, *FieldData) (*logical.Response, error)
// RollbackFunc is the callback for rollbacks.
type RollbackFunc func(*logical.Request, string, interface{}) error
2015-03-15 21:57:19 +00:00
// logical.Backend impl.
func (b *Backend) HandleRequest(req *logical.Request) (*logical.Response, error) {
2015-04-04 03:36:47 +00:00
b.once.Do(b.init)
2015-03-19 18:41:41 +00:00
// Check for special cased global operations. These don't route
// to a specific Path.
switch req.Operation {
2015-03-19 19:20:25 +00:00
case logical.RenewOperation:
fallthrough
2015-03-19 18:41:41 +00:00
case logical.RevokeOperation:
2015-03-19 19:20:25 +00:00
return b.handleRevokeRenew(req)
2015-03-19 18:41:41 +00:00
case logical.RollbackOperation:
2015-03-18 00:15:23 +00:00
return b.handleRollback(req)
}
2015-04-04 03:36:47 +00:00
// If the path is empty and it is a help operation, handle that.
if req.Path == "" && req.Operation == logical.HelpOperation {
return b.handleRootHelp()
}
2015-03-14 06:58:20 +00:00
// Find the matching route
path, captures := b.route(req.Path)
if path == nil {
2015-03-15 21:57:19 +00:00
return nil, logical.ErrUnsupportedPath
2015-03-14 06:58:20 +00:00
}
// Build up the data for the route, with the URL taking priority
// for the fields over the PUT data.
raw := make(map[string]interface{}, len(path.Fields))
for k, v := range req.Data {
raw[k] = v
}
for k, v := range captures {
raw[k] = v
}
// Look up the callback for this operation
var callback OperationFunc
var ok bool
if path.Callbacks != nil {
callback, ok = path.Callbacks[req.Operation]
}
if !ok {
if req.Operation == logical.HelpOperation {
callback = path.helpCallback
ok = true
}
}
if !ok {
2015-03-15 21:57:19 +00:00
return nil, logical.ErrUnsupportedOperation
}
fd := FieldData{
Raw: raw,
Schema: path.Fields}
if req.Operation != logical.HelpOperation {
err := fd.Validate()
if err != nil {
return nil, err
}
}
2015-03-14 06:58:20 +00:00
// Call the callback with the request and the data
return callback(req, &fd)
2015-03-14 06:58:20 +00:00
}
2015-03-15 21:57:19 +00:00
// logical.Backend impl.
2015-03-31 00:46:18 +00:00
func (b *Backend) SpecialPaths() *logical.Paths {
return b.PathsSpecial
2015-03-14 06:58:20 +00:00
}
// Setup is used to initialize the backend with the initial backend configuration
func (b *Backend) Setup(config *logical.BackendConfig) (logical.Backend, error) {
b.logger = config.Logger
return b, nil
}
// Logger can be used to get the logger. If no logger has been set,
// the logs will be discarded.
func (b *Backend) Logger() *log.Logger {
if b.logger != nil {
return b.logger
}
return log.New(ioutil.Discard, "", 0)
}
// Route looks up the path that would be used for a given path string.
func (b *Backend) Route(path string) *Path {
result, _ := b.route(path)
return result
}
// Secret is used to look up the secret with the given type.
func (b *Backend) Secret(k string) *Secret {
for _, s := range b.Secrets {
if s.Type == k {
return s
}
}
return nil
}
func (b *Backend) init() {
b.pathsRe = make([]*regexp.Regexp, len(b.Paths))
for i, p := range b.Paths {
if len(p.Pattern) == 0 {
panic(fmt.Sprintf("Routing pattern cannot be blank"))
}
// Automatically anchor the pattern
if p.Pattern[0] != '^' {
p.Pattern = "^" + p.Pattern
}
if p.Pattern[len(p.Pattern)-1] != '$' {
p.Pattern = p.Pattern + "$"
}
b.pathsRe[i] = regexp.MustCompile(p.Pattern)
}
}
func (b *Backend) route(path string) (*Path, map[string]string) {
b.once.Do(b.init)
for i, re := range b.pathsRe {
matches := re.FindStringSubmatch(path)
if matches == nil {
continue
}
// We have a match, determine the mapping of the captures and
// store that for returning.
var captures map[string]string
path := b.Paths[i]
if captureNames := re.SubexpNames(); len(captureNames) > 1 {
captures = make(map[string]string, len(captureNames))
for i, name := range captureNames {
if name != "" {
captures[name] = matches[i]
}
}
}
return path, captures
}
return nil, nil
}
2015-04-04 03:36:47 +00:00
func (b *Backend) handleRootHelp() (*logical.Response, error) {
// Build a mapping of the paths and get the paths alphabetized to
// make the output prettier.
pathsMap := make(map[string]*Path)
2015-04-04 03:36:47 +00:00
paths := make([]string, 0, len(b.Paths))
for i, p := range b.pathsRe {
2015-04-04 03:36:47 +00:00
paths = append(paths, p.String())
pathsMap[p.String()] = b.Paths[i]
}
sort.Strings(paths)
// Build the path data
pathData := make([]rootHelpTemplatePath, 0, len(paths))
for _, route := range paths {
p := pathsMap[route]
pathData = append(pathData, rootHelpTemplatePath{
Path: route,
2015-04-04 04:10:54 +00:00
Help: strings.TrimSpace(p.HelpSynopsis),
})
2015-04-04 03:36:47 +00:00
}
help, err := executeTemplate(rootHelpTemplate, &rootHelpTemplateData{
2015-04-04 04:10:54 +00:00
Help: strings.TrimSpace(b.Help),
Paths: pathData,
2015-04-04 03:36:47 +00:00
})
if err != nil {
return nil, err
}
return logical.HelpResponse(help, nil), nil
}
2015-03-19 19:20:25 +00:00
func (b *Backend) handleRevokeRenew(
2015-03-19 18:41:41 +00:00
req *logical.Request) (*logical.Response, error) {
// Special case renewal of authentication for credential backends
if req.Operation == logical.RenewOperation && req.Auth != nil {
return b.handleAuthRenew(req)
}
vault: clean up VaultID duplications, make secret responses clearer /cc @armon - This is a reasonably major refactor that I think cleans up a lot of the logic with secrets in responses. The reason for the refactor is that while implementing Renew/Revoke in logical/framework I found the existing API to be really awkward to work with. Primarily, we needed a way to send down internal data for Vault core to store since not all the data you need to revoke a key is always sent down to the user (for example the user than AWS key belongs to). At first, I was doing this manually in logical/framework with req.Storage, but this is going to be such a common event that I think its something core should assist with. Additionally, I think the added context for secrets will be useful in the future when we have a Vault API for returning orphaned out keys: we can also return the internal data that might help an operator. So this leads me to this refactor. I've removed most of the fields in `logical.Response` and replaced it with a single `*Secret` pointer. If this is non-nil, then the response represents a secret. The Secret struct encapsulates all the lease info and such. It also has some fields on it that are only populated at _request_ time for Revoke/Renew operations. There is precedent for this sort of behavior in the Go stdlib where http.Request/http.Response have fields that differ based on client/server. I copied this style. All core unit tests pass. The APIs fail for obvious reasons but I'll fix that up in the next commit.
2015-03-19 22:11:42 +00:00
if req.Secret == nil {
return nil, fmt.Errorf("request has no secret")
2015-03-19 18:41:41 +00:00
}
vault: clean up VaultID duplications, make secret responses clearer /cc @armon - This is a reasonably major refactor that I think cleans up a lot of the logic with secrets in responses. The reason for the refactor is that while implementing Renew/Revoke in logical/framework I found the existing API to be really awkward to work with. Primarily, we needed a way to send down internal data for Vault core to store since not all the data you need to revoke a key is always sent down to the user (for example the user than AWS key belongs to). At first, I was doing this manually in logical/framework with req.Storage, but this is going to be such a common event that I think its something core should assist with. Additionally, I think the added context for secrets will be useful in the future when we have a Vault API for returning orphaned out keys: we can also return the internal data that might help an operator. So this leads me to this refactor. I've removed most of the fields in `logical.Response` and replaced it with a single `*Secret` pointer. If this is non-nil, then the response represents a secret. The Secret struct encapsulates all the lease info and such. It also has some fields on it that are only populated at _request_ time for Revoke/Renew operations. There is precedent for this sort of behavior in the Go stdlib where http.Request/http.Response have fields that differ based on client/server. I copied this style. All core unit tests pass. The APIs fail for obvious reasons but I'll fix that up in the next commit.
2015-03-19 22:11:42 +00:00
rawSecretType, ok := req.Secret.InternalData["secret_type"]
2015-03-19 18:41:41 +00:00
if !ok {
vault: clean up VaultID duplications, make secret responses clearer /cc @armon - This is a reasonably major refactor that I think cleans up a lot of the logic with secrets in responses. The reason for the refactor is that while implementing Renew/Revoke in logical/framework I found the existing API to be really awkward to work with. Primarily, we needed a way to send down internal data for Vault core to store since not all the data you need to revoke a key is always sent down to the user (for example the user than AWS key belongs to). At first, I was doing this manually in logical/framework with req.Storage, but this is going to be such a common event that I think its something core should assist with. Additionally, I think the added context for secrets will be useful in the future when we have a Vault API for returning orphaned out keys: we can also return the internal data that might help an operator. So this leads me to this refactor. I've removed most of the fields in `logical.Response` and replaced it with a single `*Secret` pointer. If this is non-nil, then the response represents a secret. The Secret struct encapsulates all the lease info and such. It also has some fields on it that are only populated at _request_ time for Revoke/Renew operations. There is precedent for this sort of behavior in the Go stdlib where http.Request/http.Response have fields that differ based on client/server. I copied this style. All core unit tests pass. The APIs fail for obvious reasons but I'll fix that up in the next commit.
2015-03-19 22:11:42 +00:00
return nil, fmt.Errorf("secret is unsupported by this backend")
2015-03-19 18:41:41 +00:00
}
vault: clean up VaultID duplications, make secret responses clearer /cc @armon - This is a reasonably major refactor that I think cleans up a lot of the logic with secrets in responses. The reason for the refactor is that while implementing Renew/Revoke in logical/framework I found the existing API to be really awkward to work with. Primarily, we needed a way to send down internal data for Vault core to store since not all the data you need to revoke a key is always sent down to the user (for example the user than AWS key belongs to). At first, I was doing this manually in logical/framework with req.Storage, but this is going to be such a common event that I think its something core should assist with. Additionally, I think the added context for secrets will be useful in the future when we have a Vault API for returning orphaned out keys: we can also return the internal data that might help an operator. So this leads me to this refactor. I've removed most of the fields in `logical.Response` and replaced it with a single `*Secret` pointer. If this is non-nil, then the response represents a secret. The Secret struct encapsulates all the lease info and such. It also has some fields on it that are only populated at _request_ time for Revoke/Renew operations. There is precedent for this sort of behavior in the Go stdlib where http.Request/http.Response have fields that differ based on client/server. I copied this style. All core unit tests pass. The APIs fail for obvious reasons but I'll fix that up in the next commit.
2015-03-19 22:11:42 +00:00
secretType, ok := rawSecretType.(string)
if !ok {
2015-03-19 18:41:41 +00:00
return nil, fmt.Errorf("secret is unsupported by this backend")
}
secret := b.Secret(secretType)
if secret == nil {
return nil, fmt.Errorf("secret is unsupported by this backend")
}
2015-03-19 19:20:25 +00:00
switch req.Operation {
case logical.RenewOperation:
return secret.HandleRenew(req)
2015-03-19 19:20:25 +00:00
case logical.RevokeOperation:
return secret.HandleRevoke(req)
2015-03-19 19:20:25 +00:00
default:
return nil, fmt.Errorf(
"invalid operation for revoke/renew: %s", req.Operation)
}
2015-03-19 18:41:41 +00:00
}
func (b *Backend) handleAuthRenew(req *logical.Request) (*logical.Response, error) {
if b.AuthRenew == nil {
return logical.ErrorResponse("this auth type doesn't support renew"), nil
}
return b.AuthRenew(req, nil)
}
2015-03-18 00:15:23 +00:00
func (b *Backend) handleRollback(
req *logical.Request) (*logical.Response, error) {
if b.Rollback == nil {
return nil, logical.ErrUnsupportedOperation
}
var merr error
keys, err := ListWAL(req.Storage)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
if len(keys) == 0 {
return nil, nil
2015-03-18 00:15:23 +00:00
}
// Calculate the minimum time that the WAL entries could be
// created in order to be rolled back.
age := b.RollbackMinAge
if age == 0 {
age = 10 * time.Minute
}
minAge := time.Now().UTC().Add(-1 * age)
if _, ok := req.Data["immediate"]; ok {
minAge = time.Now().UTC().Add(1000 * time.Hour)
}
2015-03-18 00:15:23 +00:00
for _, k := range keys {
entry, err := GetWAL(req.Storage, k)
2015-03-18 00:15:23 +00:00
if err != nil {
merr = multierror.Append(merr, err)
continue
}
if entry == nil {
continue
}
// If the entry isn't old enough, then don't roll it back
if !time.Unix(entry.CreatedAt, 0).Before(minAge) {
continue
}
2015-03-18 00:15:23 +00:00
// Attempt a rollback
err = b.Rollback(req, entry.Kind, entry.Data)
if err != nil {
err = fmt.Errorf(
"Error rolling back '%s' entry: %s", entry.Kind, err)
}
if err == nil {
err = DeleteWAL(req.Storage, k)
}
if err != nil {
merr = multierror.Append(merr, err)
2015-03-18 00:15:23 +00:00
}
}
if merr == nil {
return nil, nil
}
return logical.ErrorResponse(merr.Error()), nil
}
2015-03-14 04:11:19 +00:00
// FieldSchema is a basic schema to describe the format of a path field.
type FieldSchema struct {
Type FieldType
Default interface{}
Description string
2015-03-14 04:15:20 +00:00
}
// DefaultOrZero returns the default value if it is set, or otherwise
// the zero value of the type.
func (s *FieldSchema) DefaultOrZero() interface{} {
if s.Default != nil {
return s.Default
}
return s.Type.Zero()
2015-03-14 04:11:19 +00:00
}
func (t FieldType) Zero() interface{} {
switch t {
case TypeString:
return ""
case TypeInt:
return 0
case TypeBool:
return false
case TypeMap:
return map[string]interface{}{}
case TypeDurationSecond:
return 0
2015-03-14 04:11:19 +00:00
default:
panic("unknown type: " + t.String())
}
}
2015-04-04 03:36:47 +00:00
type rootHelpTemplateData struct {
Help string
Paths []rootHelpTemplatePath
}
type rootHelpTemplatePath struct {
Path string
Help string
2015-04-04 03:36:47 +00:00
}
const rootHelpTemplate = `
## DESCRIPTION
{{.Help}}
## PATHS
The following paths are supported by this backend. To view help for
any of the paths below, use the help command with any route matching
the path pattern. Note that depending on the policy of your auth token,
you may or may not be able to access certain paths.
{{range .Paths}}{{indent 4 .Path}}
{{indent 8 .Help}}
2015-04-04 03:36:47 +00:00
{{end}}
`