1package framework
2
3import (
4	"context"
5	"crypto/rand"
6	"fmt"
7	"io"
8	"io/ioutil"
9	"net/http"
10	"regexp"
11	"sort"
12	"strings"
13	"sync"
14	"time"
15
16	"github.com/hashicorp/errwrap"
17	log "github.com/hashicorp/go-hclog"
18	"github.com/hashicorp/go-kms-wrapping/entropy"
19	"github.com/hashicorp/go-multierror"
20	"github.com/hashicorp/vault/sdk/helper/consts"
21	"github.com/hashicorp/vault/sdk/helper/errutil"
22	"github.com/hashicorp/vault/sdk/helper/license"
23	"github.com/hashicorp/vault/sdk/helper/logging"
24	"github.com/hashicorp/vault/sdk/helper/parseutil"
25	"github.com/hashicorp/vault/sdk/logical"
26)
27
28// Backend is an implementation of logical.Backend that allows
29// the implementer to code a backend using a much more programmer-friendly
30// framework that handles a lot of the routing and validation for you.
31//
32// This is recommended over implementing logical.Backend directly.
33type Backend struct {
34	// Help is the help text that is shown when a help request is made
35	// on the root of this resource. The root help is special since we
36	// show all the paths that can be requested.
37	Help string
38
39	// Paths are the various routes that the backend responds to.
40	// This cannot be modified after construction (i.e. dynamically changing
41	// paths, including adding or removing, is not allowed once the
42	// backend is in use).
43	//
44	// PathsSpecial is the list of path patterns that denote the
45	// paths above that require special privileges. These can't be
46	// regular expressions, it is either exact match or prefix match.
47	// For prefix match, append '*' as a suffix.
48	Paths        []*Path
49	PathsSpecial *logical.Paths
50
51	// Secrets is the list of secret types that this backend can
52	// return. It is used to automatically generate proper responses,
53	// and ease specifying callbacks for revocation, renewal, etc.
54	Secrets []*Secret
55
56	// InitializeFunc is the callback, which if set, will be invoked via
57	// Initialize() just after a plugin has been mounted.
58	InitializeFunc InitializeFunc
59
60	// PeriodicFunc is the callback, which if set, will be invoked when the
61	// periodic timer of RollbackManager ticks. This can be used by
62	// backends to do anything it wishes to do periodically.
63	//
64	// PeriodicFunc can be invoked to, say to periodically delete stale
65	// entries in backend's storage, while the backend is still being used.
66	// (Note the different of this action from what `Clean` does, which is
67	// invoked just before the backend is unmounted).
68	PeriodicFunc periodicFunc
69
70	// WALRollback is called when a WAL entry (see wal.go) has to be rolled
71	// back. It is called with the data from the entry.
72	//
73	// WALRollbackMinAge is the minimum age of a WAL entry before it is attempted
74	// to be rolled back. This should be longer than the maximum time it takes
75	// to successfully create a secret.
76	WALRollback       WALRollbackFunc
77	WALRollbackMinAge time.Duration
78
79	// Clean is called on unload to clean up e.g any existing connections
80	// to the backend, if required.
81	Clean CleanupFunc
82
83	// Invalidate is called when a keys is modified if required
84	Invalidate InvalidateFunc
85
86	// AuthRenew is the callback to call when a RenewRequest for an
87	// authentication comes in. By default, renewal won't be allowed.
88	// See the built-in AuthRenew helpers in lease.go for common callbacks.
89	AuthRenew OperationFunc
90
91	// Type is the logical.BackendType for the backend implementation
92	BackendType logical.BackendType
93
94	logger  log.Logger
95	system  logical.SystemView
96	once    sync.Once
97	pathsRe []*regexp.Regexp
98}
99
100// periodicFunc is the callback called when the RollbackManager's timer ticks.
101// This can be utilized by the backends to do anything it wants.
102type periodicFunc func(context.Context, *logical.Request) error
103
104// OperationFunc is the callback called for an operation on a path.
105type OperationFunc func(context.Context, *logical.Request, *FieldData) (*logical.Response, error)
106
107// ExistenceFunc is the callback called for an existence check on a path.
108type ExistenceFunc func(context.Context, *logical.Request, *FieldData) (bool, error)
109
110// WALRollbackFunc is the callback for rollbacks.
111type WALRollbackFunc func(context.Context, *logical.Request, string, interface{}) error
112
113// CleanupFunc is the callback for backend unload.
114type CleanupFunc func(context.Context)
115
116// InvalidateFunc is the callback for backend key invalidation.
117type InvalidateFunc func(context.Context, string)
118
119// InitializeFunc is the callback, which if set, will be invoked via
120// Initialize() just after a plugin has been mounted.
121type InitializeFunc func(context.Context, *logical.InitializationRequest) error
122
123// Initialize is the logical.Backend implementation.
124func (b *Backend) Initialize(ctx context.Context, req *logical.InitializationRequest) error {
125	if b.InitializeFunc != nil {
126		return b.InitializeFunc(ctx, req)
127	}
128	return nil
129}
130
131// HandleExistenceCheck is the logical.Backend implementation.
132func (b *Backend) HandleExistenceCheck(ctx context.Context, req *logical.Request) (checkFound bool, exists bool, err error) {
133	b.once.Do(b.init)
134
135	// Ensure we are only doing this when one of the correct operations is in play
136	switch req.Operation {
137	case logical.CreateOperation:
138	case logical.UpdateOperation:
139	default:
140		return false, false, fmt.Errorf("incorrect operation type %v for an existence check", req.Operation)
141	}
142
143	// Find the matching route
144	path, captures := b.route(req.Path)
145	if path == nil {
146		return false, false, logical.ErrUnsupportedPath
147	}
148
149	if path.ExistenceCheck == nil {
150		return false, false, nil
151	}
152
153	checkFound = true
154
155	// Build up the data for the route, with the URL taking priority
156	// for the fields over the PUT data.
157	raw := make(map[string]interface{}, len(path.Fields))
158	for k, v := range req.Data {
159		raw[k] = v
160	}
161	for k, v := range captures {
162		raw[k] = v
163	}
164
165	fd := FieldData{
166		Raw:    raw,
167		Schema: path.Fields}
168
169	err = fd.Validate()
170	if err != nil {
171		return false, false, errutil.UserError{Err: err.Error()}
172	}
173
174	// Call the callback with the request and the data
175	exists, err = path.ExistenceCheck(ctx, req, &fd)
176	return
177}
178
179// HandleRequest is the logical.Backend implementation.
180func (b *Backend) HandleRequest(ctx context.Context, req *logical.Request) (*logical.Response, error) {
181	b.once.Do(b.init)
182
183	// Check for special cased global operations. These don't route
184	// to a specific Path.
185	switch req.Operation {
186	case logical.RenewOperation:
187		fallthrough
188	case logical.RevokeOperation:
189		return b.handleRevokeRenew(ctx, req)
190	case logical.RollbackOperation:
191		return b.handleRollback(ctx, req)
192	}
193
194	// If the path is empty and it is a help operation, handle that.
195	if req.Path == "" && req.Operation == logical.HelpOperation {
196		return b.handleRootHelp()
197	}
198
199	// Find the matching route
200	path, captures := b.route(req.Path)
201	if path == nil {
202		return nil, logical.ErrUnsupportedPath
203	}
204
205	// Check if a feature is required and if the license has that feature
206	if path.FeatureRequired != license.FeatureNone {
207		hasFeature := b.system.HasFeature(path.FeatureRequired)
208		if !hasFeature {
209			return nil, logical.CodedError(401, "Feature Not Enabled")
210		}
211	}
212
213	// Build up the data for the route, with the URL taking priority
214	// for the fields over the PUT data.
215	raw := make(map[string]interface{}, len(path.Fields))
216	for k, v := range req.Data {
217		raw[k] = v
218	}
219	for k, v := range captures {
220		raw[k] = v
221	}
222
223	// Look up the callback for this operation, preferring the
224	// path.Operations definition if present.
225	var callback OperationFunc
226
227	if path.Operations != nil {
228		if op, ok := path.Operations[req.Operation]; ok {
229
230			// Check whether this operation should be forwarded
231			if sysView := b.System(); sysView != nil {
232				replState := sysView.ReplicationState()
233				props := op.Properties()
234
235				if props.ForwardPerformanceStandby && replState.HasState(consts.ReplicationPerformanceStandby) {
236					return nil, logical.ErrReadOnly
237				}
238
239				if props.ForwardPerformanceSecondary && !sysView.LocalMount() && replState.HasState(consts.ReplicationPerformanceSecondary) {
240					return nil, logical.ErrReadOnly
241				}
242			}
243
244			callback = op.Handler()
245		}
246	} else {
247		callback = path.Callbacks[req.Operation]
248	}
249	ok := callback != nil
250
251	if !ok {
252		if req.Operation == logical.HelpOperation {
253			callback = path.helpCallback(b)
254			ok = true
255		}
256	}
257	if !ok {
258		return nil, logical.ErrUnsupportedOperation
259	}
260
261	fd := FieldData{
262		Raw:    raw,
263		Schema: path.Fields}
264
265	if req.Operation != logical.HelpOperation {
266		err := fd.Validate()
267		if err != nil {
268			return nil, err
269		}
270	}
271
272	return callback(ctx, req, &fd)
273}
274
275// SpecialPaths is the logical.Backend implementation.
276func (b *Backend) SpecialPaths() *logical.Paths {
277	return b.PathsSpecial
278}
279
280// Cleanup is used to release resources and prepare to stop the backend
281func (b *Backend) Cleanup(ctx context.Context) {
282	if b.Clean != nil {
283		b.Clean(ctx)
284	}
285}
286
287// InvalidateKey is used to clear caches and reset internal state on key changes
288func (b *Backend) InvalidateKey(ctx context.Context, key string) {
289	if b.Invalidate != nil {
290		b.Invalidate(ctx, key)
291	}
292}
293
294// Setup is used to initialize the backend with the initial backend configuration
295func (b *Backend) Setup(ctx context.Context, config *logical.BackendConfig) error {
296	b.logger = config.Logger
297	b.system = config.System
298	return nil
299}
300
301// GetRandomReader returns an io.Reader to use for generating key material in
302// backends. If the backend has access to an external entropy source it will
303// return that, otherwise it returns crypto/rand.Reader.
304func (b *Backend) GetRandomReader() io.Reader {
305	if sourcer, ok := b.System().(entropy.Sourcer); ok {
306		return entropy.NewReader(sourcer)
307	}
308
309	return rand.Reader
310}
311
312// Logger can be used to get the logger. If no logger has been set,
313// the logs will be discarded.
314func (b *Backend) Logger() log.Logger {
315	if b.logger != nil {
316		return b.logger
317	}
318
319	return logging.NewVaultLoggerWithWriter(ioutil.Discard, log.NoLevel)
320}
321
322// System returns the backend's system view.
323func (b *Backend) System() logical.SystemView {
324	return b.system
325}
326
327// Type returns the backend type
328func (b *Backend) Type() logical.BackendType {
329	return b.BackendType
330}
331
332// Route looks up the path that would be used for a given path string.
333func (b *Backend) Route(path string) *Path {
334	result, _ := b.route(path)
335	return result
336}
337
338// Secret is used to look up the secret with the given type.
339func (b *Backend) Secret(k string) *Secret {
340	for _, s := range b.Secrets {
341		if s.Type == k {
342			return s
343		}
344	}
345
346	return nil
347}
348
349func (b *Backend) init() {
350	b.pathsRe = make([]*regexp.Regexp, len(b.Paths))
351	for i, p := range b.Paths {
352		if len(p.Pattern) == 0 {
353			panic(fmt.Sprintf("Routing pattern cannot be blank"))
354		}
355		// Automatically anchor the pattern
356		if p.Pattern[0] != '^' {
357			p.Pattern = "^" + p.Pattern
358		}
359		if p.Pattern[len(p.Pattern)-1] != '$' {
360			p.Pattern = p.Pattern + "$"
361		}
362		b.pathsRe[i] = regexp.MustCompile(p.Pattern)
363	}
364}
365
366func (b *Backend) route(path string) (*Path, map[string]string) {
367	b.once.Do(b.init)
368
369	for i, re := range b.pathsRe {
370		matches := re.FindStringSubmatch(path)
371		if matches == nil {
372			continue
373		}
374
375		// We have a match, determine the mapping of the captures and
376		// store that for returning.
377		var captures map[string]string
378		path := b.Paths[i]
379		if captureNames := re.SubexpNames(); len(captureNames) > 1 {
380			captures = make(map[string]string, len(captureNames))
381			for i, name := range captureNames {
382				if name != "" {
383					captures[name] = matches[i]
384				}
385			}
386		}
387
388		return path, captures
389	}
390
391	return nil, nil
392}
393
394func (b *Backend) handleRootHelp() (*logical.Response, error) {
395	// Build a mapping of the paths and get the paths alphabetized to
396	// make the output prettier.
397	pathsMap := make(map[string]*Path)
398	paths := make([]string, 0, len(b.Paths))
399	for i, p := range b.pathsRe {
400		paths = append(paths, p.String())
401		pathsMap[p.String()] = b.Paths[i]
402	}
403	sort.Strings(paths)
404
405	// Build the path data
406	pathData := make([]rootHelpTemplatePath, 0, len(paths))
407	for _, route := range paths {
408		p := pathsMap[route]
409		pathData = append(pathData, rootHelpTemplatePath{
410			Path: route,
411			Help: strings.TrimSpace(p.HelpSynopsis),
412		})
413	}
414
415	help, err := executeTemplate(rootHelpTemplate, &rootHelpTemplateData{
416		Help:  strings.TrimSpace(b.Help),
417		Paths: pathData,
418	})
419	if err != nil {
420		return nil, err
421	}
422
423	// Build OpenAPI response for the entire backend
424	doc := NewOASDocument()
425	if err := documentPaths(b, doc); err != nil {
426		b.Logger().Warn("error generating OpenAPI", "error", err)
427	}
428
429	return logical.HelpResponse(help, nil, doc), nil
430}
431
432func (b *Backend) handleRevokeRenew(ctx context.Context, req *logical.Request) (*logical.Response, error) {
433	// Special case renewal of authentication for credential backends
434	if req.Operation == logical.RenewOperation && req.Auth != nil {
435		return b.handleAuthRenew(ctx, req)
436	}
437
438	if req.Secret == nil {
439		return nil, fmt.Errorf("request has no secret")
440	}
441
442	rawSecretType, ok := req.Secret.InternalData["secret_type"]
443	if !ok {
444		return nil, fmt.Errorf("secret is unsupported by this backend")
445	}
446	secretType, ok := rawSecretType.(string)
447	if !ok {
448		return nil, fmt.Errorf("secret is unsupported by this backend")
449	}
450
451	secret := b.Secret(secretType)
452	if secret == nil {
453		return nil, fmt.Errorf("secret is unsupported by this backend")
454	}
455
456	switch req.Operation {
457	case logical.RenewOperation:
458		return secret.HandleRenew(ctx, req)
459	case logical.RevokeOperation:
460		return secret.HandleRevoke(ctx, req)
461	default:
462		return nil, fmt.Errorf("invalid operation for revoke/renew: %q", req.Operation)
463	}
464}
465
466// handleRollback invokes the PeriodicFunc set on the backend. It also does a
467// WAL rollback operation.
468func (b *Backend) handleRollback(ctx context.Context, req *logical.Request) (*logical.Response, error) {
469	// Response is not expected from the periodic operation.
470	var resp *logical.Response
471
472	merr := new(multierror.Error)
473	if b.PeriodicFunc != nil {
474		if err := b.PeriodicFunc(ctx, req); err != nil {
475			merr = multierror.Append(merr, err)
476		}
477	}
478
479	if b.WALRollback != nil {
480		var err error
481		resp, err = b.handleWALRollback(ctx, req)
482		if err != nil {
483			merr = multierror.Append(merr, err)
484		}
485	}
486	return resp, merr.ErrorOrNil()
487}
488
489func (b *Backend) handleAuthRenew(ctx context.Context, req *logical.Request) (*logical.Response, error) {
490	if b.AuthRenew == nil {
491		return logical.ErrorResponse("this auth type doesn't support renew"), nil
492	}
493
494	return b.AuthRenew(ctx, req, nil)
495}
496
497func (b *Backend) handleWALRollback(ctx context.Context, req *logical.Request) (*logical.Response, error) {
498	if b.WALRollback == nil {
499		return nil, logical.ErrUnsupportedOperation
500	}
501
502	var merr error
503	keys, err := ListWAL(ctx, req.Storage)
504	if err != nil {
505		return logical.ErrorResponse(err.Error()), nil
506	}
507	if len(keys) == 0 {
508		return nil, nil
509	}
510
511	// Calculate the minimum time that the WAL entries could be
512	// created in order to be rolled back.
513	age := b.WALRollbackMinAge
514	if age == 0 {
515		age = 10 * time.Minute
516	}
517	minAge := time.Now().Add(-1 * age)
518	if _, ok := req.Data["immediate"]; ok {
519		minAge = time.Now().Add(1000 * time.Hour)
520	}
521
522	for _, k := range keys {
523		entry, err := GetWAL(ctx, req.Storage, k)
524		if err != nil {
525			merr = multierror.Append(merr, err)
526			continue
527		}
528		if entry == nil {
529			continue
530		}
531
532		// If the entry isn't old enough, then don't roll it back
533		if !time.Unix(entry.CreatedAt, 0).Before(minAge) {
534			continue
535		}
536
537		// Attempt a WAL rollback
538		err = b.WALRollback(ctx, req, entry.Kind, entry.Data)
539		if err != nil {
540			err = errwrap.Wrapf(fmt.Sprintf("error rolling back %q entry: {{err}}", entry.Kind), err)
541		}
542		if err == nil {
543			err = DeleteWAL(ctx, req.Storage, k)
544		}
545		if err != nil {
546			merr = multierror.Append(merr, err)
547		}
548	}
549
550	if merr == nil {
551		return nil, nil
552	}
553
554	return logical.ErrorResponse(merr.Error()), nil
555}
556
557// FieldSchema is a basic schema to describe the format of a path field.
558type FieldSchema struct {
559	Type        FieldType
560	Default     interface{}
561	Description string
562	Required    bool
563	Deprecated  bool
564
565	// Query indicates this field will be sent as a query parameter:
566	//
567	//   /v1/foo/bar?some_param=some_value
568	//
569	// It doesn't affect handling of the value, but may be used for documentation.
570	Query bool
571
572	// AllowedValues is an optional list of permitted values for this field.
573	// This constraint is not (yet) enforced by the framework, but the list is
574	// output as part of OpenAPI generation and may effect documentation and
575	// dynamic UI generation.
576	AllowedValues []interface{}
577
578	// DisplayAttrs provides hints for UI and documentation generators. They
579	// will be included in OpenAPI output if set.
580	DisplayAttrs *DisplayAttributes
581}
582
583// DefaultOrZero returns the default value if it is set, or otherwise
584// the zero value of the type.
585func (s *FieldSchema) DefaultOrZero() interface{} {
586	if s.Default != nil {
587		switch s.Type {
588		case TypeDurationSecond, TypeSignedDurationSecond:
589			resultDur, err := parseutil.ParseDurationSecond(s.Default)
590			if err != nil {
591				return s.Type.Zero()
592			}
593			return int(resultDur.Seconds())
594
595		default:
596			return s.Default
597		}
598	}
599
600	return s.Type.Zero()
601}
602
603// Zero returns the correct zero-value for a specific FieldType
604func (t FieldType) Zero() interface{} {
605	switch t {
606	case TypeString, TypeNameString, TypeLowerCaseString:
607		return ""
608	case TypeInt:
609		return 0
610	case TypeBool:
611		return false
612	case TypeMap:
613		return map[string]interface{}{}
614	case TypeKVPairs:
615		return map[string]string{}
616	case TypeDurationSecond, TypeSignedDurationSecond:
617		return 0
618	case TypeSlice:
619		return []interface{}{}
620	case TypeStringSlice, TypeCommaStringSlice:
621		return []string{}
622	case TypeCommaIntSlice:
623		return []int{}
624	case TypeHeader:
625		return http.Header{}
626	default:
627		panic("unknown type: " + t.String())
628	}
629}
630
631type rootHelpTemplateData struct {
632	Help  string
633	Paths []rootHelpTemplatePath
634}
635
636type rootHelpTemplatePath struct {
637	Path string
638	Help string
639}
640
641const rootHelpTemplate = `
642## DESCRIPTION
643
644{{.Help}}
645
646## PATHS
647
648The following paths are supported by this backend. To view help for
649any of the paths below, use the help command with any route matching
650the path pattern. Note that depending on the policy of your auth token,
651you may or may not be able to access certain paths.
652
653{{range .Paths}}{{indent 4 .Path}}
654{{indent 8 .Help}}
655
656{{end}}
657
658`
659