blob: b7f88aa07406ee284d8b3b91e218341af863556f [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package server
import (
"bytes"
"v.io/v23/context"
"v.io/v23/naming"
"v.io/v23/rpc"
"v.io/v23/security/access"
wire "v.io/v23/services/syncbase"
"v.io/v23/services/watch"
pubutil "v.io/v23/syncbase/util"
"v.io/v23/verror"
"v.io/v23/vom"
"v.io/x/ref/services/syncbase/common"
"v.io/x/ref/services/syncbase/server/filter"
"v.io/x/ref/services/syncbase/server/interfaces"
"v.io/x/ref/services/syncbase/store"
"v.io/x/ref/services/syncbase/store/watchable"
)
// GetResumeMarker implements the wire.DatabaseWatcher interface.
func (d *database) GetResumeMarker(ctx *context.T, call rpc.ServerCall, bh wire.BatchHandle) (watch.ResumeMarker, error) {
allowGetResumeMarker := wire.AllDatabaseTags
if !d.exists {
return nil, verror.New(verror.ErrNoExist, ctx, d.id)
}
var res watch.ResumeMarker
impl := func(sntx store.SnapshotOrTransaction) (err error) {
// Check permissions on Database.
if _, err := common.GetPermsWithAuth(ctx, call, d, allowGetResumeMarker, sntx); err != nil {
return err
}
res, err = watchable.GetResumeMarker(sntx)
return err
}
if err := d.runWithExistingBatchOrNewSnapshot(ctx, bh, impl); err != nil {
return nil, err
}
return res, nil
}
// WatchGlob implements the wire.DatabaseWatcher interface.
func (d *database) WatchGlob(ctx *context.T, call watch.GlobWatcherWatchGlobServerCall, req watch.GlobRequest) error {
// Database permissions checked in d.watchWithFilter and d.processLogBatch.
sender := &watchBatchSender{
send: call.SendStream().Send,
}
gf, err := filter.NewGlobFilter(req.Pattern)
if err != nil {
return verror.New(verror.ErrBadArg, ctx, err)
}
return d.watchWithFilter(ctx, call, sender, req.ResumeMarker, gf)
}
// WatchPatterns implements the wire.DatabaseWatcher interface.
func (d *database) WatchPatterns(ctx *context.T, call wire.DatabaseWatcherWatchPatternsServerCall, resumeMarker watch.ResumeMarker, patterns []wire.CollectionRowPattern) error {
// Database permissions checked in d.watchWithFilter and d.processLogBatch.
sender := &watchBatchSender{
send: call.SendStream().Send,
}
mpf, err := filter.NewMultiPatternFilter(patterns)
if err != nil {
return verror.New(verror.ErrBadArg, ctx, err)
}
return d.watchWithFilter(ctx, call, sender, resumeMarker, mpf)
}
// watchWithFilter sends the initial state (if necessary) and watch events,
// filtered using watchFilter, to the caller using sender.
func (d *database) watchWithFilter(ctx *context.T, call rpc.ServerCall, sender *watchBatchSender, resumeMarker watch.ResumeMarker, watchFilter filter.CollectionRowFilter) error {
allowWatchDbStart := []access.Tag{access.Read}
if !d.exists {
return verror.New(verror.ErrNoExist, ctx, d.id)
}
initImpl := func(sntx store.SnapshotOrTransaction) error {
// Check permissions on Database.
if _, err := common.GetPermsWithAuth(ctx, call, d, allowWatchDbStart, sntx); err != nil {
return err
}
needInitialState := len(resumeMarker) == 0
needResumeMarker := needInitialState || bytes.Equal(resumeMarker, []byte("now"))
// Get the resume marker if necessary.
if needResumeMarker {
var err error
if resumeMarker, err = watchable.GetResumeMarker(sntx); err != nil {
return err
}
}
// Send the root update to notify the client that watch has started.
rootChangeState := watch.InitialStateSkipped
if needInitialState {
rootChangeState = watch.Exists
}
if err := sender.addChange(
"",
rootChangeState,
&wire.StoreChange{
FromSync: false,
}); err != nil {
return err
}
// Send initial state if necessary.
if needInitialState {
if err := d.scanInitialState(ctx, call, sender, sntx, watchFilter); err != nil {
return err
}
}
// Finalize initial state or root update batch.
return sender.finishBatch(resumeMarker)
}
if err := store.RunWithSnapshot(d.st, initImpl); err != nil {
return err
}
return d.watchUpdates(ctx, call, sender, resumeMarker, watchFilter)
}
// scanInitialState sends the initial state of all matching and accessible
// collections and rows in the database. Checks access on collections, but
// not on database.
// Note: Assumes Read perms on database. Careful if supporting RPCs requiring
// only Resolve.
// TODO(ivanpi): Abstract out multi-scan for scan and possibly query support.
// TODO(ivanpi): Use watch pattern prefixes to optimize scan ranges.
func (d *database) scanInitialState(ctx *context.T, call rpc.ServerCall, sender *watchBatchSender, sntx store.SnapshotOrTransaction, watchFilter filter.CollectionRowFilter) error {
// Scan matching and accessible collections.
// TODO(ivanpi): Collection scan order not alphabetical.
cxIt := sntx.Scan(common.ScanPrefixArgs(common.CollectionPermsPrefix, ""))
defer cxIt.Cancel()
cxKey, cxPermsValue := []byte{}, []byte{}
for cxIt.Advance() {
cxKey, cxPermsValue = cxIt.Key(cxKey), cxIt.Value(cxPermsValue)
// Database permissions for Watch ensure that the user is always allowed
// to know that a collection exists.
cxId, err := common.ParseCollectionPermsKey(string(cxKey))
if err != nil {
return verror.New(verror.ErrInternal, ctx, err)
}
// Filter out unnecessary collections.
if !watchFilter.CollectionMatches(cxId) {
continue
}
// Send collection info.
var cxPerms interfaces.CollectionPerms
if err := vom.Decode(cxPermsValue, &cxPerms); err != nil {
return verror.NewErrInternal(ctx) // no detailed error for cxPerms before filtering cxInfo
}
cxInfo := collectionInfoFromPerms(ctx, call, cxPerms.GetPerms())
cxInfoAsRawBytes, err := vom.RawBytesFromValue(cxInfo)
if err != nil {
return verror.New(verror.ErrInternal, ctx, err)
}
if err := sender.addChange(
pubutil.EncodeId(cxId),
watch.Exists,
&wire.StoreChange{
Value: cxInfoAsRawBytes,
// Note: FromSync cannot be reconstructed from scan.
FromSync: false,
}); err != nil {
return err
}
// Filter out rows with no read access.
// TODO(ivanpi): Collection scan already gets perms, optimize?
// TODO(ivanpi): Check service and database resolve only once.
c := &collectionReq{
id: cxId,
d: d,
}
if _, err := common.GetPermsWithAuth(ctx, call, c, []access.Tag{access.Read}, sntx); err != nil {
if verror.ErrorID(err) == verror.ErrNoAccess.ID {
// Skip sending rows if the collection is inaccessible. Caller can see
// from collection info that they have no read access and may therefore
// have missing rows.
// TODO(ivanpi): If read access is regained, should skipped rows be sent
// retroactively?
continue
}
return err
}
// Send matching rows.
if err := c.scanInitialState(ctx, call, sender, sntx, watchFilter); err != nil {
return err
}
}
if err := cxIt.Err(); err != nil {
return verror.New(verror.ErrInternal, ctx, err)
}
return nil
}
// scanInitialState sends the initial state of all matching rows in the
// collection. Does not check access.
// TODO(ivanpi): Abstract out multi-scan for scan and possibly query support.
// TODO(ivanpi): Use watch pattern prefixes to optimize scan ranges.
func (c *collectionReq) scanInitialState(ctx *context.T, call rpc.ServerCall, sender *watchBatchSender, sntx store.SnapshotOrTransaction, watchFilter filter.CollectionRowFilter) error {
// Scan matching rows.
rIt := sntx.Scan(common.ScanPrefixArgs(common.JoinKeyParts(common.RowPrefix, c.stKeyPart()), ""))
defer rIt.Cancel()
key, value := []byte{}, []byte{}
for rIt.Advance() {
key, value = rIt.Key(key), rIt.Value(value)
// See comment in util/constants.go for why we use SplitNKeyParts.
parts := common.SplitNKeyParts(string(key), 3)
externalKey := parts[2]
// Filter out unnecessary rows.
if !watchFilter.RowMatches(c.id, externalKey) {
continue
}
// Send row.
var valueAsRawBytes *vom.RawBytes
if err := vom.Decode(value, &valueAsRawBytes); err != nil {
return verror.New(verror.ErrInternal, ctx, err)
}
if err := sender.addChange(
naming.Join(pubutil.EncodeId(c.id), externalKey),
watch.Exists,
&wire.StoreChange{
Value: valueAsRawBytes,
// Note: FromSync cannot be reconstructed from scan.
FromSync: false,
}); err != nil {
return err
}
}
if err := rIt.Err(); err != nil {
return verror.New(verror.ErrInternal, ctx, err)
}
return nil
}
// watchUpdates waits for database updates and sends them to the client.
// This function does two steps in a for loop:
// - scan through the watch log until the end, sending all updates to the client
// - wait for one of two signals: new updates available or the call is canceled
// The 'new updates' signal is sent by the watcher via a Go channel.
func (d *database) watchUpdates(ctx *context.T, call rpc.ServerCall, sender *watchBatchSender, resumeMarker watch.ResumeMarker, watchFilter filter.CollectionRowFilter) error {
watcher, cancelWatch := d.st.WatchUpdates(resumeMarker)
defer cancelWatch()
for {
// Drain the log queue.
for {
// TODO(ivanpi): Switch to streaming log batch entries? Since sync and
// conflict resolution merge batches, very large batches may not be
// unrealistic. However, sync currently also processes an entire batch at
// a time, and would need to be updated as well.
logs, nextResumeMarker, err := watcher.NextBatchFromLog(d.st)
if err != nil {
// TODO(ivanpi): Log all internal errors, especially ones not returned.
return verror.NewErrInternal(ctx) // no detailed error before access check
}
if logs == nil {
// No new log records available at this time.
break
}
resumeMarker = nextResumeMarker
if err := d.processLogBatch(ctx, call, sender, watchFilter, logs); err != nil {
return err
}
if err := sender.finishBatch(resumeMarker); err != nil {
return err
}
}
// Wait for new updates or cancel.
select {
case _, ok := <-watcher.Wait():
if !ok {
return watcher.Err()
}
case <-ctx.Done():
return ctx.Err()
}
}
}
// processLogBatch converts []*watchable.LogEntry to a watch.Change stream,
// filtering out unnecessary or inaccessible log records.
// Note: Since the governing ACL for each change is no longer tracked, the
// permissions check uses the ACLs in effect at the time processLogBatch is
// called.
// Note: Assumes Read perms on database. Careful if supporting RPCs requiring
// only Resolve.
func (d *database) processLogBatch(ctx *context.T, call rpc.ServerCall, sender *watchBatchSender, watchFilter filter.CollectionRowFilter, logs []*watchable.LogEntry) error {
allowWatchDbContinue := []access.Tag{access.Read}
sn := d.st.NewSnapshot()
defer sn.Abort()
// Check permissions on Database.
if _, err := common.GetPermsWithAuth(ctx, call, d, allowWatchDbContinue, sn); err != nil {
return err
}
valueBytes := []byte{}
for _, logEntry := range logs {
var opKey string
var op interface{}
if err := logEntry.Op.ToValue(&op); err != nil {
return verror.NewErrInternal(ctx) // no detailed error before access check
}
switch op := op.(type) {
case *watchable.PutOp:
opKey = string(op.Key)
case *watchable.DeleteOp:
opKey = string(op.Key)
default:
continue
}
// TODO(rogulenko,ivanpi): Currently we only process rows and collection
// perms. Consider making watchable and processing other keys.
switch common.FirstKeyPart(opKey) {
case common.RowPrefix:
cxId, row, err := common.ParseRowKey(opKey)
if err != nil {
return verror.NewErrInternal(ctx) // no detailed error before access check
}
// Filter out unnecessary rows.
if !watchFilter.RowMatches(cxId, row) {
continue
}
// Filter out rows with no read access.
// TODO(ivanpi): Check only once per collection per batch.
// TODO(ivanpi): Check service and database resolve only once per batch.
c := &collectionReq{
id: cxId,
d: d,
}
if _, err := common.GetPermsWithAuth(ctx, call, c, []access.Tag{access.Read}, sn); err != nil {
if verror.ErrorID(err) == verror.ErrNoAccess.ID || verror.ErrorID(err) == verror.ErrNoExist.ID {
// Skip sending rows if the collection is inaccessible. Caller can see
// from collection info that they have no read access and may therefore
// have missing rows.
// Note, the collection may not exist anymore, in which case permissions
// cannot be retrieved. This case is treated the same as ErrNoAccess, by
// skipping the row.
// TODO(ivanpi): Consider using the implicit ACL instead for nonexistent
// collections.
// TODO(ivanpi): If read access is regained, should skipped rows be sent
// retroactively?
continue
}
return err
}
switch op := op.(type) {
case *watchable.PutOp:
// Note, valueBytes is reused on each iteration, so the reference must not
// be used beyond this case block. The code below is safe since only the
// VOM-decoded copy is used after the call to vom.Decode.
if valueBytes, err = watchable.GetAtVersion(ctx, sn, op.Key, valueBytes, op.Version); err != nil {
return verror.New(verror.ErrInternal, ctx, err)
}
var rowValueAsRawBytes *vom.RawBytes
if err := vom.Decode(valueBytes, &rowValueAsRawBytes); err != nil {
return verror.New(verror.ErrInternal, ctx, err)
}
if err := sender.addChange(
naming.Join(pubutil.EncodeId(cxId), row),
watch.Exists,
&wire.StoreChange{
Value: rowValueAsRawBytes,
FromSync: logEntry.FromSync,
}); err != nil {
return err
}
case *watchable.DeleteOp:
if err := sender.addChange(
naming.Join(pubutil.EncodeId(cxId), row),
watch.DoesNotExist,
&wire.StoreChange{
FromSync: logEntry.FromSync,
}); err != nil {
return err
}
}
case common.CollectionPermsPrefix:
// Database permissions for Watch ensure that the user is always allowed
// to know that a collection exists.
cxId, err := common.ParseCollectionPermsKey(opKey)
if err != nil {
return verror.New(verror.ErrInternal, ctx, err)
}
// Filter out unnecessary collections.
if !watchFilter.CollectionMatches(cxId) {
continue
}
switch op := op.(type) {
case *watchable.PutOp:
if valueBytes, err = watchable.GetAtVersion(ctx, sn, op.Key, valueBytes, op.Version); err != nil {
return verror.NewErrInternal(ctx) // no detailed error for cxPerms before filtering cxInfo
}
var cxPerms interfaces.CollectionPerms
if err := vom.Decode(valueBytes, &cxPerms); err != nil {
return verror.NewErrInternal(ctx) // no detailed error for cxPerms before filtering cxInfo
}
cxInfo := collectionInfoFromPerms(ctx, call, cxPerms.GetPerms())
cxInfoAsRawBytes, err := vom.RawBytesFromValue(cxInfo)
if err != nil {
return verror.New(verror.ErrInternal, ctx, err)
}
if err := sender.addChange(
pubutil.EncodeId(cxId),
watch.Exists,
&wire.StoreChange{
Value: cxInfoAsRawBytes,
FromSync: logEntry.FromSync,
}); err != nil {
return err
}
case *watchable.DeleteOp:
if err := sender.addChange(
pubutil.EncodeId(cxId),
watch.DoesNotExist,
&wire.StoreChange{
FromSync: logEntry.FromSync,
}); err != nil {
return err
}
}
default:
continue
}
}
return nil
}
// collectionInfoFromPerms converts a collection permissions object into a
// StoreChangeCollectionInfo tailored to the caller. The returned collection
// info is safe to send to the caller, assuming the caller is allowed to know
// the collection exists. It includes a set listing all access tags that the
// caller has on the collection. The collection permissions object itself is
// included only if the caller is allowed to see it (has Admin permissions).
func collectionInfoFromPerms(ctx *context.T, call rpc.ServerCall, cxPerms access.Permissions) *wire.StoreChangeCollectionInfo {
ci := &wire.StoreChangeCollectionInfo{
Allowed: make(map[access.Tag]struct{}),
}
for tag, acl := range cxPerms {
if acl.Authorize(ctx, call.Security()) == nil {
ci.Allowed[access.Tag(tag)] = struct{}{}
}
}
if _, isAdmin := ci.Allowed[access.Admin]; isAdmin {
ci.Perms = cxPerms
}
return ci
}
// watchBatchSender sends a sequence of watch changes forming a batch, delaying
// sends to allow setting the Continued flag on the last change.
type watchBatchSender struct {
// Function for sending changes to the stream. Must be set.
send func(item watch.Change) error
// Change set by previous addChange, sent by next addChange or finishBatch.
staged *watch.Change
}
// addChange sends the previously added change (if any) with Continued set to
// true and stages the new one to be sent by the next addChange or finishBatch.
func (w *watchBatchSender) addChange(name string, state int32, storeChange *wire.StoreChange) error {
// Encode the StoreChange for sending.
storeChangeAsRawBytes, err := vom.RawBytesFromValue(*storeChange)
if err != nil {
return verror.New(verror.ErrInternal, nil, err)
}
// Send previously staged change now that we know the batch is continuing.
if w.staged != nil {
w.staged.Continued = true
if err := w.send(*w.staged); err != nil {
return err
}
}
// Stage new change.
w.staged = &watch.Change{
Name: name,
State: state,
Value: storeChangeAsRawBytes,
}
return nil
}
// finishBatch sends the previously added change (if any) with Continued set to
// false, finishing the batch.
func (w *watchBatchSender) finishBatch(resumeMarker watch.ResumeMarker) error {
// Send previously staged change as last in batch.
if w.staged != nil {
w.staged.Continued = false
w.staged.ResumeMarker = resumeMarker
if err := w.send(*w.staged); err != nil {
return err
}
}
// Clear staged change.
w.staged = nil
return nil
}