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 }