syncbase: Enforce ACL spec on sync RPCs.

Syncgroup Manager RPCs now check permissions according to the Syncbase
ACL specification document, including recursively checking for Resolve
access where appropriate.
Documented status of Syncbase-to-Syncbase RPC permissions better.
Added other sanity checks - Collection existence and Read permission at
syncgroup create/join, prevent removing self from syncgroup ACL.
Added permission spec tests for client-to-Syncbase Syncgroup Manager and
Blob RPCs.

MultiPart: 1/2
Change-Id: I22cdb663440d2d8d39bc06a669de44d3d0c491b2
diff --git a/services/syncbase/service.vdl b/services/syncbase/service.vdl
index c8a5248..69ee828 100644
--- a/services/syncbase/service.vdl
+++ b/services/syncbase/service.vdl
@@ -19,7 +19,6 @@
 // Unless stated otherwise, each permissions tag requirement on a method also
 // implies requiring Resolve on all levels of hierarchy up to, but excluding,
 // the level requiring the tag.
-// TODO(ivanpi): Implement on SyncgroupManager methods.
 // ErrNoAccess, Err[No]Exist, ErrUnknownBatch are only returned if the caller
 // is allowed to call Exists on the receiver of the RPC (or the first missing
 // component of the hierarchy to the receiver); otherwise, the returned error
@@ -274,70 +273,90 @@
 	Delete(bh BatchHandle) error {access.Write}
 }
 
-// SyncgroupManager is the interface for syncgroup operations.
+// SyncgroupManager is the interface for syncgroup operations. The Database is
+// the parent of its syncgroups for permissions checking purposes.
 // TODO(hpucha): Add blessings to create/join and add a refresh method.
 type SyncgroupManager interface {
 	// ListSyncgroups returns the relative syncgroup ids of all syncgroups attached to
 	// this database.
+	//
+	// Requires: Read on Database.
 	ListSyncgroups() ([]Id | error) {access.Read}
 
 	// CreateSyncgroup creates a new syncgroup with the given spec.
 	//
-	// Requires: Client must have at least Read access on the Database; all
-	// Collections specified in prefixes must exist; Client must have at least
-	// Read access on each of the Collection ACLs.
-	CreateSyncgroup(sgId Id, spec SyncgroupSpec, myInfo SyncgroupMemberInfo) error {access.Read}
+	// Requires: Write on Database.
+	// Also requires the creator's blessing to match the pattern in the newly
+	// created syncgroup's id.
+	// Permissions in spec must allow the creator at least Read access.
+	// All Collections in spec must exist and the creator must have Read access
+	// on them.
+	// For each Collection in spec that isn't already part of another syncgroup,
+	// its permissions must be signed by a blessing matching the pattern in the
+	// Collection id, and all data must be signed by a blessing currently allowed
+	// to Write.
+	// TODO(ivanpi): Since signatures are currently not enforced, we only check
+	// that the Write permissions are not empty.
+	CreateSyncgroup(sgId Id, spec SyncgroupSpec, myInfo SyncgroupMemberInfo) error
 
 	// JoinSyncgroup joins the syncgroup.
 	//
-	// Requires: Client must have at least Read access on the Database and on the
-	// syncgroup ACL.
-	JoinSyncgroup(remoteSyncbaseName string, expectedSyncbaseBlessings []string, sgId Id, myInfo SyncgroupMemberInfo) (spec SyncgroupSpec | error) {access.Read}
+	// Requires: Write on Database and Read (no Resolve required) on syncgroup.
+	// For each locally existing Collection in spec, as well as each Collection
+	// in spec on the remote (joinee) Syncbase, the joiner must have Read access
+	// on it.
+	// For each locally existing Collection in spec that isn't already part of
+	// another syncgroup, its permissions must be signed by a blessing matching
+	// the pattern in the Collection id, and all data must be signed by a blessing
+	// currently allowed to Write.
+	// TODO(ivanpi): Since signatures are currently not enforced, we only check
+	// that the Write permissions are not empty.
+	JoinSyncgroup(remoteSyncbaseName string, expectedSyncbaseBlessings []string, sgId Id, myInfo SyncgroupMemberInfo) (spec SyncgroupSpec | error)
 
 	// LeaveSyncgroup leaves the syncgroup. Previously synced data will continue
-	// to be available.
+	// to be available. If the last syncgroup on a Collection is left, the data
+	// will become read-only and the Collection must be destroyed before joining
+	// a syncgroup that includes it.
 	//
-	// Requires: Client must have at least Read access on the Database.
-	LeaveSyncgroup(sgId Id) error {access.Read}
+	// Requires: Write on Database.
+	LeaveSyncgroup(sgId Id) error {access.Write}
 
 	// DestroySyncgroup destroys the syncgroup. Previously synced data will
-	// continue to be available to all members.
+	// continue to be available to all members, equivalent to all members
+	// leaving the syncgroup.
 	//
-	// Requires: Client must have at least Read access on the Database, and must
-	// have Admin access on the syncgroup ACL.
-	DestroySyncgroup(sgId Id) error {access.Read}
+	// Requires: Write on Database and Admin (no Resolve required) on syncgroup.
+	DestroySyncgroup(sgId Id) error
 
 	// EjectFromSyncgroup ejects a member from the syncgroup. The ejected member
 	// will not be able to sync further, but will retain any data it has already
-	// synced.
+	// synced, equivalent to having left the syncgroup.
 	//
-	// Requires: Client must have at least Read access on the Database, and must
-	// have Admin access on the syncgroup ACL.
-	EjectFromSyncgroup(sgId Id, member string) error {access.Read}
+	// Requires: Admin on syncgroup.
+	// The caller cannot eject themselves.
+	EjectFromSyncgroup(sgId Id, member string) error
 
 	// GetSyncgroupSpec gets the syncgroup spec. version allows for atomic
 	// read-modify-write of the spec - see comment for SetSyncgroupSpec.
 	//
-	// Requires: Client must have at least Read access on the Database and on the
-	// syncgroup ACL.
-	GetSyncgroupSpec(sgId Id) (spec SyncgroupSpec, version string | error) {access.Read}
+	// Requires: Read on syncgroup.
+	GetSyncgroupSpec(sgId Id) (spec SyncgroupSpec, version string | error)
 
 	// SetSyncgroupSpec sets the syncgroup spec. version may be either empty or
 	// the value from a previous Get. If not empty, Set will only succeed if the
 	// current version matches the specified one.
 	//
-	// Requires: Client must have at least Read access on the Database, and must
-	// have Admin access on the syncgroup ACL.
-	SetSyncgroupSpec(sgId Id, spec SyncgroupSpec, version string) error {access.Read}
+	// Requires: Admin on syncgroup.
+	// The caller must continue to have Read access.
+	SetSyncgroupSpec(sgId Id, spec SyncgroupSpec, version string) error
 
 	// GetSyncgroupMembers gets the info objects for members of the syncgroup.
 	//
-	// Requires: Client must have at least Read access on the Database and on the
-	// syncgroup ACL.
-	GetSyncgroupMembers(sgId Id) (members map[string]SyncgroupMemberInfo | error) {access.Read}
+	// Requires: Read on syncgroup.
+	GetSyncgroupMembers(sgId Id) (members map[string]SyncgroupMemberInfo | error)
 
 	// TODO(hpucha): Allow clients to tune the behavior of sync.
-	// - Suspend/ResumeSync
+	// - Suspend/ResumeSync per syncgroup instead of database
 	// - Get/SetSyncPolicy with policies such as "sync only via wifi", "sync
 	//   aggressively", "sync once per day"
 }
