| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package file |
| |
| import ( |
| "context" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| "sort" |
| "strings" |
| "sync" |
| |
| "github.com/hashicorp/errwrap" |
| log "github.com/hashicorp/go-hclog" |
| |
| "github.com/hashicorp/vault/sdk/helper/consts" |
| "github.com/hashicorp/vault/sdk/helper/jsonutil" |
| "github.com/hashicorp/vault/sdk/physical" |
| ) |
| |
| // Verify FileBackend satisfies the correct interfaces |
| var ( |
| _ physical.Backend = (*FileBackend)(nil) |
| _ physical.Transactional = (*TransactionalFileBackend)(nil) |
| _ physical.PseudoTransactional = (*FileBackend)(nil) |
| ) |
| |
| // FileBackend is a physical backend that stores data on disk |
| // at a given file path. It can be used for durable single server |
| // situations, or to develop locally where durability is not critical. |
| // |
| // WARNING: the file backend implementation is currently extremely unsafe |
| // and non-performant. It is meant mostly for local testing and development. |
| // It can be improved in the future. |
| type FileBackend struct { |
| sync.RWMutex |
| path string |
| logger log.Logger |
| permitPool *physical.PermitPool |
| } |
| |
| type TransactionalFileBackend struct { |
| FileBackend |
| } |
| |
| type fileEntry struct { |
| Value []byte |
| } |
| |
| // NewFileBackend constructs a FileBackend using the given directory |
| func NewFileBackend(conf map[string]string, logger log.Logger) (physical.Backend, error) { |
| path, ok := conf["path"] |
| if !ok { |
| return nil, fmt.Errorf("'path' must be set") |
| } |
| |
| return &FileBackend{ |
| path: path, |
| logger: logger, |
| permitPool: physical.NewPermitPool(physical.DefaultParallelOperations), |
| }, nil |
| } |
| |
| func NewTransactionalFileBackend(conf map[string]string, logger log.Logger) (physical.Backend, error) { |
| path, ok := conf["path"] |
| if !ok { |
| return nil, fmt.Errorf("'path' must be set") |
| } |
| |
| // Create a pool of size 1 so only one operation runs at a time |
| return &TransactionalFileBackend{ |
| FileBackend: FileBackend{ |
| path: path, |
| logger: logger, |
| permitPool: physical.NewPermitPool(1), |
| }, |
| }, nil |
| } |
| |
| func (b *FileBackend) Delete(ctx context.Context, path string) error { |
| b.permitPool.Acquire() |
| defer b.permitPool.Release() |
| |
| b.Lock() |
| defer b.Unlock() |
| |
| return b.DeleteInternal(ctx, path) |
| } |
| |
| func (b *FileBackend) DeleteInternal(ctx context.Context, path string) error { |
| if path == "" { |
| return nil |
| } |
| |
| if err := b.validatePath(path); err != nil { |
| return err |
| } |
| |
| basePath, key := b.expandPath(path) |
| fullPath := filepath.Join(basePath, key) |
| |
| select { |
| case <-ctx.Done(): |
| return ctx.Err() |
| default: |
| } |
| |
| err := os.Remove(fullPath) |
| if err != nil && !os.IsNotExist(err) { |
| return errwrap.Wrapf(fmt.Sprintf("failed to remove %q: {{err}}", fullPath), err) |
| } |
| |
| err = b.cleanupLogicalPath(path) |
| |
| return err |
| } |
| |
| // cleanupLogicalPath is used to remove all empty nodes, beginning with deepest |
| // one, aborting on first non-empty one, up to top-level node. |
| func (b *FileBackend) cleanupLogicalPath(path string) error { |
| nodes := strings.Split(path, fmt.Sprintf("%c", os.PathSeparator)) |
| for i := len(nodes) - 1; i > 0; i-- { |
| fullPath := filepath.Join(b.path, filepath.Join(nodes[:i]...)) |
| |
| dir, err := os.Open(fullPath) |
| if err != nil { |
| if dir != nil { |
| dir.Close() |
| } |
| if os.IsNotExist(err) { |
| return nil |
| } else { |
| return err |
| } |
| } |
| |
| list, err := dir.Readdir(1) |
| dir.Close() |
| if err != nil && err != io.EOF { |
| return err |
| } |
| |
| // If we have no entries, it's an empty directory; remove it |
| if err == io.EOF || list == nil || len(list) == 0 { |
| err = os.Remove(fullPath) |
| if err != nil { |
| return err |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| func (b *FileBackend) Get(ctx context.Context, k string) (*physical.Entry, error) { |
| b.permitPool.Acquire() |
| defer b.permitPool.Release() |
| |
| b.RLock() |
| defer b.RUnlock() |
| |
| return b.GetInternal(ctx, k) |
| } |
| |
| func (b *FileBackend) GetInternal(ctx context.Context, k string) (*physical.Entry, error) { |
| if err := b.validatePath(k); err != nil { |
| return nil, err |
| } |
| |
| path, key := b.expandPath(k) |
| path = filepath.Join(path, key) |
| |
| // If we stat it and it exists but is size zero, it may be left from some |
| // previous FS error like out-of-space. No Vault entry will ever be zero |
| // length, so simply remove it and return nil. |
| fi, err := os.Stat(path) |
| if err == nil { |
| if fi.Size() == 0 { |
| // Best effort, ignore errors |
| os.Remove(path) |
| return nil, nil |
| } |
| } |
| |
| f, err := os.Open(path) |
| if f != nil { |
| defer f.Close() |
| } |
| if err != nil { |
| if os.IsNotExist(err) { |
| return nil, nil |
| } |
| |
| return nil, err |
| } |
| |
| var entry fileEntry |
| if err := jsonutil.DecodeJSONFromReader(f, &entry); err != nil { |
| return nil, err |
| } |
| |
| select { |
| case <-ctx.Done(): |
| return nil, ctx.Err() |
| default: |
| } |
| |
| return &physical.Entry{ |
| Key: k, |
| Value: entry.Value, |
| }, nil |
| } |
| |
| func (b *FileBackend) Put(ctx context.Context, entry *physical.Entry) error { |
| b.permitPool.Acquire() |
| defer b.permitPool.Release() |
| |
| b.Lock() |
| defer b.Unlock() |
| |
| return b.PutInternal(ctx, entry) |
| } |
| |
| func (b *FileBackend) PutInternal(ctx context.Context, entry *physical.Entry) error { |
| if err := b.validatePath(entry.Key); err != nil { |
| return err |
| } |
| path, key := b.expandPath(entry.Key) |
| |
| select { |
| case <-ctx.Done(): |
| return ctx.Err() |
| default: |
| } |
| |
| // Make the parent tree |
| if err := os.MkdirAll(path, 0o700); err != nil { |
| return err |
| } |
| |
| // JSON encode the entry and write it |
| fullPath := filepath.Join(path, key) |
| tempPath := fullPath + ".temp" |
| f, err := os.OpenFile( |
| tempPath, |
| os.O_CREATE|os.O_TRUNC|os.O_WRONLY, |
| 0o600) |
| if err != nil { |
| if f != nil { |
| f.Close() |
| } |
| return err |
| } |
| if f == nil { |
| return errors.New("could not successfully get a file handle") |
| } |
| |
| enc := json.NewEncoder(f) |
| encErr := enc.Encode(&fileEntry{ |
| Value: entry.Value, |
| }) |
| f.Close() |
| if encErr == nil { |
| err = os.Rename(tempPath, fullPath) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // Everything below is best-effort and will result in encErr being returned |
| |
| // See if we ended up with a zero-byte file and if so delete it, might be a |
| // case of disk being full but the file info is in metadata that is |
| // reserved. |
| fi, err := os.Stat(tempPath) |
| if err != nil { |
| return encErr |
| } |
| if fi == nil { |
| return encErr |
| } |
| if fi.Size() == 0 { |
| os.Remove(tempPath) |
| } |
| return encErr |
| } |
| |
| func (b *FileBackend) List(ctx context.Context, prefix string) ([]string, error) { |
| b.permitPool.Acquire() |
| defer b.permitPool.Release() |
| |
| b.RLock() |
| defer b.RUnlock() |
| |
| return b.ListInternal(ctx, prefix) |
| } |
| |
| func (b *FileBackend) ListInternal(ctx context.Context, prefix string) ([]string, error) { |
| if err := b.validatePath(prefix); err != nil { |
| return nil, err |
| } |
| |
| path := b.path |
| if prefix != "" { |
| path = filepath.Join(path, prefix) |
| } |
| |
| // Read the directory contents |
| f, err := os.Open(path) |
| if f != nil { |
| defer f.Close() |
| } |
| if err != nil { |
| if os.IsNotExist(err) { |
| return nil, nil |
| } |
| |
| return nil, err |
| } |
| |
| names, err := f.Readdirnames(-1) |
| if err != nil { |
| return nil, err |
| } |
| |
| for i, name := range names { |
| fi, err := os.Stat(filepath.Join(path, name)) |
| if err != nil { |
| return nil, err |
| } |
| if fi.IsDir() { |
| names[i] = name + "/" |
| } else { |
| if name[0] == '_' { |
| names[i] = name[1:] |
| } |
| } |
| } |
| |
| select { |
| case <-ctx.Done(): |
| return nil, ctx.Err() |
| default: |
| } |
| |
| if len(names) > 0 { |
| sort.Strings(names) |
| } |
| |
| return names, nil |
| } |
| |
| func (b *FileBackend) expandPath(k string) (string, string) { |
| path := filepath.Join(b.path, k) |
| key := filepath.Base(path) |
| path = filepath.Dir(path) |
| return path, "_" + key |
| } |
| |
| func (b *FileBackend) validatePath(path string) error { |
| switch { |
| case strings.Contains(path, ".."): |
| return consts.ErrPathContainsParentReferences |
| } |
| |
| return nil |
| } |
| |
| func (b *TransactionalFileBackend) Transaction(ctx context.Context, txns []*physical.TxnEntry) error { |
| b.permitPool.Acquire() |
| defer b.permitPool.Release() |
| |
| b.Lock() |
| defer b.Unlock() |
| |
| return physical.GenericTransactionHandler(ctx, b, txns) |
| } |