syncbase: Infer id blessings and enforce on creation.

Id blessings in database, collection, and syncgroup names are
properly inferred from the context - preferring app and app:user,
falling back to ... and user. Inference fails if ambiguous
(blessings for different apps/users or no conventional blessings).

Perms are sanity checked to be non-empty, contain at least one admin,
and contain only tags relevant to the hierarchy level (DB: XRWA,
Collection: RWA, SG: RA).

Passing nil perms when creating a database or collection now defaults
to giving the creator all permissions instead of inheriting from the
parent in the hierarchy.

Implicit permissions are enforced for database, collection, and
syncgroup creation - the creator must have a blessing that matches
the blessing pattern in the id. This requirement is waived for service
admins when creating databases, but not in other cases (collection and
syncgroup metadata is synced, so the chain of trust must not be broken).

Also fixed glob (double encode step).

MultiPart: 1/4
Change-Id: I486020aaca0d076ca2cdcbe2282926df7bc72659
diff --git a/security/.api b/security/.api
index da08d4f..f3cc265 100644
--- a/security/.api
+++ b/security/.api
@@ -13,6 +13,7 @@
 pkg security, func BlessingNames(Principal, Blessings) []string
 pkg security, func CreatePrincipal(Signer, BlessingStore, BlessingRoots) (Principal, error)
 pkg security, func DefaultAuthorizer() Authorizer
+pkg security, func DefaultBlessingNames(Principal) []string
 pkg security, func DefaultBlessingPatterns(Principal) []BlessingPattern
 pkg security, func EndpointAuthorizer() Authorizer
 pkg security, func JoinPatternName(BlessingPattern, string) string
diff --git a/security/access/.api b/security/access/.api
index 76d52d5..be33c2b 100644
--- a/security/access/.api
+++ b/security/access/.api
@@ -15,6 +15,7 @@
 pkg access, func PermissionsAuthorizer(Permissions, *vdl.Type) (security.Authorizer, error)
 pkg access, func PermissionsAuthorizerFromFile(string, *vdl.Type) (security.Authorizer, error)
 pkg access, func ReadPermissions(io.Reader) (Permissions, error)
+pkg access, func TagStrings(...Tag) []string
 pkg access, func TypicalTagType() *vdl.Type
 pkg access, func TypicalTagTypePermissionsAuthorizer(Permissions) security.Authorizer
 pkg access, func WritePermissions(io.Writer, Permissions) error
diff --git a/security/access/tags.go b/security/access/tags.go
index 8fd848b..ffce6ea 100644
--- a/security/access/tags.go
+++ b/security/access/tags.go
@@ -24,3 +24,13 @@
 func AllTypicalTags() []Tag {
 	return []Tag{Admin, Read, Write, Debug, Resolve}
 }
+
+// TagStrings converts access.Tag values into []string for use with methods on
+// access.Permissions.
+func TagStrings(tags ...Tag) []string {
+	sts := make([]string, 0, len(tags))
+	for _, t := range tags {
+		sts = append(sts, string(t))
+	}
+	return sts
+}
diff --git a/security/blessings.go b/security/blessings.go
index 901b7de..abd84bc 100644
--- a/security/blessings.go
+++ b/security/blessings.go
@@ -353,7 +353,14 @@
 	return fmt.Sprintf("{%q: %v}", i.Blessing, i.Err)
 }
 
-// DefaultBlessingPatterns returns the BlessingsPatterns of the Default Blessings
+// DefaultBlessingNames returns the blessing names of the Default Blessings of
+// the provided Principal.
+func DefaultBlessingNames(p Principal) (names []string) {
+	blessings, _ := p.BlessingStore().Default()
+	return BlessingNames(p, blessings)
+}
+
+// DefaultBlessingPatterns returns the BlessingPatterns of the Default Blessings
 // of the provided Principal.
 func DefaultBlessingPatterns(p Principal) (patterns []BlessingPattern) {
 	blessings, _ := p.BlessingStore().Default()
diff --git a/services/syncbase/.api b/services/syncbase/.api
index 5c3ca60..a196cfe 100644
--- a/services/syncbase/.api
+++ b/services/syncbase/.api
@@ -39,12 +39,16 @@
 pkg syncbase, func NewErrBlobNotCommitted(*context.T) error
 pkg syncbase, func NewErrConcurrentBatch(*context.T) error
 pkg syncbase, func NewErrCorruptDatabase(*context.T, string) error
+pkg syncbase, func NewErrInferAppBlessingFailed(*context.T, string, string) error
+pkg syncbase, func NewErrInferDefaultPermsFailed(*context.T, string, string) error
+pkg syncbase, func NewErrInferUserBlessingFailed(*context.T, string, string) error
 pkg syncbase, func NewErrInvalidName(*context.T, string) error
 pkg syncbase, func NewErrInvalidPermissionsChange(*context.T) error
 pkg syncbase, func NewErrNotBoundToBatch(*context.T) error
 pkg syncbase, func NewErrNotInDevMode(*context.T) error
 pkg syncbase, func NewErrReadOnlyBatch(*context.T) error
 pkg syncbase, func NewErrSyncgroupJoinFailed(*context.T) error
+pkg syncbase, func NewErrUnauthorizedCreateId(*context.T, string, string) error
 pkg syncbase, func NewErrUnknownBatch(*context.T) error
 pkg syncbase, func ParseId(string) (Id, error)
 pkg syncbase, func ResolverTypeFromString(string) (ResolverType, error)
@@ -725,6 +729,9 @@
 pkg syncbase, type Value struct, WriteTs time.Time
 pkg syncbase, type ValueSelection int
 pkg syncbase, type ValueState int
+pkg syncbase, var AllCollectionTags []access.Tag
+pkg syncbase, var AllDatabaseTags []access.Tag
+pkg syncbase, var AllSyncgroupTags []access.Tag
 pkg syncbase, var BatchSourceAll [...]BatchSource
 pkg syncbase, var BlobFetchStateAll [...]BlobFetchState
 pkg syncbase, var BlobManagerDesc rpc.InterfaceDesc
@@ -736,12 +743,16 @@
 pkg syncbase, var ErrBlobNotCommitted unknown-type
 pkg syncbase, var ErrConcurrentBatch unknown-type
 pkg syncbase, var ErrCorruptDatabase unknown-type
+pkg syncbase, var ErrInferAppBlessingFailed unknown-type
+pkg syncbase, var ErrInferDefaultPermsFailed unknown-type
+pkg syncbase, var ErrInferUserBlessingFailed unknown-type
 pkg syncbase, var ErrInvalidName unknown-type
 pkg syncbase, var ErrInvalidPermissionsChange unknown-type
 pkg syncbase, var ErrNotBoundToBatch unknown-type
 pkg syncbase, var ErrNotInDevMode unknown-type
 pkg syncbase, var ErrReadOnlyBatch unknown-type
 pkg syncbase, var ErrSyncgroupJoinFailed unknown-type
+pkg syncbase, var ErrUnauthorizedCreateId unknown-type
 pkg syncbase, var ErrUnknownBatch unknown-type
 pkg syncbase, var ResolverTypeAll [...]ResolverType
 pkg syncbase, var RowDesc rpc.InterfaceDesc
diff --git a/services/syncbase/service.vdl b/services/syncbase/service.vdl
index 0ee0f0b..067033e 100644
--- a/services/syncbase/service.vdl
+++ b/services/syncbase/service.vdl
@@ -7,6 +7,16 @@
 //
 // 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.
+// TODO(ivanpi): Add and implement other security notes.
 package syncbase
 
 import (
@@ -17,7 +27,14 @@
 	"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.
 
@@ -38,6 +55,7 @@
 	DevModeGetTime() (time.Time | error) {access.Admin}
 
 	// SetPermissions and GetPermissions are included from the Object interface.
+	// Permissions must include at least one admin.
 	permissions.Object
 }
 
@@ -45,8 +63,8 @@
 // watch all operate at the Database level.
 // Database.Glob operates over Collection ids.
 type Database interface {
-	// Create creates this Database.
-	// TODO(sadovsky): Specify what happens if perms is nil.
+	// Create creates this Database. Permissions must be non-nil and include at
+	// least one admin.
 	// Create requires the caller to have Write permission at the Service.
 	Create(metadata ?SchemaMetadata, perms access.Permissions) error {access.Write}
 
@@ -108,6 +126,7 @@
 	ResumeSync() error {access.Write}
 
 	// SetPermissions and GetPermissions are included from the Object interface.
+	// Permissions must include at least one admin.
 	permissions.Object
 
 	// DatabaseWatcher implements the API to watch for updates in the database.
@@ -133,8 +152,8 @@
 // Collection represents a set of Rows.
 // Collection.Glob operates over keys of Rows in the Collection.
 type Collection interface {
-	// Create creates this Collection.
-	// TODO(sadovsky): Specify what happens if perms is nil.
+	// Create creates this Collection. Permissions must be non-nil and include at
+	// least one admin.
 	Create(bh BatchHandle, perms access.Permissions) error {access.Write}
 
 	// Destroy destroys this Collection, permanently removing all of its data.
@@ -145,12 +164,15 @@
 	// permissions cause Exists to return false instead of an error.
 	// TODO(ivanpi): Exists may fail with an error if higher levels of hierarchy
 	// do not exist.
-	Exists(bh BatchHandle) (bool | error) {access.Resolve}
+	// TODO(ivanpi): Temporarily set to Read access because Resolve is now invalid
+	// on Collection.
+	Exists(bh BatchHandle) (bool | error) {access.Read}
 
 	// GetPermissions returns the current Permissions for the Collection.
 	GetPermissions(bh BatchHandle) (access.Permissions | error) {access.Admin}
 
 	// SetPermissions replaces the current Permissions for the Collection.
+	// Permissions must include at least one admin.
 	SetPermissions(bh BatchHandle, perms access.Permissions) error {access.Admin}
 
 	// DeleteRange deletes all rows in the given half-open range [start, limit).
@@ -381,7 +403,7 @@
 
 	// WatchPatterns returns a stream of changes that match any of the specified
 	// patterns. At least one pattern must be specified.
-	WatchPatterns(resumeMarker watch.ResumeMarker, patterns []CollectionRowPattern) stream<_, watch.Change> error {access.Resolve}
+	WatchPatterns(resumeMarker watch.ResumeMarker, patterns []CollectionRowPattern) stream<_, watch.Change> error {access.Read}
 
 	watch.GlobWatcher
 }
@@ -398,4 +420,8 @@
 	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}'{:_}"}
 )
diff --git a/services/syncbase/syncbase.vdl.go b/services/syncbase/syncbase.vdl.go
index c4e3a5d..86a750c 100644
--- a/services/syncbase/syncbase.vdl.go
+++ b/services/syncbase/syncbase.vdl.go
@@ -10,6 +10,16 @@
 //
 // 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.
+// TODO(ivanpi): Add and implement other security notes.
 package syncbase
 
 import (
@@ -448,10 +458,12 @@
 type SyncgroupSpec struct {
 	// Human-readable description of this syncgroup.
 	Description string
-	// Optional. If present then any syncbase that is the admin of this syncgroup
-	// is responsible for ensuring that the syncgroup is published to this syncbase instance.
+	// Optional. If present, any syncbase that is the admin of this syncgroup
+	// is responsible for ensuring that the syncgroup is published to this
+	// syncbase instance.
 	PublishSyncbaseName string
-	// Permissions governing access to this syncgroup.
+	// Permissions governing access to this syncgroup. Must include at least one
+	// admin.
 	Perms access.Permissions
 	// Data (set of collectionIds) covered by this syncgroup.
 	Collections []Id
@@ -2771,6 +2783,27 @@
 //////////////////////////////////////////////////
 // Const definitions
 
+// Access tags used in Syncbase database ACLs.
+var AllDatabaseTags = []access.Tag{
+	"Admin",
+	"Read",
+	"Write",
+	"Resolve",
+}
+
+// Access tags used in Syncbase collection ACLs.
+var AllCollectionTags = []access.Tag{
+	"Admin",
+	"Read",
+	"Write",
+}
+
+// Access tags used in Syncbase syncgroup ACLs.
+var AllSyncgroupTags = []access.Tag{
+	"Admin",
+	"Read",
+}
+
 const BlobDevTypeServer = int32(0) // Blobs migrate toward servers, which store them.  (example: server in cloud)
 const BlobDevTypeNormal = int32(1) // Ordinary devices (example: laptop)
 const BlobDevTypeLeaf = int32(2)   // Blobs migrate from leaves, which have less storage (examples: a camera, phone)
@@ -2807,6 +2840,10 @@
 	ErrSyncgroupJoinFailed      = verror.Register("v.io/v23/services/syncbase.SyncgroupJoinFailed", verror.NoRetry, "{1:}{2:} syncgroup join failed{:_}")
 	ErrBadExecStreamHeader      = verror.Register("v.io/v23/services/syncbase.BadExecStreamHeader", verror.NoRetry, "{1:}{2:} Exec stream header improperly formatted")
 	ErrInvalidPermissionsChange = verror.Register("v.io/v23/services/syncbase.InvalidPermissionsChange", verror.NoRetry, "{1:}{2:} the sequence of permission changes is invalid")
+	ErrUnauthorizedCreateId     = verror.Register("v.io/v23/services/syncbase.UnauthorizedCreateId", verror.NoRetry, "{1:}{2:} not authorized to create object with id blessing '{3}' (name '{4}'){:_}")
+	ErrInferAppBlessingFailed   = verror.Register("v.io/v23/services/syncbase.InferAppBlessingFailed", verror.NoRetry, "{1:}{2:} failed to infer app blessing pattern for {3} '{4}'{:_}")
+	ErrInferUserBlessingFailed  = verror.Register("v.io/v23/services/syncbase.InferUserBlessingFailed", verror.NoRetry, "{1:}{2:} failed to infer user blessing pattern for {3} '{4}'{:_}")
+	ErrInferDefaultPermsFailed  = verror.Register("v.io/v23/services/syncbase.InferDefaultPermsFailed", verror.NoRetry, "{1:}{2:} failed to infer default perms for user for {3} '{4}'{:_}")
 )
 
 // NewErrNotInDevMode returns an error with the ErrNotInDevMode ID.
@@ -2864,6 +2901,26 @@
 	return verror.New(ErrInvalidPermissionsChange, ctx)
 }
 
+// NewErrUnauthorizedCreateId returns an error with the ErrUnauthorizedCreateId ID.
+func NewErrUnauthorizedCreateId(ctx *context.T, blessing string, name string) error {
+	return verror.New(ErrUnauthorizedCreateId, ctx, blessing, name)
+}
+
+// NewErrInferAppBlessingFailed returns an error with the ErrInferAppBlessingFailed ID.
+func NewErrInferAppBlessingFailed(ctx *context.T, entity string, name string) error {
+	return verror.New(ErrInferAppBlessingFailed, ctx, entity, name)
+}
+
+// NewErrInferUserBlessingFailed returns an error with the ErrInferUserBlessingFailed ID.
+func NewErrInferUserBlessingFailed(ctx *context.T, entity string, name string) error {
+	return verror.New(ErrInferUserBlessingFailed, ctx, entity, name)
+}
+
+// NewErrInferDefaultPermsFailed returns an error with the ErrInferDefaultPermsFailed ID.
+func NewErrInferDefaultPermsFailed(ctx *context.T, entity string, id string) error {
+	return verror.New(ErrInferDefaultPermsFailed, ctx, entity, id)
+}
+
 //////////////////////////////////////////////////
 // Interface definitions
 
@@ -3365,7 +3422,7 @@
 				{"resumeMarker", ``}, // watch.ResumeMarker
 				{"patterns", ``},     // []CollectionRowPattern
 			},
-			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Resolve"))},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
 		},
 	},
 }
@@ -5068,8 +5125,8 @@
 	// ConflictManager interface provides all the methods necessary to handle
 	// conflict resolution for a given database.
 	ConflictManagerClientMethods
-	// Create creates this Database.
-	// TODO(sadovsky): Specify what happens if perms is nil.
+	// Create creates this Database. Permissions must be non-nil and include at
+	// least one admin.
 	// Create requires the caller to have Write permission at the Service.
 	Create(_ *context.T, metadata *SchemaMetadata, perms access.Permissions, _ ...rpc.CallOpt) error
 	// Destroy destroys this Database, permanently removing all of its data.
@@ -5361,8 +5418,8 @@
 	// ConflictManager interface provides all the methods necessary to handle
 	// conflict resolution for a given database.
 	ConflictManagerServerMethods
-	// Create creates this Database.
-	// TODO(sadovsky): Specify what happens if perms is nil.
+	// Create creates this Database. Permissions must be non-nil and include at
+	// least one admin.
 	// Create requires the caller to have Write permission at the Service.
 	Create(_ *context.T, _ rpc.ServerCall, metadata *SchemaMetadata, perms access.Permissions) error
 	// Destroy destroys this Database, permanently removing all of its data.
@@ -5508,8 +5565,8 @@
 	// ConflictManager interface provides all the methods necessary to handle
 	// conflict resolution for a given database.
 	ConflictManagerServerStubMethods
-	// Create creates this Database.
-	// TODO(sadovsky): Specify what happens if perms is nil.
+	// Create creates this Database. Permissions must be non-nil and include at
+	// least one admin.
 	// Create requires the caller to have Write permission at the Service.
 	Create(_ *context.T, _ rpc.ServerCall, metadata *SchemaMetadata, perms access.Permissions) error
 	// Destroy destroys this Database, permanently removing all of its data.