@@ -504,6 +523,7 @@
 	// Requires: Read on Database.
 	WatchPatterns(resumeMarker watch.ResumeMarker, patterns []CollectionRowPattern) stream<_, watch.Change> error {access.Read}
 
+	// Requires: Read on Database (instead of Resolve).
 	watch.GlobWatcher
 }
 
diff --git a/services/syncbase/syncbase.vdl.go b/services/syncbase/syncbase.vdl.go
index e6925f7..cb9b477 100644
--- a/services/syncbase/syncbase.vdl.go
+++ b/services/syncbase/syncbase.vdl.go
@@ -22,7 +22,6 @@
 // Unless stated otherwise, each permissions tag requirement on a method also
 // implies requiring Resolve on all levels of hierarchy up to, but excluding,
 // the level requiring the tag.
-// TODO(ivanpi): Implement on SyncgroupManager methods.
 // ErrNoAccess, Err[No]Exist, ErrUnknownBatch are only returned if the caller
 // is allowed to call Exists on the receiver of the RPC (or the first missing
 // component of the hierarchy to the receiver); otherwise, the returned error
@@ -3807,58 +3806,78 @@
 // SyncgroupManagerClientMethods is the client interface
 // containing SyncgroupManager methods.
 //
-// SyncgroupManager is the interface for syncgroup operations.
+// SyncgroupManager is the interface for syncgroup operations. The Database is
+// the parent of its syncgroups for permissions checking purposes.
 // TODO(hpucha): Add blessings to create/join and add a refresh method.
 type SyncgroupManagerClientMethods interface {
 	// ListSyncgroups returns the relative syncgroup ids of all syncgroups attached to
 	// this database.
+	//
+	// Requires: Read on Database.
 	ListSyncgroups(*context.T, ...rpc.CallOpt) ([]Id, error)
 	// CreateSyncgroup creates a new syncgroup with the given spec.
 	//
-	// Requires: Client must have at least Read access on the Database; all
-	// Collections specified in prefixes must exist; Client must have at least
-	// Read access on each of the Collection ACLs.
+	// Requires: Write on Database.
+	// Also requires the creator's blessing to match the pattern in the newly
+	// created syncgroup's id.
+	// Permissions in spec must allow the creator at least Read access.
+	// All Collections in spec must exist and the creator must have Read access
+	// on them.
+	// For each Collection in spec that isn't already part of another syncgroup,
+	// its permissions must be signed by a blessing matching the pattern in the
+	// Collection id, and all data must be signed by a blessing currently allowed
+	// to Write.
+	// TODO(ivanpi): Since signatures are currently not enforced, we only check
+	// that the Write permissions are not empty.
 	CreateSyncgroup(_ *context.T, sgId Id, spec SyncgroupSpec, myInfo SyncgroupMemberInfo, _ ...rpc.CallOpt) error
 	// JoinSyncgroup joins the syncgroup.
 	//
-	// Requires: Client must have at least Read access on the Database and on the
-	// syncgroup ACL.
+	// Requires: Write on Database and Read (no Resolve required) on syncgroup.
+	// For each locally existing Collection in spec, as well as each Collection
+	// in spec on the remote (joinee) Syncbase, the joiner must have Read access
+	// on it.
+	// For each locally existing Collection in spec that isn't already part of
+	// another syncgroup, its permissions must be signed by a blessing matching
+	// the pattern in the Collection id, and all data must be signed by a blessing
+	// currently allowed to Write.
+	// TODO(ivanpi): Since signatures are currently not enforced, we only check
+	// that the Write permissions are not empty.
 	JoinSyncgroup(_ *context.T, remoteSyncbaseName string, expectedSyncbaseBlessings []string, sgId Id, myInfo SyncgroupMemberInfo, _ ...rpc.CallOpt) (spec SyncgroupSpec, _ error)
 	// LeaveSyncgroup leaves the syncgroup. Previously synced data will continue
-	// to be available.
+	// to be available. If the last syncgroup on a Collection is left, the data
+	// will become read-only and the Collection must be destroyed before joining
+	// a syncgroup that includes it.
 	//
-	// Requires: Client must have at least Read access on the Database.
+	// Requires: Write on Database.
 	LeaveSyncgroup(_ *context.T, sgId Id, _ ...rpc.CallOpt) error
 	// DestroySyncgroup destroys the syncgroup. Previously synced data will
-	// continue to be available to all members.
+	// continue to be available to all members, equivalent to all members
+	// leaving the syncgroup.
 	//
-	// Requires: Client must have at least Read access on the Database, and must
-	// have Admin access on the syncgroup ACL.
+	// Requires: Write on Database and Admin (no Resolve required) on syncgroup.
 	DestroySyncgroup(_ *context.T, sgId Id, _ ...rpc.CallOpt) error
 	// EjectFromSyncgroup ejects a member from the syncgroup. The ejected member
 	// will not be able to sync further, but will retain any data it has already
-	// synced.
+	// synced, equivalent to having left the syncgroup.
 	//
-	// Requires: Client must have at least Read access on the Database, and must
-	// have Admin access on the syncgroup ACL.
+	// Requires: Admin on syncgroup.
+	// The caller cannot eject themselves.
 	EjectFromSyncgroup(_ *context.T, sgId Id, member string, _ ...rpc.CallOpt) error
 	// GetSyncgroupSpec gets the syncgroup spec. version allows for atomic
 	// read-modify-write of the spec - see comment for SetSyncgroupSpec.
 	//
-	// Requires: Client must have at least Read access on the Database and on the
-	// syncgroup ACL.
+	// Requires: Read on syncgroup.
 	GetSyncgroupSpec(_ *context.T, sgId Id, _ ...rpc.CallOpt) (spec SyncgroupSpec, version string, _ error)
 	// SetSyncgroupSpec sets the syncgroup spec. version may be either empty or
 	// the value from a previous Get. If not empty, Set will only succeed if the
 	// current version matches the specified one.
 	//
-	// Requires: Client must have at least Read access on the Database, and must
-	// have Admin access on the syncgroup ACL.
+	// Requires: Admin on syncgroup.
+	// The caller must continue to have Read access.
 	SetSyncgroupSpec(_ *context.T, sgId Id, spec SyncgroupSpec, version string, _ ...rpc.CallOpt) error
 	// GetSyncgroupMembers gets the info objects for members of the syncgroup.
 	//
-	// Requires: Client must have at least Read access on the Database and on the
-	// syncgroup ACL.
+	// Requires: Read on syncgroup.
 	GetSyncgroupMembers(_ *context.T, sgId Id, _ ...rpc.CallOpt) (members map[string]SyncgroupMemberInfo, _ error)
 }
 
