github.com/hashicorp/vault/sdk@v0.13.0/physical/file/file.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package file
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"path/filepath"
    14  	"sort"
    15  	"strings"
    16  	"sync"
    17  
    18  	"github.com/hashicorp/errwrap"
    19  	log "github.com/hashicorp/go-hclog"
    20  	"github.com/hashicorp/vault/sdk/helper/consts"
    21  	"github.com/hashicorp/vault/sdk/helper/jsonutil"
    22  	"github.com/hashicorp/vault/sdk/physical"
    23  )
    24  
    25  // Verify FileBackend satisfies the correct interfaces
    26  var (
    27  	_ physical.Backend             = (*FileBackend)(nil)
    28  	_ physical.Transactional       = (*TransactionalFileBackend)(nil)
    29  	_ physical.PseudoTransactional = (*FileBackend)(nil)
    30  )
    31  
    32  // FileBackend is a physical backend that stores data on disk
    33  // at a given file path. It can be used for durable single server
    34  // situations, or to develop locally where durability is not critical.
    35  //
    36  // WARNING: the file backend implementation is currently extremely unsafe
    37  // and non-performant. It is meant mostly for local testing and development.
    38  // It can be improved in the future.
    39  type FileBackend struct {
    40  	sync.RWMutex
    41  	path       string
    42  	logger     log.Logger
    43  	permitPool *physical.PermitPool
    44  }
    45  
    46  type TransactionalFileBackend struct {
    47  	FileBackend
    48  }
    49  
    50  type fileEntry struct {
    51  	Value []byte
    52  }
    53  
    54  // NewFileBackend constructs a FileBackend using the given directory
    55  func NewFileBackend(conf map[string]string, logger log.Logger) (physical.Backend, error) {
    56  	path, ok := conf["path"]
    57  	if !ok {
    58  		return nil, fmt.Errorf("'path' must be set")
    59  	}
    60  
    61  	return &FileBackend{
    62  		path:       path,
    63  		logger:     logger,
    64  		permitPool: physical.NewPermitPool(physical.DefaultParallelOperations),
    65  	}, nil
    66  }
    67  
    68  func NewTransactionalFileBackend(conf map[string]string, logger log.Logger) (physical.Backend, error) {
    69  	path, ok := conf["path"]
    70  	if !ok {
    71  		return nil, fmt.Errorf("'path' must be set")
    72  	}
    73  
    74  	// Create a pool of size 1 so only one operation runs at a time
    75  	return &TransactionalFileBackend{
    76  		FileBackend: FileBackend{
    77  			path:       path,
    78  			logger:     logger,
    79  			permitPool: physical.NewPermitPool(1),
    80  		},
    81  	}, nil
    82  }
    83  
    84  func (b *FileBackend) Delete(ctx context.Context, path string) error {
    85  	b.permitPool.Acquire()
    86  	defer b.permitPool.Release()
    87  
    88  	b.Lock()
    89  	defer b.Unlock()
    90  
    91  	return b.DeleteInternal(ctx, path)
    92  }
    93  
    94  func (b *FileBackend) DeleteInternal(ctx context.Context, path string) error {
    95  	if path == "" {
    96  		return nil
    97  	}
    98  
    99  	if err := b.validatePath(path); err != nil {
   100  		return err
   101  	}
   102  
   103  	basePath, key := b.expandPath(path)
   104  	fullPath := filepath.Join(basePath, key)
   105  
   106  	select {
   107  	case <-ctx.Done():
   108  		return ctx.Err()
   109  	default:
   110  	}
   111  
   112  	err := os.Remove(fullPath)
   113  	if err != nil && !os.IsNotExist(err) {
   114  		return errwrap.Wrapf(fmt.Sprintf("failed to remove %q: {{err}}", fullPath), err)
   115  	}
   116  
   117  	err = b.cleanupLogicalPath(path)
   118  
   119  	return err
   120  }
   121  
   122  // cleanupLogicalPath is used to remove all empty nodes, beginning with deepest
   123  // one, aborting on first non-empty one, up to top-level node.
   124  func (b *FileBackend) cleanupLogicalPath(path string) error {
   125  	nodes := strings.Split(path, fmt.Sprintf("%c", os.PathSeparator))
   126  	for i := len(nodes) - 1; i > 0; i-- {
   127  		fullPath := filepath.Join(b.path, filepath.Join(nodes[:i]...))
   128  
   129  		dir, err := os.Open(fullPath)
   130  		if err != nil {
   131  			if dir != nil {
   132  				dir.Close()
   133  			}
   134  			if os.IsNotExist(err) {
   135  				return nil
   136  			} else {
   137  				return err
   138  			}
   139  		}
   140  
   141  		list, err := dir.Readdir(1)
   142  		dir.Close()
   143  		if err != nil && err != io.EOF {
   144  			return err
   145  		}
   146  
   147  		// If we have no entries, it's an empty directory; remove it
   148  		if err == io.EOF || list == nil || len(list) == 0 {
   149  			err = os.Remove(fullPath)
   150  			if err != nil {
   151  				return err
   152  			}
   153  		}
   154  	}
   155  
   156  	return nil
   157  }
   158  
   159  func (b *FileBackend) Get(ctx context.Context, k string) (*physical.Entry, error) {
   160  	b.permitPool.Acquire()
   161  	defer b.permitPool.Release()
   162  
   163  	b.RLock()
   164  	defer b.RUnlock()
   165  
   166  	return b.GetInternal(ctx, k)
   167  }
   168  
   169  func (b *FileBackend) GetInternal(ctx context.Context, k string) (*physical.Entry, error) {
   170  	if err := b.validatePath(k); err != nil {
   171  		return nil, err
   172  	}
   173  
   174  	path, key := b.expandPath(k)
   175  	path = filepath.Join(path, key)
   176  
   177  	// If we stat it and it exists but is size zero, it may be left from some
   178  	// previous FS error like out-of-space. No Vault entry will ever be zero
   179  	// length, so simply remove it and return nil.
   180  	fi, err := os.Stat(path)
   181  	if err == nil {
   182  		if fi.Size() == 0 {
   183  			// Best effort, ignore errors
   184  			os.Remove(path)
   185  			return nil, nil
   186  		}
   187  	}
   188  
   189  	f, err := os.Open(path)
   190  	if f != nil {
   191  		defer f.Close()
   192  	}
   193  	if err != nil {
   194  		if os.IsNotExist(err) {
   195  			return nil, nil
   196  		}
   197  
   198  		return nil, err
   199  	}
   200  
   201  	var entry fileEntry
   202  	if err := jsonutil.DecodeJSONFromReader(f, &entry); err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	select {
   207  	case <-ctx.Done():
   208  		return nil, ctx.Err()
   209  	default:
   210  	}
   211  
   212  	return &physical.Entry{
   213  		Key:   k,
   214  		Value: entry.Value,
   215  	}, nil
   216  }
   217  
   218  func (b *FileBackend) Put(ctx context.Context, entry *physical.Entry) error {
   219  	b.permitPool.Acquire()
   220  	defer b.permitPool.Release()
   221  
   222  	b.Lock()
   223  	defer b.Unlock()
   224  
   225  	return b.PutInternal(ctx, entry)
   226  }
   227  
   228  func (b *FileBackend) PutInternal(ctx context.Context, entry *physical.Entry) error {
   229  	if err := b.validatePath(entry.Key); err != nil {
   230  		return err
   231  	}
   232  	path, key := b.expandPath(entry.Key)
   233  
   234  	select {
   235  	case <-ctx.Done():
   236  		return ctx.Err()
   237  	default:
   238  	}
   239  
   240  	// Make the parent tree
   241  	if err := os.MkdirAll(path, 0o700); err != nil {
   242  		return err
   243  	}
   244  
   245  	// JSON encode the entry and write it
   246  	fullPath := filepath.Join(path, key)
   247  	f, err := os.CreateTemp(path, key)
   248  	if err != nil {
   249  		if f != nil {
   250  			f.Close()
   251  		}
   252  		return err
   253  	}
   254  
   255  	if err = os.Chmod(f.Name(), 0o600); err != nil {
   256  		if f != nil {
   257  			f.Close()
   258  		}
   259  		return err
   260  	}
   261  
   262  	if f == nil {
   263  		return errors.New("could not successfully get a file handle")
   264  	}
   265  
   266  	enc := json.NewEncoder(f)
   267  	encErr := enc.Encode(&fileEntry{
   268  		Value: entry.Value,
   269  	})
   270  	f.Close()
   271  	if encErr == nil {
   272  		err = os.Rename(f.Name(), fullPath)
   273  		if err != nil {
   274  			return err
   275  		}
   276  		return nil
   277  	}
   278  
   279  	// Everything below is best-effort and will result in encErr being returned
   280  
   281  	// See if we ended up with a zero-byte file and if so delete it, might be a
   282  	// case of disk being full but the file info is in metadata that is
   283  	// reserved.
   284  	fi, err := os.Stat(f.Name())
   285  	if err != nil {
   286  		return encErr
   287  	}
   288  	if fi == nil {
   289  		return encErr
   290  	}
   291  	if fi.Size() == 0 {
   292  		os.Remove(f.Name())
   293  	}
   294  	return encErr
   295  }
   296  
   297  func (b *FileBackend) List(ctx context.Context, prefix string) ([]string, error) {
   298  	b.permitPool.Acquire()
   299  	defer b.permitPool.Release()
   300  
   301  	b.RLock()
   302  	defer b.RUnlock()
   303  
   304  	return b.ListInternal(ctx, prefix)
   305  }
   306  
   307  func (b *FileBackend) ListInternal(ctx context.Context, prefix string) ([]string, error) {
   308  	if err := b.validatePath(prefix); err != nil {
   309  		return nil, err
   310  	}
   311  
   312  	path := b.path
   313  	if prefix != "" {
   314  		path = filepath.Join(path, prefix)
   315  	}
   316  
   317  	// Read the directory contents
   318  	f, err := os.Open(path)
   319  	if f != nil {
   320  		defer f.Close()
   321  	}
   322  	if err != nil {
   323  		if os.IsNotExist(err) {
   324  			return nil, nil
   325  		}
   326  
   327  		return nil, err
   328  	}
   329  
   330  	names, err := f.Readdirnames(-1)
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  
   335  	for i, name := range names {
   336  		fi, err := os.Stat(filepath.Join(path, name))
   337  		if err != nil {
   338  			return nil, err
   339  		}
   340  		if fi.IsDir() {
   341  			names[i] = name + "/"
   342  		} else {
   343  			if name[0] == '_' {
   344  				names[i] = name[1:]
   345  			}
   346  		}
   347  	}
   348  
   349  	select {
   350  	case <-ctx.Done():
   351  		return nil, ctx.Err()
   352  	default:
   353  	}
   354  
   355  	if len(names) > 0 {
   356  		sort.Strings(names)
   357  	}
   358  
   359  	return names, nil
   360  }
   361  
   362  func (b *FileBackend) expandPath(k string) (string, string) {
   363  	path := filepath.Join(b.path, k)
   364  	key := filepath.Base(path)
   365  	path = filepath.Dir(path)
   366  	return path, "_" + key
   367  }
   368  
   369  func (b *FileBackend) validatePath(path string) error {
   370  	switch {
   371  	case strings.Contains(path, ".."):
   372  		return consts.ErrPathContainsParentReferences
   373  	}
   374  
   375  	return nil
   376  }
   377  
   378  func (b *TransactionalFileBackend) Transaction(ctx context.Context, txns []*physical.TxnEntry) error {
   379  	b.permitPool.Acquire()
   380  	defer b.permitPool.Release()
   381  
   382  	b.Lock()
   383  	defer b.Unlock()
   384  
   385  	return physical.GenericTransactionHandler(ctx, b, txns)
   386  }