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