@@ -3925,58 +3944,78 @@
 // SyncgroupManagerServerMethods is the interface a server writer
 // implements for SyncgroupManager.
 //
-// SyncgroupManager is the interface for syncgroup operations.
+// SyncgroupManager is the interface for syncgroup operations. The Database is
+// the parent of its syncgroups for permissions checking purposes.
 // TODO(hpucha): Add blessings to create/join and add a refresh method.
 type SyncgroupManagerServerMethods interface {
 	// ListSyncgroups returns the relative syncgroup ids of all syncgroups attached to
 	// this database.
+	//
+	// Requires: Read on Database.
 	ListSyncgroups(*context.T, rpc.ServerCall) ([]Id, error)
 	// CreateSyncgroup creates a new syncgroup with the given spec.
 	//
-	// Requires: Client must have at least Read access on the Database; all
-	// Collections specified in prefixes must exist; Client must have at least
-	// Read access on each of the Collection ACLs.
+	// Requires: Write on Database.
+	// Also requires the creator's blessing to match the pattern in the newly
+	// created syncgroup's id.
+	// Permissions in spec must allow the creator at least Read access.
+	// All Collections in spec must exist and the creator must have Read access
+	// on them.
+	// For each Collection in spec that isn't already part of another syncgroup,
+	// its permissions must be signed by a blessing matching the pattern in the
+	// Collection id, and all data must be signed by a blessing currently allowed
+	// to Write.
+	// TODO(ivanpi): Since signatures are currently not enforced, we only check
+	// that the Write permissions are not empty.
 	CreateSyncgroup(_ *context.T, _ rpc.ServerCall, sgId Id, spec SyncgroupSpec, myInfo SyncgroupMemberInfo) error
 	// JoinSyncgroup joins the syncgroup.
 	//
-	// Requires: Client must have at least Read access on the Database and on the
-	// syncgroup ACL.
+	// Requires: Write on Database and Read (no Resolve required) on syncgroup.
+	// For each locally existing Collection in spec, as well as each Collection
+	// in spec on the remote (joinee) Syncbase, the joiner must have Read access
+	// on it.
+	// For each locally existing Collection in spec that isn't already part of
+	// another syncgroup, its permissions must be signed by a blessing matching
+	// the pattern in the Collection id, and all data must be signed by a blessing
+	// currently allowed to Write.
+	// TODO(ivanpi): Since signatures are currently not enforced, we only check
+	// that the Write permissions are not empty.
 	JoinSyncgroup(_ *context.T, _ rpc.ServerCall, remoteSyncbaseName string, expectedSyncbaseBlessings []string, sgId Id, myInfo SyncgroupMemberInfo) (spec SyncgroupSpec, _ error)
 	// LeaveSyncgroup leaves the syncgroup. Previously synced data will continue
-	// to be available.
+	// to be available. If the last syncgroup on a Collection is left, the data
+	// will become read-only and the Collection must be destroyed before joining
+	// a syncgroup that includes it.
 	//
-	// Requires: Client must have at least Read access on the Database.
+	// Requires: Write on Database.
 	LeaveSyncgroup(_ *context.T, _ rpc.ServerCall, sgId Id) error
 	// DestroySyncgroup destroys the syncgroup. Previously synced data will
-	// continue to be available to all members.
+	// continue to be available to all members, equivalent to all members
+	// leaving the syncgroup.
 	//
-	// Requires: Client must have at least Read access on the Database, and must
-	// have Admin access on the syncgroup ACL.
+	// Requires: Write on Database and Admin (no Resolve required) on syncgroup.
 	DestroySyncgroup(_ *context.T, _ rpc.ServerCall, sgId Id) error
 	// EjectFromSyncgroup ejects a member from the syncgroup. The ejected member
 	// will not be able to sync further, but will retain any data it has already
-	// synced.
+	// synced, equivalent to having left the syncgroup.
 	//
-	// Requires: Client must have at least Read access on the Database, and must
-	// have Admin access on the syncgroup ACL.
+	// Requires: Admin on syncgroup.
+	// The caller cannot eject themselves.
 	EjectFromSyncgroup(_ *context.T, _ rpc.ServerCall, sgId Id, member string) error
 	// GetSyncgroupSpec gets the syncgroup spec. version allows for atomic
 	// read-modify-write of the spec - see comment for SetSyncgroupSpec.
 	//
-	// Requires: Client must have at least Read access on the Database and on the
-	// syncgroup ACL.
+	// Requires: Read on syncgroup.
 	GetSyncgroupSpec(_ *context.T, _ rpc.ServerCall, sgId Id) (spec SyncgroupSpec, version string, _ error)
 	// SetSyncgroupSpec sets the syncgroup spec. version may be either empty or
 	// the value from a previous Get. If not empty, Set will only succeed if the
 	// current version matches the specified one.
 	//
-	// Requires: Client must have at least Read access on the Database, and must
-	// have Admin access on the syncgroup ACL.
+	// Requires: Admin on syncgroup.
+	// The caller must continue to have Read access.
 	SetSyncgroupSpec(_ *context.T, _ rpc.ServerCall, sgId Id, spec SyncgroupSpec, version string) error
 	// GetSyncgroupMembers gets the info objects for members of the syncgroup.
 	//
-	// Requires: Client must have at least Read access on the Database and on the
-	// syncgroup ACL.
+	// Requires: Read on syncgroup.
 	GetSyncgroupMembers(_ *context.T, _ rpc.ServerCall, sgId Id) (members map[string]SyncgroupMemberInfo, _ error)
 }
 
