1// Package fdb allows for the use of a filesystem directory as a simple 2// database on UNIX-like systems. 3package fdb 4 5import ( 6 "fmt" 7 deos "github.com/hlandau/goutils/os" 8 "github.com/hlandau/xlog" 9 "gopkg.in/hlandau/svcutils.v1/passwd" 10 "io/ioutil" 11 "os" 12 "path/filepath" 13 "strings" 14) 15 16var log, Log = xlog.New("fdb") 17 18// FDB instance. 19type DB struct { 20 cfg Config 21 path string 22 extantDirs map[string]struct{} 23 effectivePermissions []Permission 24} 25 26// FDB configuration. 27type Config struct { 28 Path string 29 Permissions []Permission 30 PermissionsPath string // If not "", allow permissions to be overriden from this file. 31} 32 33// Expresses the permission policy for a given path. The first match is used. 34type Permission struct { 35 // The path to which the permission applies. May contain wildcards and must 36 // match a collection path, not an object path. e.g. 37 // accounts/*/* 38 // tmp 39 // The directory will receive the DirMode and any objects inside will receive 40 // the FileMode. Since all new files are initially created in tmp, it is 41 // essential that tp have permissions specified which are strictly as strict 42 // or stricter than the permissions of the strictest collection. 43 // The root collection matches the path ".". 44 Path string 45 FileMode os.FileMode 46 DirMode os.FileMode 47 48 UID string // if not "", user/UID to enforce 49 GID string // if not "", group/GID to enforce 50} 51 52// Merge b into a. 53func mergePermissions(a, b []Permission) ([]Permission, error) { 54 var r []Permission 55 r = append(r, a...) 56 57 am := map[string]int{} 58 for i := range a { 59 am[a[i].Path] = i 60 } 61 62 for i := range b { 63 ai, ok := am[b[i].Path] 64 if ok { 65 r[ai] = b[i] 66 } else { 67 r = append(r, b[i]) 68 } 69 } 70 71 return r, nil 72} 73 74// Return a copy of a but without any permissions with paths in pathsToErase. 75func erasePermissionsByPath(a []Permission, pathsToErase map[string]struct{}) []Permission { 76 var r []Permission 77 for i := range a { 78 _, erase := pathsToErase[a[i].Path] 79 if !erase { 80 r = append(r, a[i]) 81 } 82 } 83 return r 84} 85 86// Open a fdb database or create a new database. 87func Open(cfg Config) (*DB, error) { 88 path, err := filepath.Abs(cfg.Path) 89 if err != nil { 90 return nil, err 91 } 92 93 db := &DB{ 94 cfg: cfg, 95 path: path, 96 extantDirs: map[string]struct{}{}, 97 } 98 99 err = db.clearTmp() 100 if err != nil { 101 return nil, err 102 } 103 104 err = db.Verify() 105 if err != nil { 106 return nil, err 107 } 108 109 return db, nil 110} 111 112// Closes the database. 113func (db *DB) Close() error { 114 return nil 115} 116 117// Integrity checks 118 119// Verify the consistency and validity of the database (e.g. link targets). 120// This is called automatically when opening the database so you shouldn't need 121// to call it. 122func (db *DB) Verify() error { 123 err := db.loadPermissions() 124 if err != nil { 125 return err 126 } 127 128 err = db.createDirs() 129 if err != nil { 130 return err 131 } 132 133 // don't do this until now as EvalSymlinks requires the directory to exist 134 db.path, err = filepath.EvalSymlinks(db.path) 135 if err != nil { 136 return err 137 } 138 139 if len(db.cfg.Permissions) > 0 { 140 err = db.conformPermissions() 141 if err != nil { 142 return err 143 } 144 } 145 146 return nil 147} 148 149func (db *DB) createDirs() error { 150 for _, p := range db.effectivePermissions { 151 if strings.IndexByte(p.Path, '*') >= 0 { 152 continue 153 } 154 155 uid, gid, err := resolveUIDGID(&p) 156 if err != nil { 157 return err 158 } 159 160 err = mkdirAllWithOwner(filepath.Join(db.path, p.Path), p.DirMode, uid, gid) 161 if err != nil { 162 return err 163 } 164 } 165 166 return nil 167} 168 169func (db *DB) loadPermissions() error { 170 db.effectivePermissions = db.cfg.Permissions 171 172 if db.cfg.PermissionsPath == "" { 173 return nil 174 } 175 176 r, err := db.Collection("").Open(db.cfg.PermissionsPath) 177 if err != nil { 178 if os.IsNotExist(err) { 179 return nil 180 } 181 182 return err 183 } 184 defer r.Close() 185 186 ps, erasePaths, err := parsePermissions(r) 187 if err != nil { 188 return fmt.Errorf("badly formatted permissions file: %v", err) 189 } 190 191 mergedPermissions, err := mergePermissions(db.cfg.Permissions, ps) 192 if err != nil { 193 return err 194 } 195 196 mergedPermissions = erasePermissionsByPath(mergedPermissions, erasePaths) 197 db.effectivePermissions = mergedPermissions 198 return nil 199} 200 201// Returns the UID and GID to enforce. If the UID or GID is -1, it is not 202// to be enforced. Neither or both or either of the UID or GID may be -1. 203func resolveUIDGID(p *Permission) (uid, gid int, err error) { 204 if p.UID == "$r" { 205 uid = os.Getuid() 206 } else if p.UID != "" { 207 uid, err = passwd.ParseUID(p.UID) 208 if err != nil { 209 return 210 } 211 } else { 212 uid = -1 213 } 214 215 if p.GID == "$r" { 216 gid = os.Getgid() 217 } else if p.GID != "" { 218 gid, err = passwd.ParseGID(p.GID) 219 if err != nil { 220 return 221 } 222 } else { 223 gid = -1 224 } 225 226 return 227} 228 229func isHiddenRelPath(rp string) bool { 230 return strings.HasPrefix(rp, ".") || strings.Index(rp, "/.") >= 0 231} 232 233// Change all directory permissions to be correct. 234func (db *DB) conformPermissions() error { 235 err := filepath.Walk(db.path, func(path string, info os.FileInfo, err error) error { 236 if err != nil { 237 return err 238 } 239 240 rpath, err := filepath.Rel(db.path, path) 241 if err != nil { 242 return err 243 } 244 245 // Some people want to store hidden files/directories inside the ACME state 246 // directory without permissions enforcement. Since it's reasonable to 247 // assume I'll never want to amend the ACME-SSS specification to specify 248 // top-level directories inside a state directory, this shouldn't have any 249 // security implications. Symlinks inside the state directory (whose state 250 // directory paths themselves don't contain "/." and are thus ignored) 251 // cannot reference ignored paths, as their permissions are not managed and 252 // this is not safe. This is enforced elsewhere. 253 if isHiddenRelPath(rpath) { 254 return nil 255 } 256 257 mode := info.Mode() 258 switch mode & os.ModeType { 259 case 0: 260 case os.ModeDir: 261 db.extantDirs[rpath] = struct{}{} 262 case os.ModeSymlink: 263 l, err := os.Readlink(path) 264 if err != nil { 265 return err 266 } 267 268 if filepath.IsAbs(l) { 269 return fmt.Errorf("database symlinks must not have absolute targets: %v: %v", path, l) 270 } 271 272 ll := filepath.Join(filepath.Dir(path), l) 273 ll, err = filepath.Abs(ll) 274 if err != nil { 275 return err 276 } 277 278 ok, err := pathIsWithin(ll, db.path) 279 if err != nil { 280 return err 281 } 282 if !ok { 283 return fmt.Errorf("database symlinks must point to within the database directory: %v: %v", path, ll) 284 } 285 286 rll, err := filepath.Rel(db.path, ll) 287 if err != nil { 288 return err 289 } 290 if isHiddenRelPath(rll) { 291 return fmt.Errorf("database symlinks cannot target hidden files within the database directory: %v: %v", path, ll) 292 } 293 294 _, err = os.Stat(ll) 295 if os.IsNotExist(err) { 296 log.Warnf("broken symlink, removing: %v -> %v", path, l) 297 err := os.Remove(path) 298 if err != nil { 299 return err 300 } 301 } else if err != nil { 302 log.Errore(err, "stat symlink") 303 return err 304 } 305 306 default: 307 return fmt.Errorf("unexpected file type in state directory: %s", mode) 308 } 309 310 perm := db.longestMatching(rpath) 311 if perm == nil { 312 log.Warnf("object without any permissions specified: %v", rpath) 313 } else { 314 correctPerm := perm.FileMode 315 if (mode & os.ModeType) == os.ModeDir { 316 correctPerm = perm.DirMode 317 } 318 319 if (mode & os.ModeType) != os.ModeSymlink { 320 if fperm := mode.Perm(); fperm != correctPerm { 321 log.Warnf("%#v has wrong mode %v, changing to %v", rpath, fperm, correctPerm) 322 323 err := os.Chmod(path, correctPerm) 324 if err != nil { 325 return err 326 } 327 } 328 } 329 330 correctUID, correctGID, err := resolveUIDGID(perm) 331 if err != nil { 332 return err 333 } 334 335 if correctUID >= 0 || correctGID >= 0 { 336 curUID, err := deos.GetFileUID(info) 337 if err != nil { 338 return err 339 } 340 341 curGID, err := deos.GetFileGID(info) 342 if err != nil { 343 return err 344 } 345 346 if correctUID < 0 { 347 correctUID = curUID 348 } 349 350 if correctGID < 0 { 351 correctGID = curGID 352 } 353 354 if curUID != correctUID || curGID != correctGID { 355 log.Warnf("%#v has wrong UID/GID %v/%v, changing to %v/%v", rpath, curUID, curGID, correctUID, correctGID) 356 357 err := os.Lchown(path, correctUID, correctGID) 358 // Can't chown if not root so be a bit forgiving, but always moan 359 log.Errore(err, "could not lchown file ", rpath) 360 } 361 } 362 } 363 364 return nil 365 }) 366 if err != nil { 367 return err 368 } 369 370 return nil 371} 372 373func (db *DB) longestMatching(path string) *Permission { 374 pattern := "" 375 var perm *Permission 376 377 if filepath.IsAbs(path) { 378 panic("do not call longestMatching with an absolute path") 379 } 380 381 for { 382 for _, p := range db.effectivePermissions { 383 m, err := filepath.Match(p.Path, path) 384 if err != nil { 385 return nil 386 } 387 388 if m && len(p.Path) > len(pattern) { 389 pattern = p.Path 390 p2 := p 391 perm = &p2 392 } 393 } 394 395 if perm != nil { 396 return perm 397 } 398 399 if path == "." { 400 break 401 } 402 403 path = filepath.Join(path, "..") 404 } 405 406 return nil 407} 408 409func pathIsWithin(subject, root string) (bool, error) { 410 return strings.HasPrefix(subject, ensureSeparator(root)), nil 411} 412 413func ensureSeparator(p string) string { 414 if !strings.HasSuffix(p, string(filepath.Separator)) { 415 return p + string(filepath.Separator) 416 } 417 418 return p 419} 420 421func (db *DB) clearTmp() error { 422 ms, err := filepath.Glob(filepath.Join(db.path, "tmp", "*")) 423 if err != nil { 424 return err 425 } 426 427 for _, m := range ms { 428 err := os.RemoveAll(m) 429 if err != nil { 430 return err 431 } 432 } 433 434 return nil 435} 436 437func (db *DB) ensurePath(path string) error { 438 _, ok := db.extantDirs[path] 439 if ok { 440 return nil 441 } 442 443 mode := os.FileMode(0755) 444 perm := db.longestMatching(path) 445 if perm != nil { 446 mode = perm.DirMode 447 } 448 449 uid, gid, err := resolveUIDGID(perm) 450 if err != nil { 451 return err 452 } 453 454 err = mkdirAllWithOwner(filepath.Join(db.path, path), mode, uid, gid) 455 if err != nil { 456 return err 457 } 458 459 db.extantDirs[path] = struct{}{} 460 return nil 461} 462 463// Database Access 464 465// Collection represents a collection of objects in an fdb database. More 466// accurately, it is a contextual point in the database hierarchy which object 467// references are interpreted relative to. 468type Collection struct { 469 db *DB 470 name string 471 ensuredPath bool 472} 473 474// Obtain a collection. The collection will be created automatically if it does 475// not already exist. Guaranteed to return a non-nil value. 476func (db *DB) Collection(collectionName string) *Collection { 477 return &Collection{ 478 db: db, 479 name: collectionName, 480 } 481} 482 483// Obtain a collection underneath the given collection. The collection will be 484// created automatically if it does not already exist. Guaranteed to return a 485// non-nil value. 486func (c *Collection) Collection(name string) *Collection { 487 return &Collection{ 488 db: c.db, 489 name: filepath.Join(c.name, name), 490 } 491} 492 493func (c *Collection) ensurePath() error { 494 if c.ensuredPath { 495 return nil 496 } 497 498 err := c.db.ensurePath(c.name) 499 if err != nil { 500 return err 501 } 502 503 c.ensuredPath = true 504 return nil 505} 506 507// Stream for reading an object from the database. 508type ReadStream interface { 509 Close() error 510 Read([]byte) (int, error) 511 Seek(int64, int) (int64, error) 512} 513 514// Stream for writing an object to the database. Changes do not take effect 515// until the stream is closed, at which case they are applied atomically. 516type WriteStream interface { 517 ReadStream 518 Write([]byte) (int, error) 519 520 // Abort writing of the file. The file is not changed. Calling Close or 521 // CloseAbort after calling this has no effect. 522 CloseAbort() error 523} 524 525// A link points to a given name in a given collection. The database ensures 526// referential integrity. 527type Link struct { 528 Target string // "collection1/subcollection/etc/objectName" 529} 530 531// Returns the database from which the collection was created. 532func (c *Collection) DB() *DB { 533 return c.db 534} 535 536// Returns the collection path. 537func (c *Collection) Name() string { 538 return c.name 539} 540 541// Returns the OS path to the file with the given name inside the collection. 542// If name is "", returns the OS path to the collection. 543func (c *Collection) OSPath(name string) string { 544 c.ensurePath() // ignore error 545 546 return filepath.Join(c.db.path, c.name, name) 547} 548 549// Atomically delete an existing object or link or subcollection in the given 550// collection with the given name. Returns nil if the object does not exist. 551func (c *Collection) Delete(name string) error { 552 return os.RemoveAll(filepath.Join(c.db.path, c.name, name)) 553} 554 555// Returned when calling Open() on a symlink. (To open symlinks, use Openl.) 556var ErrIsLink = fmt.Errorf("cannot open symlink") 557 558// Open an existing object in the given collection with the given name. The 559// object is read-only. Returns an error if the object does not exist or 560// is a link. 561func (c *Collection) Open(name string) (ReadStream, error) { 562 return c.open(name, false) 563} 564 565// Like Open(), but follows links automatically. 566func (c *Collection) Openl(name string) (ReadStream, error) { 567 return c.open(name, true) 568} 569 570func (c *Collection) open(name string, allowSymlinks bool) (ReadStream, error) { 571 fi, err := os.Lstat(filepath.Join(c.db.path, c.name, name)) 572again: 573 if err != nil { 574 return nil, err 575 } 576 577 m := fi.Mode() 578 switch m & os.ModeType { 579 case 0: 580 case os.ModeSymlink: 581 if !allowSymlinks { 582 return nil, ErrIsLink 583 } 584 585 fi, err = os.Stat(filepath.Join(c.db.path, c.name, name)) 586 goto again 587 588 case os.ModeDir: 589 return nil, fmt.Errorf("cannot open a collection") 590 default: 591 return nil, fmt.Errorf("unknown file type") 592 } 593 594 f, err := os.Open(filepath.Join(c.db.path, c.name, name)) 595 if err != nil { 596 return nil, err 597 } 598 599 return f, nil 600} 601 602// Create a new object in the given collection with the given name. If the 603// object already exists, it will be overwritten atomically. Changes only take 604// effect once the stream is closed. 605func (c *Collection) Create(name string) (WriteStream, error) { 606 err := c.ensurePath() 607 if err != nil { 608 return nil, err 609 } 610 611 f, err := ioutil.TempFile(filepath.Join(c.db.path, "tmp"), "tmp.") 612 if err != nil { 613 return nil, err 614 } 615 616 return &closeWrapper{ 617 db: c.db, 618 f: f, 619 finalName: filepath.Join(c.db.path, c.name, name), 620 finalNameRel: filepath.Join(c.name, name), 621 writing: true, 622 }, nil 623} 624 625type closeWrapper struct { 626 db *DB 627 f *os.File 628 finalName string 629 finalNameRel string 630 closed bool 631 writing bool 632} 633 634func (cw *closeWrapper) Close() error { 635 if cw.closed { 636 return nil 637 } 638 639 n := cw.f.Name() 640 641 err := cw.f.Close() 642 if err != nil { 643 return err 644 } 645 646 err = os.Rename(n, cw.finalName) 647 if err != nil { 648 return err 649 } 650 651 if cw.writing { 652 err = cw.db.enforcePermissionsOnFile(cw.finalNameRel, cw.finalNameRel, false) 653 if err != nil { 654 return err 655 } 656 } 657 658 cw.closed = true 659 return nil 660} 661 662func (db *DB) enforcePermissionsOnFile(rpath, rpathFinal string, symlink bool) error { 663 p := db.longestMatching(rpathFinal) 664 if p == nil { 665 return nil 666 } 667 668 fpath := filepath.Join(db.path, rpath) 669 670 // TempFile creates files with mode 0600, so it's OK to chmod/chown it here, race-wise. 671 correctUID, correctGID, err := resolveUIDGID(p) 672 if err != nil { 673 return err 674 } 675 676 curUID, curGID := os.Getuid(), os.Getgid() 677 if correctUID < 0 { 678 correctUID = curUID 679 } 680 if correctGID < 0 { 681 correctGID = curGID 682 } 683 684 log.Debugf("enforce permissions: %s %d/%d %d/%d", rpath, curUID, curGID, correctUID, correctGID) 685 if correctUID != curUID || correctGID != curGID { 686 err := os.Lchown(fpath, correctUID, correctGID) 687 // failure is nonfatal, may not be root 688 log.Errore(err, "could not set correct owner for file", fpath) 689 } 690 691 if !symlink { 692 err = os.Chmod(fpath, p.FileMode) 693 if err != nil { 694 return err 695 } 696 } 697 698 return nil 699} 700 701func (cw *closeWrapper) CloseAbort() error { 702 if cw.closed { 703 return nil 704 } 705 706 err := cw.f.Close() 707 if err != nil { 708 return err 709 } 710 711 err = os.Remove(cw.f.Name()) 712 if err != nil { 713 return err 714 } 715 716 cw.closed = true 717 return nil 718} 719 720func (cw *closeWrapper) Read(b []byte) (int, error) { 721 return cw.f.Read(b) 722} 723 724func (cw *closeWrapper) Write(b []byte) (int, error) { 725 return cw.f.Write(b) 726} 727 728func (cw *closeWrapper) Seek(p int64, w int) (int64, error) { 729 return cw.f.Seek(p, w) 730} 731 732// Read a link in the given collection with the given name. Returns an error 733// if the object does not exist or is not a link. 734func (c *Collection) ReadLink(name string) (Link, error) { 735 fpath := filepath.Join(c.db.path, c.name, name) 736 737 l, err := os.Readlink(fpath) 738 if err != nil { 739 return Link{}, err 740 } 741 742 flink := filepath.Join(filepath.Dir(fpath), l) 743 744 lr, err := filepath.Rel(c.db.path, flink) 745 if err != nil { 746 return Link{}, err 747 } 748 749 return Link{Target: lr}, nil 750} 751 752// Write a link in the given collection with the given name. Any existing 753// object or link is overwritten atomically. 754func (c *Collection) WriteLink(name string, target Link) error { 755 err := c.ensurePath() 756 if err != nil { 757 return err 758 } 759 760 from := filepath.Join(c.db.path, c.name, name) 761 to := filepath.Join(c.db.path, target.Target) 762 toRel, err := filepath.Rel(filepath.Dir(from), to) 763 if err != nil { 764 return err 765 } 766 767 tmpName, err := tempSymlink(toRel, filepath.Join(c.db.path, "tmp")) 768 if err != nil { 769 return err 770 } 771 772 err = c.db.enforcePermissionsOnFile(filepath.Join("tmp", filepath.Base(tmpName)), 773 filepath.Join(c.name, name), true) 774 if err != nil { 775 return err 776 } 777 778 return os.Rename(tmpName, from) 779} 780 781func (c *Collection) ListAll() ([]string, error) { 782 ms, err := filepath.Glob(filepath.Join(c.db.path, c.name, "*")) 783 if err != nil { 784 return nil, err 785 } 786 787 var objs []string 788 for _, m := range ms { 789 objs = append(objs, filepath.Base(m)) 790 } 791 792 return objs, nil 793} 794 795// List the objects and collections in the collection. Filenames beginning with 796// '.' are hidden. 797func (c *Collection) List() ([]string, error) { 798 s, err := c.ListAll() 799 if err != nil { 800 return nil, err 801 } 802 803 s2 := make([]string, 0, len(s)) 804 for _, x := range s { 805 if strings.HasPrefix(x, ".") { 806 continue 807 } 808 809 s2 = append(s2, x) 810 } 811 812 return s2, nil 813} 814