@@ -5670,7 +5727,7 @@
 	Methods: []rpc.MethodDesc{
 		{
 			Name: "Create",
-			Doc:  "// Create creates this Database.\n// TODO(sadovsky): Specify what happens if perms is nil.\n// Create requires the caller to have Write permission at the Service.",
+			Doc:  "// Create creates this Database. Permissions must be non-nil and include at\n// least one admin.\n// Create requires the caller to have Write permission at the Service.",
 			InArgs: []rpc.ArgDesc{
 				{"metadata", ``}, // *SchemaMetadata
 				{"perms", ``},    // access.Permissions
@@ -5800,8 +5857,8 @@
 // Collection represents a set of Rows.
 // Collection.Glob operates over keys of Rows in the Collection.
 type CollectionClientMethods interface {
-	// Create creates this Collection.
-	// TODO(sadovsky): Specify what happens if perms is nil.
+	// Create creates this Collection. Permissions must be non-nil and include at
+	// least one admin.
 	Create(_ *context.T, bh BatchHandle, perms access.Permissions, _ ...rpc.CallOpt) error
 	// Destroy destroys this Collection, permanently removing all of its data.
 	// TODO(sadovsky): Specify what happens to syncgroups.
@@ -5810,10 +5867,13 @@
 	// permissions cause Exists to return false instead of an error.
 	// TODO(ivanpi): Exists may fail with an error if higher levels of hierarchy
 	// do not exist.
+	// TODO(ivanpi): Temporarily set to Read access because Resolve is now invalid
+	// on Collection.
 	Exists(_ *context.T, bh BatchHandle, _ ...rpc.CallOpt) (bool, error)
 	// GetPermissions returns the current Permissions for the Collection.
 	GetPermissions(_ *context.T, bh BatchHandle, _ ...rpc.CallOpt) (access.Permissions, error)
 	// SetPermissions replaces the current Permissions for the Collection.
+	// Permissions must include at least one admin.
 	SetPermissions(_ *context.T, bh BatchHandle, perms access.Permissions, _ ...rpc.CallOpt) error
 	// DeleteRange deletes all rows in the given half-open range [start, limit).
 	// If limit is "", all rows with keys >= start are included.
@@ -5955,8 +6015,8 @@
 // Collection represents a set of Rows.
 // Collection.Glob operates over keys of Rows in the Collection.
 type CollectionServerMethods interface {
-	// Create creates this Collection.
-	// TODO(sadovsky): Specify what happens if perms is nil.
+	// Create creates this Collection. Permissions must be non-nil and include at
+	// least one admin.
 	Create(_ *context.T, _ rpc.ServerCall, bh BatchHandle, perms access.Permissions) error
 	// Destroy destroys this Collection, permanently removing all of its data.
 	// TODO(sadovsky): Specify what happens to syncgroups.
@@ -5965,10 +6025,13 @@
 	// permissions cause Exists to return false instead of an error.
 	// TODO(ivanpi): Exists may fail with an error if higher levels of hierarchy
 	// do not exist.
+	// TODO(ivanpi): Temporarily set to Read access because Resolve is now invalid
+	// on Collection.
 	Exists(_ *context.T, _ rpc.ServerCall, bh BatchHandle) (bool, error)
 	// GetPermissions returns the current Permissions for the Collection.
 	GetPermissions(_ *context.T, _ rpc.ServerCall, bh BatchHandle) (access.Permissions, error)
 	// SetPermissions replaces the current Permissions for the Collection.
+	// Permissions must include at least one admin.
 	SetPermissions(_ *context.T, _ rpc.ServerCall, bh BatchHandle, perms access.Permissions) error
 	// DeleteRange deletes all rows in the given half-open range [start, limit).
 	// If limit is "", all rows with keys >= start are included.
@@ -5986,8 +6049,8 @@
 // The only difference between this interface and CollectionServerMethods
 // is the streaming methods.
 type CollectionServerStubMethods interface {
-	// Create creates this Collection.
-	// TODO(sadovsky): Specify what happens if perms is nil.
+	// Create creates this Collection. Permissions must be non-nil and include at
+	// least one admin.
 	Create(_ *context.T, _ rpc.ServerCall, bh BatchHandle, perms access.Permissions) error
 	// Destroy destroys this Collection, permanently removing all of its data.
 	// TODO(sadovsky): Specify what happens to syncgroups.
@@ -5996,10 +6059,13 @@
 	// permissions cause Exists to return false instead of an error.
 	// TODO(ivanpi): Exists may fail with an error if higher levels of hierarchy
 	// do not exist.
+	// TODO(ivanpi): Temporarily set to Read access because Resolve is now invalid
+	// on Collection.
 	Exists(_ *context.T, _ rpc.ServerCall, bh BatchHandle) (bool, error)
 	// GetPermissions returns the current Permissions for the Collection.
 	GetPermissions(_ *context.T, _ rpc.ServerCall, bh BatchHandle) (access.Permissions, error)
 	// SetPermissions replaces the current Permissions for the Collection.
+	// Permissions must include at least one admin.
 	SetPermissions(_ *context.T, _ rpc.ServerCall, bh BatchHandle, perms access.Permissions) error
 	// DeleteRange deletes all rows in the given half-open range [start, limit).
 	// If limit is "", all rows with keys >= start are included.
@@ -6088,7 +6154,7 @@
 	Methods: []rpc.MethodDesc{
 		{
 			Name: "Create",
-			Doc:  "// Create creates this Collection.\n// TODO(sadovsky): Specify what happens if perms is nil.",
+			Doc:  "// Create creates this Collection. Permissions must be non-nil and include at\n// least one admin.",
 			InArgs: []rpc.ArgDesc{
 				{"bh", ``},    // BatchHandle
 				{"perms", ``}, // access.Permissions
@@ -6105,14 +6171,14 @@
 		},
 		{
 			Name: "Exists",
-			Doc:  "// Exists returns true only if this Collection exists. Insufficient\n// permissions cause Exists to return false instead of an error.\n// TODO(ivanpi): Exists may fail with an error if higher levels of hierarchy\n// do not exist.",
+			Doc:  "// Exists returns true only if this Collection exists. Insufficient\n// permissions cause Exists to return false instead of an error.\n// TODO(ivanpi): Exists may fail with an error if higher levels of hierarchy\n// do not exist.\n// TODO(ivanpi): Temporarily set to Read access because Resolve is now invalid\n// on Collection.",
 			InArgs: []rpc.ArgDesc{
 				{"bh", ``}, // BatchHandle
 			},
 			OutArgs: []rpc.ArgDesc{
 				{"", ``}, // bool
 			},
-			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Resolve"))},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
 		},
 		{
 			Name: "GetPermissions",
@@ -6127,7 +6193,7 @@
 		},
 		{
 			Name: "SetPermissions",
-			Doc:  "// SetPermissions replaces the current Permissions for the Collection.",
+			Doc:  "// SetPermissions replaces the current Permissions for the Collection.\n// Permissions must include at least one admin.",
 			InArgs: []rpc.ArgDesc{
 				{"bh", ``},    // BatchHandle
 				{"perms", ``}, // access.Permissions
@@ -6530,6 +6596,10 @@
 	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrSyncgroupJoinFailed.ID), "{1:}{2:} syncgroup join failed{:_}")
 	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrBadExecStreamHeader.ID), "{1:}{2:} Exec stream header improperly formatted")
 	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrInvalidPermissionsChange.ID), "{1:}{2:} the sequence of permission changes is invalid")
+	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrUnauthorizedCreateId.ID), "{1:}{2:} not authorized to create object with id blessing '{3}' (name '{4}'){:_}")
+	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrInferAppBlessingFailed.ID), "{1:}{2:} failed to infer app blessing pattern for {3} '{4}'{:_}")
+	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrInferUserBlessingFailed.ID), "{1:}{2:} failed to infer user blessing pattern for {3} '{4}'{:_}")
+	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrInferDefaultPermsFailed.ID), "{1:}{2:} failed to infer default perms for user for {3} '{4}'{:_}")
 
 	return struct{}{}
 }
diff --git a/services/syncbase/types.vdl b/services/syncbase/types.vdl
index 225f5be..f58f042 100644
--- a/services/syncbase/types.vdl
+++ b/services/syncbase/types.vdl
@@ -67,11 +67,13 @@
 	// Human-readable description of this syncgroup.
 	Description string
 
-	// Optional. If present then any syncbase that is the admin of this syncgroup
-	// is responsible for ensuring that the syncgroup is published to this syncbase instance.
+	// Optional. If present, any syncbase that is the admin of this syncgroup
+	// is responsible for ensuring that the syncgroup is published to this
+	// syncbase instance.
 	PublishSyncbaseName string
 
-	// Permissions governing access to this syncgroup.
+	// Permissions governing access to this syncgroup. Must include at least one
+	// admin.
 	Perms access.Permissions
 
 	// Data (set of collectionIds) covered by this syncgroup.
diff --git a/syncbase/batch_test.go b/syncbase/batch_test.go
index e09fe7b..46e9e78 100644
--- a/syncbase/batch_test.go
+++ b/syncbase/batch_test.go
@@ -36,7 +36,7 @@
 		t.Fatalf("d.BeginBatch() failed: %v", err)
 	}
 
