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