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) }