blob: 394cbac268f2d7520c1c57a5d649a0a59b1d15fe [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 syncbase defines the wire API for a structured store that supports
// peer-to-peer synchronization.
//
// TODO(sadovsky): Write a detailed package description, or provide a reference
// to the Syncbase documentation.
//
// Security notes:
// The Syncbase service uses permissions tags from v23/security/access.Tag,
// restricted on each hierarchy level to tags used at that level:
// - Valid Service permissions tags are all v23/security/access.Tag tags.
// - Valid Database permissions tags are Admin, Read, Write, Resolve.
// - Valid Collection permissions tags are Admin, Read, Write.
// - Valid Syncgroup permissions tags are Admin, Read.
// Other tags are not allowed and are reserved for future use.
// Unless stated otherwise, each permissions tag requirement on a method also
// implies requiring Resolve on all levels of hierarchy up to, but excluding,
// the level requiring the tag.
// ErrNoAccess, Err[No]Exist, ErrUnknownBatch are only returned if the caller
// is allowed to call Exists on the receiver of the RPC (or the first missing
// component of the hierarchy to the receiver); otherwise, the returned error
// is ErrNoExistOrNoAccess.
package syncbase
import (
"time"
"v.io/v23/security/access"
"v.io/v23/services/permissions"
"v.io/v23/services/watch"
)
const (
// Access tags used in Syncbase database ACLs.
AllDatabaseTags = []access.Tag{access.Admin, access.Read, access.Write, access.Resolve}
// Access tags used in Syncbase collection ACLs.
AllCollectionTags = []access.Tag{access.Admin, access.Read, access.Write}
// Access tags used in Syncbase syncgroup ACLs.
AllSyncgroupTags = []access.Tag{access.Admin, access.Read}
)
// NOTE(sadovsky): Various methods below may end up needing additional options.
// TODO(sadovsky): Move "DevMode" methods elsewhere, so that they are completely
// hidden from clients. Relatedly, configure the server to not even export these
// RPC methods if the --dev flag is not set.
// Service represents a Vanadium Syncbase service.
// Service.Glob operates over Database ids, requiring Read on Service, returning
// ids sorted by blessing, then by name.
type Service interface {
// DevModeUpdateVClock updates various bits of Syncbase virtual clock and
// clock daemon state based on the specified options.
//
// Requires: Admin on Service.
// Also requires --dev flag to be set.
DevModeUpdateVClock(uco DevModeUpdateVClockOpts) error {access.Admin}
// DevModeGetTime returns the current time per the Syncbase clock.
//
// Requires: Admin on Service.
// Also requires --dev flag to be set.
DevModeGetTime() (time.Time | error) {access.Admin}
// SetPermissions and GetPermissions are included from the Object interface.
// Permissions must include at least one admin.
//
// Requires: Admin on Service.
permissions.Object
}
// Database represents a set of Collections. Batches, queries, syncgroups, and
// watch all operate at the Database level.
// Database.Glob operates over Collection ids, requiring Read on Database,
// returning ids sorted by blessing, then by name.
type Database interface {
// Create creates this Database. Permissions must be non-nil and include at
// least one admin.
//
// Requires: Write on Service.
// Also requires the creator's blessing to match the pattern in the newly
// created Database's id. This requirement is waived for Admin on Service.
Create(metadata ?SchemaMetadata, perms access.Permissions) error {access.Write}
// Destroy destroys this Database, permanently removing all of its data.
// TODO(sadovsky): Specify what happens to syncgroups.
//
// Requires: Admin on Database or Service.
Destroy() error {access.Admin}
// Exists returns true only if this Database exists.
//
// Requires: at least one tag on Database, or Read or Write on Service.
// Otherwise, ErrNoExistOrNoAccess is returned.
Exists() (bool | error)
// ListCollections returns a list of ids of all Collections in this Database.
// The list is sorted by blessing, then by name.
// This method exists on Database but not on Service because for the latter
// we can simply use glob, while for the former glob lists only Collections
// visible in a new snapshot of the Database, ignoring user batches.
// (Note that the same issue is present in glob on Collection, where Scan can
// be used instead if batch awareness is required.)
// TODO(sadovsky): Maybe switch to streaming RPC.
//
// Requires: Read on Database.
ListCollections(bh BatchHandle) ([]Id | error) {access.Read}
// Exec executes a syncQL query with positional parameters and returns all
// results as specified by the query's select/delete statement.
// Concurrency semantics are documented in model.go.
//
// Requires: Read and/or Write on Collection, depending on the query:
// - Read for select
// - Read and Write for delete
// TODO(ivanpi): Write should suffice for delete without v in WHERE clause.
Exec(bh BatchHandle, query string, params []any) stream<_, []any> error
// BeginBatch creates a new batch. It returns a batch handle to pass in when
// calling batch-aware RPCs.
// Concurrency semantics are documented in model.go.
// All batch-aware RPCs can also be called outside a batch (with an empty
// handle), with the exception of Commit and Abort which only make sense on
// a batch. Note that glob RPCs are not batch-aware.
// TODO(sadovsky): Maybe make BatchOptions optional. Also, rename 'bo' to
// 'opts' once v.io/i/912 is resolved for Java.
//
// Requires: at least one tag on Database.
BeginBatch(bo BatchOptions) (BatchHandle | error)
// Commit persists the pending changes to the database.
// If the batch is readonly, Commit() will fail with ErrReadOnlyBatch; Abort()
// should be used instead.
// If the BatchHandle is empty, Commit() will fail with ErrNotBoundToBatch.
//
// Requires: at least one tag on Database.
// Also verifies that any changes to data and ACLs are allowed for the caller,
// since the batch is signed by the committer. Since only the final value for
// each key is committed and synced, changes to data need to be allowed by
// the ACL before or after the batch. Specifically, adding Write permission,
// changing a value based on it, then removing Write permission within a batch
// is not allowed because it cannot be verified by remote peers.
Commit(bh BatchHandle) error
// Abort notifies the server that any pending changes can be discarded.
// It is not strictly required, but it may allow the server to release locks
// or other resources sooner than if it was not called.
// If the BatchHandle is empty, Abort() will fail with ErrNotBoundToBatch.
//
// Requires: at least one tag on Database.
Abort(bh BatchHandle) error
// PauseSync pauses sync for this database. Incoming sync, as well as outgoing
// sync of subsequent writes, will be disabled until ResumeSync is called.
// PauseSync is idempotent.
//
// Requires: Admin on Database.
PauseSync() error {access.Admin}
// ResumeSync resumes sync for this database. ResumeSync is idempotent.
//
// Requires: Admin on Database.
ResumeSync() error {access.Admin}
// SetPermissions and GetPermissions are included from the Object interface.
// Permissions must include at least one admin.
//
// Requires: Admin on Database.
permissions.Object
// DatabaseWatcher implements the API to watch for updates in the database.
DatabaseWatcher
// SyncgroupManager implements the API for managing syncgroups attached to a
// Database.
SyncgroupManager
// BlobManager implements the API for managing blobs attached to rows in
// a Database.
BlobManager
// SchemaManager implements the API for managing schema metadata attached
// to a Database.
SchemaManager
// ConflictManager implements the API for registering resolvers, receiving
// conflicts and sending resolutions.
ConflictManager
}
// Collection represents a set of Rows.
// Collection.Glob operates over keys of Rows in the Collection, requiring Read
// on Collection, returning keys in a lexicographically sorted order.
type Collection interface {
// Create creates this Collection. Permissions must be non-nil and include at
// least one admin.
//
// Requires: Write on Database.
// Also requires the creator's blessing to match the pattern in the newly
// created Collection's id.
Create(bh BatchHandle, perms access.Permissions) error {access.Write}
// Destroy destroys this Collection, permanently removing all of its data.
//
// Requires: Admin on Collection or on Database.
// TODO(ivanpi): Prevent for synced Collections.
Destroy(bh BatchHandle) error {access.Admin}
// Exists returns true only if this Collection exists.
//
// Requires: at least one tag on Collection, or Read or Write on Database.
// Otherwise, ErrNoExistOrNoAccess is returned.
// If Database does not exist, returned value is identical to
// Database.Exists().
Exists(bh BatchHandle) (bool | error)
// GetPermissions returns the current Permissions for the Collection.
//
// Requires: Admin on Collection.
GetPermissions(bh BatchHandle) (access.Permissions | error) {access.Admin}
// SetPermissions replaces the current Permissions for the Collection.
// Permissions must include at least one admin.
//
// Requires: Admin on Collection.
SetPermissions(bh BatchHandle, perms access.Permissions) error {access.Admin}
// DeleteRange deletes all rows in the given half-open range [start, limit).
// If limit is "", all rows with keys >= start are included.
//
// Requires: Write on Collection.
DeleteRange(bh BatchHandle, start, limit []byte) error {access.Write}
// Scan returns all rows in the given half-open range [start, limit). If limit
// is "", all rows with keys >= start are included.
// Concurrency semantics are documented in model.go.
// Note, we use []byte rather than string for start and limit because they
// need not be valid UTF-8; VDL expects strings to be valid UTF-8.
//
// Requires: Read on Collection.
Scan(bh BatchHandle, start, limit []byte) stream<_, KeyValue> error {access.Read}
}
// Row represents a single row in a Collection.
// All access checks are performed against the Collection ACL.
type Row interface {
// Exists returns true only if this Row exists.
//
// Requires: Read or Write on Collection.
// Otherwise, ErrNoExistOrNoAccess is returned.
// If Collection does not exist, returned value is identical to
// Collection.Exists().
// Note, write methods on Row do not leak information whether the Row existed
// before, but Write is sufficient to call Exists. Therefore, Read protects
// Row data and listing, but not Row existence.
Exists(bh BatchHandle) (bool | error)
// Get returns the value for this Row.
//
// Requires: Read on Collection.
Get(bh BatchHandle) (any | error) {access.Read}
// Put writes the given value for this Row.
//
// Requires: Write on Collection.
Put(bh BatchHandle, value any) error {access.Write}
// Delete deletes this Row.
//
// Requires: Write on Collection.
Delete(bh BatchHandle) error {access.Write}
}
// SyncgroupManager is the interface for syncgroup operations. The Database is
// the parent of its syncgroups for permissions checking purposes.
// TODO(hpucha): Add blessings to create/join and add a refresh method.
type SyncgroupManager interface {
// ListSyncgroups returns a list of ids of all syncgroups attached to this
// Database. The list is sorted by blessing, then by name.
//
// Requires: Read on Database.
ListSyncgroups() ([]Id | error) {access.Read}
// CreateSyncgroup creates a new syncgroup with the given spec.
//
// Requires: Write on Database.
// Also requires the creator's blessing to match the pattern in the newly
// created syncgroup's id.
// Permissions in spec must allow the creator at least Read access.
// All Collections in spec must exist and the creator must have Read access
// on them.
// For each Collection in spec that isn't already part of another syncgroup,
// its permissions must be signed by a blessing matching the pattern in the
// Collection id, and all data must be signed by a blessing currently allowed
// to Write.
// TODO(ivanpi): Since signatures are currently not enforced, we only check
// that the Write permissions are not empty.
CreateSyncgroup(sgId Id, spec SyncgroupSpec, myInfo SyncgroupMemberInfo) error
// JoinSyncgroup joins the syncgroup.
//
// Requires: Write on Database and Read (no Resolve required) on syncgroup.
// For each locally existing Collection in spec, as well as each Collection
// in spec on the remote (joinee) Syncbase, the joiner must have Read access
// on it.
// For each locally existing Collection in spec that isn't already part of
// another syncgroup, its permissions must be signed by a blessing matching
// the pattern in the Collection id, and all data must be signed by a blessing
// currently allowed to Write.
// TODO(ivanpi): Since signatures are currently not enforced, we only check
// that the Write permissions are not empty.
JoinSyncgroup(remoteSyncbaseName string, expectedSyncbaseBlessings []string, sgId Id, myInfo SyncgroupMemberInfo) (spec SyncgroupSpec | error)
// LeaveSyncgroup leaves the syncgroup. Previously synced data will continue
// to be available. If the last syncgroup on a Collection is left, the data
// will become read-only and the Collection must be destroyed before joining
// a syncgroup that includes it.
//
// Requires: Write on Database.
LeaveSyncgroup(sgId Id) error {access.Write}
// DestroySyncgroup destroys the syncgroup. Previously synced data will
// continue to be available to all members, equivalent to all members
// leaving the syncgroup.
//
// Requires: Write on Database and Admin (no Resolve required) on syncgroup.
DestroySyncgroup(sgId Id) error
// EjectFromSyncgroup ejects a member from the syncgroup. The ejected member
// will not be able to sync further, but will retain any data it has already
// synced, equivalent to having left the syncgroup.
//
// Requires: Admin on syncgroup.
// The caller cannot eject themselves.
EjectFromSyncgroup(sgId Id, member string) error
// GetSyncgroupSpec gets the syncgroup spec. version allows for atomic
// read-modify-write of the spec - see comment for SetSyncgroupSpec.
//
// Requires: Read on syncgroup.
GetSyncgroupSpec(sgId Id) (spec SyncgroupSpec, version string | error)
// SetSyncgroupSpec sets the syncgroup spec. version may be either empty or
// the value from a previous Get. If not empty, Set will only succeed if the
// current version matches the specified one.
//
// Requires: Admin on syncgroup.
// The caller must continue to have Read access.
SetSyncgroupSpec(sgId Id, spec SyncgroupSpec, version string) error
// GetSyncgroupMembers gets the info objects for members of the syncgroup.
//
// Requires: Read on syncgroup.
GetSyncgroupMembers(sgId Id) (members map[string]SyncgroupMemberInfo | error)
// TODO(hpucha): Allow clients to tune the behavior of sync.
// - Suspend/ResumeSync per syncgroup instead of database
// - Get/SetSyncPolicy with policies such as "sync only via wifi", "sync
// aggressively", "sync once per day"
}
// SchemaManager implements the API for managing schema metadata attached
// to a Database.
type SchemaManager interface {
// GetSchemaMetadata retrieves schema metadata for this database.
//
// Requires: Read on Database.
GetSchemaMetadata() (SchemaMetadata | error) {access.Read}
// SetSchemaMetadata stores schema metadata for this database.
//
// Requires: Admin on Database.
SetSchemaMetadata(metadata SchemaMetadata) error {access.Admin}
}
// ConflictManager interface provides all the methods necessary to handle
// conflict resolution for a given database.
type ConflictManager interface {
// StartConflictResolver registers a resolver for the database that is
// associated with this ConflictManager and creates a stream to receive
// conflicts and send resolutions.
// Batches of ConflictInfos will be sent over with the Continued field
// within the ConflictInfo representing the batch boundary. Client must
// respond with a batch of ResolutionInfos in the same fashion.
// A key is under conflict if two different values were written to it
// concurrently (in logical time), i.e. neither value is an ancestor of the
// other in the history graph.
// A key under conflict can be a part of a batch committed on local or
// remote or both syncbases. ConflictInfos for all keys in these two batches
// are grouped together. These keys may themselves be under conflict; the
// presented batch is a transitive closure of all batches containing keys
// under conflict.
// For example, for local batch {key1, key2} and remote batch {key1, key3},
// the batch sent for conflict resolution will be {key1, key2, key3}.
// If there was another concurrent batch {key2, key4}, then the batch sent
// for conflict resolution will be {key1, key2, key3, key4}.
//
// Requires: Admin on Database.
StartConflictResolver() stream<ResolutionInfo, ConflictInfo> error {access.Admin}
}
// BlobManager is the interface for blob operations.
//
// Description of API for resumable blob creation (append-only):
// - Up until commit, a BlobRef may be used with PutBlob, GetBlobSize,
// DeleteBlob, and CommitBlob. Blob creation may be resumed by obtaining the
// current blob size via GetBlobSize and appending to the blob via PutBlob.
// - After commit, a blob is immutable, at which point PutBlob and CommitBlob
// may no longer be used.
// - All other methods (GetBlob, FetchBlob, PinBlob, etc.) may only be used
// after commit.
type BlobManager interface {
// CreateBlob returns a BlobRef for a newly created blob.
//
// Requires: Write on Database.
CreateBlob() (br BlobRef | error) {access.Write}
// PutBlob appends the byte stream to the blob.
//
// Requires: Write on Database and valid BlobRef.
PutBlob(br BlobRef) stream<[]byte, _> error {access.Write}
// CommitBlob marks the blob as immutable.
//
// Requires: Write on Database and valid BlobRef.
CommitBlob(br BlobRef) error {access.Write}
// GetBlobSize returns the count of bytes written as part of the blob
// (committed or uncommitted).
//
// Requires: at least one tag on Database and valid BlobRef.
GetBlobSize(br BlobRef) (int64 | error)
// DeleteBlob locally deletes the blob (committed or uncommitted).
//
// Requires: at least one tag on Database and valid BlobRef.
DeleteBlob(br BlobRef) error
// GetBlob returns the byte stream from a committed blob starting at offset.
//
// Requires: at least one tag on Database and valid BlobRef.
GetBlob(br BlobRef, offset int64) stream<_, []byte> error
// FetchBlob initiates fetching a blob if not locally found. priority
// controls the network priority of the blob. Higher priority blobs are
// fetched before the lower priority ones. However, an ongoing blob
// transfer is not interrupted. Status updates are streamed back to the
// client as fetch is in progress.
//
// Requires: at least one tag on Database and valid BlobRef.
FetchBlob(br BlobRef, priority uint64) stream<_, BlobFetchStatus> error
// PinBlob locally pins the blob so that it is not evicted.
//
// Requires: Write on Database and valid BlobRef.
PinBlob(br BlobRef) error {access.Write}
// UnpinBlob locally unpins the blob so that it can be evicted if needed.
//
// Requires: at least one tag on Database and valid BlobRef.
UnpinBlob(br BlobRef) error
// KeepBlob locally caches the blob with the specified rank. Lower
// ranked blobs are more eagerly evicted.
//
// Requires: at least one tag on Database and valid BlobRef.
KeepBlob(br BlobRef, rank uint64) error
// TODO(hpucha): Clarify how to pick priority and rank. Add API for
// efficient blob cloning. Options include: (1) CloneBlob RPC with an
// array of mods that specify the offset and len for the new bytes. This
// might need two len fields to support growing a blob in the middle
// instead of just replacing byte for byte in the src blob. Or perhaps
// Offset>=0 to mean "read from old blob at this offset for Length
// bytes", and Offset<0 to mean "read the next Length Bytes from the
// PutBlob() stream". (2) We could adopt API similar to the local blob
// store with BlockOrFile segments, giving a more flexible way to clone
// blobs. Also provide support for parallel blob upload.
}
// DatabaseWatcher allows a client to watch for updates to the database. For
// each watch request, the client will receive a reliable stream of watch events
// without re-ordering. Only rows and collections matching at least one of the
// patterns are returned. Rows in collections with no Read access are also
// filtered out.
//
// Watching is done by starting a streaming RPC. The RPC takes a ResumeMarker
// argument that points to a particular place in the database event log. If an
// empty ResumeMarker is provided, the WatchStream will begin with a Change
// batch containing the initial state, always starting with an empty update for
// the root entity. Otherwise, the WatchStream will contain only changes since
// the provided ResumeMarker.
// See watch.GlobWatcher for a detailed explanation of the behavior.
//
// The result stream consists of a never-ending sequence of Change messages
// (until the call fails or is canceled). Each Change contains the Name field
// with the Vanadium name of the watched entity relative to the database:
// - "<encCxId>/<rowKey>" for row updates
// - "<encCxId>" for collection updates
// - "" for the initial root entity update
// The Value field is a StoreChange.
// If the client has no access to a row specified in a change, that change is
// excluded from the result stream. Collection updates are always sent and can
// be used to determine that access to a collection is denied, potentially
// skipping rows.
//
// Note: A single Watch Change batch may contain changes from more than one
// batch as originally committed on a remote Syncbase or obtained from conflict
// resolution. However, changes from a single original batch will always appear
// in the same Change batch.
type DatabaseWatcher interface {
// GetResumeMarker returns the ResumeMarker that points to the current end
// of the event log. GetResumeMarker() can be called on a batch.
//
// Requires: at least one tag on Database.
GetResumeMarker(bh BatchHandle) (watch.ResumeMarker | error)
// WatchPatterns returns a stream of changes that match any of the specified
// patterns. At least one pattern must be specified.
//
// Requires: Read on Database.
WatchPatterns(resumeMarker watch.ResumeMarker, patterns []CollectionRowPattern) stream<_, watch.Change> error {access.Read}
// Requires: Read on Database (instead of Resolve).
watch.GlobWatcher
}
error (
NotInDevMode() {"en": "not running with --dev=true"}
InvalidName(name string) {"en": "invalid name: '{name}'{:_}"}
CorruptDatabase(path string) {"en": "database corrupt, moved to '{path}'; client must create a new database"}
UnknownBatch() {"en": "unknown batch, perhaps the server restarted"}
NotBoundToBatch() {"en": "not bound to batch"}
ReadOnlyBatch() {"en": "batch is read-only"}
ConcurrentBatch() {"en": "concurrent batch"}
BlobNotCommitted() {"en": "blob is not yet committed"}
SyncgroupJoinFailed() {"en": "syncgroup join failed{:_}"}
BadExecStreamHeader() {"en": "Exec stream header improperly formatted"}
InvalidPermissionsChange() {"en": "the sequence of permission changes is invalid"}
UnauthorizedCreateId(blessing, name string) {"en": "not authorized to create object with id blessing '{blessing}' (name '{name}'){:_}"}
InferAppBlessingFailed(entity, name string) {"en": "failed to infer app blessing pattern for {entity} '{name}'{:_}"}
InferUserBlessingFailed(entity, name string) {"en": "failed to infer user blessing pattern for {entity} '{name}'{:_}"}
InferDefaultPermsFailed(entity, id string) {"en": "failed to infer default perms for user for {entity} '{id}'{:_}"}
)