@@ -4066,11 +4105,11 @@
 var descSyncgroupManager = rpc.InterfaceDesc{
 	Name:    "SyncgroupManager",
 	PkgPath: "v.io/v23/services/syncbase",
-	Doc:     "// SyncgroupManager is the interface for syncgroup operations.\n// TODO(hpucha): Add blessings to create/join and add a refresh method.",
+	Doc:     "// SyncgroupManager is the interface for syncgroup operations. The Database is\n// the parent of its syncgroups for permissions checking purposes.\n// TODO(hpucha): Add blessings to create/join and add a refresh method.",
 	Methods: []rpc.MethodDesc{
 		{
 			Name: "ListSyncgroups",
-			Doc:  "// ListSyncgroups returns the relative syncgroup ids of all syncgroups attached to\n// this database.",
+			Doc:  "// ListSyncgroups returns the relative syncgroup ids of all syncgroups attached to\n// this database.\n//\n// Requires: Read on Database.",
 			OutArgs: []rpc.ArgDesc{
 				{"", ``}, // []Id
 			},
@@ -4078,17 +4117,16 @@
 		},
 		{
 			Name: "CreateSyncgroup",
-			Doc:  "// CreateSyncgroup creates a new syncgroup with the given spec.\n//\n// Requires: Client must have at least Read access on the Database; all\n// Collections specified in prefixes must exist; Client must have at least\n// Read access on each of the Collection ACLs.",
+			Doc:  "// CreateSyncgroup creates a new syncgroup with the given spec.\n//\n// Requires: Write on Database.\n// Also requires the creator's blessing to match the pattern in the newly\n// created syncgroup's id.\n// Permissions in spec must allow the creator at least Read access.\n// All Collections in spec must exist and the creator must have Read access\n// on them.\n// For each Collection in spec that isn't already part of another syncgroup,\n// its permissions must be signed by a blessing matching the pattern in the\n// Collection id, and all data must be signed by a blessing currently allowed\n// to Write.\n// TODO(ivanpi): Since signatures are currently not enforced, we only check\n// that the Write permissions are not empty.",
 			InArgs: []rpc.ArgDesc{
 				{"sgId", ``},   // Id
 				{"spec", ``},   // SyncgroupSpec
 				{"myInfo", ``}, // SyncgroupMemberInfo
 			},
-			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
 		},
 		{
 			Name: "JoinSyncgroup",
-			Doc:  "// JoinSyncgroup joins the syncgroup.\n//\n// Requires: Client must have at least Read access on the Database and on the\n// syncgroup ACL.",
+			Doc:  "// JoinSyncgroup joins the syncgroup.\n//\n// Requires: Write on Database and Read (no Resolve required) on syncgroup.\n// For each locally existing Collection in spec, as well as each Collection\n// in spec on the remote (joinee) Syncbase, the joiner must have Read access\n// on it.\n// For each locally existing Collection in spec that isn't already part of\n// another syncgroup, its permissions must be signed by a blessing matching\n// the pattern in the Collection id, and all data must be signed by a blessing\n// currently allowed to Write.\n// TODO(ivanpi): Since signatures are currently not enforced, we only check\n// that the Write permissions are not empty.",
 			InArgs: []rpc.ArgDesc{
 				{"remoteSyncbaseName", ``},        // string
 				{"expectedSyncbaseBlessings", ``}, // []string
@@ -4098,36 +4136,33 @@
 			OutArgs: []rpc.ArgDesc{
 				{"spec", ``}, // SyncgroupSpec
 			},
-			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
 		},
 		{
 			Name: "LeaveSyncgroup",
-			Doc:  "// LeaveSyncgroup leaves the syncgroup. Previously synced data will continue\n// to be available.\n//\n// Requires: Client must have at least Read access on the Database.",
+			Doc:  "// LeaveSyncgroup leaves the syncgroup. Previously synced data will continue\n// to be available. If the last syncgroup on a Collection is left, the data\n// will become read-only and the Collection must be destroyed before joining\n// a syncgroup that includes it.\n//\n// Requires: Write on Database.",
 			InArgs: []rpc.ArgDesc{
 				{"sgId", ``}, // Id
 			},
-			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
 		},
 		{
 			Name: "DestroySyncgroup",
-			Doc:  "// DestroySyncgroup destroys the syncgroup. Previously synced data will\n// continue to be available to all members.\n//\n// Requires: Client must have at least Read access on the Database, and must\n// have Admin access on the syncgroup ACL.",
+			Doc:  "// DestroySyncgroup destroys the syncgroup. Previously synced data will\n// continue to be available to all members, equivalent to all members\n// leaving the syncgroup.\n//\n// Requires: Write on Database and Admin (no Resolve required) on syncgroup.",
 			InArgs: []rpc.ArgDesc{
 				{"sgId", ``}, // Id
 			},
-			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
 		},
 		{
 			Name: "EjectFromSyncgroup",
-			Doc:  "// EjectFromSyncgroup ejects a member from the syncgroup. The ejected member\n// will not be able to sync further, but will retain any data it has already\n// synced.\n//\n// Requires: Client must have at least Read access on the Database, and must\n// have Admin access on the syncgroup ACL.",
+			Doc:  "// EjectFromSyncgroup ejects a member from the syncgroup. The ejected member\n// will not be able to sync further, but will retain any data it has already\n// synced, equivalent to having left the syncgroup.\n//\n// Requires: Admin on syncgroup.\n// The caller cannot eject themselves.",
 			InArgs: []rpc.ArgDesc{
 				{"sgId", ``},   // Id
 				{"member", ``}, // string
 			},
-			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
 		},
 		{
 			Name: "GetSyncgroupSpec",
-			Doc:  "// GetSyncgroupSpec gets the syncgroup spec. version allows for atomic\n// read-modify-write of the spec - see comment for SetSyncgroupSpec.\n//\n// Requires: Client must have at least Read access on the Database and on the\n// syncgroup ACL.",
+			Doc:  "// GetSyncgroupSpec gets the syncgroup spec. version allows for atomic\n// read-modify-write of the spec - see comment for SetSyncgroupSpec.\n//\n// Requires: Read on syncgroup.",
 			InArgs: []rpc.ArgDesc{
 				{"sgId", ``}, // Id
 			},
@@ -4135,28 +4170,25 @@
 				{"spec", ``},    // SyncgroupSpec
 				{"version", ``}, // string
 			},
-			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
 		},
 		{
 			Name: "SetSyncgroupSpec",
-			Doc:  "// SetSyncgroupSpec sets the syncgroup spec. version may be either empty or\n// the value from a previous Get. If not empty, Set will only succeed if the\n// current version matches the specified one.\n//\n// Requires: Client must have at least Read access on the Database, and must\n// have Admin access on the syncgroup ACL.",
+			Doc:  "// SetSyncgroupSpec sets the syncgroup spec. version may be either empty or\n// the value from a previous Get. If not empty, Set will only succeed if the\n// current version matches the specified one.\n//\n// Requires: Admin on syncgroup.\n// The caller must continue to have Read access.",
 			InArgs: []rpc.ArgDesc{
 				{"sgId", ``},    // Id
 				{"spec", ``},    // SyncgroupSpec
 				{"version", ``}, // string
 			},
-			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
 		},
 		{
 			Name: "GetSyncgroupMembers",
-			Doc:  "// GetSyncgroupMembers gets the info objects for members of the syncgroup.\n//\n// Requires: Client must have at least Read access on the Database and on the\n// syncgroup ACL.",
+			Doc:  "// GetSyncgroupMembers gets the info objects for members of the syncgroup.\n//\n// Requires: Read on syncgroup.",
 			InArgs: []rpc.ArgDesc{
 				{"sgId", ``}, // Id
 			},
 			OutArgs: []rpc.ArgDesc{
 				{"members", ``}, // map[string]SyncgroupMemberInfo
 			},
-			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
 		},
 	},
 }