-	if d.Id() != (tu.DbId("d")) {
+	if d.Id() != (wire.Id{"root:o:app", "d"}) {
 		t.Errorf("Wrong id: %q", d.Id())
 	}
 	if d.FullName() != naming.Join(sName, util.EncodeId(d.Id())) {
@@ -52,63 +52,63 @@
 
 // Test that a batch cannot add a permission, make a change, and remove a permission
 // since that change isn't able to be validated by remote syncbases.
-func TestMakeCollectionInBatch(test *testing.T) {
+func TestPermsChangeInBatch(test *testing.T) {
 	ctx, serverName, cleanup := tu.SetupOrDie(nil)
 	defer cleanup()
 	service := syncbase.NewService(serverName)
 	db := tu.CreateDatabase(test, ctx, service, "d")
 
+	// Create the collection outside of batch, ensuring that the initial perms
+	// do not have write permission. (Had the collection been created inside the
+	// batch, initial perms in batch verification would have been the implicit
+	// perms, which allow the creator to write, so would not trigger the failure.
+	// Creating before the batch results in initial batch perms being ones passed
+	// in to collection Create.)
+	// TODO(ivanpi): Test that the other case doesn't fail.
+	perms := tu.DefaultPerms(wire.AllCollectionTags, "root:o:app:client")
+	perms.Clear("root:o:app:client", "Write")
+	if err := db.Collection(ctx, "newname").Create(ctx, perms); err != nil {
+		test.Fatalf("db.Collection().Create() failed, %v", err)
+	}
+
 	batch, err := db.BeginBatch(ctx, wire.BatchOptions{})
 	if err != nil {
-		test.Fatalf("d.BeginBatch() failed: %v", err)
+		test.Fatalf("db.BeginBatch() failed: %v", err)
 	}
-
-	// Create the collection, ensuring that the initial perms do not have write permission.
-	dbperms, _, err := db.GetPermissions(ctx)
-	if err != nil {
-		test.Fatalf("d.GetPermissions() failed: %v", err)
-	}
-	dbperms.Clear("root:client", "Write")
-	if err := batch.Collection(ctx, "newname").Create(ctx, dbperms); err != nil {
-		test.Fatalf("batch.Collection().Create() failed, %v", err)
-	}
-	batchCollection := batch.CollectionForId(tu.CxId("newname"))
+	batchCollection := batch.Collection(ctx, "newname")
 
 	// Add the Write permission.
-	perms, err := batchCollection.GetPermissions(ctx)
+	perms, err = batchCollection.GetPermissions(ctx)
 	if err != nil {
-		test.Fatalf("d.GetPermissions() failed: %v", err)
+		test.Fatalf("bc.GetPermissions() failed: %v", err)
 	}
-	perms.Add("root:client", "Write")
+	perms.Add("root:o:app:client", "Write")
 	if err := batchCollection.SetPermissions(ctx, perms); err != nil {
-		test.Fatalf("SetPermissions() failed: %v", err)
+		test.Fatalf("bc.SetPermissions() failed: %v", err)
 	}
 
 	// Attempt a Put.
 	if err := batchCollection.Put(ctx, "fooKey", "fooValue"); err != nil {
-		test.Fatalf("Put() failed: %v", err)
+		test.Fatalf("bc.Put() failed: %v", err)
 	}
 
 	// Remove the Write permission.
 	perms, err = batchCollection.GetPermissions(ctx)
-	perms.Clear("root:client", "Write")
+	perms.Clear("root:o:app:client", "Write")
 	if err := batchCollection.SetPermissions(ctx, perms); err != nil {
-		test.Fatalf("SetPermissions() failed: %v", err)
+		test.Fatalf("bc.SetPermissions() failed: %v", err)
 	}
 
-	// Commit the batch
-	err = batch.Commit(ctx)
-	if err == nil {
-		test.Fatalf("commit should have failed but instead it succeeded")
+	// Commit the batch.
+	if err := batch.Commit(ctx); verror.ErrorID(err) != wire.ErrInvalidPermissionsChange.ID {
+		test.Fatalf("b.Commit() should have failed with ErrInvalidPermissionsChange, got: %v", err)
 	}
 
-	collection := db.CollectionForId(tu.CxId("newname"))
-	exists, err := collection.Exists(ctx)
-	if err != nil {
-		test.Fatalf("Exists() failed, %v", err)
-	}
-	if exists {
-		test.Fatalf("the collection should not exist since the commit failed")
+	row := db.Collection(ctx, "newname").Row("fooKey")
+	if exists, err := row.Exists(ctx); err != nil {
+		test.Fatalf("r.Exists() failed: %v", err)
+	} else if exists {
+		test.Fatalf("the row should not exist since the commit failed")
 	}
 }
 
@@ -130,7 +130,7 @@
 	if err != nil {
 		t.Fatalf("d.BeginBatch() failed: %v", err)
 	}
-	b1c = b1.CollectionForId(tu.CxId("c"))
+	b1c = b1.Collection(ctx, "c")
 
 	if err := b1c.Put(ctx, "fooKey", "fooValue"); err != nil {
 		t.Fatalf("Put() failed: %v", err)
@@ -199,7 +199,7 @@
 	if b2, err = d.BeginBatch(ctx, wire.BatchOptions{}); err != nil {
 		t.Fatalf("d.BeginBatch() failed: %v", err)
 	}
-	b1c, b2c = b1.CollectionForId(tu.CxId("c")), b2.CollectionForId(tu.CxId("c"))
+	b1c, b2c = b1.Collection(ctx, "c"), b2.Collection(ctx, "c")
 
 	if err := b1c.Put(ctx, "barKey", "barValue"); err != nil {
 		t.Fatalf("Put() failed: %v", err)
@@ -240,10 +240,11 @@
 	defer cleanup()
 	d := tu.CreateDatabase(t, ctx, syncbase.NewService(sName), "d")
 	tu.CreateCollection(t, ctx, d, "c")
+	user := "root:o:app:client"
 	b, err := d.BeginBatch(ctx, wire.BatchOptions{})
 
 	got, err := d.ListCollections(ctx)
-	want := []wire.Id{tu.CxId("c")}
+	want := []wire.Id{{user, "c"}}
 	if err != nil {
 		t.Fatalf("self.ListCollections() failed: %v", err)
 	}
@@ -255,7 +256,7 @@
 
 	// Non-batch should see c_nonbatch; batch should only see c.
 	got, err = d.ListCollections(ctx)
-	want = []wire.Id{tu.CxId("c"), tu.CxId("c_nonbatch")}
+	want = []wire.Id{{user, "c"}, {user, "c_nonbatch"}}
 	if err != nil {
 		t.Fatalf("self.ListCollections() failed: %v", err)
 	}
@@ -264,7 +265,7 @@
 	}
 
 	got, err = b.ListCollections(ctx)
-	want = []wire.Id{tu.CxId("c")}
+	want = []wire.Id{{user, "c"}}
 	if err != nil {
 		t.Fatalf("self.ListCollections() failed: %v", err)
 	}
@@ -273,16 +274,16 @@
 	}
 
 	// Create and destroy collections within a batch.
-	if err := b.CollectionForId(tu.CxId("c_batch")).Create(ctx, nil); err != nil {
+	if err := b.Collection(ctx, "c_batch").Create(ctx, nil); err != nil {
 		t.Fatalf("b.c_batch.Create() failed: %v", err)
 	}
-	if err := b.CollectionForId(tu.CxId("c")).Destroy(ctx); err != nil {
+	if err := b.Collection(ctx, "c").Destroy(ctx); err != nil {
 		t.Fatalf("b.c.Destroy() failed: %v", err)
 	}
 
 	// Non-batch should see c and c_nonbatch; batch should only see c_batch.
 	got, err = d.ListCollections(ctx)
-	want = []wire.Id{tu.CxId("c"), tu.CxId("c_nonbatch")}
+	want = []wire.Id{{user, "c"}, {user, "c_nonbatch"}}
 	if err != nil {
 		t.Fatalf("self.ListCollections() failed: %v", err)
 	}
@@ -291,7 +292,7 @@
 	}
 
 	got, err = b.ListCollections(ctx)
-	want = []wire.Id{tu.CxId("c_batch")}
+	want = []wire.Id{{user, "c_batch"}}
 	if err != nil {
 		t.Fatalf("self.ListCollections() failed: %v", err)
 	}
@@ -468,7 +469,7 @@
 			{vom.RawBytesOf("foo"), vom.RawBytesOf(foo)},
 		})
 
-	rwBatchTb := rwBatch.CollectionForId(tu.CxId("c"))
+	rwBatchTb := rwBatch.Collection(ctx, "c")
 
 	// Add a row in this batch
 	newRow := Baz{Name: "Snow White", Active: true}
@@ -563,7 +564,7 @@
 	if err != nil {
 		t.Fatalf("d.BeginBatch() failed: %v", err)
 	}
-	b1c := b1.CollectionForId(tu.CxId("c"))
+	b1c := b1.Collection(ctx, "c")
 
 	if err := b1c.Put(ctx, "barKey", "barValue"); verror.ErrorID(err) != wire.ErrReadOnlyBatch.ID {
 		t.Fatalf("Put() should have failed: %v", err)
@@ -586,7 +587,7 @@
 	// TODO(sadovsky): Add some sort of "op after finalize" error type and check
 	// for it specifically below.
 	checkOpsFail := func(b syncbase.BatchDatabase) {
-		bc := b.CollectionForId(tu.CxId("c"))
+		bc := b.Collection(ctx, "c")
 		var got string
 		if err := bc.Get(ctx, "fooKey", &got); err == nil {
 			tu.Fatal(t, "Get() should have failed")
@@ -615,7 +616,7 @@
 	if err != nil {
 		t.Fatalf("d.BeginBatch() failed: %v", err)
 	}
-	b1c := b1.CollectionForId(tu.CxId("c"))
+	b1c := b1.Collection(ctx, "c")
 
 	if err := b1c.Put(ctx, "fooKey", "fooValue"); err != nil {
 		t.Fatalf("Put() failed: %v", err)
@@ -630,7 +631,7 @@
 	if b1, err = d.BeginBatch(ctx, wire.BatchOptions{}); err != nil {
 		t.Fatalf("d.BeginBatch() failed: %v", err)
 	}
-	b1c = b1.CollectionForId(tu.CxId("c"))
+	b1c = b1.Collection(ctx, "c")
 
 	// Conflicts with future b1c.Get().
 	if err := c.Put(ctx, "fooKey", "v2"); err != nil {
@@ -655,7 +656,7 @@
 	if b1, err = d.BeginBatch(ctx, wire.BatchOptions{}); err != nil {
 		t.Fatalf("d.BeginBatch() failed: %v", err)
 	}
-	b1c = b1.CollectionForId(tu.CxId("c"))
+	b1c = b1.Collection(ctx, "c")
 	b1.Abort(ctx)
 	checkOpsFail(b1)
 }
@@ -693,18 +694,18 @@
 	return syncbase.RunInBatch(ctx, d, wire.BatchOptions{}, func(b syncbase.BatchDatabase) error {
 		retries++
 		// Read foo.
-		if err := b.CollectionForId(tu.CxId("c")).Get(ctx, fmt.Sprintf("foo-%d", retries), &value); verror.ErrorID(err) != verror.ErrNoExist.ID {
+		if err := b.Collection(ctx, "c").Get(ctx, fmt.Sprintf("foo-%d", retries), &value); verror.ErrorID(err) != verror.ErrNoExist.ID {
 			t.Errorf("b.Get() should have failed with ErrNoExist, got: %v", err)
 		}
 		// If we need to fail, write to foo in a separate concurrent batch. This
 		// is always written on every attempt.
 		if retries < failTimes {
-			if err := d.CollectionForId(tu.CxId("c")).Put(ctx, fmt.Sprintf("foo-%d", retries), "foo"); err != nil {
+			if err := d.Collection(ctx, "c").Put(ctx, fmt.Sprintf("foo-%d", retries), "foo"); err != nil {
 				t.Errorf("d.Put() failed: %v", err)
 			}
 		}
 		// Write to bar. This is only committed on a successful attempt.
-		if err := b.CollectionForId(tu.CxId("c")).Put(ctx, fmt.Sprintf("bar-%d", retries), "bar"); err != nil {
+		if err := b.Collection(ctx, "c").Put(ctx, fmt.Sprintf("bar-%d", retries), "bar"); err != nil {
 			t.Errorf("b.Put() failed: %v", err)
 		}
 		// Return user defined error.
@@ -780,26 +781,26 @@
 	if err := syncbase.RunInBatch(ctx, d, wire.BatchOptions{ReadOnly: true}, func(b syncbase.BatchDatabase) error {
 		var value int32
 		// Read foo.
-		if err := b.CollectionForId(tu.CxId("c")).Get(ctx, "foo", &value); err != nil {
+		if err := b.Collection(ctx, "c").Get(ctx, "foo", &value); err != nil {
 			t.Fatalf("b.Get() failed: %v", err)
 		}
 		newValue := value + 1
 		// Write to foo in a separate concurrent batch. This is always written on
 		// every iteration. It should not cause a retry since readonly batches are
 		// not committed.
-		if err := d.CollectionForId(tu.CxId("c")).Put(ctx, "foo", newValue); err != nil {
+		if err := d.Collection(ctx, "c").Put(ctx, "foo", newValue); err != nil {
 			t.Errorf("d.Put() failed: %v", err)
 		}
 		// Read foo again. Batch should not see the incremented value.
 		var rereadValue int32
-		if err := b.CollectionForId(tu.CxId("c")).Get(ctx, "foo", &rereadValue); err != nil {
+		if err := b.Collection(ctx, "c").Get(ctx, "foo", &rereadValue); err != nil {
 			t.Fatalf("b.Get() failed: %v", err)
 		}
 		if value != rereadValue {
 			t.Fatal("batch should not see value change outside batch")
 		}
 		// Try writing to bar. This should fail since the batch is readonly.
-		if err := b.CollectionForId(tu.CxId("c")).Put(ctx, "bar", value); verror.ErrorID(err) != wire.ErrReadOnlyBatch.ID {
+		if err := b.Collection(ctx, "c").Put(ctx, "bar", value); verror.ErrorID(err) != wire.ErrReadOnlyBatch.ID {
 			t.Errorf("b.Put() should have failed with ErrReadOnlyBatch, got: %v", err)
 		}
 		return nil
diff --git a/syncbase/blob_test.go b/syncbase/blob_test.go
index 82e3057..082b4f3 100644
--- a/syncbase/blob_test.go
+++ b/syncbase/blob_test.go
@@ -17,7 +17,7 @@
 
 // Tests local blob get following a put.
 func TestLocalBlobPutGet(t *testing.T) {
-	ctx, sName, cleanup := tu.SetupOrDie(perms("root:client"))
+	ctx, sName, cleanup := tu.SetupOrDie(nil)
 	defer cleanup()
 	d := tu.CreateDatabase(t, ctx, syncbase.NewService(sName), "d")
 
diff --git a/syncbase/client_test.go b/syncbase/client_test.go
index 4505b41..cb01b96 100644
--- a/syncbase/client_test.go
+++ b/syncbase/client_test.go
@@ -58,9 +58,9 @@
 
 // Tests that Service.ListDatabases works as expected.
 func TestListDatabases(t *testing.T) {
-	ctx, sName, cleanup := tu.SetupOrDie(nil)
+	_, ctx, sName, rootp, cleanup := tu.SetupOrDieCustom("u:client", "server", tu.DefaultPerms(access.AllTypicalTags(), "root"))
 	defer cleanup()
-	tu.TestListChildIds(t, ctx, syncbase.NewService(sName), tu.OkAppUserBlessings, tu.OkDbCxNames)
+	tu.TestListChildIds(t, ctx, rootp, syncbase.NewService(sName), tu.OkAppUserBlessings, tu.OkDbCxNames)
 }
 
 // Tests that Service.{Set,Get}Permissions work as expected.
@@ -185,10 +185,13 @@
 
 // Tests that Database.ListCollections works as expected.
 func TestListCollections(t *testing.T) {
-	ctx, sName, cleanup := tu.SetupOrDie(nil)
+	_, ctx, sName, rootp, cleanup := tu.SetupOrDieCustom("u:client", "server", tu.DefaultPerms(access.AllTypicalTags(), "root"))
 	defer cleanup()
-	d := tu.CreateDatabase(t, ctx, syncbase.NewService(sName), "d")
-	tu.TestListChildIds(t, ctx, d, tu.OkAppUserBlessings, tu.OkDbCxNames)
+	d := syncbase.NewService(sName).DatabaseForId(wire.Id{"root", "d"}, nil)
+	if err := d.Create(ctx, tu.DefaultPerms(wire.AllDatabaseTags, "root")); err != nil {
+		t.Fatalf("d.Create() failed: %v", err)
+	}
+	tu.TestListChildIds(t, ctx, rootp, d, tu.OkAppUserBlessings, tu.OkDbCxNames)
 }
 
 // Tests that Database.{Set,Get}Permissions work as expected.
@@ -240,10 +243,8 @@
 	}
 	/*
 		// Remove write permissions from collection.
-		fullPerms := tu.DefaultPerms("root:client").Normalize()
-		readPerms := fullPerms.Copy()
-		readPerms.Clear("root:client", string(access.Write))
-		readPerms.Normalize()
+		fullPerms := tu.DefaultPerms(wire.AllCollectionTags, "root:u:client")
+		readPerms := fullPerms.Copy().Clear("root:u:client", string(access.Write))
 		if err := c.SetPermissions(ctx, readPerms); err != nil {
 			t.Fatalf("c.SetPermissions() failed: %v", err)
 		}
@@ -463,7 +464,7 @@
 // Collection.{Scan, DeleteRange}.
 // TODO(ivanpi): Redundant with permissions_test?
 func TestRowPermissions(t *testing.T) {
-	_, clientACtx, sName, _, cleanup := tu.SetupOrDieCustom("clientA", "server", nil)
+	_, clientACtx, sName, _, cleanup := tu.SetupOrDieCustom("u:clientA", "server", nil)
 	defer cleanup()
 	d := tu.CreateDatabase(t, clientACtx, syncbase.NewService(sName), "d")
 	c := tu.CreateCollection(t, clientACtx, d, "c")
@@ -475,7 +476,7 @@
 	}
 
 	// Lock A out of c.
-	bOnly := tu.DefaultPerms("root:clientB")
+	bOnly := tu.DefaultPerms(wire.AllCollectionTags, "root:u:clientB")
 	if err := c.SetPermissions(clientACtx, bOnly); err != nil {
 		t.Fatalf("c.SetPermissions() failed: %v", err)
 	}
@@ -506,15 +507,15 @@
 // Tests collection perms where get is allowed but put is not.
 // TODO(ivanpi): Redundant with permissions_test?
 func TestMixedCollectionPerms(t *testing.T) {
-	ctx, clientACtx, sName, rootp, cleanup := tu.SetupOrDieCustom("clientA", "server", nil)
+	ctx, clientACtx, sName, rootp, cleanup := tu.SetupOrDieCustom("u:clientA", "server", nil)
 	defer cleanup()
-	clientBCtx := tu.NewCtx(ctx, rootp, "clientB")
+	clientBCtx := tu.NewCtx(ctx, rootp, "u:clientB")
 	d := tu.CreateDatabase(t, clientACtx, syncbase.NewService(sName), "d")
 	c := tu.CreateCollection(t, clientACtx, d, "c")
 
 	// Set permissions.
-	aAllBRead := tu.DefaultPerms("root:clientA")
-	aAllBRead.Add(security.BlessingPattern("root:clientB"), string(access.Read))
+	aAllBRead := tu.DefaultPerms(wire.AllCollectionTags, "root:u:clientA")
+	aAllBRead.Add(security.BlessingPattern("root:u:clientB"), string(access.Read))
 	if err := c.SetPermissions(clientACtx, aAllBRead); err != nil {
 		t.Fatalf("c.SetPermissions() failed: %v", err)
 	}
@@ -589,7 +590,7 @@
 	allChanges := []tu.WatchChangeTest{
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection:   tu.CxId("c"),
+				Collection:   c.Id(),
 				Row:          "abc%",
 				ChangeType:   syncbase.PutChange,
 				ResumeMarker: resumeMarkers[1],
@@ -598,7 +599,7 @@
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection:   tu.CxId("c"),
+				Collection:   c.Id(),
 				Row:          "abc%",
 				ChangeType:   syncbase.DeleteChange,
 				ResumeMarker: resumeMarkers[2],
@@ -606,7 +607,7 @@
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection:   tu.CxId("c"),
+				Collection:   c.Id(),
 				Row:          "a",
 				ChangeType:   syncbase.PutChange,
 				ResumeMarker: resumeMarkers[3],
@@ -616,32 +617,32 @@
 	}
 	ctxWithTimeout, cancel := context.WithTimeout(ctx, 10*time.Second)
 	defer cancel()
-	wstream := d.Watch(ctxWithTimeout, resumeMarkers[0], []wire.CollectionRowPattern{util.RowPrefixPattern(tu.CxId("c"), "a")})
+	wstream := d.Watch(ctxWithTimeout, resumeMarkers[0], []wire.CollectionRowPattern{util.RowPrefixPattern(c.Id(), "a")})
 	tu.CheckWatch(t, wstream, allChanges)
-	wstream = d.Watch(ctxWithTimeout, resumeMarkers[1], []wire.CollectionRowPattern{util.RowPrefixPattern(tu.CxId("c"), "a")})
+	wstream = d.Watch(ctxWithTimeout, resumeMarkers[1], []wire.CollectionRowPattern{util.RowPrefixPattern(c.Id(), "a")})
 	tu.CheckWatch(t, wstream, allChanges[1:])
-	wstream = d.Watch(ctxWithTimeout, resumeMarkers[2], []wire.CollectionRowPattern{util.RowPrefixPattern(tu.CxId("c"), "a")})
+	wstream = d.Watch(ctxWithTimeout, resumeMarkers[2], []wire.CollectionRowPattern{util.RowPrefixPattern(c.Id(), "a")})
 	tu.CheckWatch(t, wstream, allChanges[2:])
 
-	wstream = d.Watch(ctxWithTimeout, resumeMarkers[0], []wire.CollectionRowPattern{util.RowPrefixPattern(tu.CxId("c"), "abc")})
+	wstream = d.Watch(ctxWithTimeout, resumeMarkers[0], []wire.CollectionRowPattern{util.RowPrefixPattern(c.Id(), "abc")})
 	tu.CheckWatch(t, wstream, allChanges[:2])
-	wstream = d.Watch(ctxWithTimeout, resumeMarkers[1], []wire.CollectionRowPattern{util.RowPrefixPattern(tu.CxId("c"), "abc")})
+	wstream = d.Watch(ctxWithTimeout, resumeMarkers[1], []wire.CollectionRowPattern{util.RowPrefixPattern(c.Id(), "abc")})
 	tu.CheckWatch(t, wstream, allChanges[1:2])
 }
 
 // TestWatchWithBatchAndInitialState tests that the client watch correctly
 // handles batches, perms, and fetching initial state on empty resume marker.
 func TestWatchWithBatchAndInitialState(t *testing.T) {
-	ctx, adminCtx, sName, rootp, cleanup := tu.SetupOrDieCustom("admin", "server", nil)
+	ctx, adminCtx, sName, rootp, cleanup := tu.SetupOrDieCustom("u:admin", "server", nil)
 	defer cleanup()
-	clientCtx := tu.NewCtx(ctx, rootp, "client")
+	clientCtx := tu.NewCtx(ctx, rootp, "u:client")
 	d := tu.CreateDatabase(t, adminCtx, syncbase.NewService(sName), "d")
 	cp := tu.CreateCollection(t, adminCtx, d, "cpublic")
 	ch := tu.CreateCollection(t, adminCtx, d, "chidden")
 
 	// Set permissions. Lock client out of ch.
-	openAcl := tu.DefaultPerms("root:admin", "root:client")
-	adminAcl := tu.DefaultPerms("root:admin")
+	openAcl := tu.DefaultPerms(wire.AllCollectionTags, "root:u:admin", "root:u:client")
+	adminAcl := tu.DefaultPerms(wire.AllCollectionTags, "root:u:admin")
 	if err := cp.SetPermissions(adminCtx, openAcl); err != nil {
 		t.Fatalf("cp.SetPermissions() failed: %v", err)
 	}
@@ -651,11 +652,11 @@
 
 	// Put cp:"a/1" and ch:"b/1" in a batch.
 	if err := syncbase.RunInBatch(adminCtx, d, wire.BatchOptions{}, func(b syncbase.BatchDatabase) error {
-		cp := b.CollectionForId(tu.CxId("cpublic"))
+		cp := b.Collection(adminCtx, "cpublic")
 		if err := cp.Put(adminCtx, "a/1", "value"); err != nil {
 			return err
 		}
-		ch := b.CollectionForId(tu.CxId("chidden"))
+		ch := b.Collection(adminCtx, "chidden")
 		return ch.Put(adminCtx, "b/1", "value")
 	}); err != nil {
 		t.Fatalf("RunInBatch failed: %v", err)
@@ -671,15 +672,15 @@
 	defer cancelAdmin()
 	// Start watches with empty resume marker.
 	wstreamAll := d.Watch(ctxWithTimeout, nil, []wire.CollectionRowPattern{
-		util.RowPrefixPattern(tu.CxId("cpublic"), ""),
-		util.RowPrefixPattern(tu.CxId("chidden"), ""),
+		util.RowPrefixPattern(cp.Id(), ""),
+		util.RowPrefixPattern(ch.Id(), ""),
 	})
 	wstreamD := d.Watch(ctxWithTimeout, nil, []wire.CollectionRowPattern{
-		util.RowPrefixPattern(tu.CxId("cpublic"), "d"),
+		util.RowPrefixPattern(cp.Id(), "d"),
 	})
 	wstreamAllAdmin := d.Watch(adminCtxWithTimeout, nil, []wire.CollectionRowPattern{
-		util.RowPrefixPattern(tu.CxId("cpublic"), ""),
-		util.RowPrefixPattern(tu.CxId("chidden"), ""),
+		util.RowPrefixPattern(cp.Id(), ""),
+		util.RowPrefixPattern(ch.Id(), ""),
 	})
 
 	resumeMarkerInitial, err := d.GetResumeMarker(clientCtx)
@@ -691,7 +692,7 @@
 	initialChanges := []tu.WatchChangeTest{
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection:   tu.CxId("chidden"),
+				Collection:   ch.Id(),
 				Row:          "b/1",
 				ChangeType:   syncbase.PutChange,
 				ResumeMarker: nil,
@@ -701,7 +702,7 @@
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection:   tu.CxId("cpublic"),
+				Collection:   cp.Id(),
 				Row:          "a/1",
 				ChangeType:   syncbase.PutChange,
 				ResumeMarker: nil,
@@ -711,7 +712,7 @@
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection:   tu.CxId("cpublic"),
+				Collection:   cp.Id(),
 				Row:          "c/1",
 				ChangeType:   syncbase.PutChange,
 				ResumeMarker: resumeMarkerInitial,
@@ -729,12 +730,12 @@
 	// More writes.
 	// Put ch:"b/2" and cp:"a/2" in a batch.
 	if err := syncbase.RunInBatch(adminCtx, d, wire.BatchOptions{}, func(b syncbase.BatchDatabase) error {
-		cp := b.CollectionForId(tu.CxId("chidden"))
-		if err := cp.Put(adminCtx, "b/2", "value"); err != nil {
+		ch := b.Collection(adminCtx, "chidden")
+		if err := ch.Put(adminCtx, "b/2", "value"); err != nil {
 			return err
 		}
-		ch := b.CollectionForId(tu.CxId("cpublic"))
-		return ch.Put(adminCtx, "a/2", "value")
+		cp := b.Collection(adminCtx, "cpublic")
+		return cp.Put(adminCtx, "a/2", "value")
 	}); err != nil {
 		t.Fatalf("RunInBatch failed: %v", err)
 	}
@@ -744,7 +745,7 @@
 	}
 	// Put cp:"a/1" (overwrite) and cp:"d/1" in a batch.
 	if err := syncbase.RunInBatch(adminCtx, d, wire.BatchOptions{}, func(b syncbase.BatchDatabase) error {
-		cp := b.CollectionForId(tu.CxId("cpublic"))
+		cp := b.Collection(adminCtx, "cpublic")
 		if err := cp.Put(adminCtx, "a/1", "value"); err != nil {
 			return err
 		}
@@ -760,7 +761,7 @@
 	continuedChanges := []tu.WatchChangeTest{
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection:   tu.CxId("chidden"),
+				Collection:   ch.Id(),
 				Row:          "b/2",
 				ChangeType:   syncbase.PutChange,
 				ResumeMarker: nil,
@@ -770,7 +771,7 @@
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection:   tu.CxId("cpublic"),
+				Collection:   cp.Id(),
 				Row:          "a/2",
 				ChangeType:   syncbase.PutChange,
 				ResumeMarker: resumeMarkerAfterB2A2,
@@ -779,7 +780,7 @@
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection:   tu.CxId("cpublic"),
+				Collection:   cp.Id(),
 				Row:          "a/1",
 				ChangeType:   syncbase.PutChange,
 				ResumeMarker: nil,
@@ -789,7 +790,7 @@
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection:   tu.CxId("cpublic"),
+				Collection:   cp.Id(),
 				Row:          "d/1",
 				ChangeType:   syncbase.PutChange,
 				ResumeMarker: resumeMarkerAfterA1rD1,
@@ -823,7 +824,7 @@
 	ctxWithTimeout, cancel := context.WithTimeout(ctx, 10*time.Second)
 	defer cancel()
 	wstream := d.Watch(ctxWithTimeout, resumeMarker, []wire.CollectionRowPattern{
-		util.RowPrefixPattern(tu.CxId("c"), "a"),
+		util.RowPrefixPattern(c.Id(), "a"),
 	})
 	valueBytes, _ := vom.RawBytesFromValue("value")
 	for i := 0; i < 10; i++ {
@@ -840,7 +841,7 @@
 		}
 		want := tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection:   tu.CxId("c"),
+				Collection:   c.Id(),
 				Row:          "abc",
 				ChangeType:   syncbase.PutChange,
 				ResumeMarker: resumeMarker,
@@ -867,7 +868,7 @@
 	ctxWithTimeout, cancel := context.WithTimeout(ctx, 10*time.Second)
 	defer cancel()
 	wstream := d.Watch(ctxWithTimeout, resumeMarker, []wire.CollectionRowPattern{
-		util.RowPrefixPattern(tu.CxId("c"), "a"),
+		util.RowPrefixPattern(wire.Id{"root:o:app:client", "c"}, "a"),
 	})
 	if err := wstream.Err(); err != nil {
 		t.Fatalf("d.Watch() failed: %v", err)
@@ -884,20 +885,24 @@
 // TestWatchMulti tests that watch properly filters collections and includes
 // matching rows only once per update.
 func TestWatchMulti(t *testing.T) {
-	ctx, sName, cleanup := tu.SetupOrDie(nil)
+	_, ctx, sName, rootp, cleanup := tu.SetupOrDieCustom("x", "server", nil)
 	defer cleanup()
-	d := tu.CreateDatabase(t, ctx, syncbase.NewService(sName), "d")
+	d := syncbase.NewService(sName).DatabaseForId(wire.Id{"root:x", "d"}, nil)
+	if err := d.Create(ctx, tu.DefaultPerms(wire.AllDatabaseTags, "root:x")); err != nil {
+		t.Fatalf("d.Create() failed: %v", err)
+	}
 	// Create three collections.
-	cAFoo := d.CollectionForId(wire.Id{"root:alice", "foo"})
-	if err := cAFoo.Create(ctx, nil); err != nil {
+	cxPerms := tu.DefaultPerms(wire.AllCollectionTags, "root:x")
+	cAFoo := d.CollectionForId(wire.Id{"root:x:alice", "foo"})
+	if err := cAFoo.Create(tu.NewCtx(ctx, rootp, "x:alice"), cxPerms); err != nil {
 		t.Fatalf("cAFoo.Create() failed: %v", err)
 	}
-	cAFoobar := d.CollectionForId(wire.Id{"root:alice", "foobar"})
-	if err := cAFoobar.Create(ctx, nil); err != nil {
+	cAFoobar := d.CollectionForId(wire.Id{"root:x:alice", "foobar"})
+	if err := cAFoobar.Create(tu.NewCtx(ctx, rootp, "x:alice"), cxPerms); err != nil {
 		t.Fatalf("cAFoobar.Create() failed: %v", err)
 	}
-	cBFoo := d.CollectionForId(wire.Id{"root:%", "foo"})
-	if err := cBFoo.Create(ctx, nil); err != nil {
+	cBFoo := d.CollectionForId(wire.Id{"root:x:%", "foo"})
+	if err := cBFoo.Create(tu.NewCtx(ctx, rootp, "x:%"), cxPerms); err != nil {
 		t.Fatalf("cBFoo.Create() failed: %v", err)
 	}
 
@@ -928,13 +933,13 @@
 	})
 	// prefix pattern - only literal 'root:\%'
 	wstream2 := d.Watch(ctxWithTimeout, nil, []wire.CollectionRowPattern{
-		util.RowPrefixPattern(wire.Id{"root:%", "foo"}, "e"),
+		util.RowPrefixPattern(wire.Id{"root:x:%", "foo"}, "e"),
 	})
-	// partially overlapping - '%:alice' and 'root:%', '%cd' and 'ab%'
+	// partially overlapping - '%:alice' and 'root:x:%', '%cd' and 'ab%'
 	wstream3 := d.Watch(ctxWithTimeout, nil, []wire.CollectionRowPattern{
 		{"%:alice", "%", "%cd"},
-		{"root:%", "%", "ab%"},
-		{"root:\\%", "%", "x%"},
+		{"root:x:%", "%", "ab%"},
+		{"root:x:\\%", "%", "x%"},
 	})
 
 	resumeMarkerInitial, err := d.GetResumeMarker(ctxWithTimeout)
@@ -946,28 +951,28 @@
 	initialChanges1 := []tu.WatchChangeTest{
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foobar"}, Row: "a",
+				Collection: wire.Id{"root:x:alice", "foobar"}, Row: "a",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foobar"}, Row: "abc",
+				Collection: wire.Id{"root:x:alice", "foobar"}, Row: "abc",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foo"}, Row: "abcd",
+				Collection: wire.Id{"root:x:alice", "foo"}, Row: "abcd",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foo"}, Row: "cd",
+				Collection: wire.Id{"root:x:alice", "foo"}, Row: "cd",
 				ChangeType: syncbase.PutChange, ResumeMarker: resumeMarkerInitial,
 			},
 			ValueBytes: valueBytes,
@@ -978,14 +983,14 @@
 	initialChanges2 := []tu.WatchChangeTest{
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:%", "foo"}, Row: "ef",
+				Collection: wire.Id{"root:x:%", "foo"}, Row: "ef",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:%", "foo"}, Row: "efg",
+				Collection: wire.Id{"root:x:%", "foo"}, Row: "efg",
 				ChangeType: syncbase.PutChange, ResumeMarker: resumeMarkerInitial,
 			},
 			ValueBytes: valueBytes,
@@ -996,49 +1001,49 @@
 	initialChanges3 := []tu.WatchChangeTest{
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:%", "foo"}, Row: "ab",
+				Collection: wire.Id{"root:x:%", "foo"}, Row: "ab",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:%", "foo"}, Row: "x\\yz",
+				Collection: wire.Id{"root:x:%", "foo"}, Row: "x\\yz",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foobar"}, Row: "abc",
+				Collection: wire.Id{"root:x:alice", "foobar"}, Row: "abc",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foobar"}, Row: "cd",
+				Collection: wire.Id{"root:x:alice", "foobar"}, Row: "cd",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foo"}, Row: "abc",
+				Collection: wire.Id{"root:x:alice", "foo"}, Row: "abc",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foo"}, Row: "abcd",
+				Collection: wire.Id{"root:x:alice", "foo"}, Row: "abcd",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foo"}, Row: "cd",
+				Collection: wire.Id{"root:x:alice", "foo"}, Row: "cd",
 				ChangeType: syncbase.PutChange, ResumeMarker: resumeMarkerInitial,
 			},
 			ValueBytes: valueBytes,
@@ -1047,15 +1052,15 @@
 	tu.CheckWatch(t, wstream3, initialChanges3)
 
 	// More writes.
-	// Put root:alice,foo:"abcd" and root:alice,foobar:"abcd", delete root:%,foo:"ef%" in a batch.
+	// Put root:x:alice,foo:"abcd" and root:x:alice,foobar:"abcd", delete root:x:%,foo:"ef%" in a batch.
 	if err := syncbase.RunInBatch(ctxWithTimeout, d, wire.BatchOptions{}, func(b syncbase.BatchDatabase) error {
-		if err := b.CollectionForId(wire.Id{"root:alice", "foo"}).Put(ctxWithTimeout, "abcd", "value"); err != nil {
+		if err := b.CollectionForId(wire.Id{"root:x:alice", "foo"}).Put(ctxWithTimeout, "abcd", "value"); err != nil {
 			return err
 		}
-		if err := b.CollectionForId(wire.Id{"root:alice", "foobar"}).Put(ctxWithTimeout, "abcd", "value"); err != nil {
+		if err := b.CollectionForId(wire.Id{"root:x:alice", "foobar"}).Put(ctxWithTimeout, "abcd", "value"); err != nil {
 			return err
 		}
-		return b.CollectionForId(wire.Id{"root:%", "foo"}).Delete(ctxWithTimeout, "ef%")
+		return b.CollectionForId(wire.Id{"root:x:%", "foo"}).Delete(ctxWithTimeout, "ef%")
 	}); err != nil {
 		t.Fatalf("RunInBatch failed: %v", err)
 	}
@@ -1063,22 +1068,23 @@
 	if err != nil {
 		t.Fatalf("d.GetResumeMarker() failed: %v", err)
 	}
-	// Create collection root:bob,foobar and put "xyz", "acd", "abcd", root:alice,foo:"bcd" in a batch.
-	if err := syncbase.RunInBatch(ctxWithTimeout, d, wire.BatchOptions{}, func(b syncbase.BatchDatabase) error {
-		cNew := b.CollectionForId(wire.Id{"root:bob", "foobar"})
-		if err := cNew.Create(ctxWithTimeout, nil); err != nil {
+	// Create collection root:x:bob,foobar and put "xyz", "acd", "abcd", root:x:alice,foo:"bcd" in a batch.
+	ctxBob := tu.NewCtx(ctxWithTimeout, rootp, "x:bob")
+	if err := syncbase.RunInBatch(ctxBob, d, wire.BatchOptions{}, func(b syncbase.BatchDatabase) error {
+		cNew := b.CollectionForId(wire.Id{"root:x:bob", "foobar"})
+		if err := cNew.Create(ctxBob, cxPerms); err != nil {
 			return err
 		}
-		if err := cNew.Put(ctxWithTimeout, "xyz", "value"); err != nil {
+		if err := cNew.Put(ctxBob, "xyz", "value"); err != nil {
 			return err
 		}
-		if err := cNew.Put(ctxWithTimeout, "acd", "value"); err != nil {
+		if err := cNew.Put(ctxBob, "acd", "value"); err != nil {
 			return err
 		}
-		if err := cNew.Put(ctxWithTimeout, "abcd", "value"); err != nil {
+		if err := cNew.Put(ctxBob, "abcd", "value"); err != nil {
 			return err
 		}
-		return b.CollectionForId(wire.Id{"root:alice", "foo"}).Put(ctxWithTimeout, "bcd", "value")
+		return b.CollectionForId(wire.Id{"root:x:alice", "foo"}).Put(ctxBob, "bcd", "value")
 	}); err != nil {
 		t.Fatalf("RunInBatch failed: %v", err)
 	}
@@ -1091,35 +1097,35 @@
 	continuedChanges1 := []tu.WatchChangeTest{
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foo"}, Row: "abcd",
+				Collection: wire.Id{"root:x:alice", "foo"}, Row: "abcd",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foobar"}, Row: "abcd",
+				Collection: wire.Id{"root:x:alice", "foobar"}, Row: "abcd",
 				ChangeType: syncbase.PutChange, ResumeMarker: resumeMarkerAfterBatch1,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:bob", "foobar"}, Row: "acd",
+				Collection: wire.Id{"root:x:bob", "foobar"}, Row: "acd",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:bob", "foobar"}, Row: "abcd",
+				Collection: wire.Id{"root:x:bob", "foobar"}, Row: "abcd",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foo"}, Row: "bcd",
+				Collection: wire.Id{"root:x:alice", "foo"}, Row: "bcd",
 				ChangeType: syncbase.PutChange, ResumeMarker: resumeMarkerAfterBatch2,
 			},
 			ValueBytes: valueBytes,
@@ -1130,7 +1136,7 @@
 	continuedChanges2 := []tu.WatchChangeTest{
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:%", "foo"}, Row: "ef%",
+				Collection: wire.Id{"root:x:%", "foo"}, Row: "ef%",
 				ChangeType: syncbase.DeleteChange, ResumeMarker: resumeMarkerAfterBatch1,
 			},
 		},
@@ -1140,28 +1146,28 @@
 	continuedChanges3 := []tu.WatchChangeTest{
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foo"}, Row: "abcd",
+				Collection: wire.Id{"root:x:alice", "foo"}, Row: "abcd",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foobar"}, Row: "abcd",
+				Collection: wire.Id{"root:x:alice", "foobar"}, Row: "abcd",
 				ChangeType: syncbase.PutChange, ResumeMarker: resumeMarkerAfterBatch1,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:bob", "foobar"}, Row: "abcd",
+				Collection: wire.Id{"root:x:bob", "foobar"}, Row: "abcd",
 				ChangeType: syncbase.PutChange, Continued: true,
 			},
 			ValueBytes: valueBytes,
 		},
 		tu.WatchChangeTest{
 			WatchChange: syncbase.WatchChange{
-				Collection: wire.Id{"root:alice", "foo"}, Row: "bcd",
+				Collection: wire.Id{"root:x:alice", "foo"}, Row: "bcd",
 				ChangeType: syncbase.PutChange, ResumeMarker: resumeMarkerAfterBatch2,
 			},
 			ValueBytes: valueBytes,
diff --git a/syncbase/collection.go b/syncbase/collection.go
index 27e6833..ae9010f 100644
--- a/syncbase/collection.go
+++ b/syncbase/collection.go
@@ -5,11 +5,14 @@
 package syncbase
 
 import (
+	"v.io/v23"
 	"v.io/v23/context"
 	"v.io/v23/naming"
+	"v.io/v23/security"
 	"v.io/v23/security/access"
 	wire "v.io/v23/services/syncbase"
 	"v.io/v23/syncbase/util"
+	"v.io/v23/verror"
 )
 
 func newCollection(parentFullName string, id wire.Id, bh wire.BatchHandle) Collection {
@@ -48,6 +51,14 @@
 
 // Create implements Collection.Create.
 func (c *collection) Create(ctx *context.T, perms access.Permissions) error {
+	if perms == nil {
+		// Default to giving full permissions to the creator.
+		_, user, err := util.AppAndUserPatternFromBlessings(security.DefaultBlessingNames(v23.GetPrincipal(ctx))...)
+		if err != nil {
+			return verror.New(wire.ErrInferDefaultPermsFailed, ctx, "Collection", util.EncodeId(c.id), err)
+		}
+		perms = access.Permissions{}.Add(user, access.TagStrings(wire.AllCollectionTags...)...)
+	}
 	return c.c.Create(ctx, c.bh, perms)
 }
 
diff --git a/syncbase/database.go b/syncbase/database.go
index 7fe5e0a..ca606b6 100644
--- a/syncbase/database.go
+++ b/syncbase/database.go
@@ -8,7 +8,9 @@
 	"sync"
 	"time"
 
+	"v.io/v23"
 	"v.io/v23/context"
+	"v.io/v23/security"
 	"v.io/v23/security/access"
 	wire "v.io/v23/services/syncbase"
 	"v.io/v23/services/watch"
@@ -71,6 +73,14 @@
 	if d.schema != nil {
 		schemaMetadata = &d.schema.Metadata
 	}
+	if perms == nil {
+		// Default to giving full permissions to the creator.
+		_, user, err := util.AppAndUserPatternFromBlessings(security.DefaultBlessingNames(v23.GetPrincipal(ctx))...)
+		if err != nil {
+			return verror.New(wire.ErrInferDefaultPermsFailed, ctx, "Database", util.EncodeId(d.id), err)
+		}
+		perms = access.Permissions{}.Add(user, access.TagStrings(wire.AllDatabaseTags...)...)
+	}
 	return d.c.Create(ctx, schemaMetadata, perms)
 }
 
@@ -113,21 +123,23 @@
 	return newWatchStream(cancel, call)
 }
 
+// Syncgroup implements Database.Syncgroup.
+func (d *database) Syncgroup(ctx *context.T, name string) Syncgroup {
+	_, user, err := util.AppAndUserPatternFromBlessings(security.DefaultBlessingNames(v23.GetPrincipal(ctx))...)
+	if err != nil {
+		ctx.Error(verror.New(wire.ErrInferUserBlessingFailed, ctx, "Syncgroup", name, err))
+		// A handle with a no-match Id blessing is returned, so all RPCs will fail.
+		// TODO(ivanpi): Return the more specific error from RPCs instead of logging
+		// it here.
+	}
+	return newSyncgroup(d.fullName, wire.Id{Blessing: string(user), Name: name})
+}
+
 // SyncgroupForId implements Database.SyncgroupForId.
 func (d *database) SyncgroupForId(id wire.Id) Syncgroup {
 	return newSyncgroup(d.fullName, id)
 }
 
-// Syncgroup implements Database.Syncgroup.
-func (d *database) Syncgroup(ctx *context.T, name string) Syncgroup {
-	blessing, err := util.UserBlessingFromContext(ctx)
-	if err != nil {
-		// TODO(sadovsky): Return invalid Syncgroup handle.
-		panic(err)
-	}
-	return newSyncgroup(d.fullName, wire.Id{Name: name, Blessing: blessing})
-}
-
 // ListSyncgroups implements Database.ListSyncgroups.
 func (d *database) ListSyncgroups(ctx *context.T) ([]wire.Id, error) {
 	return d.c.ListSyncgroups(ctx)
diff --git a/syncbase/database_batch.go b/syncbase/database_batch.go
index 265cc5a..9309112 100644
--- a/syncbase/database_batch.go
+++ b/syncbase/database_batch.go
@@ -5,8 +5,10 @@
 package syncbase
 
 import (
+	"v.io/v23"
 	"v.io/v23/context"
 	"v.io/v23/naming"
+	"v.io/v23/security"
 	wire "v.io/v23/services/syncbase"
 	"v.io/v23/services/watch"
 	"v.io/v23/syncbase/util"
@@ -47,12 +49,14 @@
 
 // Collection implements DatabaseHandle.Collection.
 func (d *databaseBatch) Collection(ctx *context.T, name string) Collection {
-	blessing, err := util.UserBlessingFromContext(ctx)
+	_, user, err := util.AppAndUserPatternFromBlessings(security.DefaultBlessingNames(v23.GetPrincipal(ctx))...)
 	if err != nil {
-		// TODO(sadovsky): Return invalid Collection handle.
-		panic(err)
+		ctx.Error(verror.New(wire.ErrInferUserBlessingFailed, ctx, "Collection", name, err))
+		// A handle with a no-match Id blessing is returned, so all RPCs will fail.
+		// TODO(ivanpi): Return the more specific error from RPCs instead of logging
+		// it here.
 	}
-	return newCollection(d.fullName, wire.Id{Blessing: blessing, Name: name}, d.bh)
+	return newCollection(d.fullName, wire.Id{Blessing: string(user), Name: name}, d.bh)
 }
 
 // CollectionForId implements DatabaseHandle.CollectionForId.
diff --git a/syncbase/discovery_test.go b/syncbase/discovery_test.go
index cc2acaf..8408b0d 100644
--- a/syncbase/discovery_test.go
+++ b/syncbase/discovery_test.go
@@ -14,6 +14,7 @@
 	"v.io/v23/context"
 	"v.io/v23/discovery"
 	"v.io/v23/security"
+	"v.io/v23/security/access"
 	wire "v.io/v23/services/syncbase"
 	"v.io/v23/syncbase"
 	"v.io/v23/verror"
@@ -22,29 +23,27 @@
 )
 
 func TestSyncgroupDiscovery(t *testing.T) {
-	_, ctx, sName, rootp, cleanup := tu.SetupOrDieCustom(
-		"client1", "server", perms("root:client1"))
+	_, ctx, sName, rootp, cleanup := tu.SetupOrDieCustom("o:app1:client1", "server",
+		tu.DefaultPerms(access.AllTypicalTags(), "root:o:app1:client1"))
 	defer cleanup()
 	d := tu.CreateDatabase(t, ctx, syncbase.NewService(sName), "d")
-	collection1 := wire.Id{"v.io:u:sam", "c1"}
-	collection2 := wire.Id{"v.io:u:sam", "c2"}
-	tu.CreateCollection(t, ctx, d, collection1.Name)
-	tu.CreateCollection(t, ctx, d, collection2.Name)
+	collection1 := tu.CreateCollection(t, ctx, d, "c1")
+	collection2 := tu.CreateCollection(t, ctx, d, "c2")
 
-	c1Updates, err := scanAs(ctx, rootp, "client1")
+	c1Updates, err := scanAs(ctx, rootp, "o:app1:client1")
 	if err != nil {
 		panic(err)
 	}
-	c2Updates, err := scanAs(ctx, rootp, "client2")
+	c2Updates, err := scanAs(ctx, rootp, "o:app1:client2")
 	if err != nil {
 		panic(err)
 	}
 
-	sgId := wire.Id{Name: "sg1", Blessing: "b1"}
+	sgId := d.Syncgroup(ctx, "sg1").Id()
 	spec := wire.SyncgroupSpec{
 		Description: "test syncgroup sg1",
-		Perms:       perms("root:server", "root:client1"),
-		Collections: []wire.Id{collection1},
+		Perms:       tu.DefaultPerms(wire.AllSyncgroupTags, "root:server", "root:o:app1:client1"),
+		Collections: []wire.Id{collection1.Id()},
 	}
 	createSyncgroup(t, ctx, d, sgId, spec, verror.ID(""))
 
@@ -62,15 +61,15 @@
 
 	sg1Attrs := discovery.Attributes{
 		wire.DiscoveryAttrDatabaseName:      "d",
-		wire.DiscoveryAttrDatabaseBlessing:  "v.io:a:xyz",
+		wire.DiscoveryAttrDatabaseBlessing:  "root:o:app1",
 		wire.DiscoveryAttrSyncgroupName:     "sg1",
-		wire.DiscoveryAttrSyncgroupBlessing: "b1",
+		wire.DiscoveryAttrSyncgroupBlessing: "root:o:app1:client1",
 	}
 	sg2Attrs := discovery.Attributes{
 		wire.DiscoveryAttrDatabaseName:      "d",
-		wire.DiscoveryAttrDatabaseBlessing:  "v.io:a:xyz",
+		wire.DiscoveryAttrDatabaseBlessing:  "root:o:app1",
 		wire.DiscoveryAttrSyncgroupName:     "sg2",
-		wire.DiscoveryAttrSyncgroupBlessing: "b1",
+		wire.DiscoveryAttrSyncgroupBlessing: "root:o:app1:client1",
 	}
 
 	// Then we should see an update for the created syncgroup.
@@ -80,7 +79,7 @@
 	}
 
 	// Now update the spec to add client2 to the permissions.
-	spec.Perms = perms("root:server", "root:client1", "root:client2")
+	spec.Perms = tu.DefaultPerms(wire.AllSyncgroupTags, "root:server", "root:o:app1:client1", "root:o:app1:client2")
 	if err := d.SyncgroupForId(sgId).SetSpec(ctx, spec, ""); err != nil {
 		t.Fatalf("sg.SetSpec failed: %v", err)
 	}
@@ -95,11 +94,11 @@
 	}
 
 	// Now create a second syncgroup.
-	sg2Id := wire.Id{Name: "sg2", Blessing: "b1"}
+	sg2Id := d.Syncgroup(ctx, "sg2").Id()
 	spec2 := wire.SyncgroupSpec{
 		Description: "test syncgroup sg2",
-		Perms:       perms("root:server", "root:client1", "root:client2"),
-		Collections: []wire.Id{collection2},
+		Perms:       tu.DefaultPerms(wire.AllSyncgroupTags, "root:server", "root:o:app1:client1", "root:o:app1:client2"),
+		Collections: []wire.Id{collection2.Id()},
 	}
 	createSyncgroup(t, ctx, d, sg2Id, spec2, verror.ID(""))
 
@@ -112,7 +111,7 @@
 		t.Error(err)
 	}
 
-	spec2.Perms = perms("root:server", "root:client1")
+	spec2.Perms = tu.DefaultPerms(wire.AllSyncgroupTags, "root:server", "root:o:app1:client1")
 	if err := d.SyncgroupForId(sg2Id).SetSpec(ctx, spec2, ""); err != nil {
 		t.Fatalf("sg.SetSpec failed: %v", err)
 	}
diff --git a/syncbase/exec_test.go b/syncbase/exec_test.go
index 112d7c3..7ec7e68 100644
--- a/syncbase/exec_test.go
+++ b/syncbase/exec_test.go
@@ -2316,7 +2316,7 @@
 			// The following error text is dependent on the implementation of the query.Database interface.
 			// TODO(sadovsky): Error messages should never contain storage engine
 			// prefixes ("c") and delimiters ("\xfe").
-			syncql.NewErrTableCantAccess(ctx, 14, "Unknown", errors.New("syncbase.test:\"v.io:a:xyz,d\".Exec: Does not exist: c\xfev.io:u:sam,Unknown\xfe")),
+			syncql.NewErrTableCantAccess(ctx, 14, "Unknown", errors.New("syncbase.test:\"root:o:app,d\".Exec: Does not exist: c\xferoot:o:app:client,Unknown\xfe")),
 		},
 		{
 			"select v from Customer offset -1",
@@ -2487,7 +2487,7 @@
 			"select k from Blah",
 			// TODO(sadovsky): Error messages should never contain storage engine
 			// prefixes ("c") and delimiters ("\xfe").
-			syncql.NewErrTableCantAccess(ctx, 14, "Blah", errors.New("syncbase.test:\"v.io:a:xyz,d\".Exec: Does not exist: c\xfev.io:u:sam,Blah\xfe")),
+			syncql.NewErrTableCantAccess(ctx, 14, "Blah", errors.New("syncbase.test:\"root:o:app,d\".Exec: Does not exist: c\xferoot:o:app:client,Blah\xfe")),
 		},
 		{
 			"select k, v from Customer where a = b)",
diff --git a/syncbase/featuretests/blob_v23_test.go b/syncbase/featuretests/blob_v23_test.go
index fbd5ee8..258305f 100644
--- a/syncbase/featuretests/blob_v23_test.go
+++ b/syncbase/featuretests/blob_v23_test.go
@@ -26,7 +26,7 @@
 
 	sbs := setupSyncbases(t, sh, 2, false)
 
-	sgId := wire.Id{Name: "SG1", Blessing: sbBlessings(sbs)}
+	sgId := wire.Id{Name: "SG1", Blessing: testCx.Blessing}
 
 	ok(t, createCollection(sbs[0].clientCtx, sbs[0].sbName, testCx.Name))
 	ok(t, populateData(sbs[0].clientCtx, sbs[0].sbName, testCx.Name, "foo", 0, 10))
diff --git a/syncbase/featuretests/client_v23_test.go b/syncbase/featuretests/client_v23_test.go
index 7ff0e4e..cf8d635 100644
--- a/syncbase/featuretests/client_v23_test.go
+++ b/syncbase/featuretests/client_v23_test.go
@@ -26,11 +26,11 @@
 	serverCreds := sh.ForkCredentials("server")
 	// TODO(aghassemi): Resolve permission is currently needed for Watch.
 	// See https://github.com/vanadium/issues/issues/1110
-	sh.StartSyncbase(serverCreds, syncbaselib.Opts{Name: testSbName}, `{"Resolve": {"In":["root:server", "root:client"]}, "Read": {"In":["root:server", "root:client"]}, "Write": {"In":["root:server", "root:client"]}}`)
+	sh.StartSyncbase(serverCreds, syncbaselib.Opts{Name: testSbName}, `{"Resolve": {"In":["root:server", "root:o:app:client"]}, "Read": {"In":["root:server", "root:o:app:client"]}, "Write": {"In":["root:server", "root:o:app:client"]}, "Admin": {"In":["root:server", "root:o:app:client"]}}`)
 
 	// Create database and collection.
 	// TODO(ivanpi): Use setupAppA.
-	ctx := sh.ForkContext("client")
+	ctx := sh.ForkContext("o:app:client")
 	d := syncbase.NewService(testSbName).DatabaseForId(testDb, nil)
 	if err := d.Create(ctx, nil); err != nil {
 		t.Fatalf("unable to create a database: %v", err)
diff --git a/syncbase/featuretests/cr_v23_test.go b/syncbase/featuretests/cr_v23_test.go
index 29757c8..52ffcde 100644
--- a/syncbase/featuretests/cr_v23_test.go
+++ b/syncbase/featuretests/cr_v23_test.go
@@ -322,7 +322,7 @@
 	sh.StartRootMountTable()
 	sbs := setupSyncbases(t, sh, 2, devMode)
 
-	sgId = wire.Id{Name: "SG1", Blessing: sbBlessings(sbs)}
+	sgId = wire.Id{Name: "SG1", Blessing: testCx.Blessing}
 
 	// Create syncgroup and populate data on s0.
 	ok(t, createSyncgroup(sbs[0].clientCtx, sbs[0].sbName, sgId, "c", "", sbBlessings(sbs), nil, clBlessings(sbs)))
diff --git a/syncbase/featuretests/ping_pong_test.go b/syncbase/featuretests/ping_pong_test.go
index eabed2a..a7d943f 100644
--- a/syncbase/featuretests/ping_pong_test.go
+++ b/syncbase/featuretests/ping_pong_test.go
@@ -58,7 +58,7 @@
 		// Setup *numGroup Syncgroups
 		for g := 0; g < *numGroup; g++ {
 			// Syncbase s0 is the creator.
-			sgId := wire.Id{Name: fmt.Sprintf("SG%d", g+1), Blessing: sbBlessings(sbs)}
+			sgId := wire.Id{Name: fmt.Sprintf("SG%d", g+1), Blessing: testCx.Blessing}
 
 			ok(b, createSyncgroup(sbs[0].clientCtx, sbs[0].sbName, sgId, testCx.Name, "", sbBlessings(sbs), nil, clBlessings(sbs)))
 
diff --git a/syncbase/featuretests/restartability_v23_test.go b/syncbase/featuretests/restartability_v23_test.go
index 316884b..46f1ce3 100644
--- a/syncbase/featuretests/restartability_v23_test.go
+++ b/syncbase/featuretests/restartability_v23_test.go
@@ -25,14 +25,14 @@
 )
 
 const (
-	acl = `{"Read": {"In":["root:u:client"]}, "Write": {"In":["root:u:client"]}, "Resolve": {"In":["root:u:client"]}}`
+	acl = `{"Read": {"In":["root:o:app:client"]}, "Write": {"In":["root:o:app:client"]}, "Admin": {"In":["root:o:app:client"]}, "Resolve": {"In":["root:o:app:client"]}}`
 )
 
 func restartabilityInit(sh *v23test.Shell) (rootDir string, clientCtx *context.T, serverCreds *v23test.Credentials) {
 	sh.StartRootMountTable()
 
 	rootDir = sh.MakeTempDir()
-	clientCtx = sh.ForkContext("u:client")
+	clientCtx = sh.ForkContext("o:app:client")
 	serverCreds = sh.ForkCredentials("r:server")
 	return
 }
@@ -82,8 +82,8 @@
 }
 
 var (
-	dbIds = []wire.Id{{"a1", "d1"}, {"a1", "d2"}, {"a2", "d1"}, {"a2", "d2"}}
-	cxIds = []wire.Id{{"u", "c1"}, {"u", "c2"}}
+	dbIds = []wire.Id{{"root", "d1"}, {"root", "d2"}, {"root:o:app", "d1"}, {"root:o:app", "d2"}}
+	cxIds = []wire.Id{{"root:o:app:client", "c1"}, {"root:o:app:client", "c2"}}
 )
 
 // Creates dbs, collections, and rows.
@@ -124,7 +124,7 @@
 	}
 	for _, dbId := range wantIds {
 		d := s.DatabaseForId(dbId, nil)
-		var got, want []wire.Id = nil, []wire.Id{{"u", "c1"}, {"u", "c2"}}
+		var got, want []wire.Id = nil, cxIds
 		if got, err = d.ListCollections(ctx); err != nil {
 			tu.Fatalf(t, "d.ListCollections() failed: %v", err)
 		}
@@ -464,12 +464,12 @@
 
 	cleanup = sh.StartSyncbase(serverCreds, syncbaselib.Opts{Name: testSbName, RootDir: rootDir}, acl)
 
-	// Recreate a1/d1 since that is the one that got corrupted.
-	d := syncbase.NewService(testSbName).DatabaseForId(wire.Id{"a1", "d1"}, nil)
+	// Recreate root/d1 since that is the one that got corrupted.
+	d := syncbase.NewService(testSbName).DatabaseForId(wire.Id{"root", "d1"}, nil)
 	if err := d.Create(clientCtx, nil); err != nil {
 		t.Fatalf("d.Create() failed: %v", err)
 	}
-	for _, cxId := range []wire.Id{{"u", "c1"}, {"u", "c2"}} {
+	for _, cxId := range cxIds {
 		c := d.CollectionForId(cxId)
 		if err := c.Create(clientCtx, nil); err != nil {
 			t.Fatalf("c.Create() failed: %v", err)
diff --git a/syncbase/featuretests/sync_v23_test.go b/syncbase/featuretests/sync_v23_test.go
index d036255..9008697 100644
--- a/syncbase/featuretests/sync_v23_test.go
+++ b/syncbase/featuretests/sync_v23_test.go
@@ -55,7 +55,7 @@
 	sbs := setupSyncbases(t, sh, 2, false)
 
 	sbName := sbs[0].sbName
-	sgId := wire.Id{Name: "SG1", Blessing: sbBlessings(sbs)}
+	sgId := wire.Id{Name: "SG1", Blessing: testCx.Blessing}
 
 	ok(t, createSyncgroup(sbs[0].clientCtx, sbs[0].sbName, sgId, "c", "", sbBlessings(sbs), nil, clBlessings(sbs)))
 	ok(t, populateData(sbs[0].clientCtx, sbs[0].sbName, testCx.Name, "foo", 0, 10))
@@ -66,7 +66,7 @@
 	// on the first syncgroup do not get any data belonging to this
 	// syncgroup. This triggers the handling of filtered log records in the
 	// restartability code.
-	sgId1 := wire.Id{Name: "SG2", Blessing: sbBlessings(sbs)}
+	sgId1 := wire.Id{Name: "SG2", Blessing: testCx.Blessing}
 
 	// Verify data syncing (client0 updates).
 	ok(t, createSyncgroup(sbs[0].clientCtx, sbs[0].sbName, sgId1, "c1", "", sbBlessings(sbs), nil, clBlessings(sbs)))
@@ -135,7 +135,7 @@
 	sbs := setupSyncbases(t, sh, 2, false)
 
 	sbName := sbs[0].sbName
-	sgId := wire.Id{Name: "SG1", Blessing: sbBlessings(sbs)}
+	sgId := wire.Id{Name: "SG1", Blessing: testCx.Blessing}
 
 	ok(t, createSyncgroup(sbs[0].clientCtx, sbs[0].sbName, sgId, "c1,c2", "", sbBlessings(sbs), nil, clBlessings(sbs)))
 	ok(t, populateData(sbs[0].clientCtx, sbs[0].sbName, "c1", "foo", 0, 10))
@@ -177,17 +177,17 @@
 	sbs := setupSyncbases(t, sh, 2, false)
 
 	sbName := sbs[0].sbName
-	sgId := wire.Id{Name: "SG1", Blessing: sbBlessings(sbs)}
+	sgId := wire.Id{Name: "SG1", Blessing: testCx.Blessing}
 
 	ok(t, createSyncgroup(sbs[0].clientCtx, sbs[0].sbName, sgId, "c", "", sbBlessings(sbs), nil, clBlessings(sbs)))
 	ok(t, populateData(sbs[0].clientCtx, sbs[0].sbName, "c", "foo", 0, 10))
 	ok(t, joinSyncgroup(sbs[1].clientCtx, sbs[1].sbName, sbName, sgId))
 	ok(t, verifySyncgroupData(sbs[1].clientCtx, sbs[1].sbName, "c", "foo", "", 0, 10))
 
-	ok(t, setCollectionPermissions(sbs[1].clientCtx, sbs[1].sbName, "root:c1"))
+	ok(t, setCollectionPermissions(sbs[1].clientCtx, sbs[1].sbName, "root:o:app:client:c1"))
 	ok(t, verifyLostAccess(sbs[0].clientCtx, sbs[0].sbName, "c", "foo", 0, 10))
 
-	ok(t, setCollectionPermissions(sbs[1].clientCtx, sbs[1].sbName, "root:c0;root:c1"))
+	ok(t, setCollectionPermissions(sbs[1].clientCtx, sbs[1].sbName, "root:o:app:client:c0;root:o:app:client:c1"))
 	ok(t, verifySyncgroupData(sbs[0].clientCtx, sbs[0].sbName, "c", "foo", "", 0, 10))
 }
 
@@ -209,9 +209,9 @@
 	sbs := setupSyncbases(t, sh, 3, false)
 
 	sb1Name := sbs[0].sbName
-	sg1Id := wire.Id{Name: "SG1", Blessing: sbBlessings(sbs)}
+	sg1Id := wire.Id{Name: "SG1", Blessing: testCx.Blessing}
 	sb2Name := sbs[1].sbName
-	sg2Id := wire.Id{Name: "SG2", Blessing: sbBlessings(sbs)}
+	sg2Id := wire.Id{Name: "SG2", Blessing: testCx.Blessing}
 
 	// Pre-populate the data before creating the syncgroup.
 	ok(t, createCollection(sbs[0].clientCtx, sbs[0].sbName, "c"))
@@ -246,7 +246,7 @@
 	sbs := setupSyncbases(t, sh, 3, false)
 
 	sbName := sbs[0].sbName
-	sgId := wire.Id{Name: "SG1", Blessing: sbBlessings(sbs)}
+	sgId := wire.Id{Name: "SG1", Blessing: testCx.Blessing}
 
 	ok(t, createSyncgroup(sbs[0].clientCtx, sbs[0].sbName, sgId, "c", "", sbBlessings(sbs), nil, clBlessings(sbs)))
 	ok(t, populateData(sbs[0].clientCtx, sbs[0].sbName, "c", "foo", 0, 10))
@@ -304,7 +304,7 @@
 	d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
 
 	if perms == nil {
-		perms = tu.DefaultPerms(strings.Split(blessingPatterns, ";")...)
+		perms = tu.DefaultPerms(wire.AllSyncgroupTags, strings.Split(blessingPatterns, ";")...)
 	}
 
 	spec := wire.SyncgroupSpec{
@@ -327,7 +327,7 @@
 	}
 
 	d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
-	collectionId := wire.Id{Blessing: "u", Name: collectionName}
+	collectionId := wire.Id{Blessing: testCx.Blessing, Name: collectionName}
 	c := d.CollectionForId(collectionId)
 
 	for i := start; i < end; i++ {
@@ -344,7 +344,7 @@
 	d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
 	c := d.CollectionForId(testCx)
 
-	perms := tu.DefaultPerms(strings.Split(blessingPatterns, ";")...)
+	perms := tu.DefaultPerms(wire.AllCollectionTags, strings.Split(blessingPatterns, ";")...)
 
 	if err := c.SetPermissions(ctx, perms); err != nil {
 		return fmt.Errorf("c.SetPermissions() failed: %v\n", err)
@@ -373,8 +373,8 @@
 	}
 	mtName := roots[0]
 
-	sbperms := tu.DefaultPerms(strings.Split(sbBlessings, ";")...)
-	clperms := tu.DefaultPerms(strings.Split(clBlessings, ";")...)
+	sgperms := tu.DefaultPerms(wire.AllSyncgroupTags, strings.Split(sbBlessings, ";")...)
+	clperms := tu.DefaultPerms(wire.AllCollectionTags, strings.Split(clBlessings, ";")...)
 
 	svc := syncbase.NewService(syncbaseName)
 
@@ -391,7 +391,7 @@
 			var sgColls []wire.Id
 			for k := 0; k < numCxs; k++ {
 				cName := fmt.Sprintf("c%d", k)
-				cId := wire.Id{"u", cName}
+				cId := wire.Id{testCx.Blessing, cName}
 				c := d.CollectionForId(cId)
 				if err := c.Create(ctx, clperms); err != nil {
 					return fmt.Errorf("{%q, %v} c.Create failed %v", syncbaseName, cId, err)
@@ -413,10 +413,10 @@
 
 			// Create one syncgroup per database across all collections.
 			sgName := fmt.Sprintf("%s_%s", appName, dbName)
-			sgId := wire.Id{Name: sgName, Blessing: "blessing"}
+			sgId := wire.Id{Name: sgName, Blessing: testCx.Blessing}
 			spec := wire.SyncgroupSpec{
 				Description: fmt.Sprintf("test sg %s/%s", appName, dbName),
-				Perms:       sbperms,
+				Perms:       sgperms,
 				Collections: sgColls,
 				MountTables: []string{mtName},
 			}
@@ -442,7 +442,7 @@
 			d := svc.DatabaseForId(wire.Id{appName, dbName}, nil)
 
 			sgName := fmt.Sprintf("%s_%s", appName, dbName)
-			sg := d.SyncgroupForId(wire.Id{Name: sgName, Blessing: "blessing"})
+			sg := d.SyncgroupForId(wire.Id{Name: sgName, Blessing: testCx.Blessing})
 			info := wire.SyncgroupMemberInfo{SyncPriority: 10}
 			if _, err := sg.Join(ctx, sbNameRemote, nil, info); err != nil {
 				return fmt.Errorf("Join SG Multi %q failed: %v\n", sgName, err)
@@ -472,7 +472,7 @@
 	sg := d.SyncgroupForId(sgId)
 
 	wantCollections := parseSgCollections(wantColls)
-	wantPerms := tu.DefaultPerms(strings.Split(wantBlessingPatterns, ";")...)
+	wantPerms := tu.DefaultPerms(wire.AllSyncgroupTags, strings.Split(wantBlessingPatterns, ";")...)
 
 	var spec wire.SyncgroupSpec
 	var err error
@@ -495,7 +495,7 @@
 
 func verifySyncgroupDeletedData(ctx *context.T, syncbaseName, collectionName, keyPrefix, valuePrefix string, start, count int) error {
 	d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
-	collectionId := wire.Id{Blessing: "u", Name: collectionName}
+	collectionId := wire.Id{Blessing: testCx.Blessing, Name: collectionName}
 	c := d.CollectionForId(collectionId)
 
 	// Wait for a bit for deletions to propagate.
@@ -551,7 +551,7 @@
 	}
 
 	d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
-	collectionId := wire.Id{Blessing: "u", Name: collectionName}
+	collectionId := wire.Id{Blessing: testCx.Blessing, Name: collectionName}
 
 	ctxWithTimeout, cancel := context.WithTimeout(ctx, 10*time.Second)
 	defer cancel()
@@ -606,7 +606,7 @@
 
 func verifyLostAccess(ctx *context.T, syncbaseName, collectionName, keyPrefix string, start, count int) error {
 	d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
-	collectionId := wire.Id{Blessing: "u", Name: collectionName}
+	collectionId := wire.Id{Blessing: testCx.Blessing, Name: collectionName}
 	c := d.CollectionForId(collectionId)
 
 	lastKey := fmt.Sprintf("%s%d", keyPrefix, start+count-1)
@@ -643,7 +643,7 @@
 
 			for k := 0; k < numCxs; k++ {
 				cName := fmt.Sprintf("c%d", k)
-				c := d.CollectionForId(wire.Id{"u", cName})
+				c := d.CollectionForId(wire.Id{testCx.Blessing, cName})
 
 				prefixes := strings.Split(prefixStr, ",")
 				for _, pfx := range prefixes {
diff --git a/syncbase/featuretests/syncgroup_v23_test.go b/syncbase/featuretests/syncgroup_v23_test.go
index d934ea9..d92b6bf 100644
--- a/syncbase/featuretests/syncgroup_v23_test.go
+++ b/syncbase/featuretests/syncgroup_v23_test.go
@@ -35,7 +35,7 @@
 
 	// Syncbase s0 is the creator.
 	sbName := sbs[0].sbName
-	sgId := wire.Id{Name: "SG1", Blessing: sbBlessings(sbs)}
+	sgId := wire.Id{Name: "SG1", Blessing: testCx.Blessing}
 
 	ok(t, createSyncgroup(sbs[0].clientCtx, sbs[0].sbName, sgId, testCx.Name, "", sbBlessings(sbs), nil, clBlessings(sbs)))
 
@@ -78,7 +78,7 @@
 
 	// Syncbase s0 is the creator, and sN is the cloud.
 	sbName := sbs[N].sbName
-	sgId := wire.Id{Name: "SG1", Blessing: sbBlessings(sbs)}
+	sgId := wire.Id{Name: "SG1", Blessing: testCx.Blessing}
 
 	ok(t, createSyncgroup(sbs[0].clientCtx, sbs[0].sbName, sgId, testCx.Name, "", sbBlessings(sbs), nil, clBlessings(sbs)))
 
@@ -131,10 +131,8 @@
 	// TODO(hpucha): Change it to multi-admin scenario.
 	principals := sbBlessings(sbs)
 	perms := access.Permissions{}
-	for _, tag := range []access.Tag{access.Read, access.Write} {
-		for _, pattern := range strings.Split(principals, ";") {
-			perms.Add(security.BlessingPattern(pattern), string(tag))
-		}
+	for _, pattern := range strings.Split(principals, ";") {
+		perms.Add(security.BlessingPattern(pattern), string(access.Read))
 	}
 	perms.Add(security.BlessingPattern("root:"+sbs[0].sbName), string(access.Admin))
 
@@ -177,7 +175,7 @@
 	// Syncbase s0 is the first to join or create. Run s0 separately to
 	// stagger the process.
 	sbName := sbs[0].sbName
-	sgId := wire.Id{Name: "SG1", Blessing: sbBlessings(sbs)}
+	sgId := wire.Id{Name: "SG1", Blessing: testCx.Blessing}
 	ok(t, joinOrCreateSyncgroup(sbs[0].clientCtx, sbs[0].sbName, sbName, sgId, testCx.Name, "", sbBlessings(sbs), clBlessings(sbs)))
 
 	// Remaining syncbases join the syncgroup concurrently.
diff --git a/syncbase/featuretests/test_util_test.go b/syncbase/featuretests/test_util_test.go
index e7148f6..d13c1ab 100644
--- a/syncbase/featuretests/test_util_test.go
+++ b/syncbase/featuretests/test_util_test.go
@@ -28,8 +28,8 @@
 )
 
 var (
-	testDb = wire.Id{Blessing: "a", Name: "d"}
-	testCx = wire.Id{Blessing: "u", Name: "c"}
+	testDb = wire.Id{Blessing: "root:o:app", Name: "d"}
+	testCx = wire.Id{Blessing: "root:o:app:client", Name: "c"}
 )
 
 ////////////////////////////////////////////////////////////
@@ -42,7 +42,7 @@
 
 func createCollection(ctx *context.T, syncbaseName, collectionName string) error {
 	d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
-	collectionId := wire.Id{Blessing: "u", Name: collectionName}
+	collectionId := wire.Id{Blessing: testCx.Blessing, Name: collectionName}
 	return d.CollectionForId(collectionId).Create(ctx, nil)
 }
 
@@ -59,7 +59,7 @@
 func setupSyncbases(t testing.TB, sh *v23test.Shell, num int, devMode bool) []*testSyncbase {
 	sbs := make([]*testSyncbase, num)
 	for i, _ := range sbs {
-		sbName, clientId := fmt.Sprintf("s%d", i), fmt.Sprintf("c%d", i)
+		sbName, clientId := fmt.Sprintf("s%d", i), fmt.Sprintf("o:app:client:c%d", i)
 		sbs[i] = &testSyncbase{
 			sbName:    sbName,
 			sbCreds:   sh.ForkCredentials(sbName),
@@ -105,7 +105,7 @@
 	}
 
 	d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
-	collectionId := wire.Id{Blessing: "u", Name: collectionName}
+	collectionId := wire.Id{Blessing: testCx.Blessing, Name: collectionName}
 	c := d.CollectionForId(collectionId)
 
 	for i := start; i < end; i++ {
@@ -183,7 +183,7 @@
 
 func verifySyncgroupData(ctx *context.T, syncbaseName, collectionName, keyPrefix, valuePrefix string, start, count int) error {
 	d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
-	collectionId := wire.Id{Blessing: "u", Name: collectionName}
+	collectionId := wire.Id{Blessing: testCx.Blessing, Name: collectionName}
 	c := d.CollectionForId(collectionId)
 
 	// Wait a bit (up to 10 seconds) for the last key to appear.
@@ -231,9 +231,9 @@
 	d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
 
 	if perms == nil {
-		perms = tu.DefaultPerms(strings.Split(sbBlessings, ";")...)
+		perms = tu.DefaultPerms(wire.AllSyncgroupTags, strings.Split(sbBlessings, ";")...)
 	}
-	clperms := tu.DefaultPerms(strings.Split(clBlessings, ";")...)
+	clperms := tu.DefaultPerms(wire.AllCollectionTags, strings.Split(clBlessings, ";")...)
 
 	spec := wire.SyncgroupSpec{
 		Description: "test syncgroup sg",
@@ -244,6 +244,9 @@
 
 	// Change the collection ACLs to enable syncing.
 	for _, cId := range spec.Collections {
+		// TODO(ivanpi,hpucha): Switch to blessings of the form "idp:o:root:c<n>"
+		// (different users instead of delegates of one user) and get the collection
+		// id from context.
 		c := d.CollectionForId(cId)
 		// Ignore the error since sometimes a collection might already exist.
 		c.Create(ctx, nil)
@@ -305,13 +308,13 @@
 // Syncbase-specific testing helpers
 
 // parseSgCollections converts, for example, "a,c" to
-// [Collection: {"u", "a"}, Collection: {"u", "c"}].
-// TODO(ivanpi): Change format to support user blessings other than "u".
+// [Collection: {"root:o:app:client", "a"}, Collection: {"root:o:app:client", "c"}].
+// TODO(ivanpi): Change format to support user blessings other than "root:o:app:client".
 func parseSgCollections(csv string) []wire.Id {
 	strs := strings.Split(csv, ",")
 	res := make([]wire.Id, len(strs))
 	for i, v := range strs {
-		res[i] = wire.Id{"u", v}
+		res[i] = wire.Id{"root:o:app:client", v}
 	}
 	return res
 }
diff --git a/syncbase/model.go b/syncbase/model.go
index ab22e0b..b8640c9 100644
--- a/syncbase/model.go
+++ b/syncbase/model.go
@@ -106,7 +106,8 @@
 	DatabaseHandle
 
 	// Create creates this Database.
-	// TODO(sadovsky): Specify what happens if perms is nil.
+	// If perms is nil, the user blessing derived from the context is given all
+	// permissions.
 	Create(ctx *context.T, perms access.Permissions) error
 
 	// Destroy destroys this Database, permanently removing all of its data.
@@ -154,13 +155,13 @@
 	// See watch.GlobWatcher for a detailed explanation of the behavior.
 	Watch(ctx *context.T, resumeMarker watch.ResumeMarker, patterns []wire.CollectionRowPattern) WatchStream
 
-	// SyncgroupForId returns a handle to the syncgroup with the given Id.
-	SyncgroupForId(id wire.Id) Syncgroup
-
-	// Syncgroup returns a handle to the syncgroup with the given name and with the user
-	// blessing.
+	// Syncgroup returns the Syncgroup with the given relative name.
+	// The user blessing is derived from the context.
 	Syncgroup(ctx *context.T, name string) Syncgroup
 
+	// SyncgroupForId returns the Syncgroup with the given user blessing and name.
+	SyncgroupForId(id wire.Id) Syncgroup
+
 	// ListSyncgroups returns all Syncgroups attached to this database.
 	ListSyncgroups(ctx *context.T) ([]wire.Id, error)
 
@@ -232,7 +233,8 @@
 	Exists(ctx *context.T) (bool, error)
 
 	// Create creates this Collection.
-	// TODO(sadovsky): Specify what happens if perms is nil.
+	// If perms is nil, the user blessing derived from the context is given all
+	// permissions.
 	Create(ctx *context.T, perms access.Permissions) error
 
 	// Destroy destroys this Collection, permanently removing all of its data.
diff --git a/syncbase/permissions_test.go b/syncbase/permissions_test.go
index 671757f..b1b29ca 100644
--- a/syncbase/permissions_test.go
+++ b/syncbase/permissions_test.go
@@ -6,14 +6,18 @@
 
 import (
 	"fmt"
+	"reflect"
 	"testing"
 
+	"v.io/v23"
 	"v.io/v23/context"
 	"v.io/v23/security"
 	"v.io/v23/security/access"
 	wire "v.io/v23/services/syncbase"
 	"v.io/v23/syncbase"
+	"v.io/v23/syncbase/util"
 	"v.io/v23/verror"
+	vsecurity "v.io/x/ref/lib/security"
 	_ "v.io/x/ref/runtime/factories/roaming"
 	tu "v.io/x/ref/services/syncbase/testutil"
 )
@@ -75,7 +79,7 @@
 	},
 	{
 		layer: serviceTest{f: func(ctx *context.T, s syncbase.Service) error {
-			return s.SetPermissions(ctx, nil, "")
+			return s.SetPermissions(ctx, tu.DefaultPerms(access.AllTypicalTags(), "root"), "")
 		}},
 		name:     "service.SetPermissions",
 		patterns: []string{"A___"},
@@ -85,7 +89,7 @@
 	// Database tests.
 	{
 		layer: serviceTest{f: func(ctx *context.T, s syncbase.Service) error {
-			return s.DatabaseForId(wire.Id{"a", "dNew"}, nil).Create(ctx, nil)
+			return s.DatabaseForId(wire.Id{"root", "dNew"}, nil).Create(ctx, tu.DefaultPerms(wire.AllDatabaseTags, "root"))
 		}},
 		name:     "database.Create",
 		patterns: []string{"W___"},
@@ -109,7 +113,7 @@
 	},
 	{
 		layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
-			return d.SetPermissions(ctx, nil, "")
+			return d.SetPermissions(ctx, tu.DefaultPerms(wire.AllDatabaseTags, "root"), "")
 		}},
 		name:     "database.SetPermissions",
 		patterns: []string{"_A__"},
@@ -232,14 +236,10 @@
 
 func runTests(t *testing.T, expectSuccess bool, tests ...securitySpecTest) {
 	// Create permissions.
-	servicePerms := tu.DefaultPerms("root:admin")
-	addPerms(servicePerms, 0, tests...)
-	databasePerms := tu.DefaultPerms("root:admin")
-	addPerms(databasePerms, 1, tests...)
-	collectionPerms := tu.DefaultPerms("root:admin")
-	addPerms(collectionPerms, 2, tests...)
-	sgPerms := tu.DefaultPerms("root:admin")
-	addPerms(sgPerms, 3, tests...)
+	servicePerms := makePerms("root:admin", 0, access.AllTypicalTags(), tests...)
+	databasePerms := makePerms("root:admin", 1, wire.AllDatabaseTags, tests...)
+	collectionPerms := makePerms("root:admin", 2, wire.AllCollectionTags, tests...)
+	//sgPerms := makePerms("root:admin", 3, wire.AllSyncgroupTags, tests...)
 
 	// Create service/database/collection/row with permissions above.
 	ctx, adminCtx, sName, rootp, cleanup := tu.SetupOrDieCustom("admin", "server", nil)
@@ -248,11 +248,11 @@
 	if err := s.SetPermissions(adminCtx, servicePerms, ""); err != nil {
 		tu.Fatalf(t, "s.SetPermissions failed: %v", err)
 	}
-	d := s.DatabaseForId(wire.Id{"a", "d"}, nil)
+	d := s.DatabaseForId(wire.Id{"root", "d"}, nil)
 	if err := d.Create(adminCtx, databasePerms); err != nil {
 		tu.Fatalf(t, "d.Create failed: %v", err)
 	}
-	c := d.CollectionForId(wire.Id{"u", "c"})
+	c := d.CollectionForId(wire.Id{"root:admin", "c"})
 	if err := c.Create(adminCtx, collectionPerms); err != nil {
 		tu.Fatalf(t, "c.Create failed: %v", err)
 	}
@@ -281,10 +281,11 @@
 	}
 }
 
-// addPerms add permissions to the perms object for each test.
+// makePerms creates a perms object with permissions for each test.
 // For each test we add a blessing pattern "root:clientXX" with a tag
 // corresponding test.pattern[index].
-func addPerms(perms access.Permissions, index int, tests ...securitySpecTest) {
+func makePerms(admin string, index int, allowTags []access.Tag, tests ...securitySpecTest) access.Permissions {
+	perms := tu.DefaultPerms(allowTags, admin)
 	tagsMap := map[byte]string{
 		'X': "Resolve",
 		'R': "Read",
@@ -296,4 +297,415 @@
 			perms.Add(security.BlessingPattern(fmt.Sprintf("root:client%d", i)), tag)
 		}
 	}
+	return util.FilterTags(perms, allowTags...)
+}
+
+var (
+	inferBlessingsOk = []struct {
+		exts     []string
+		wantApp  string
+		wantUser string
+	}{
+		{
+			[]string{"o:angrybirds:alice"},
+			"root:o:angrybirds", "root:o:angrybirds:alice",
+		},
+		{
+			[]string{"foo", "o:angrybirds:alice", "x:baz"},
+			"root:o:angrybirds", "root:o:angrybirds:alice",
+		},
+		{
+			[]string{"foo", "u:alice", "o:angrybirds:alice:device:phone", "x:baz"},
+			"root:o:angrybirds", "root:o:angrybirds:alice",
+		},
+		{
+			[]string{"foo", "u:bob", "o:todos:dave:friend:alice", "u:carol", "x:baz"},
+			"root:o:todos", "root:o:todos:dave",
+		},
+		{
+			[]string{"foo", "u:bob", "o:todos:dave:friend:alice", "u:carol", "o:todos:dave:device:phone", "x:baz"},
+			"root:o:todos", "root:o:todos:dave",
+		},
+		{
+			[]string{"u:bob"},
+			"...", "root:u:bob",
+		},
+		{
+			[]string{"foo", "u:bob", "x:baz"},
+			"...", "root:u:bob",
+		},
+		{
+			[]string{"foo", "u:bob:angrybirds", "u:bob:todos:phone", "x:baz"},
+			"...", "root:u:bob",
+		},
+	}
+
+	inferBlessingsFail = [][]string{
+		{},
+		{"foo", "x:baz"},
+		{"foo", "u:bob", "o:todos:dave:friend:alice", "o:angrybirds:dave", "o:todos:dave:device:phone", "x:baz"},
+		{"foo", "u:bob", "o:todos:dave:friend:alice", "o:todos:fred"},
+		{"foo", "u:bob:angrybirds", "u:bob:todos:phone", "u:carol", "u:dave", "x:baz"},
+	}
+
+	dummyPerms = access.Permissions{}.Add("root:u:nobody", string(access.Admin))
+)
+
+// TestIdBlessingInfer tests that Database/Collection/Syncgroup getter variants
+// taking a context and name correctly infer the Id blessing from the context,
+// failing when ambiguous.
+func TestIdBlessingInfer(t *testing.T) {
+	anchorPerms := access.Permissions{}.Add("root:u:admin", string(access.Admin)).Add("root", string(access.Resolve), string(access.Read), string(access.Write))
+	rootCtx, adminCtx, sName, rootp, cleanup := tu.SetupOrDieCustom("u:admin", "server", anchorPerms)
+	defer cleanup()
+	s := syncbase.NewService(sName)
+	rd := s.Database(adminCtx, "anchor", nil)
+	if err := rd.Create(adminCtx, anchorPerms); err != nil {
+		t.Fatalf("rd.Create() failed: %v", err)
+	}
+	rc := rd.Collection(adminCtx, "anchor")
+	if err := rc.Create(adminCtx, util.FilterTags(anchorPerms, wire.AllCollectionTags...)); err != nil {
+		t.Fatalf("rc.Create() failed: %v", err)
+	}
+	sgSpec := wire.SyncgroupSpec{
+		Description: "dummy syncgroup",
+		Collections: []wire.Id{rc.Id()},
+		Perms:       dummyPerms,
+	}
+	sgInfo := wire.SyncgroupMemberInfo{
+		SyncPriority: 42,
+	}
+
+	for i, test := range inferBlessingsOk {
+		ctx := forkContextMultiPattern(rootCtx, rootp, test.exts)
+		nameSuffix := fmt.Sprintf("ok_%02d", i)
+
+		d := s.Database(ctx, "db_"+nameSuffix, nil)
+		if got, want := d.Id(), (wire.Id{Blessing: test.wantApp, Name: "db_" + nameSuffix}); got != want {
+			t.Errorf("blessings %v: expected inferred database id %v, got %v", test.exts, want, got)
+		}
+		if err := d.Create(ctx, dummyPerms); err != nil {
+			t.Errorf("blessings %v: d.Create() failed: %v", test.exts, err)
+		}
+
+		c := rd.Collection(ctx, "cx_"+nameSuffix)
+		if got, want := c.Id(), (wire.Id{Blessing: test.wantUser, Name: "cx_" + nameSuffix}); got != want {
+			t.Errorf("blessings %v: expected inferred collection id %v, got %v", test.exts, want, got)
+		}
+		if err := c.Create(ctx, dummyPerms); err != nil {
+			t.Errorf("blessings %v: c.Create() failed: %v", test.exts, err)
+		}
+
+		sg := rd.Syncgroup(ctx, "sg_"+nameSuffix)
+		if got, want := sg.Id(), (wire.Id{Blessing: test.wantUser, Name: "sg_" + nameSuffix}); got != want {
+			t.Errorf("blessings %v: expected inferred syncgroup id %v, got %v", test.exts, want, got)
+		}
+		if err := sg.Create(ctx, sgSpec, sgInfo); err != nil {
+			t.Errorf("blessings %v: sg.Create() failed: %v", test.exts, err)
+		}
+	}
+
+	for i, exts := range inferBlessingsFail {
+		ctx := forkContextMultiPattern(rootCtx, rootp, exts)
+		nameSuffix := fmt.Sprintf("notok_%02d", i)
+
+		d := s.Database(ctx, "db_"+nameSuffix, nil)
+		if got, want := d.Id(), (wire.Id{Blessing: "$", Name: "db_" + nameSuffix}); got != want {
+			t.Errorf("blessings %v: expected inferred database id %v, got %v", exts, want, got)
+		}
+		if err := d.Create(ctx, dummyPerms); verror.ErrorID(err) != wire.ErrInvalidName.ID {
+			t.Errorf("blessings %v: d.Create() should have failed with ErrInvalidName, got: %v", exts, err)
+		}
+
+		c := rd.Collection(ctx, "cx_"+nameSuffix)
+		if got, want := c.Id(), (wire.Id{Blessing: "$", Name: "cx_" + nameSuffix}); got != want {
+			t.Errorf("blessings %v: expected inferred collection id %v, got %v", exts, want, got)
+		}
+		if err := c.Create(ctx, dummyPerms); verror.ErrorID(err) != wire.ErrInvalidName.ID {
+			t.Errorf("blessings %v: c.Create() should have failed with ErrInvalidName, got: %v", exts, err)
+		}
+
+		sg := rd.Syncgroup(ctx, "sg_"+nameSuffix)
+		if got, want := sg.Id(), (wire.Id{Blessing: "$", Name: "sg_" + nameSuffix}); got != want {
+			t.Errorf("blessings %v: expected inferred syncgroup id %v, got %v", exts, want, got)
+		}
+		if err := sg.Create(ctx, sgSpec, sgInfo); verror.ErrorID(err) != wire.ErrInvalidName.ID {
+			t.Errorf("blessings %v: sg.Create() should have failed with ErrInvalidName, got: %v", exts, err)
+		}
+	}
+}
+
+// TestIdBlessingInfer tests that Database/Collection Create() calls correctly
+// infer default perms from the context when nil perms are passed in, failing
+// when ambiguous.
+func TestCreatePermsInfer(t *testing.T) {
+	anchorPerms := access.Permissions{}.Add("root:u:admin", string(access.Admin)).Add("root", string(access.Resolve), string(access.Read), string(access.Write))
+	rootCtx, adminCtx, sName, rootp, cleanup := tu.SetupOrDieCustom("u:admin", "server", anchorPerms)
+	defer cleanup()
+	s := syncbase.NewService(sName)
+	rd := s.Database(adminCtx, "anchor", nil)
+	if err := rd.Create(adminCtx, anchorPerms); err != nil {
+		t.Fatalf("rd.Create() failed: %v", err)
+	}
+
+	for i, test := range inferBlessingsOk {
+		ctx := forkContextMultiPattern(rootCtx, rootp, test.exts)
+		nameSuffix := fmt.Sprintf("ok_%02d", i)
+
+		d := s.DatabaseForId(wire.Id{Blessing: "root", Name: "db_" + nameSuffix}, nil)
+		if err := d.Create(ctx, nil); err != nil {
+			t.Errorf("blessings %v: d.Create() failed: %v", test.exts, err)
+		}
+		if got, _, err := d.GetPermissions(ctx); err != nil {
+			t.Fatalf("d.GetPermissions() failed: %v", err)
+		} else if want := tu.DefaultPerms(wire.AllDatabaseTags, test.wantUser); !reflect.DeepEqual(got, want) {
+			t.Errorf("blessings %v: expected inferred database perms %v, got %v", test.exts, want, got)
+		}
+
+		c := rd.CollectionForId(wire.Id{Blessing: "root", Name: "cx_" + nameSuffix})
+		if err := c.Create(ctx, nil); err != nil {
+			t.Errorf("blessings %v: c.Create() failed: %v", test.exts, err)
+		}
+		if got, err := c.GetPermissions(ctx); err != nil {
+			t.Fatalf("c.GetPermissions() failed: %v", err)
+		} else if want := tu.DefaultPerms(wire.AllCollectionTags, test.wantUser); !reflect.DeepEqual(got, want) {
+			t.Errorf("blessings %v: expected inferred collection perms %v, got %v", test.exts, want, got)
+		}
+	}
+
+	for i, exts := range inferBlessingsFail {
+		if len(exts) == 0 {
+			continue
+		}
+		ctx := forkContextMultiPattern(rootCtx, rootp, exts)
+		nameSuffix := fmt.Sprintf("notok_%02d", i)
+
+		d := s.DatabaseForId(wire.Id{Blessing: "root", Name: "db_" + nameSuffix}, nil)
+		if err := d.Create(ctx, nil); verror.ErrorID(err) != wire.ErrInferDefaultPermsFailed.ID {
+			t.Errorf("blessings %v: d.Create() should have failed with ErrInferDefaultPermsFailed, got: %v", exts, err)
+		}
+		if err := d.Create(ctx, dummyPerms); err != nil {
+			t.Errorf("blessings %v: d.Create() with explicit perms failed: %v", exts, err)
+		}
+
+		c := rd.CollectionForId(wire.Id{Blessing: "root", Name: "cx_" + nameSuffix})
+		if err := c.Create(ctx, nil); verror.ErrorID(err) != wire.ErrInferDefaultPermsFailed.ID {
+			t.Errorf("blessings %v: c.Create() should have failed with ErrInferDefaultPermsFailed, got: %v", exts, err)
+		}
+		if err := c.Create(ctx, dummyPerms); err != nil {
+			t.Errorf("blessings %v: c.Create() with explicit perms failed: %v", exts, err)
+		}
+	}
+}
+
+func forkContextMultiPattern(rootCtx *context.T, rootp security.Principal, extensions []string) *context.T {
+	p, _ := vsecurity.NewPrincipal()
+	db, _ := rootp.BlessingStore().Default()
+	security.AddToRoots(p, db)
+	bs := make([]security.Blessings, 0, len(extensions))
+	for _, ext := range extensions {
+		b, _ := rootp.Bless(p.PublicKey(), db, ext, security.UnconstrainedUse())
+		bs = append(bs, b)
+	}
+	b, _ := security.UnionOfBlessings(bs...)
+	p.BlessingStore().SetDefault(b)
+	p.BlessingStore().Set(b, "...")
+	ctx, _ := v23.WithPrincipal(rootCtx, p)
+	return ctx
+}
+
+// TestCreateIdBlessingEnforce tests that only clients matching the blessing
+// pattern in the Id are allowed to create a Database/Collection/Syncgroup.
+// This requirement is waived for admin clients for Database, but not Collection
+// or Syncgroup creation.
+func TestCreateIdBlessingEnforce(t *testing.T) {
+	anchorPerms := access.Permissions{}.Add("root:u:admin", string(access.Admin)).Add("root", string(access.Resolve), string(access.Read), string(access.Write))
+	rootCtx, adminCtx, sName, rootp, cleanup := tu.SetupOrDieCustom("u:admin", "server", anchorPerms)
+	defer cleanup()
+	clientCtx := tu.NewCtx(rootCtx, rootp, "u:client")
+	s := syncbase.NewService(sName)
+	rd := s.Database(adminCtx, "anchor", nil)
+	if err := rd.Create(adminCtx, anchorPerms); err != nil {
+		t.Fatalf("rd.Create() failed: %v", err)
+	}
+	rc := rd.Collection(adminCtx, "anchor")
+	if err := rc.Create(adminCtx, util.FilterTags(anchorPerms, wire.AllCollectionTags...)); err != nil {
+		t.Fatalf("rc.Create() failed: %v", err)
+	}
+	sgSpec := wire.SyncgroupSpec{
+		Description: "dummy syncgroup",
+		Collections: []wire.Id{rc.Id()},
+		Perms:       dummyPerms,
+	}
+	sgInfo := wire.SyncgroupMemberInfo{
+		SyncPriority: 42,
+	}
+
+	patternsOk := []string{"...", "root", "root:u", "root:u:client", "root:u:client:$"}
+	patternsFail := []string{"root:$", "root:u:$", "root:u:admin", "root:u:client:phone", "foobar"}
+
+	for i, pattern := range patternsOk {
+		nameSuffix := fmt.Sprintf("ok_%02d", i)
+
+		d := s.DatabaseForId(wire.Id{Blessing: pattern, Name: "db_" + nameSuffix}, nil)
+		if err := d.Create(clientCtx, dummyPerms); err != nil {
+			t.Errorf("blessing %q: d.Create() failed: %v", pattern, err)
+		}
+
+		c := rd.CollectionForId(wire.Id{Blessing: pattern, Name: "cx_" + nameSuffix})
+		if err := c.Create(clientCtx, dummyPerms); err != nil {
+			t.Errorf("blessing %q: c.Create() failed: %v", pattern, err)
+		}
+
+		sg := rd.SyncgroupForId(wire.Id{Blessing: pattern, Name: "sg_" + nameSuffix})
+		if err := sg.Create(clientCtx, sgSpec, sgInfo); err != nil {
+			t.Errorf("blessing %q: sg.Create() failed: %v", pattern, err)
+		}
+	}
+
+	for i, pattern := range patternsFail {
+		nameSuffix := fmt.Sprintf("notok_%02d", i)
+
+		d := s.DatabaseForId(wire.Id{Blessing: pattern, Name: "db_" + nameSuffix}, nil)
+		if err := d.Create(clientCtx, dummyPerms); verror.ErrorID(err) != wire.ErrUnauthorizedCreateId.ID {
+			t.Errorf("blessing %q: d.Create() should have failed with ErrUnauthorizedCreateId, got: %v", pattern, err)
+		}
+
+		c := rd.CollectionForId(wire.Id{Blessing: pattern, Name: "cx_" + nameSuffix})
+		if err := c.Create(clientCtx, dummyPerms); verror.ErrorID(err) != wire.ErrUnauthorizedCreateId.ID {
+			t.Errorf("blessing %q: c.Create() should have failed with ErrUnauthorizedCreateId, got: %v", pattern, err)
+		}
+
+		sg := rd.SyncgroupForId(wire.Id{Blessing: pattern, Name: "sg_" + nameSuffix})
+		if err := sg.Create(clientCtx, sgSpec, sgInfo); verror.ErrorID(err) != wire.ErrUnauthorizedCreateId.ID {
+			t.Errorf("blessing %q: sg.Create() should have failed with ErrUnauthorizedCreateId, got: %v", pattern, err)
+		}
+	}
+
+	// Give full admin permissions to the client.
+	clientAdminPerms := anchorPerms.Copy().Add("root:u:client", string(access.Admin))
+	if err := s.SetPermissions(adminCtx, clientAdminPerms, ""); err != nil {
+		t.Fatalf("s.SetPermissions() failed: %v", err)
+	}
+	if err := rd.SetPermissions(adminCtx, clientAdminPerms, ""); err != nil {
+		t.Fatalf("rd.SetPermissions() failed: %v", err)
+	}
+	if err := rc.SetPermissions(adminCtx, util.FilterTags(clientAdminPerms, wire.AllCollectionTags...)); err != nil {
+		t.Fatalf("rc.SetPermissions() failed: %v", err)
+	}
+
+	// Admin permissions override the Id blessing enforcement for databases, but
+	// not for collections or syncgroups.
+	for i, pattern := range patternsFail {
+		nameSuffix := fmt.Sprintf("admin_%02d", i)
+
+		d := s.DatabaseForId(wire.Id{Blessing: pattern, Name: "db_" + nameSuffix}, nil)
+		if err := d.Create(clientCtx, dummyPerms); err != nil {
+			t.Errorf("blessing %q: admin d.Create() failed: %v", pattern, err)
+		}
+
+		c := rd.CollectionForId(wire.Id{Blessing: pattern, Name: "cx_" + nameSuffix})
+		if err := c.Create(clientCtx, dummyPerms); verror.ErrorID(err) != wire.ErrUnauthorizedCreateId.ID {
+			t.Errorf("blessing %q: admin c.Create() should have failed with ErrUnauthorizedCreateId, got: %v", pattern, err)
+		}
+
+		sg := rd.SyncgroupForId(wire.Id{Blessing: pattern, Name: "sg_" + nameSuffix})
+		if err := sg.Create(clientCtx, sgSpec, sgInfo); verror.ErrorID(err) != wire.ErrUnauthorizedCreateId.ID {
+			t.Errorf("blessing %q: admin sg.Create() should have failed with ErrUnauthorizedCreateId, got: %v", pattern, err)
+		}
+	}
+}
+
+// TestPermsValidation tests that all operations that create or modify
+// permissions properly validate the new permissions.
+func TestPermsValidation(t *testing.T) {
+	ctx, sName, cleanup := tu.SetupOrDie(nil)
+	defer cleanup()
+	s := syncbase.NewService(sName)
+	rd := tu.CreateDatabase(t, ctx, s, "anchor")
+	rc := tu.CreateCollection(t, ctx, rd, "anchor")
+	rsg := tu.CreateSyncgroup(t, ctx, rd, rc, "anchor", "anchor syncgroup")
+
+	permsEmpty := access.Permissions{}
+	permsNoAdmin := access.Permissions{}.Add("root:o:app:client", string(access.Read))
+	permsXRWAD := access.Permissions{}.Add("root:o:app:client", access.TagStrings(access.AllTypicalTags()...)...)
+	permsXRWA := access.Permissions{}.Add("root:o:app:client", access.TagStrings(wire.AllDatabaseTags...)...)
+	permsRWA := access.Permissions{}.Add("root:o:app:client", access.TagStrings(wire.AllCollectionTags...)...)
+	permsRA := access.Permissions{}.Add("root:o:app:client", access.TagStrings(wire.AllSyncgroupTags...)...)
+	permsAdminOnly := access.Permissions{}.Add("root:o:app:client", string(access.Admin))
+	permsComplex := permsRA.Copy().Add("root:u:client", string(access.Read)).Blacklist("root:o:app:client:phone", string(access.Admin))
+
+	testPermsValidationOp(t, "d.Create()",
+		[]access.Permissions{nil /* infer */, permsAdminOnly, permsComplex, permsRA, permsRWA, permsXRWA},
+		[]access.Permissions{permsEmpty, permsNoAdmin, permsXRWAD},
+		func(nameSuffix string, perms access.Permissions) error {
+			d := s.DatabaseForId(wire.Id{Blessing: "root", Name: "db_" + nameSuffix}, nil)
+			return d.Create(ctx, perms)
+		})
+
+	testPermsValidationOp(t, "c.Create()",
+		[]access.Permissions{nil /* infer */, permsAdminOnly, permsComplex, permsRA, permsRWA},
+		[]access.Permissions{permsEmpty, permsNoAdmin, permsXRWA, permsXRWAD},
+		func(nameSuffix string, perms access.Permissions) error {
+			c := rd.CollectionForId(wire.Id{Blessing: "root", Name: "cx_" + nameSuffix})
+			return c.Create(ctx, perms)
+		})
+
+	testPermsValidationOp(t, "sg.Create()",
+		[]access.Permissions{permsAdminOnly, permsComplex, permsRA},
+		[]access.Permissions{nil, permsEmpty, permsNoAdmin, permsRWA, permsXRWA, permsXRWAD},
+		func(nameSuffix string, perms access.Permissions) error {
+			sg := rd.SyncgroupForId(wire.Id{Blessing: "root", Name: "sg_" + nameSuffix})
+			return sg.Create(ctx, wire.SyncgroupSpec{
+				Collections: []wire.Id{rc.Id()},
+				Perms:       perms,
+			}, wire.SyncgroupMemberInfo{SyncPriority: 42})
+		})
+
+	testPermsValidationOp(t, "s.SetPermissions()",
+		[]access.Permissions{permsAdminOnly, permsComplex, permsRA, permsRWA, permsXRWA, permsXRWAD},
+		[]access.Permissions{nil, permsEmpty, permsNoAdmin},
+		func(_ string, perms access.Permissions) error {
+			return s.SetPermissions(ctx, perms, "")
+		})
+
+	testPermsValidationOp(t, "d.SetPermissions()",
+		[]access.Permissions{permsAdminOnly, permsComplex, permsRA, permsRWA, permsXRWA},
+		[]access.Permissions{nil, permsEmpty, permsNoAdmin, permsXRWAD},
+		func(_ string, perms access.Permissions) error {
+			return rd.SetPermissions(ctx, perms, "")
+		})
+
+	testPermsValidationOp(t, "c.SetPermissions()",
+		[]access.Permissions{permsAdminOnly, permsComplex, permsRA, permsRWA},
+		[]access.Permissions{nil, permsEmpty, permsNoAdmin, permsXRWA, permsXRWAD},
+		func(_ string, perms access.Permissions) error {
+			return rc.SetPermissions(ctx, perms)
+		})
+
+	testPermsValidationOp(t, "sg.SetSpec()",
+		[]access.Permissions{permsAdminOnly, permsComplex, permsRA},
+		[]access.Permissions{nil, permsEmpty, permsNoAdmin, permsRWA, permsXRWA, permsXRWAD},
+		func(_ string, perms access.Permissions) error {
+			return rsg.SetSpec(ctx, wire.SyncgroupSpec{
+				Collections: []wire.Id{rc.Id()},
+				Perms:       perms,
+			}, "")
+		})
+}
+
+func testPermsValidationOp(t *testing.T, desc string, validPerms, invalidPerms []access.Permissions, op func(nameSuffix string, perms access.Permissions) error) {
+	for i, p := range validPerms {
+		nameSuffix := fmt.Sprintf("ok_%02d", i)
+		if err := op(nameSuffix, p); err != nil {
+			t.Errorf("perms %v: %s failed: %v", p, desc, err)
+		}
+	}
+
+	for i, p := range invalidPerms {
+		nameSuffix := fmt.Sprintf("notok_%02d", i)
+		if err := op(nameSuffix, p); err == nil {
+			t.Errorf("perms %v: %s should have failed", p, desc)
+		}
+	}
 }
diff --git a/syncbase/service.go b/syncbase/service.go
index 619e262..282295a 100644
--- a/syncbase/service.go
+++ b/syncbase/service.go
@@ -5,10 +5,13 @@
 package syncbase
 
 import (
+	"v.io/v23"
 	"v.io/v23/context"
+	"v.io/v23/security"
 	"v.io/v23/security/access"
 	wire "v.io/v23/services/syncbase"
 	"v.io/v23/syncbase/util"
+	"v.io/v23/verror"
 )
 
 func NewService(fullName string) Service {
@@ -32,12 +35,14 @@
 
 // Database implements Service.Database.
 func (s *service) Database(ctx *context.T, name string, schema *Schema) Database {
-	blessing, err := util.AppBlessingFromContext(ctx)
+	app, _, err := util.AppAndUserPatternFromBlessings(security.DefaultBlessingNames(v23.GetPrincipal(ctx))...)
 	if err != nil {
-		// TODO(sadovsky): Return invalid Database handle.
-		panic(err)
+		ctx.Error(verror.New(wire.ErrInferAppBlessingFailed, ctx, "Database", name, err))
+		// A handle with a no-match Id blessing is returned, so all RPCs will fail.
+		// TODO(ivanpi): Return the more specific error from RPCs instead of logging
+		// it here.
 	}
-	return newDatabase(s.fullName, wire.Id{Blessing: blessing, Name: name}, schema)
+	return newDatabase(s.fullName, wire.Id{Blessing: string(app), Name: name}, schema)
 }
 
 // DatabaseForId implements Service.DatabaseForId.
diff --git a/syncbase/syncgroup_test.go b/syncbase/syncgroup_test.go
index 4e26d13..4ddd5e8 100644
--- a/syncbase/syncgroup_test.go
+++ b/syncbase/syncgroup_test.go
@@ -9,7 +9,6 @@
 	"testing"
 
 	"v.io/v23/context"
-	"v.io/v23/security"
 	"v.io/v23/security/access"
 	wire "v.io/v23/services/syncbase"
 	"v.io/v23/syncbase"
@@ -17,17 +16,15 @@
 	tu "v.io/x/ref/services/syncbase/testutil"
 )
 
-var testCollection = wire.Id{"v.io:u:sam", "c"}
-
 // Tests that Syncgroup.Create works as expected.
 func TestCreateSyncgroup(t *testing.T) {
-	ctx, sName, cleanup := tu.SetupOrDie(perms("root:client"))
+	ctx, sName, cleanup := tu.SetupOrDie(tu.DefaultPerms(access.AllTypicalTags(), "root:o:app:client"))
 	defer cleanup()
 	d := tu.CreateDatabase(t, ctx, syncbase.NewService(sName), "d")
 
 	// Check if create fails with empty spec.
 	spec := wire.SyncgroupSpec{}
-	sg1 := wire.Id{Name: "sg1", Blessing: "b1"}
+	sg1 := d.Syncgroup(ctx, "sg1").Id()
 
 	createSyncgroup(t, ctx, d, sg1, spec, verror.ErrBadArg.ID)
 
@@ -46,8 +43,8 @@
 	// Create successfully.
 	spec = wire.SyncgroupSpec{
 		Description: "test syncgroup sg1",
-		Perms:       nil,
-		Collections: []wire.Id{testCollection},
+		Perms:       tu.DefaultPerms(wire.AllSyncgroupTags, "root:o:app:client"),
+		Collections: []wire.Id{c.Id()},
 	}
 	createSyncgroup(t, ctx, d, sg1, spec, verror.ID(""))
 
@@ -62,7 +59,7 @@
 
 	// Create a peer syncgroup.
 	spec.Description = "test syncgroup sg2"
-	sg2 := wire.Id{Name: "sg2", Blessing: "b2"}
+	sg2 := d.Syncgroup(ctx, "sg2").Id()
 	createSyncgroup(t, ctx, d, sg2, spec, verror.ID(""))
 
 	wantGroups = []wire.Id{sg1, sg2}
@@ -72,19 +69,19 @@
 	// Check if creating a syncgroup on a non-existing collection fails.
 	spec.Description = "test syncgroup sg3"
 	spec.Collections = []wire.Id{wire.Id{"u", "c1"}}
-	sg3 := wire.Id{Name: "sg3", Blessing: "b3"}
+	sg3 := d.Syncgroup(ctx, "sg3").Id()
 	createSyncgroup(t, ctx, d, sg3, spec, verror.ErrNoExist.ID)
 	verifySyncgroups(t, ctx, d, wantGroups, verror.ID(""))
 
 	// Check that create fails if the perms disallow access.
-	perms := perms("root:client")
-	perms.Blacklist("root:client", string(access.Read))
+	perms := tu.DefaultPerms(wire.AllDatabaseTags, "root:o:app:client")
+	perms.Blacklist("root:o:app:client", string(access.Read))
 	if err := d.SetPermissions(ctx, perms, ""); err != nil {
 		t.Fatalf("d.SetPermissions() failed: %v", err)
 	}
 	spec.Description = "test syncgroup sg4"
 	spec.Collections = []wire.Id{wire.Id{"u", "c"}}
-	sg4 := wire.Id{Name: "sg4", Blessing: "b4"}
+	sg4 := d.Syncgroup(ctx, "sg4").Id()
 	createSyncgroup(t, ctx, d, sg4, spec, verror.ErrNoAccess.ID)
 	verifySyncgroups(t, ctx, d, nil, verror.ErrNoAccess.ID)
 }
@@ -94,25 +91,25 @@
 // join it.
 func TestJoinSyncgroup(t *testing.T) {
 	// Create client1-server pair.
-	ctx, ctx1, sName, rootp, cleanup := tu.SetupOrDieCustom("client1", "server", perms("root:client1"))
+	ctx, ctx1, sName, rootp, cleanup := tu.SetupOrDieCustom("o:app:client1", "server", tu.DefaultPerms(access.AllTypicalTags(), "root:o:app:client1"))
 	defer cleanup()
 
 	d1 := tu.CreateDatabase(t, ctx1, syncbase.NewService(sName), "d")
-	tu.CreateCollection(t, ctx1, d1, "c")
+	c := tu.CreateCollection(t, ctx1, d1, "c")
 	specA := wire.SyncgroupSpec{
 		Description: "test syncgroup sgA",
-		Perms:       perms("root:client1"),
-		Collections: []wire.Id{testCollection},
+		Perms:       tu.DefaultPerms(wire.AllSyncgroupTags, "root:o:app:client1"),
+		Collections: []wire.Id{c.Id()},
 	}
-	sgIdA := wire.Id{Name: "sgA", Blessing: "bA"}
+	sgIdA := wire.Id{Name: "sgA", Blessing: "root:o:app:client1"}
 	createSyncgroup(t, ctx1, d1, sgIdA, specA, verror.ID(""))
 
 	// Check that creator can call join successfully.
 	joinSyncgroup(t, ctx1, d1, sName, sgIdA, verror.ID(""))
 
 	// Create client2.
-	ctx2 := tu.NewCtx(ctx, rootp, "client2")
-	d2 := syncbase.NewService(sName).DatabaseForId(tu.DbId("d"), nil)
+	ctx2 := tu.NewCtx(ctx, rootp, "o:app:client2")
+	d2 := syncbase.NewService(sName).Database(ctx2, "d", nil)
 
 	// Check that client2's join fails if the perms disallow access.
 	joinSyncgroup(t, ctx2, d2, sName, sgIdA, verror.ErrNoAccess.ID)
@@ -120,12 +117,12 @@
 	verifySyncgroups(t, ctx2, d2, nil, verror.ErrNoAccess.ID)
 
 	// Client1 gives access to client2.
-	if err := d1.SetPermissions(ctx1, perms("root:client1", "root:client2"), ""); err != nil {
+	if err := d1.SetPermissions(ctx1, tu.DefaultPerms(wire.AllDatabaseTags, "root:o:app:client1", "root:o:app:client2"), ""); err != nil {
 		t.Fatalf("d.SetPermissions() failed: %v", err)
 	}
 
 	// Verify client2 has access.
-	if err := d2.SetPermissions(ctx2, perms("root:client1", "root:client2"), ""); err != nil {
+	if err := d2.SetPermissions(ctx2, tu.DefaultPerms(wire.AllDatabaseTags, "root:o:app:client1", "root:o:app:client2"), ""); err != nil {
 		t.Fatalf("d.SetPermissions() failed: %v", err)
 	}
 
@@ -135,10 +132,10 @@
 	// Create a different syncgroup.
 	specB := wire.SyncgroupSpec{
 		Description: "test syncgroup sgB",
-		Perms:       perms("root:client1", "root:client2"),
-		Collections: []wire.Id{testCollection},
+		Perms:       tu.DefaultPerms(wire.AllSyncgroupTags, "root:o:app:client1", "root:o:app:client2"),
+		Collections: []wire.Id{c.Id()},
 	}
-	sgIdB := wire.Id{Name: "sgB", Blessing: "bB"}
+	sgIdB := wire.Id{Name: "sgB", Blessing: "root:o:app:client1"}
 	createSyncgroup(t, ctx1, d1, sgIdB, specB, verror.ID(""))
 
 	// Check that client2's join now succeeds.
@@ -155,17 +152,17 @@
 
 // Tests that Syncgroup.SetSpec works as expected.
 func TestSetSpecSyncgroup(t *testing.T) {
-	ctx, sName, cleanup := tu.SetupOrDie(perms("root:client"))
+	ctx, sName, cleanup := tu.SetupOrDie(tu.DefaultPerms(access.AllTypicalTags(), "root:o:app:client"))
 	defer cleanup()
 	d := tu.CreateDatabase(t, ctx, syncbase.NewService(sName), "d")
-	tu.CreateCollection(t, ctx, d, "c")
+	c := tu.CreateCollection(t, ctx, d, "c")
 
 	// Create successfully.
-	sgId := wire.Id{Name: "sg1", Blessing: "b1"}
+	sgId := wire.Id{Name: "sg1", Blessing: "root:o:app:client"}
 	spec := wire.SyncgroupSpec{
 		Description: "test syncgroup sg1",
-		Perms:       perms("root:client"),
-		Collections: []wire.Id{testCollection},
+		Perms:       tu.DefaultPerms(wire.AllSyncgroupTags, "root:o:app:client"),
+		Collections: []wire.Id{c.Id()},
 	}
 	createSyncgroup(t, ctx, d, sgId, spec, verror.ID(""))
 
@@ -175,7 +172,7 @@
 	verifySyncgroupInfo(t, ctx, d, sgId, spec, 1)
 
 	spec.Description = "test syncgroup sg1 update"
-	spec.Perms = perms("root:client", "root:client1")
+	spec.Perms = tu.DefaultPerms(wire.AllSyncgroupTags, "root:o:app:client", "root:o:app:client1")
 
 	sg := d.SyncgroupForId(sgId)
 	if err := sg.SetSpec(ctx, spec, ""); err != nil {
@@ -227,15 +224,3 @@
 		t.Fatalf("sg.GetMembers() failed, got %v, want %v, err %v", members, wantMembers, err)
 	}
 }
-
-// TODO(sadovsky): This appears to be identical to tu.DefaultPerms(). We should
-// just use that.
-func perms(bps ...string) access.Permissions {
-	perms := access.Permissions{}
-	for _, bp := range bps {
-		for _, tag := range access.AllTypicalTags() {
-			perms.Add(security.BlessingPattern(bp), string(tag))
-		}
-	}
-	return perms
-}
diff --git a/syncbase/util/.api b/syncbase/util/.api
index 7210dd2..951fa94 100644
--- a/syncbase/util/.api
+++ b/syncbase/util/.api
@@ -1,18 +1,24 @@
-pkg util, func AppBlessingFromContext(*context.T) (string, error)
+pkg util, func AppAndUserPatternFromBlessings(...string) (security.BlessingPattern, security.BlessingPattern, error)
 pkg util, func Decode(string) (string, error)
 pkg util, func DecodeId(string) (wire.Id, error)
 pkg util, func Encode(string) string
 pkg util, func EncodeId(wire.Id) string
+pkg util, func FilterTags(access.Permissions, ...access.Tag) access.Permissions
 pkg util, func IsPrefix(string, string) bool
 pkg util, func ListChildIds(*context.T, string) ([]wire.Id, error)
+pkg util, func NewErrFoundMultipleAppUserBlessings(*context.T, string, string) error
+pkg util, func NewErrFoundMultipleUserBlessings(*context.T, string, string) error
+pkg util, func NewErrFoundNoConventionalBlessings(*context.T) error
 pkg util, func ParseCollectionRowPair(*context.T, string) (wire.Id, string, error)
 pkg util, func PrefixRangeLimit(string) string
 pkg util, func PrefixRangeStart(string) string
 pkg util, func RowPrefixPattern(wire.Id, string) wire.CollectionRowPattern
 pkg util, func SortIds([]wire.Id)
-pkg util, func UserBlessingFromContext(*context.T) (string, error)
 pkg util, func ValidateId(wire.Id) error
 pkg util, func ValidateRowKey(string) error
 pkg util, type AccessController interface { GetPermissions, SetPermissions }
 pkg util, type AccessController interface, GetPermissions(*context.T) (access.Permissions, string, error)
 pkg util, type AccessController interface, SetPermissions(*context.T, access.Permissions, string) error
+pkg util, var ErrFoundMultipleAppUserBlessings unknown-type
+pkg util, var ErrFoundMultipleUserBlessings unknown-type
+pkg util, var ErrFoundNoConventionalBlessings unknown-type
diff --git a/syncbase/util/errors.vdl b/syncbase/util/errors.vdl
new file mode 100644
index 0000000..a27756d
--- /dev/null
+++ b/syncbase/util/errors.vdl
@@ -0,0 +1,11 @@
+// Copyright 2016 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 util
+
+error (
+  FoundMultipleAppUserBlessings(au1, au2 string) {"en": "found multiple app:user blessings for different apps or users: {au1} and {au2}"}
+  FoundMultipleUserBlessings(u1, u2 string) {"en": "found multiple user blessings for different users: {u1} and {u2}"}
+  FoundNoConventionalBlessings()  {"en": "found no blessings matching app:user or user conventions"}
+)
diff --git a/syncbase/util/util.go b/syncbase/util/util.go
index 748390c..261cda0 100644
--- a/syncbase/util/util.go
+++ b/syncbase/util/util.go
@@ -10,6 +10,7 @@
 	"strings"
 
 	"v.io/v23/context"
+	"v.io/v23/conventions"
 	"v.io/v23/naming"
 	"v.io/v23/query/pattern"
 	"v.io/v23/security"
@@ -78,6 +79,7 @@
 
 // ValidateId returns nil iff the given Id is a valid database, collection, or
 // syncgroup Id.
+// TODO(ivanpi): Use verror.New instead of fmt.Errorf everywhere.
 func ValidateId(id wire.Id) error {
 	if x := len([]byte(id.Blessing)); x == 0 {
 		return fmt.Errorf("Id blessing cannot be empty")
@@ -89,7 +91,9 @@
 	} else if x > maxNameLen {
 		return fmt.Errorf("Id name %q exceeds %d bytes", id.Name, maxNameLen)
 	}
-	if !security.BlessingPattern(id.Blessing).IsValid() {
+	if bp := security.BlessingPattern(id.Blessing); bp == security.NoExtension {
+		return fmt.Errorf("Id blessing %q cannot match any blessings, check blessing conventions", id.Blessing)
+	} else if !bp.IsValid() {
 		return fmt.Errorf("Id blessing %q is not a valid blessing pattern", id.Blessing)
 	}
 	if containsAnyOf(id.Blessing, reservedBytes) {
@@ -185,20 +189,56 @@
 	GetPermissions(ctx *context.T) (perms access.Permissions, version string, err error)
 }
 
-// AppBlessingFromContext returns an app blessing pattern from the given
-// context.
-// TODO(sadovsky,ashankar): Implement.
-func AppBlessingFromContext(ctx *context.T) (string, error) {
-	// NOTE(sadovsky): For now, we use a blessing string that will be easy to
-	// find-replace when we actually implement this method.
-	return "v.io:a:xyz", nil
+// AppAndUserPatternFromBlessings infers the app and user blessing pattern from
+// the given set of blessing names.
+// <idp>:o:<app>:<user> blessings are preferred, with a fallback to
+// <idp>:u:<user> and unrestricted app. Returns an error and no-match patterns
+// if the inferred pattern is ambiguous (multiple blessings for different apps
+// or users are found), or if no blessings matching conventions are found.
+// TODO(ivanpi): Allow caller to restrict format to app:user or user instead of
+// automatic fallback?
+func AppAndUserPatternFromBlessings(blessings ...string) (app, user security.BlessingPattern, err error) {
+	pbs := conventions.ParseBlessingNames(blessings...)
+	found := false
+	// Find a blessing of the form app:user; ensure there is only one app:user pair.
+	for _, b := range pbs {
+		a, au := b.AppPattern(), b.AppUserPattern()
+		if a != security.NoExtension && au != security.NoExtension {
+			if found && (a != app || au != user) {
+				return security.NoExtension, security.NoExtension, NewErrFoundMultipleAppUserBlessings(nil, string(user), string(au))
+			}
+			app, user, found = a, au, true
+		}
+	}
+	if found {
+		return app, user, nil
+	}
+	// Fall back to a user blessing; ensure there is only one user.
+	for _, b := range pbs {
+		u := b.UserPattern()
+		if u != security.NoExtension {
+			if found && (u != user) {
+				return security.NoExtension, security.NoExtension, NewErrFoundMultipleUserBlessings(nil, string(user), string(u))
+			}
+			app, user, found = security.AllPrincipals, u, true
+		}
+	}
+	if found {
+		return app, user, nil
+	}
+	// No app:user or user blessings found.
+	return security.NoExtension, security.NoExtension, NewErrFoundNoConventionalBlessings(nil)
 }
 
-// UserBlessingFromContext returns a user blessing pattern from the given
-// context.
-// TODO(sadovsky,ashankar): Implement.
-func UserBlessingFromContext(ctx *context.T) (string, error) {
-	// NOTE(sadovsky): For now, we use a blessing string that will be easy to
-	// find-replace when we actually implement this method.
-	return "v.io:u:sam", nil
+// FilterTags returns a copy of the provided perms, filtered to include only
+// entries for tags allowed by the allowTags whitelist.
+func FilterTags(perms access.Permissions, allowTags ...access.Tag) (filtered access.Permissions) {
+	filtered = access.Permissions{}
+	for _, allowTag := range allowTags {
+		if acl, ok := perms[string(allowTag)]; ok {
+			filtered[string(allowTag)] = acl
+		}
+	}
+	// Copy to make sure lists in ACLs don't share backing arrays.
+	return filtered.Copy()
 }
diff --git a/syncbase/util/util.vdl.go b/syncbase/util/util.vdl.go
new file mode 100644
index 0000000..5fb9256
--- /dev/null
+++ b/syncbase/util/util.vdl.go
@@ -0,0 +1,69 @@
+// Copyright 2016 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.
+
+// This file was auto-generated by the vanadium vdl tool.
+// Package: util
+
+package util
+
+import (
+	"v.io/v23/context"
+	"v.io/v23/i18n"
+	"v.io/v23/verror"
+)
+
+var _ = __VDLInit() // Must be first; see __VDLInit comments for details.
+
+//////////////////////////////////////////////////
+// Error definitions
+
+var (
+	ErrFoundMultipleAppUserBlessings = verror.Register("v.io/v23/syncbase/util.FoundMultipleAppUserBlessings", verror.NoRetry, "{1:}{2:} found multiple app:user blessings for different apps or users: {3} and {4}")
+	ErrFoundMultipleUserBlessings    = verror.Register("v.io/v23/syncbase/util.FoundMultipleUserBlessings", verror.NoRetry, "{1:}{2:} found multiple user blessings for different users: {3} and {4}")
+	ErrFoundNoConventionalBlessings  = verror.Register("v.io/v23/syncbase/util.FoundNoConventionalBlessings", verror.NoRetry, "{1:}{2:} found no blessings matching app:user or user conventions")
+)
+
+// NewErrFoundMultipleAppUserBlessings returns an error with the ErrFoundMultipleAppUserBlessings ID.
+func NewErrFoundMultipleAppUserBlessings(ctx *context.T, au1 string, au2 string) error {
+	return verror.New(ErrFoundMultipleAppUserBlessings, ctx, au1, au2)
+}
+
+// NewErrFoundMultipleUserBlessings returns an error with the ErrFoundMultipleUserBlessings ID.
+func NewErrFoundMultipleUserBlessings(ctx *context.T, u1 string, u2 string) error {
+	return verror.New(ErrFoundMultipleUserBlessings, ctx, u1, u2)
+}
+
+// NewErrFoundNoConventionalBlessings returns an error with the ErrFoundNoConventionalBlessings ID.
+func NewErrFoundNoConventionalBlessings(ctx *context.T) error {
+	return verror.New(ErrFoundNoConventionalBlessings, ctx)
+}
+
+var __VDLInitCalled bool
+
+// __VDLInit performs vdl initialization.  It is safe to call multiple times.
+// If you have an init ordering issue, just insert the following line verbatim
+// into your source files in this package, right after the "package foo" clause:
+//
+//    var _ = __VDLInit()
+//
+// The purpose of this function is to ensure that vdl initialization occurs in
+// the right order, and very early in the init sequence.  In particular, vdl
+// registration and package variable initialization needs to occur before
+// functions like vdl.TypeOf will work properly.
+//
+// This function returns a dummy value, so that it can be used to initialize the
+// first var in the file, to take advantage of Go's defined init order.
+func __VDLInit() struct{} {
+	if __VDLInitCalled {
+		return struct{}{}
+	}
+	__VDLInitCalled = true
+
+	// Set error format strings.
+	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrFoundMultipleAppUserBlessings.ID), "{1:}{2:} found multiple app:user blessings for different apps or users: {3} and {4}")
+	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrFoundMultipleUserBlessings.ID), "{1:}{2:} found multiple user blessings for different users: {3} and {4}")
+	i18n.Cat().SetWithBase(i18n.LangID("en"), i18n.MsgID(ErrFoundNoConventionalBlessings.ID), "{1:}{2:} found no blessings matching app:user or user conventions")
+
+	return struct{}{}
+}
diff --git a/syncbase/util/util_test.go b/syncbase/util/util_test.go
index 10bad78..f89331f 100644
--- a/syncbase/util/util_test.go
+++ b/syncbase/util/util_test.go
@@ -5,11 +5,15 @@
 package util_test
 
 import (
+	"fmt"
+	"reflect"
 	"strings"
 	"testing"
 
+	"v.io/v23/security/access"
 	wire "v.io/v23/services/syncbase"
 	"v.io/v23/syncbase/util"
+	"v.io/v23/verror"
 	tu "v.io/x/ref/services/syncbase/testutil"
 )
 
@@ -153,3 +157,180 @@
 		}
 	}
 }
+
+func TestAppAndUserPatternFromBlessings(t *testing.T) {
+	for _, test := range []struct {
+		blessings []string
+		wantApp   string
+		wantUser  string
+		wantErr   error
+	}{
+		{
+			[]string{},
+			"$",
+			"$",
+			util.NewErrFoundNoConventionalBlessings(nil),
+		},
+		{
+			[]string{"foo", "bar:x:baz"},
+			"$",
+			"$",
+			util.NewErrFoundNoConventionalBlessings(nil),
+		},
+		// non-conventional blessings are ignored if a conventional blessing is present
+		{
+			[]string{"foo", "root:o:angrybirds:alice", "bar:x:baz"},
+			"root:o:angrybirds",
+			"root:o:angrybirds:alice",
+			nil,
+		},
+		// user blessings are ignored when an app:user blessing is present
+		{
+			[]string{"foo", "root:u:alice", "root:o:angrybirds:alice:device:phone", "bar:x:baz"},
+			"root:o:angrybirds",
+			"root:o:angrybirds:alice",
+			nil,
+		},
+		// user blessings are ignored when an app:user blessing is present, even if multiple
+		{
+			[]string{"foo", "root:u:bob", "root:o:todos:dave:friend:alice", "root:u:carol", "bar:x:baz"},
+			"root:o:todos",
+			"root:o:todos:dave",
+			nil,
+		},
+		// multiple blessings for the same app:user are allowed
+		{
+			[]string{"foo", "root:u:bob", "root:o:todos:dave:friend:alice", "root:u:carol", "root:o:todos:dave:device:phone", "bar:x:baz"},
+			"root:o:todos",
+			"root:o:todos:dave",
+			nil,
+		},
+		// multiple blessings for different apps, users, or identity providers are not allowed
+		{
+			[]string{"foo", "root:u:bob", "root:o:todos:dave:friend:alice", "root:o:angrybirds:dave", "root:o:todos:dave:device:phone", "bar:x:baz"},
+			"$",
+			"$",
+			util.NewErrFoundMultipleAppUserBlessings(nil, "root:o:todos:dave", "root:o:angrybirds:dave"),
+		},
+		{
+			[]string{"foo", "root:u:bob", "root:o:todos:dave:friend:alice", "root:o:todos:fred"},
+			"$",
+			"$",
+			util.NewErrFoundMultipleAppUserBlessings(nil, "root:o:todos:dave", "root:o:todos:fred"),
+		},
+		{
+			[]string{"foo", "root:u:bob", "root:o:todos:dave:friend:alice", "google:o:todos:dave"},
+			"$",
+			"$",
+			util.NewErrFoundMultipleAppUserBlessings(nil, "root:o:todos:dave", "google:o:todos:dave"),
+		},
+		// non-conventional blessings are ignored if a conventional blessing is present
+		{
+			[]string{"foo", "root:u:bob", "bar:x:baz"},
+			"...",
+			"root:u:bob",
+			nil,
+		},
+		// multiple blessings for the same user are allowed
+		{
+			[]string{"foo", "root:u:bob:angrybirds", "root:u:bob:todos:phone", "bar:x:baz"},
+			"...",
+			"root:u:bob",
+			nil,
+		},
+		// multiple blessings for different users or identity providers are not allowed
+		{
+			[]string{"foo", "root:u:bob:angrybirds", "root:u:bob:todos:phone", "root:u:carol", "root:u:dave", "bar:x:baz"},
+			"$",
+			"$",
+			util.NewErrFoundMultipleUserBlessings(nil, "root:u:bob", "root:u:carol"),
+		},
+		{
+			[]string{"foo", "root:u:bob:angrybirds", "google:u:bob:todos:phone", "bar:x:baz"},
+			"$",
+			"$",
+			util.NewErrFoundMultipleUserBlessings(nil, "root:u:bob", "google:u:bob"),
+		},
+	} {
+		app, user, err := util.AppAndUserPatternFromBlessings(test.blessings...)
+		if verror.ErrorID(err) != verror.ErrorID(test.wantErr) || fmt.Sprint(err) != fmt.Sprint(test.wantErr) {
+			t.Errorf("AppAndUserPatternFromBlessings(%v): got error %v, want %v", test.blessings, err, test.wantErr)
+		}
+		if string(app) != test.wantApp {
+			t.Errorf("AppAndUserPatternFromBlessings(%v): got app %s, want %s", test.blessings, app, test.wantApp)
+		}
+		if string(user) != test.wantUser {
+			t.Errorf("AppAndUserPatternFromBlessings(%v): got user %s, want %s", test.blessings, user, test.wantUser)
+		}
+	}
+}
+
+func TestFilterTags(t *testing.T) {
+	for _, test := range []struct {
+		input     access.Permissions
+		allowTags []access.Tag
+		want      access.Permissions
+	}{
+		{
+			access.Permissions{}.
+				Add("root", access.TagStrings(access.Read, access.Write, access.Admin)...).
+				Add("root:alice", access.TagStrings(access.Read, access.Write)...),
+			[]access.Tag{},
+			access.Permissions{},
+		},
+		{
+			access.Permissions{}.
+				Add("root", access.TagStrings(access.Read, access.Write, access.Admin)...).
+				Add("root:alice", access.TagStrings(access.Read, access.Write)...),
+			[]access.Tag{access.Admin, access.Read},
+			access.Permissions{}.
+				Add("root", access.TagStrings(access.Read, access.Admin)...).
+				Add("root:alice", access.TagStrings(access.Read)...),
+		},
+		{
+			access.Permissions{}.
+				Add("alice", access.TagStrings(access.Admin)...).
+				Add("bob", access.TagStrings(access.Read, access.Write)...).
+				Add("carol", access.TagStrings(access.Write, access.Admin)...),
+			[]access.Tag{access.Read, access.Write},
+			access.Permissions{}.
+				Add("bob", access.TagStrings(access.Read, access.Write)...).
+				Add("carol", access.TagStrings(access.Write)...),
+		},
+		{
+			access.Permissions{},
+			[]access.Tag{access.Read, access.Write, access.Admin},
+			access.Permissions{},
+		},
+		{
+			access.Permissions{}.
+				Add("alice", access.TagStrings(access.Admin)...).
+				Add("bob", access.TagStrings(access.Read, access.Write, access.Admin)...).
+				Blacklist("bob:tablet", access.TagStrings(access.Write, access.Admin)...),
+			[]access.Tag{access.Read, access.Write},
+			access.Permissions{}.
+				Add("bob", access.TagStrings(access.Read, access.Write)...).
+				Blacklist("bob:tablet", access.TagStrings(access.Write)...),
+		},
+		{
+			access.Permissions{}.
+				Add("alice", access.TagStrings(access.Admin)...).
+				Add("bob", access.TagStrings(access.Read, access.Write)...).
+				Blacklist("bob:tablet", access.TagStrings(access.Write)...),
+			[]access.Tag{access.Admin, access.Read},
+			access.Permissions{}.
+				Add("alice", access.TagStrings(access.Admin)...).
+				Add("bob", access.TagStrings(access.Read)...),
+		},
+	} {
+		inputCopy := test.input.Copy()
+		filtered := util.FilterTags(inputCopy, test.allowTags...)
+		// Modify the input perms to ensure the filtered copy is unaffected.
+		if adminAcl, ok := inputCopy[string(access.Admin)]; ok && len(adminAcl.In) > 0 {
+			adminAcl.In[0] = "mallory"
+		}
+		if got, want := filtered, test.want.Normalize(); !reflect.DeepEqual(got, want) {
+			t.Errorf("FilterTags(%v, %v): got %v, want %v", test.input, test.allowTags, got, want)
+		}
+	}
+}