github.com/safing/portbase@v0.19.5/database/interface.go (about)

     1  package database
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/bluele/gcache"
    11  	"github.com/tevino/abool"
    12  
    13  	"github.com/safing/portbase/database/accessor"
    14  	"github.com/safing/portbase/database/iterator"
    15  	"github.com/safing/portbase/database/query"
    16  	"github.com/safing/portbase/database/record"
    17  )
    18  
    19  const (
    20  	getDBFromKey = ""
    21  )
    22  
    23  // Interface provides a method to access the database with attached options.
    24  type Interface struct {
    25  	options *Options
    26  	cache   gcache.Cache
    27  
    28  	writeCache        map[string]record.Record
    29  	writeCacheLock    sync.Mutex
    30  	triggerCacheWrite chan struct{}
    31  }
    32  
    33  // Options holds options that may be set for an Interface instance.
    34  type Options struct {
    35  	// Local specifies if the interface is used by an actor on the local device.
    36  	// Setting both the Local and Internal flags will bring performance
    37  	// improvements because less checks are needed.
    38  	Local bool
    39  
    40  	// Internal specifies if the interface is used by an actor within the
    41  	// software. Setting both the Local and Internal flags will bring performance
    42  	// improvements because less checks are needed.
    43  	Internal bool
    44  
    45  	// AlwaysMakeSecret will have the interface mark all saved records as secret.
    46  	// This means that they will be only accessible by an internal interface.
    47  	AlwaysMakeSecret bool
    48  
    49  	// AlwaysMakeCrownjewel will have the interface mark all saved records as
    50  	// crown jewels. This means that they will be only accessible by a local
    51  	// interface.
    52  	AlwaysMakeCrownjewel bool
    53  
    54  	// AlwaysSetRelativateExpiry will have the interface set a relative expiry,
    55  	// based on the current time, on all saved records.
    56  	AlwaysSetRelativateExpiry int64
    57  
    58  	// AlwaysSetAbsoluteExpiry will have the interface set an absolute expiry on
    59  	// all saved records.
    60  	AlwaysSetAbsoluteExpiry int64
    61  
    62  	// CacheSize defines that a cache should be used for this interface and
    63  	// defines it's size.
    64  	// Caching comes with an important caveat: If database records are changed
    65  	// from another interface, the cache will not be invalidated for these
    66  	// records. It will therefore serve outdated data until that record is
    67  	// evicted from the cache.
    68  	CacheSize int
    69  
    70  	// DelayCachedWrites defines a database name for which cache writes should
    71  	// be cached and batched. The database backend must support the Batcher
    72  	// interface. This option is only valid if used with a cache.
    73  	// Additionally, this may only be used for internal and local interfaces.
    74  	// Please note that this means that other interfaces will not be able to
    75  	// guarantee to serve the latest record if records are written this way.
    76  	DelayCachedWrites string
    77  }
    78  
    79  // Apply applies options to the record metadata.
    80  func (o *Options) Apply(r record.Record) {
    81  	r.UpdateMeta()
    82  	if o.AlwaysMakeSecret {
    83  		r.Meta().MakeSecret()
    84  	}
    85  	if o.AlwaysMakeCrownjewel {
    86  		r.Meta().MakeCrownJewel()
    87  	}
    88  	if o.AlwaysSetAbsoluteExpiry > 0 {
    89  		r.Meta().SetAbsoluteExpiry(o.AlwaysSetAbsoluteExpiry)
    90  	} else if o.AlwaysSetRelativateExpiry > 0 {
    91  		r.Meta().SetRelativateExpiry(o.AlwaysSetRelativateExpiry)
    92  	}
    93  }
    94  
    95  // HasAllPermissions returns whether the options specify the highest possible
    96  // permissions for operations.
    97  func (o *Options) HasAllPermissions() bool {
    98  	return o.Local && o.Internal
    99  }
   100  
   101  // hasAccessPermission checks if the interface options permit access to the
   102  // given record, locking the record for accessing it's attributes.
   103  func (o *Options) hasAccessPermission(r record.Record) bool {
   104  	// Check if the options specify all permissions, which makes checking the
   105  	// record unnecessary.
   106  	if o.HasAllPermissions() {
   107  		return true
   108  	}
   109  
   110  	r.Lock()
   111  	defer r.Unlock()
   112  
   113  	// Check permissions against record.
   114  	return r.Meta().CheckPermission(o.Local, o.Internal)
   115  }
   116  
   117  // NewInterface returns a new Interface to the database.
   118  func NewInterface(opts *Options) *Interface {
   119  	if opts == nil {
   120  		opts = &Options{}
   121  	}
   122  
   123  	newIface := &Interface{
   124  		options: opts,
   125  	}
   126  	if opts.CacheSize > 0 {
   127  		cacheBuilder := gcache.New(opts.CacheSize).ARC()
   128  		if opts.DelayCachedWrites != "" {
   129  			cacheBuilder.EvictedFunc(newIface.cacheEvictHandler)
   130  			newIface.writeCache = make(map[string]record.Record, opts.CacheSize/2)
   131  			newIface.triggerCacheWrite = make(chan struct{})
   132  		}
   133  		newIface.cache = cacheBuilder.Build()
   134  	}
   135  	return newIface
   136  }
   137  
   138  // Exists return whether a record with the given key exists.
   139  func (i *Interface) Exists(key string) (bool, error) {
   140  	_, err := i.Get(key)
   141  	if err != nil {
   142  		switch {
   143  		case errors.Is(err, ErrNotFound):
   144  			return false, nil
   145  		case errors.Is(err, ErrPermissionDenied):
   146  			return true, nil
   147  		default:
   148  			return false, err
   149  		}
   150  	}
   151  	return true, nil
   152  }
   153  
   154  // Get return the record with the given key.
   155  func (i *Interface) Get(key string) (record.Record, error) {
   156  	r, _, err := i.getRecord(getDBFromKey, key, false)
   157  	return r, err
   158  }
   159  
   160  func (i *Interface) getRecord(dbName string, dbKey string, mustBeWriteable bool) (r record.Record, db *Controller, err error) { //nolint:unparam
   161  	if dbName == "" {
   162  		dbName, dbKey = record.ParseKey(dbKey)
   163  	}
   164  
   165  	db, err = getController(dbName)
   166  	if err != nil {
   167  		return nil, nil, err
   168  	}
   169  
   170  	if mustBeWriteable && db.ReadOnly() {
   171  		return nil, db, ErrReadOnly
   172  	}
   173  
   174  	r = i.checkCache(dbName + ":" + dbKey)
   175  	if r != nil {
   176  		if !i.options.hasAccessPermission(r) {
   177  			return nil, db, ErrPermissionDenied
   178  		}
   179  		return r, db, nil
   180  	}
   181  
   182  	r, err = db.Get(dbKey)
   183  	if err != nil {
   184  		return nil, db, err
   185  	}
   186  
   187  	if !i.options.hasAccessPermission(r) {
   188  		return nil, db, ErrPermissionDenied
   189  	}
   190  
   191  	r.Lock()
   192  	ttl := r.Meta().GetRelativeExpiry()
   193  	r.Unlock()
   194  	i.updateCache(
   195  		r,
   196  		false, // writing
   197  		false, // remove
   198  		ttl,   // expiry
   199  	)
   200  
   201  	return r, db, nil
   202  }
   203  
   204  func (i *Interface) getMeta(dbName string, dbKey string, mustBeWriteable bool) (m *record.Meta, db *Controller, err error) { //nolint:unparam
   205  	if dbName == "" {
   206  		dbName, dbKey = record.ParseKey(dbKey)
   207  	}
   208  
   209  	db, err = getController(dbName)
   210  	if err != nil {
   211  		return nil, nil, err
   212  	}
   213  
   214  	if mustBeWriteable && db.ReadOnly() {
   215  		return nil, db, ErrReadOnly
   216  	}
   217  
   218  	r := i.checkCache(dbName + ":" + dbKey)
   219  	if r != nil {
   220  		if !i.options.hasAccessPermission(r) {
   221  			return nil, db, ErrPermissionDenied
   222  		}
   223  		return r.Meta(), db, nil
   224  	}
   225  
   226  	m, err = db.GetMeta(dbKey)
   227  	if err != nil {
   228  		return nil, db, err
   229  	}
   230  
   231  	if !m.CheckPermission(i.options.Local, i.options.Internal) {
   232  		return nil, db, ErrPermissionDenied
   233  	}
   234  
   235  	return m, db, nil
   236  }
   237  
   238  // InsertValue inserts a value into a record.
   239  func (i *Interface) InsertValue(key string, attribute string, value interface{}) error {
   240  	r, db, err := i.getRecord(getDBFromKey, key, true)
   241  	if err != nil {
   242  		return err
   243  	}
   244  
   245  	r.Lock()
   246  	defer r.Unlock()
   247  
   248  	var acc accessor.Accessor
   249  	if r.IsWrapped() {
   250  		wrapper, ok := r.(*record.Wrapper)
   251  		if !ok {
   252  			return errors.New("record is malformed (reports to be wrapped but is not of type *record.Wrapper)")
   253  		}
   254  		acc = accessor.NewJSONBytesAccessor(&wrapper.Data)
   255  	} else {
   256  		acc = accessor.NewStructAccessor(r)
   257  	}
   258  
   259  	err = acc.Set(attribute, value)
   260  	if err != nil {
   261  		return fmt.Errorf("failed to set value with %s: %w", acc.Type(), err)
   262  	}
   263  
   264  	i.options.Apply(r)
   265  	return db.Put(r)
   266  }
   267  
   268  // Put saves a record to the database.
   269  func (i *Interface) Put(r record.Record) (err error) {
   270  	// get record or only database
   271  	var db *Controller
   272  	if !i.options.HasAllPermissions() {
   273  		_, db, err = i.getMeta(r.DatabaseName(), r.DatabaseKey(), true)
   274  		if err != nil && !errors.Is(err, ErrNotFound) {
   275  			return err
   276  		}
   277  	} else {
   278  		db, err = getController(r.DatabaseName())
   279  		if err != nil {
   280  			return err
   281  		}
   282  	}
   283  
   284  	// Check if database is read only.
   285  	if db.ReadOnly() {
   286  		return ErrReadOnly
   287  	}
   288  
   289  	r.Lock()
   290  	i.options.Apply(r)
   291  	remove := r.Meta().IsDeleted()
   292  	ttl := r.Meta().GetRelativeExpiry()
   293  	r.Unlock()
   294  
   295  	// The record may not be locked when updating the cache.
   296  	written := i.updateCache(r, true, remove, ttl)
   297  	if written {
   298  		return nil
   299  	}
   300  
   301  	r.Lock()
   302  	defer r.Unlock()
   303  	return db.Put(r)
   304  }
   305  
   306  // PutNew saves a record to the database as a new record (ie. with new timestamps).
   307  func (i *Interface) PutNew(r record.Record) (err error) {
   308  	// get record or only database
   309  	var db *Controller
   310  	if !i.options.HasAllPermissions() {
   311  		_, db, err = i.getMeta(r.DatabaseName(), r.DatabaseKey(), true)
   312  		if err != nil && !errors.Is(err, ErrNotFound) {
   313  			return err
   314  		}
   315  	} else {
   316  		db, err = getController(r.DatabaseName())
   317  		if err != nil {
   318  			return err
   319  		}
   320  	}
   321  
   322  	// Check if database is read only.
   323  	if db.ReadOnly() {
   324  		return ErrReadOnly
   325  	}
   326  
   327  	r.Lock()
   328  	if r.Meta() != nil {
   329  		r.Meta().Reset()
   330  	}
   331  	i.options.Apply(r)
   332  	remove := r.Meta().IsDeleted()
   333  	ttl := r.Meta().GetRelativeExpiry()
   334  	r.Unlock()
   335  
   336  	// The record may not be locked when updating the cache.
   337  	written := i.updateCache(r, true, remove, ttl)
   338  	if written {
   339  		return nil
   340  	}
   341  
   342  	r.Lock()
   343  	defer r.Unlock()
   344  	return db.Put(r)
   345  }
   346  
   347  // PutMany stores many records in the database.
   348  // Warning: This is nearly a direct database access and omits many things:
   349  // - Record locking
   350  // - Hooks
   351  // - Subscriptions
   352  // - Caching
   353  // Use with care.
   354  func (i *Interface) PutMany(dbName string) (put func(record.Record) error) {
   355  	interfaceBatch := make(chan record.Record, 100)
   356  
   357  	// permission check
   358  	if !i.options.HasAllPermissions() {
   359  		return func(r record.Record) error {
   360  			return ErrPermissionDenied
   361  		}
   362  	}
   363  
   364  	// get database
   365  	db, err := getController(dbName)
   366  	if err != nil {
   367  		return func(r record.Record) error {
   368  			return err
   369  		}
   370  	}
   371  
   372  	// Check if database is read only.
   373  	if db.ReadOnly() {
   374  		return func(r record.Record) error {
   375  			return ErrReadOnly
   376  		}
   377  	}
   378  
   379  	// start database access
   380  	dbBatch, errs := db.PutMany()
   381  	finished := abool.New()
   382  	var internalErr error
   383  
   384  	// interface options proxy
   385  	go func() {
   386  		defer close(dbBatch) // signify that we are finished
   387  		for {
   388  			select {
   389  			case r := <-interfaceBatch:
   390  				// finished?
   391  				if r == nil {
   392  					return
   393  				}
   394  				// apply options
   395  				i.options.Apply(r)
   396  				// pass along
   397  				dbBatch <- r
   398  			case <-time.After(1 * time.Second):
   399  				// bail out
   400  				internalErr = errors.New("timeout: putmany unused for too long")
   401  				finished.Set()
   402  				return
   403  			}
   404  		}
   405  	}()
   406  
   407  	return func(r record.Record) error {
   408  		// finished?
   409  		if finished.IsSet() {
   410  			// check for internal error
   411  			if internalErr != nil {
   412  				return internalErr
   413  			}
   414  			// check for previous error
   415  			select {
   416  			case err := <-errs:
   417  				return err
   418  			default:
   419  				return errors.New("batch is closed")
   420  			}
   421  		}
   422  
   423  		// finish?
   424  		if r == nil {
   425  			finished.Set()
   426  			interfaceBatch <- nil // signify that we are finished
   427  			// do not close, as this fn could be called again with nil.
   428  			return <-errs
   429  		}
   430  
   431  		// check record scope
   432  		if r.DatabaseName() != dbName {
   433  			return errors.New("record out of database scope")
   434  		}
   435  
   436  		// submit
   437  		select {
   438  		case interfaceBatch <- r:
   439  			return nil
   440  		case err := <-errs:
   441  			return err
   442  		}
   443  	}
   444  }
   445  
   446  // SetAbsoluteExpiry sets an absolute record expiry.
   447  func (i *Interface) SetAbsoluteExpiry(key string, time int64) error {
   448  	r, db, err := i.getRecord(getDBFromKey, key, true)
   449  	if err != nil {
   450  		return err
   451  	}
   452  
   453  	r.Lock()
   454  	defer r.Unlock()
   455  
   456  	i.options.Apply(r)
   457  	r.Meta().SetAbsoluteExpiry(time)
   458  	return db.Put(r)
   459  }
   460  
   461  // SetRelativateExpiry sets a relative (self-updating) record expiry.
   462  func (i *Interface) SetRelativateExpiry(key string, duration int64) error {
   463  	r, db, err := i.getRecord(getDBFromKey, key, true)
   464  	if err != nil {
   465  		return err
   466  	}
   467  
   468  	r.Lock()
   469  	defer r.Unlock()
   470  
   471  	i.options.Apply(r)
   472  	r.Meta().SetRelativateExpiry(duration)
   473  	return db.Put(r)
   474  }
   475  
   476  // MakeSecret marks the record as a secret, meaning interfacing processes, such as an UI, are denied access to the record.
   477  func (i *Interface) MakeSecret(key string) error {
   478  	r, db, err := i.getRecord(getDBFromKey, key, true)
   479  	if err != nil {
   480  		return err
   481  	}
   482  
   483  	r.Lock()
   484  	defer r.Unlock()
   485  
   486  	i.options.Apply(r)
   487  	r.Meta().MakeSecret()
   488  	return db.Put(r)
   489  }
   490  
   491  // MakeCrownJewel marks a record as a crown jewel, meaning it will only be accessible locally.
   492  func (i *Interface) MakeCrownJewel(key string) error {
   493  	r, db, err := i.getRecord(getDBFromKey, key, true)
   494  	if err != nil {
   495  		return err
   496  	}
   497  
   498  	r.Lock()
   499  	defer r.Unlock()
   500  
   501  	i.options.Apply(r)
   502  	r.Meta().MakeCrownJewel()
   503  	return db.Put(r)
   504  }
   505  
   506  // Delete deletes a record from the database.
   507  func (i *Interface) Delete(key string) error {
   508  	r, db, err := i.getRecord(getDBFromKey, key, true)
   509  	if err != nil {
   510  		return err
   511  	}
   512  
   513  	// Check if database is read only.
   514  	if db.ReadOnly() {
   515  		return ErrReadOnly
   516  	}
   517  
   518  	i.options.Apply(r)
   519  	r.Meta().Delete()
   520  	return db.Put(r)
   521  }
   522  
   523  // Query executes the given query on the database.
   524  // Will not see data that is in the write cache, waiting to be written.
   525  // Use with care with caching.
   526  func (i *Interface) Query(q *query.Query) (*iterator.Iterator, error) {
   527  	_, err := q.Check()
   528  	if err != nil {
   529  		return nil, err
   530  	}
   531  
   532  	db, err := getController(q.DatabaseName())
   533  	if err != nil {
   534  		return nil, err
   535  	}
   536  
   537  	// TODO: Finish caching system integration.
   538  	// Flush the cache before we query the database.
   539  	// i.FlushCache()
   540  
   541  	return db.Query(q, i.options.Local, i.options.Internal)
   542  }
   543  
   544  // Purge deletes all records that match the given query. It returns the number
   545  // of successful deletes and an error.
   546  func (i *Interface) Purge(ctx context.Context, q *query.Query) (int, error) {
   547  	_, err := q.Check()
   548  	if err != nil {
   549  		return 0, err
   550  	}
   551  
   552  	db, err := getController(q.DatabaseName())
   553  	if err != nil {
   554  		return 0, err
   555  	}
   556  
   557  	// Check if database is read only before we add to the cache.
   558  	if db.ReadOnly() {
   559  		return 0, ErrReadOnly
   560  	}
   561  
   562  	return db.Purge(ctx, q, i.options.Local, i.options.Internal)
   563  }
   564  
   565  // Subscribe subscribes to updates matching the given query.
   566  func (i *Interface) Subscribe(q *query.Query) (*Subscription, error) {
   567  	_, err := q.Check()
   568  	if err != nil {
   569  		return nil, err
   570  	}
   571  
   572  	c, err := getController(q.DatabaseName())
   573  	if err != nil {
   574  		return nil, err
   575  	}
   576  
   577  	sub := &Subscription{
   578  		q:        q,
   579  		local:    i.options.Local,
   580  		internal: i.options.Internal,
   581  		Feed:     make(chan record.Record, 1000),
   582  	}
   583  	c.addSubscription(sub)
   584  	return sub, nil
   585  }