@@ -5507,7 +5539,8 @@
 	// resolution. However, changes from a single original batch will always appear
 	// in the same Change batch.
 	DatabaseWatcherClientMethods
-	// SyncgroupManager is the interface for syncgroup operations.
+	// SyncgroupManager is the interface for syncgroup operations. The Database is
+	// the parent of its syncgroups for permissions checking purposes.
 	// TODO(hpucha): Add blessings to create/join and add a refresh method.
 	SyncgroupManagerClientMethods
 	// BlobManager is the interface for blob operations.
@@ -5834,7 +5867,8 @@
 	// resolution. However, changes from a single original batch will always appear
 	// in the same Change batch.
 	DatabaseWatcherServerMethods
-	// SyncgroupManager is the interface for syncgroup operations.
+	// SyncgroupManager is the interface for syncgroup operations. The Database is
+	// the parent of its syncgroups for permissions checking purposes.
 	// TODO(hpucha): Add blessings to create/join and add a refresh method.
 	SyncgroupManagerServerMethods
 	// BlobManager is the interface for blob operations.
@@ -6015,7 +6049,8 @@
 	// resolution. However, changes from a single original batch will always appear
 	// in the same Change batch.
 	DatabaseWatcherServerStubMethods
-	// SyncgroupManager is the interface for syncgroup operations.
+	// SyncgroupManager is the interface for syncgroup operations. The Database is
+	// the parent of its syncgroups for permissions checking purposes.
 	// TODO(hpucha): Add blessings to create/join and add a refresh method.
 	SyncgroupManagerServerStubMethods
 	// BlobManager is the interface for blob operations.
@@ -6215,7 +6250,7 @@
 	Embeds: []rpc.EmbedDesc{
 		{"Object", "v.io/v23/services/permissions", "// Object provides access control for Vanadium objects.\n//\n// Vanadium services implementing dynamic access control would typically embed\n// this interface and tag additional methods defined by the service with one of\n// Admin, Read, Write, Resolve etc. For example, the VDL definition of the\n// object would be:\n//\n//   package mypackage\n//\n//   import \"v.io/v23/security/access\"\n//   import \"v.io/v23/services/permissions\"\n//\n//   type MyObject interface {\n//     permissions.Object\n//     MyRead() (string, error) {access.Read}\n//     MyWrite(string) error    {access.Write}\n//   }\n//\n// If the set of pre-defined tags is insufficient, services may define their\n// own tag type and annotate all methods with this new type.\n//\n// Instead of embedding this Object interface, define SetPermissions and\n// GetPermissions in their own interface. Authorization policies will typically\n// respect annotations of a single type. For example, the VDL definition of an\n// object would be:\n//\n//  package mypackage\n//\n//  import \"v.io/v23/security/access\"\n//\n//  type MyTag string\n//\n//  const (\n//    Blue = MyTag(\"Blue\")\n//    Red  = MyTag(\"Red\")\n//  )\n//\n//  type MyObject interface {\n//    MyMethod() (string, error) {Blue}\n//\n//    // Allow clients to change access via the access.Object interface:\n//    SetPermissions(perms access.Permissions, version string) error         {Red}\n//    GetPermissions() (perms access.Permissions, version string, err error) {Blue}\n//  }"},
 		{"DatabaseWatcher", "v.io/v23/services/syncbase", "// DatabaseWatcher allows a client to watch for updates to the database. For\n// each watch request, the client will receive a reliable stream of watch events\n// without re-ordering. Only rows and collections matching at least one of the\n// patterns are returned. Rows in collections with no Read access are also\n// filtered out.\n//\n// Watching is done by starting a streaming RPC. The RPC takes a ResumeMarker\n// argument that points to a particular place in the database event log. If an\n// empty ResumeMarker is provided, the WatchStream will begin with a Change\n// batch containing the initial state, always starting with an empty update for\n// the root entity. Otherwise, the WatchStream will contain only changes since\n// the provided ResumeMarker.\n// See watch.GlobWatcher for a detailed explanation of the behavior.\n//\n// The result stream consists of a never-ending sequence of Change messages\n// (until the call fails or is canceled). Each Change contains the Name field\n// with the Vanadium name of the watched entity relative to the database:\n// - \"<encCxId>/<rowKey>\" for row updates\n// - \"<encCxId>\" for collection updates\n// - \"\" for the initial root entity update\n// The Value field is a StoreChange.\n// If the client has no access to a row specified in a change, that change is\n// excluded from the result stream. Collection updates are always sent and can\n// be used to determine that access to a collection is denied, potentially\n// skipping rows.\n//\n// Note: A single Watch Change batch may contain changes from more than one\n// batch as originally committed on a remote Syncbase or obtained from conflict\n// resolution. However, changes from a single original batch will always appear\n// in the same Change batch."},
-		{"SyncgroupManager", "v.io/v23/services/syncbase", "// SyncgroupManager is the interface for syncgroup operations.\n// TODO(hpucha): Add blessings to create/join and add a refresh method."},
+		{"SyncgroupManager", "v.io/v23/services/syncbase", "// SyncgroupManager is the interface for syncgroup operations. The Database is\n// the parent of its syncgroups for permissions checking purposes.\n// TODO(hpucha): Add blessings to create/join and add a refresh method."},
 		{"BlobManager", "v.io/v23/services/syncbase", "// BlobManager is the interface for blob operations.\n//\n// Description of API for resumable blob creation (append-only):\n// - Up until commit, a BlobRef may be used with PutBlob, GetBlobSize,\n//   DeleteBlob, and CommitBlob. Blob creation may be resumed by obtaining the\n//   current blob size via GetBlobSize and appending to the blob via PutBlob.\n// - After commit, a blob is immutable, at which point PutBlob and CommitBlob\n//   may no longer be used.\n// - All other methods (GetBlob, FetchBlob, PinBlob, etc.) may only be used\n//   after commit."},
 		{"SchemaManager", "v.io/v23/services/syncbase", "// SchemaManager implements the API for managing schema metadata attached\n// to a Database."},
 		{"ConflictManager", "v.io/v23/services/syncbase", "// ConflictManager interface provides all the methods necessary to handle\n// conflict resolution for a given database."},
diff --git a/syncbase/permissions_test.go b/syncbase/permissions_test.go
index 4260fca..289bde4 100644
--- a/syncbase/permissions_test.go
+++ b/syncbase/permissions_test.go
@@ -35,14 +35,33 @@
 	f func(ctx *context.T, b syncbase.BatchDatabase) error
 }
 
+type blobTest struct {
+	commit bool
+	f      func(ctx *context.T, d syncbase.Database, blob syncbase.Blob) error
+}
+
+type syncgroupTest struct {
+	f func(ctx *context.T, sg syncbase.Syncgroup) error
+}
+
 type collectionTest struct {
-	f func(ctx *context.T, c syncbase.Collection) error
+	f func(ctx *context.T, d syncbase.Database, c syncbase.Collection) error
 }
 
 type rowTest struct {
 	f func(ctx *context.T, r syncbase.Row) error
 }
 
+func expectNotImplemented(err error) error {
+	if err == nil {
+		return verror.New(verror.ErrInternal, nil, "unimplemented method returned nil error")
+	}
+	if verror.ErrorID(err) == verror.ErrNotImplemented.ID {
+		return nil
+	}
+	return err
+}
+
 type securitySpecTest struct {
 	layer   interface{}
 	name    string
@@ -255,10 +274,177 @@
 		patterns: []string{"XW__"},
 		mutating: true,
 	},
-	// TODO(ivanpi): Test other blob RPCs.
+	{
+		layer: blobTest{commit: false, f: func(ctx *context.T, d syncbase.Database, blob syncbase.Blob) error {
+			w, err := blob.Put(ctx)
+			if err != nil {
+				return err
+			}
+			if err := w.Send([]byte("foo")); err != nil {
+				return err
+			}
+			return w.Close()
+		}},
+		name:     "blob.Put",
+		patterns: []string{"XW__"},
+		mutating: true,
+	},
+	{
+		layer: blobTest{commit: false, f: func(ctx *context.T, d syncbase.Database, blob syncbase.Blob) error {
+			return blob.Commit(ctx)
+		}},
+		name:     "blob.Commit",
+		patterns: []string{"XW__"},
+		mutating: true,
+	},
+	{
+		layer: blobTest{commit: true, f: func(ctx *context.T, d syncbase.Database, blob syncbase.Blob) error {
+			_, err := blob.Size(ctx)
+			return err
+		}},
+		name:     "blob.Size",
+		patterns: []string{"XX__", "XR__", "XW__", "XA__"},
+	},
+	{
+		layer: blobTest{commit: true, f: func(ctx *context.T, d syncbase.Database, blob syncbase.Blob) error {
+			return expectNotImplemented(blob.Delete(ctx))
+		}},
+		name:     "blob.Delete",
+		patterns: []string{"XX__", "XR__", "XW__", "XA__"},
+		mutating: true,
+	},
+	{
+		layer: blobTest{commit: true, f: func(ctx *context.T, d syncbase.Database, blob syncbase.Blob) error {
+			r, err := blob.Get(ctx, 0)
+			if err != nil {
+				return err
+			}
+			r.Advance()
+			return r.Err()
+		}},
+		name:     "blob.Get",
+		patterns: []string{"XX__", "XR__", "XW__", "XA__"},
+	},
+	{
+		layer: blobTest{commit: true, f: func(ctx *context.T, d syncbase.Database, blob syncbase.Blob) error {
+			s, err := blob.Fetch(ctx, 0)
+			if err != nil {
+				return err
+			}
+			s.Advance()
+			return s.Err()
+		}},
+		name:     "blob.Fetch",
+		patterns: []string{"XX__", "XR__", "XW__", "XA__"},
+		mutating: true,
+	},
+	{
+		layer: blobTest{commit: true, f: func(ctx *context.T, d syncbase.Database, blob syncbase.Blob) error {
+			return expectNotImplemented(blob.Pin(ctx))
+		}},
+		name:     "blob.Pin",
+		patterns: []string{"XW__"},
+		mutating: true,
+	},
+	{
+		layer: blobTest{commit: true, f: func(ctx *context.T, d syncbase.Database, blob syncbase.Blob) error {
+			return expectNotImplemented(blob.Unpin(ctx))
+		}},
+		name:     "blob.Unpin",
+		patterns: []string{"XX__", "XR__", "XW__", "XA__"},
+		mutating: true,
+	},
+	{
+		layer: blobTest{commit: true, f: func(ctx *context.T, d syncbase.Database, blob syncbase.Blob) error {
+			return expectNotImplemented(blob.Keep(ctx, 0))
+		}},
+		name:     "blob.Keep",
+		patterns: []string{"XX__", "XR__", "XW__", "XA__"},
+		mutating: true,
+	},
+	// TODO(ivanpi): Test Syncbase-to-Syncbase blob RPCs.
 
 	// Syncgroup manager tests.
-	// TODO(ivanpi): Add.
+	{
+		layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
+			_, err := d.ListSyncgroups(ctx)
+			return err
+		}},
+		name:     "database.ListSyncgroups",
+		patterns: []string{"XR__"},
+	},
+	{
+		layer: collectionTest{f: func(ctx *context.T, d syncbase.Database, c syncbase.Collection) error {
+			return d.SyncgroupForId(wire.Id{"root", "sgNew"}).Create(ctx, wire.SyncgroupSpec{
+				Perms:       tu.DefaultPerms(wire.AllSyncgroupTags, "root"),
+				Collections: []wire.Id{c.Id()},
+			}, wire.SyncgroupMemberInfo{})
+		}},
+		name:     "syncgroup.Create",
+		patterns: []string{"XWR_"},
+		mutating: true,
+	},
+	{
+		layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup) error {
+			_, err := sg.Join(ctx, "", []string{}, wire.SyncgroupMemberInfo{})
+			return err
+		}},
+		name:     "syncgroup.Join (already joined)",
+		patterns: []string{"XWRR"},
+		mutating: true,
+	},
+	{
+		layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup) error {
+			return expectNotImplemented(sg.Leave(ctx))
+		}},
+		name:     "syncgroup.Leave",
+		patterns: []string{"XW__"},
+		mutating: true,
+	},
+	{
+		layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup) error {
+			return expectNotImplemented(sg.Destroy(ctx))
+		}},
+		name:     "syncgroup.Destroy",
+		patterns: []string{"XW_A"},
+		mutating: true,
+	},
+	{
+		layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup) error {
+			return expectNotImplemented(sg.Eject(ctx, "root:nobody"))
+		}},
+		name:     "syncgroup.Eject",
+		patterns: []string{"XX_A"},
+		mutating: true,
+	},
+	{
+		layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup) error {
+			_, _, err := sg.GetSpec(ctx)
+			return err
+		}},
+		name:     "syncgroup.GetSpec",
+		patterns: []string{"XX_R"},
+	},
+	{
+		layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup) error {
+			return sg.SetSpec(ctx, wire.SyncgroupSpec{
+				Perms:       tu.DefaultPerms(wire.AllSyncgroupTags, "root"),
+				Collections: []wire.Id{{"root:admin", "c"}},
+			}, "")
+		}},
+		name:     "syncgroup.SetSpec",
+		patterns: []string{"XX_A"},
+		mutating: true,
+	},
+	{
+		layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup) error {
+			_, err := sg.GetMembers(ctx)
+			return err
+		}},
+		name:     "syncgroup.GetMembers",
+		patterns: []string{"XX_R"},
+	},
+	// TODO(ivanpi): Test Syncbase-to-Syncbase sync RPCs.
 
 	// Collection tests.
 	{
@@ -270,7 +456,7 @@
 		mutating: true,
 	},
 	{
-		layer: collectionTest{f: func(ctx *context.T, c syncbase.Collection) error {
+		layer: collectionTest{f: func(ctx *context.T, _ syncbase.Database, c syncbase.Collection) error {
 			return c.Destroy(ctx)
 		}},
 		name:     "collection.Destroy",
@@ -278,7 +464,7 @@
 		mutating: true,
 	},
 	{
-		layer: collectionTest{f: func(ctx *context.T, c syncbase.Collection) error {
+		layer: collectionTest{f: func(ctx *context.T, _ syncbase.Database, c syncbase.Collection) error {
 			_, err := c.Exists(ctx)
 			return err
 		}},
@@ -286,7 +472,7 @@
 		patterns: []string{"XXR_", "XXW_", "XXA_", "XR__", "XW__"},
 	},
 	{
-		layer: collectionTest{f: func(ctx *context.T, c syncbase.Collection) error {
+		layer: collectionTest{f: func(ctx *context.T, _ syncbase.Database, c syncbase.Collection) error {
 			_, err := c.GetPermissions(ctx)
 			return err
 		}},
@@ -294,7 +480,7 @@
 		patterns: []string{"XXA_"},
 	},
 	{
-		layer: collectionTest{f: func(ctx *context.T, c syncbase.Collection) error {
+		layer: collectionTest{f: func(ctx *context.T, _ syncbase.Database, c syncbase.Collection) error {
 			return c.SetPermissions(ctx, tu.DefaultPerms(wire.AllCollectionTags, "root"))
 		}},
 		name:     "collection.SetPermissions",
@@ -302,7 +488,7 @@
 		mutating: true,
 	},
 	{
-		layer: collectionTest{f: func(ctx *context.T, c syncbase.Collection) error {
+		layer: collectionTest{f: func(ctx *context.T, _ syncbase.Database, c syncbase.Collection) error {
 			ss := c.Scan(ctx, syncbase.Prefix(""))
 			ss.Advance()
 			return ss.Err()
@@ -311,7 +497,7 @@
 		patterns: []string{"XXR_"},
 	},
 	{
-		layer: collectionTest{f: func(ctx *context.T, c syncbase.Collection) error {
+		layer: collectionTest{f: func(ctx *context.T, _ syncbase.Database, c syncbase.Collection) error {
 			return c.DeleteRange(ctx, syncbase.Prefix(""))
 		}},
 		name:     "collection.DeleteRange",
@@ -471,7 +657,7 @@
 	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...)
+	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)
@@ -490,6 +676,25 @@
 	}
 	r := c.Row("prefix")
 	r.Put(adminCtx, "value")
+	sg := d.SyncgroupForId(wire.Id{"root:admin", "sg"})
+	sgSpec := wire.SyncgroupSpec{
+		Perms:       sgPerms,
+		Collections: []wire.Id{c.Id()},
+	}
+	if err := sg.Create(adminCtx, sgSpec, wire.SyncgroupMemberInfo{}); err != nil {
+		tu.Fatalf(t, "sg.Create failed: %v", err)
+	}
+	blobCommitted, err := d.CreateBlob(adminCtx)
+	if err != nil {
+		tu.Fatalf(t, "d.CreateBlob failed: %v", err)
+	}
+	if err := blobCommitted.Commit(adminCtx); err != nil {
+		tu.Fatalf(t, "blobCommitted.Commit failed: %v", err)
+	}
+	blobUncommitted, err := d.CreateBlob(adminCtx)
+	if err != nil {
+		tu.Fatalf(t, "d.CreateBlob failed: %v", err)
+	}
 
 	// Verify tests.
 	for i, test := range tests {
@@ -506,8 +711,16 @@
 				tu.Fatalf(t, "d.BeginBatch failed: %v", bErr)
 			}
 			err = layer.f(clientCtx, b)
+		case blobTest:
+			if layer.commit {
+				err = layer.f(clientCtx, d, blobCommitted)
+			} else {
+				err = layer.f(clientCtx, d, blobUncommitted)
+			}
+		case syncgroupTest:
+			err = layer.f(clientCtx, sg)
 		case collectionTest:
-			err = layer.f(clientCtx, c)
+			err = layer.f(clientCtx, d, c)
 		case rowTest:
 			err = layer.f(clientCtx, r)
 		default:
@@ -589,7 +802,7 @@
 		{"foo", "u:bob:angrybirds", "u:bob:todos:phone", "u:carol", "u:dave", "x:baz"},
 	}
 
-	dummyPerms = access.Permissions{}.Add("root:u:nobody", string(access.Admin))
+	dummyPerms = access.Permissions{}.Add("...", string(access.Read)).Add("root:u:nobody", string(access.Admin))
 )
 
 // TestIdBlessingInfer tests that Database/Collection/Syncgroup getter variants
@@ -874,7 +1087,7 @@
 	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))
+	permsComplex := permsRA.Copy().Add("root:u:client", access.TagStrings(access.Read, access.Admin)...).Blacklist("root:o:app:client:phone", string(access.Admin))
 
 	testPermsValidationOp(t, "d.Create()",
 		[]access.Permissions{nil /* infer */, permsAdminOnly, permsComplex, permsRA, permsRWA, permsXRWA},
@@ -892,9 +1105,10 @@
 			return c.Create(ctx, perms)
 		})
 
+	// Syncgroup ACL must allow Read access to client.
 	testPermsValidationOp(t, "sg.Create()",
-		[]access.Permissions{permsAdminOnly, permsComplex, permsRA},
-		[]access.Permissions{nil, permsEmpty, permsNoAdmin, permsRWA, permsXRWA, permsXRWAD},
+		[]access.Permissions{permsComplex, permsRA},
+		[]access.Permissions{nil, permsEmpty, permsNoAdmin, permsAdminOnly, 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{
@@ -924,9 +1138,10 @@
 			return rc.SetPermissions(ctx, perms)
 		})
 
+	// New syncgroup ACL must allow Read access to client.
 	testPermsValidationOp(t, "sg.SetSpec()",
-		[]access.Permissions{permsAdminOnly, permsComplex, permsRA},
-		[]access.Permissions{nil, permsEmpty, permsNoAdmin, permsRWA, permsXRWA, permsXRWAD},
+		[]access.Permissions{permsComplex, permsRA},
+		[]access.Permissions{nil, permsEmpty, permsNoAdmin, permsAdminOnly, permsRWA, permsXRWA, permsXRWAD},
 		func(_ string, perms access.Permissions) error {
 			return rsg.SetSpec(ctx, wire.SyncgroupSpec{
 				Collections: []wire.Id{rc.Id()},
diff --git a/syncbase/syncgroup_test.go b/syncbase/syncgroup_test.go
index 86da93c..bd77128 100644
--- a/syncbase/syncgroup_test.go
+++ b/syncbase/syncgroup_test.go
@@ -73,16 +73,27 @@
 	createSyncgroup(t, ctx, d, sg3, spec, verror.ErrNoExist.ID)
 	verifySyncgroups(t, ctx, d, wantGroups, verror.ID(""))
 
+	// Check if creating a syncgroup on an inaccessible collection fails.
+	c2 := tu.CreateCollection(t, ctx, d, "c2")
+	if err := c2.SetPermissions(ctx, access.Permissions{}.Add("root:u:nobody", string(access.Admin))); err != nil {
+		t.Fatalf("c2.SetPermissions() failed: %v", err)
+	}
+	spec.Description = "test syncgroup sg4"
+	spec.Collections = []wire.Id{c2.Id()}
+	sg4 := d.Syncgroup(ctx, "sg4").Id()
+	createSyncgroup(t, ctx, d, sg4, spec, verror.ErrNoAccess.ID)
+	verifySyncgroups(t, ctx, d, wantGroups, verror.ID(""))
+
 	// Check that create fails if the perms disallow access.
 	perms := tu.DefaultPerms(wire.AllDatabaseTags, "root:o:app:client")
-	perms.Blacklist("root:o:app:client", string(access.Read))
+	perms.Blacklist("root:o:app:client", string(access.Read), string(access.Write))
 	if err := d.SetPermissions(ctx, perms, ""); err != nil {
 		t.Fatalf("d.SetPermissions() failed: %v", err)
 	}
-	spec.Description = "test syncgroup sg4"
+	spec.Description = "test syncgroup sg5"
 	spec.Collections = []wire.Id{wire.Id{"u", "c"}}
-	sg4 := d.Syncgroup(ctx, "sg4").Id()
-	createSyncgroup(t, ctx, d, sg4, spec, verror.ErrNoAccess.ID)
+	sg5 := d.Syncgroup(ctx, "sg5").Id()
+	createSyncgroup(t, ctx, d, sg5, spec, verror.ErrNoAccess.ID)
 	verifySyncgroups(t, ctx, d, nil, verror.ErrNoAccess.ID)
 }
 
@@ -92,11 +103,14 @@
 func TestJoinSyncgroup(t *testing.T) {
 	// Create client1-server pair.
 	ctx, ctx1, sName, rootp, cleanup := tu.SetupOrDieCustom("o:app:client1", "server",
-		tu.DefaultPerms(access.AllTypicalTags(), "root:o:app:client1").Add("root:o:app:client2", string(access.Resolve)))
+		tu.DefaultPerms(access.AllTypicalTags(), "root:o:app:client1").Add("root:o:app:client2", string(access.Resolve), string(access.Write)))
 	defer cleanup()
 
 	d1 := tu.CreateDatabase(t, ctx1, syncbase.NewService(sName), "d", nil)
 	c := tu.CreateCollection(t, ctx1, d1, "c")
+	if err := c.SetPermissions(ctx1, access.Permissions{}.Add("...", access.TagStrings(wire.AllCollectionTags...)...)); err != nil {
+		t.Fatalf("c.SetPermissions() failed: %v", err)
+	}
 	specA := wire.SyncgroupSpec{
 		Description: "test syncgroup sgA",
 		Perms:       tu.DefaultPerms(wire.AllSyncgroupTags, "root:o:app:client1"),
@@ -151,6 +165,8 @@
 	verifySyncgroupInfo(t, ctx2, d2, sgIdB, specB, 1)
 }
 
+// TODO(ivanpi): Test security in more complex join scenario (2 syncbases).
+
 // Tests that Syncgroup.SetSpec works as expected.
 func TestSetSpecSyncgroup(t *testing.T) {
 	ctx, sName, cleanup := tu.SetupOrDie(tu.DefaultPerms(access.AllTypicalTags(), "root:o:app:client"))
@@ -175,11 +191,33 @@
 	spec.Description = "test syncgroup sg1 update"
 	spec.Perms = tu.DefaultPerms(wire.AllSyncgroupTags, "root:o:app:client", "root:o:app:client1")
 
+	// Verify setting spec works.
 	sg := d.SyncgroupForId(sgId)
 	if err := sg.SetSpec(ctx, spec, ""); err != nil {
 		t.Fatalf("sg.SetSpec failed: %v", err)
 	}
 	verifySyncgroupInfo(t, ctx, d, sgId, spec, 1)
+
+	specChangedCxs := spec
+	specChangedCxs.Description = "test syncgroup sg1 update - changed collections"
+	c2 := tu.CreateCollection(t, ctx, d, "c2")
+	specChangedCxs.Collections = []wire.Id{c.Id(), c2.Id()}
+
+	// Verify setting spec with changed Collections fails.
+	if err := sg.SetSpec(ctx, specChangedCxs, ""); verror.ErrorID(err) != verror.ErrBadArg.ID {
+		t.Fatalf("sg.SetSpec should have failed with ErrBadArg, got: %v", err)
+	}
+	verifySyncgroupInfo(t, ctx, d, sgId, spec, 1)
+
+	specInvalidPerms := spec
+	specInvalidPerms.Description = "test syncgroup sg1 update - invalid perms"
+	specInvalidPerms.Perms = tu.DefaultPerms(wire.AllSyncgroupTags, "root:o:app:client1")
+
+	// Verify setting spec with self missing from Read ACL fails.
+	if err := sg.SetSpec(ctx, specInvalidPerms, ""); verror.ErrorID(err) != verror.ErrBadArg.ID {
+		t.Fatalf("sg.SetSpec should have failed with ErrBadArg, got: %v", err)
+	}
+	verifySyncgroupInfo(t, ctx, d, sgId, spec, 1)
 }
 
 ///////////////////