blob: 28376484ce379bc8144ac178a1272c4c1270dcff [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package syncbase_test
import (
"fmt"
"reflect"
"strings"
"testing"
"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/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"
vtestutil "v.io/x/ref/test/testutil"
)
type serviceTest struct {
f func(ctx *context.T, s syncbase.Service) error
}
type databaseTest struct {
f func(ctx *context.T, d syncbase.Database) error
}
type batchDatabaseTest struct {
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, c syncbase.Collection) error
}
type collectionTest struct {
noBatch bool
f func(ctx *context.T, d syncbase.Database, c syncbase.Collection) error
}
type rowTest struct {
noBatch bool
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
}
// checkLayer specifies the layer where the primary authorization and
// existence check of the method is carried out.
type checkLayer string
const (
clService checkLayer = "S"
clDatabase checkLayer = "D"
clCollection checkLayer = "C"
clRow checkLayer = "R"
clSyncgroup checkLayer = "G"
)
type securitySpecTest struct {
layer interface{}
name string
checkLayer checkLayer
pattern string
}
type securitySpecTestGroup struct {
layer interface{}
name string
checkLayer checkLayer
patterns []string
mutating bool
}
// TODO(rogulenko): Add more test groups.
var securitySpecTestGroups = []securitySpecTestGroup{
// Service tests.
{
layer: serviceTest{f: func(ctx *context.T, s syncbase.Service) error {
_, err := wire.ServiceClient(s.FullName()).DevModeGetTime(ctx)
return err
}},
name: "service.DevModeGetTime",
checkLayer: clService,
patterns: []string{"A___"},
},
{
layer: serviceTest{f: func(ctx *context.T, s syncbase.Service) error {
return wire.ServiceClient(s.FullName()).DevModeUpdateVClock(ctx, wire.DevModeUpdateVClockOpts{})
}},
name: "service.DevModeUpdateVClock",
checkLayer: clService,
patterns: []string{"A___"},
mutating: true,
},
{
layer: serviceTest{f: func(ctx *context.T, s syncbase.Service) error {
_, _, err := s.GetPermissions(ctx)
return err
}},
name: "service.GetPermissions",
checkLayer: clService,
patterns: []string{"A___"},
},
{
layer: serviceTest{f: func(ctx *context.T, s syncbase.Service) error {
return s.SetPermissions(ctx, tu.DefaultPerms(access.AllTypicalTags(), "root"), "")
}},
name: "service.SetPermissions",
checkLayer: clService,
patterns: []string{"A___"},
mutating: true,
},
{
layer: serviceTest{f: func(ctx *context.T, s syncbase.Service) error {
_, err := s.ListDatabases(ctx)
return err
}},
name: "service.ListDatabases",
checkLayer: clService,
patterns: []string{"R___"},
},
// Database tests.
{
layer: serviceTest{f: func(ctx *context.T, s syncbase.Service) error {
return s.DatabaseForId(wire.Id{"root", "dNew"}, nil).Create(ctx, tu.DefaultPerms(wire.AllDatabaseTags, "root"))
}},
name: "database.Create",
checkLayer: clService,
patterns: []string{"W___"},
mutating: true,
},
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
return d.Destroy(ctx)
}},
name: "database.Destroy",
checkLayer: clDatabase, // (also Service, depending on specific perms)
patterns: []string{"XA__", "A___"},
mutating: true,
},
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
_, err := d.Exists(ctx)
return err
}},
name: "database.Exists",
checkLayer: clDatabase,
patterns: []string{"XX__", "XR__", "XW__", "XA__", "R___", "W___"},
},
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
_, _, err := d.GetPermissions(ctx)
return err
}},
name: "database.GetPermissions",
checkLayer: clDatabase,
patterns: []string{"XA__"},
},
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
return d.SetPermissions(ctx, tu.DefaultPerms(wire.AllDatabaseTags, "root"), "")
}},
name: "database.SetPermissions",
checkLayer: clDatabase,
patterns: []string{"XA__"},
mutating: true,
},
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
_, err := d.ListCollections(ctx)
return err
}},
name: "database.ListCollections - nonbatch",
checkLayer: clDatabase,
patterns: []string{"XR__"},
},
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
ws := d.Watch(ctx, nil, []wire.CollectionRowPattern{util.RowPrefixPattern(wire.Id{"u", "c"}, "")})
defer ws.Cancel()
// Advance() will not block indefinitely because at least the root update
// is always sent for nil resume marker.
ws.Advance()
return ws.Err()
}},
name: "database.Watch",
checkLayer: clDatabase,
patterns: []string{"XR__"},
},
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
_, err := d.BeginBatch(ctx, wire.BatchOptions{})
return err
}},
name: "database.BeginBatch",
checkLayer: clDatabase,
patterns: []string{"XX__", "XR__", "XW__", "XA__"},
},
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
_, err := d.GetResumeMarker(ctx)
return err
}},
name: "database.GetResumeMarker",
checkLayer: clDatabase,
patterns: []string{"XX__", "XR__", "XW__", "XA__"},
},
// TODO(ivanpi): Test Exec.
// TODO(ivanpi): Test RPC-only methods such as Glob.
// Batch database tests.
{
layer: batchDatabaseTest{f: func(ctx *context.T, b syncbase.BatchDatabase) error {
return b.Commit(ctx)
}},
name: "database.Commit",
checkLayer: clDatabase,
patterns: []string{"XX__", "XR__", "XW__", "XA__"},
mutating: true,
},
{
layer: batchDatabaseTest{f: func(ctx *context.T, b syncbase.BatchDatabase) error {
return b.Abort(ctx)
}},
name: "database.Abort",
checkLayer: clDatabase,
patterns: []string{"XX__", "XR__", "XW__", "XA__"},
mutating: true,
},
{
layer: batchDatabaseTest{f: func(ctx *context.T, b syncbase.BatchDatabase) error {
_, err := b.ListCollections(ctx)
return err
}},
name: "database.ListCollections - batch",
checkLayer: clDatabase,
patterns: []string{"XR__"},
},
// TODO(ivanpi): Test Exec.
// Conflict resolver tests.
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
_, err := wire.DatabaseClient(d.FullName()).GetSchemaMetadata(ctx)
if verror.ErrorID(err) == verror.ErrNoExist.ID {
return nil
}
return err
}},
name: "database.GetSchemaMetadata",
checkLayer: clDatabase,
patterns: []string{"XR__"},
},
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
return wire.DatabaseClient(d.FullName()).SetSchemaMetadata(ctx, wire.SchemaMetadata{})
}},
name: "database.SetSchemaMetadata",
checkLayer: clDatabase,
patterns: []string{"XA__"},
mutating: true,
},
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
return wire.DatabaseClient(d.FullName()).PauseSync(ctx)
}},
name: "database.PauseSync",
checkLayer: clDatabase,
patterns: []string{"XA__"},
mutating: true,
},
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
return wire.DatabaseClient(d.FullName()).ResumeSync(ctx)
}},
name: "database.ResumeSync",
checkLayer: clDatabase,
patterns: []string{"XA__"},
mutating: true,
},
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
s, err := wire.DatabaseClient(d.FullName()).StartConflictResolver(ctx)
if err != nil {
return err
}
cancelTimer := time.AfterFunc(5*time.Second, cancel) // slows down only the allowed case
defer cancelTimer.Stop()
s.RecvStream().Advance()
err = s.RecvStream().Err()
if verror.ErrorID(err) == verror.ErrCanceled.ID {
return nil
}
return err
}},
name: "database.StartConflictResolver",
checkLayer: clDatabase,
patterns: []string{"XA__"},
mutating: true,
},
// Blob manager tests.
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
_, err := d.CreateBlob(ctx)
return err
}},
name: "database.CreateBlob",
checkLayer: clDatabase,
patterns: []string{"XW__"},
mutating: true,
},
{
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",
checkLayer: clDatabase,
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",
checkLayer: clDatabase,
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",
checkLayer: clDatabase,
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",
checkLayer: clDatabase,
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
}
defer r.Cancel()
r.Advance()
return r.Err()
}},
name: "blob.Get",
checkLayer: clDatabase,
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
}
defer s.Cancel()
s.Advance()
return s.Err()
}},
name: "blob.Fetch",
checkLayer: clDatabase,
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",
checkLayer: clDatabase,
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",
checkLayer: clDatabase,
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",
checkLayer: clDatabase,
patterns: []string{"XX__", "XR__", "XW__", "XA__"},
mutating: true,
},
// TODO(ivanpi): Test Syncbase-to-Syncbase blob RPCs.
// Syncgroup manager tests.
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
_, err := d.ListSyncgroups(ctx)
return err
}},
name: "database.ListSyncgroups",
checkLayer: clDatabase,
patterns: []string{"XR__"},
},
{
layer: collectionTest{noBatch: true, 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",
checkLayer: clDatabase, // (Collection check is not recursive, Database check is sufficient against leaks locally)
patterns: []string{"XWR_"},
mutating: true,
},
{
layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup, _ syncbase.Collection) error {
_, err := sg.Join(ctx, "", []string{}, wire.SyncgroupMemberInfo{})
return err
}},
name: "syncgroup.Join (already joined)",
checkLayer: clDatabase, // (Collection and Syncgroup check is not recursive, Database check is sufficient against leaks locally)
patterns: []string{"XWRR"},
mutating: true,
},
{
layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup, _ syncbase.Collection) error {
return expectNotImplemented(sg.Leave(ctx))
}},
name: "syncgroup.Leave",
checkLayer: clDatabase,
patterns: []string{"XW__"},
mutating: true,
},
{
layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup, _ syncbase.Collection) error {
return expectNotImplemented(sg.Destroy(ctx))
}},
name: "syncgroup.Destroy",
checkLayer: clDatabase, // (Syncgroup check is not recursive, Database check is sufficient against leaks locally)
patterns: []string{"XW_A"},
mutating: true,
},
{
layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup, _ syncbase.Collection) error {
return expectNotImplemented(sg.Eject(ctx, "root:nobody"))
}},
name: "syncgroup.Eject",
checkLayer: clSyncgroup,
patterns: []string{"XX_A"},
mutating: true,
},
{
layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup, _ syncbase.Collection) error {
_, _, err := sg.GetSpec(ctx)
return err
}},
name: "syncgroup.GetSpec",
checkLayer: clSyncgroup,
patterns: []string{"XX_R"},
},
{
layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup, c syncbase.Collection) error {
return sg.SetSpec(ctx, wire.SyncgroupSpec{
Perms: tu.DefaultPerms(wire.AllSyncgroupTags, "root"),
Collections: []wire.Id{c.Id()},
}, "")
}},
name: "syncgroup.SetSpec",
checkLayer: clSyncgroup,
patterns: []string{"XX_A"},
mutating: true,
},
{
layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup, _ syncbase.Collection) error {
_, err := sg.GetMembers(ctx)
return err
}},
name: "syncgroup.GetMembers",
checkLayer: clSyncgroup,
patterns: []string{"XX_R"},
},
// TODO(ivanpi): Test Syncbase-to-Syncbase sync RPCs.
// Collection tests.
{
layer: databaseTest{f: func(ctx *context.T, d syncbase.Database) error {
return d.CollectionForId(wire.Id{"root", "cNew"}).Create(ctx, tu.DefaultPerms(wire.AllCollectionTags, "root"))
}},
name: "collection.Create",
checkLayer: clDatabase,
patterns: []string{"XW__"},
mutating: true,
},
{
layer: collectionTest{f: func(ctx *context.T, _ syncbase.Database, c syncbase.Collection) error {
return c.Destroy(ctx)
}},
name: "collection.Destroy",
checkLayer: clCollection, // (also Database, depending on specific perms)
patterns: []string{"XXA_", "XA__"},
mutating: true,
},
{
layer: collectionTest{f: func(ctx *context.T, _ syncbase.Database, c syncbase.Collection) error {
_, err := c.Exists(ctx)
return err
}},
name: "collection.Exists",
checkLayer: clCollection,
patterns: []string{"XXR_", "XXW_", "XXA_", "XR__", "XW__"},
},
{
layer: collectionTest{f: func(ctx *context.T, _ syncbase.Database, c syncbase.Collection) error {
_, err := c.GetPermissions(ctx)
return err
}},
name: "collection.GetPermissions",
checkLayer: clCollection,
patterns: []string{"XXA_"},
},
{
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",
checkLayer: clCollection,
patterns: []string{"XXA_"},
mutating: true,
},
{
layer: collectionTest{f: func(ctx *context.T, _ syncbase.Database, c syncbase.Collection) error {
ss := c.Scan(ctx, syncbase.Prefix(""))
defer ss.Cancel()
ss.Advance()
return ss.Err()
}},
name: "collection.Scan",
checkLayer: clCollection,
patterns: []string{"XXR_"},
},
{
layer: collectionTest{f: func(ctx *context.T, _ syncbase.Database, c syncbase.Collection) error {
return c.DeleteRange(ctx, syncbase.Prefix(""))
}},
name: "collection.DeleteRange",
checkLayer: clCollection,
patterns: []string{"XXW_"},
mutating: true,
},
// TODO(ivanpi): Test RPC-only methods such as Glob. (Note, Glob is non-batch only.)
// Row tests.
{
layer: rowTest{f: func(ctx *context.T, r syncbase.Row) error {
_, err := r.Exists(ctx)
return err
}},
name: "row.Exists",
checkLayer: clRow,
patterns: []string{"XXR_", "XXW_"},
},
{
layer: rowTest{f: func(ctx *context.T, r syncbase.Row) error {
var value string
return r.Get(ctx, &value)
}},
name: "row.Get",
checkLayer: clCollection, // (Row check is only meaningful for Exists since Rows don't have separate ACLs)
patterns: []string{"XXR_"},
},
{
layer: rowTest{f: func(ctx *context.T, r syncbase.Row) error {
value := "NCC-1701-D"
return r.Put(ctx, &value)
}},
name: "row.Put",
checkLayer: clCollection,
patterns: []string{"XXW_"},
mutating: true,
},
{
layer: rowTest{f: func(ctx *context.T, r syncbase.Row) error {
return r.Delete(ctx)
}},
name: "row.Delete",
checkLayer: clCollection,
patterns: []string{"XXW_"},
mutating: true,
},
}
// TestSecuritySpec tests the Syncbase ACL specification. It runs a set of test
// groups.
//
// Each test group describes a Syncbase public method and a list of security
// patterns that should allow a client to call the method.
// A method might be service.GetPermissions and the pattern for it is "A___".
// A security pattern is a string of 4 bytes where each byte is an '_' or
// one of X (resolve), R (read), W (write), A (admin).
// The character index stands for:
// 0 - service ACL
// 1 - database ACL
// 2 - collection ACL
// 3 - syncgroup ACL
// A pattern defines a set of per-ACL permissions required for a client to call
// the method.
//
// For each test group we generate two sets of tests: allowed tests and denied
// tests.
// Allowed tests are generated by splitting the list of security patterns
// of the test group into one pattern per test.
// Denied tests are generated the following way: we take each allowed pattern
// and generate all patterns that differ in one character, preserving all '_'
// (excluding patterns that are a subset of an allowed pattern). This way we
// make sure that every character in a security pattern is important.
// Allowed tests are split in two categories: mutating tests and static tests.
// Mutating tests change the state of Syncbase, static don't.
// Denied tests are always static.
//
// Then we pack all tests into runs, where each run has only one type of tests
// (allowed or denied) and at most one mutating test. For each run we rebuild
// the following Syncbase structure:
// service s; database {a,d}; collection c; row prefix.
//
// For each test inside a run we generate 5 huge ACLs with a record for each
// of the test + one admin record.
func TestSecuritySpec(t *testing.T) {
deniedTests, staticAllowedTests, mutatingAllowedTests := prepareTests()
checkDenied := func(t *testing.T, test securitySpecTest, batch bool, err error) {
if verror.ErrorID(err) != verror.ErrNoAccess.ID && verror.ErrorID(err) != verror.ErrNoExistOrNoAccess.ID {
tu.Fatalf(t, "test %v (batch: %v) didn't fail with ErrNoAccess or ErrNoExistOrNoAccess error: %v", test, batch, err)
}
}
runTests(t, csAll, checkDenied, deniedTests...)
checkAllowed := func(t *testing.T, test securitySpecTest, batch bool, err error) {
if err != nil {
tu.Fatalf(t, "test %v (batch: %v) failed with non-nil error: %v", test, batch, err)
}
}
runTests(t, csAll, checkAllowed, staticAllowedTests...)
for _, test := range mutatingAllowedTests {
runTests(t, csAll, checkAllowed, test)
}
}
// TestErrorExistLeakSpec tests that the Syncbase RPCs do not leak information
// about database, collection, syncgroup [non]existence to unauthorized clients.
// It uses the test group infrastructure from TestSecuritySpec.
//
// For simplicity and speed, tests are run only with denied tests, avoiding
// mutable tests. Each test group specifies the layer where its primary
// authorization and existence check is carried out in checkLayer.
//
// All tests are run against different configurations of layer existence as
// specified by createSpec. Batch existence is only relevant for tests that
// specify batch usage.
//
// Since the main intent is to verify that ErrNoExistOrNoAccess is returned
// consistently when required, variance in other errors for different RPCs is
// more loosely checked.
func TestErrorExistLeakSpec(t *testing.T) {
deniedTests, _, _ := prepareTests()
createSpecs := []string{
"",
csDatabase,
csDatabase + csBatch,
csDatabase + csCollection,
csDatabase + csBatch + csCollection,
csDatabase + csCollection + csRow,
csDatabase + csBatch + csCollection + csRow,
csDatabase + csCollection + csSyncgroup,
csDatabase + csBlob,
csAll,
}
// Race tests run out of memory when all combinations are run. Run on a
// representative subset instead.
// TODO(ivanpi): Investigate further. Error encountered is:
// "ThreadSanitizer: DenseSlabAllocator overflow. Dying."
if vtestutil.RaceEnabled {
createSpecs = []string{
"",
csDatabase + csBatch,
csDatabase + csCollection + csSyncgroup,
}
}
for _, createSpec := range createSpecs {
runTests(t, createSpec, makeErrorChecker(createSpec), deniedTests...)
}
// TODO(ivanpi): Consider testing more complex scenarios, such as allowed
// patterns and different ACLs inside vs outside batch.
}
func prepareTests() (deniedTests, staticAllowedTests, mutatingAllowedTests []securitySpecTest) {
for _, testGroup := range securitySpecTestGroups {
// Collect allowed patterns.
allowedPatterns := map[string]bool{}
for _, pattern := range testGroup.patterns {
allowedPatterns[pattern] = true
}
// Fill in allowed test slices.
for pattern := range allowedPatterns {
test := securitySpecTest{
layer: testGroup.layer,
name: testGroup.name,
checkLayer: testGroup.checkLayer,
pattern: pattern,
}
if testGroup.mutating {
mutatingAllowedTests = append(mutatingAllowedTests, test)
} else {
staticAllowedTests = append(staticAllowedTests, test)
}
}
// Collect denied patterns.
deniedPatterns := map[string]bool{}
for _, pattern := range testGroup.patterns {
for i := 0; i < len(pattern); i++ {
if pattern[i] != '_' {
patternBytes := []byte(pattern)
for _, c := range []byte{'_', 'X', 'R', 'W', 'A'} {
patternBytes[i] = c
if !isSubsetOfAny(string(patternBytes), allowedPatterns) {
deniedPatterns[string(patternBytes)] = true
}
}
}
}
}
// Fill in denied test slice.
for pattern := range deniedPatterns {
test := securitySpecTest{
layer: testGroup.layer,
name: testGroup.name,
checkLayer: testGroup.checkLayer,
pattern: pattern,
}
deniedTests = append(deniedTests, test)
}
}
return
}
func isSubsetOf(subset, superset string) bool {
for i := 0; i < len(superset); i++ {
if superset[i] != '_' && superset[i] != subset[i] {
return false
}
}
// All perms in subset correspond to the same perm or '_' in superset.
return true
}
func isSubsetOfAny(needle string, haystack map[string]bool) bool {
for p := range haystack {
if isSubsetOf(needle, p) {
return true
}
}
return false
}
const (
// createSpec is a concatenation of zero or more of the constants below,
// describing which layers should exist for a given test. Layer dependencies
// are not automatically handled - if a layer requires another layer, both
// layers must be specified.
csDatabase = "D"
csBatch = "T"
csCollection = "C"
csRow = "R"
csSyncgroup = "G"
csBlob = "B"
csAll = csDatabase + csBatch + csCollection + csRow + csSyncgroup + csBlob
)
type checkerFunc func(t *testing.T, test securitySpecTest, batch bool, err error)
// runTest creates the Syncbase hierarchy according to the provided createSpec
// (see TestErrorExistLeakSpec comment for description) and runs the provided
// set of tests against it, running checkerFunc
func runTests(t *testing.T, createSpec string, check checkerFunc, tests ...securitySpecTest) {
// Create permissions.
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)
defer cleanup()
// Service
s := syncbase.NewService(sName)
if err := s.SetPermissions(adminCtx, servicePerms, ""); err != nil {
tu.Fatalf(t, "s.SetPermissions failed: %v", err)
}
// Database
d := s.DatabaseForId(wire.Id{"root", "d"}, nil)
if strings.Contains(createSpec, csDatabase) {
if err := d.Create(adminCtx, databasePerms); err != nil {
tu.Fatalf(t, "d.Create failed: %v", err)
}
}
// Collection
c := d.CollectionForId(wire.Id{"root:admin", "c"})
if strings.Contains(createSpec, csCollection) {
if err := c.Create(adminCtx, collectionPerms); err != nil {
tu.Fatalf(t, "c.Create failed: %v", err)
}
}
// Row
r := c.Row("prefix")
if strings.Contains(createSpec, csRow) {
if err := r.Put(adminCtx, "value"); err != nil {
tu.Fatalf(t, "r.Put failed: %v", err)
}
}
// Syncgroup
sg := d.SyncgroupForId(wire.Id{"root:admin", "sg"})
if strings.Contains(createSpec, csSyncgroup) {
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)
}
}
// Blob
blobCommitted := d.Blob("_fake_committed")
blobUncommitted := d.Blob("_fake_uncommitted")
if strings.Contains(createSpec, csBlob) {
var err error
if blobCommitted, err = d.CreateBlob(adminCtx); 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)
}
if blobUncommitted, err = d.CreateBlob(adminCtx); err != nil {
tu.Fatalf(t, "d.CreateBlob failed: %v", err)
}
}
// Batch
dBatch := syncbase.NewNonexistentBatchForTest(d)
if strings.Contains(createSpec, csBatch) {
var err error
if dBatch, err = d.BeginBatch(adminCtx, wire.BatchOptions{}); err != nil {
tu.Fatalf(t, "d.BeginBatch failed: %v", err)
}
defer dBatch.Abort(adminCtx)
}
cBatch := dBatch.CollectionForId(c.Id())
rBatch := cBatch.Row(r.Key())
// Verify tests.
for i, test := range tests {
if filteredPattern := filterPermsPattern(createSpec, test.pattern); filteredPattern != test.pattern {
// Skip tests whose ACL patterns are unsatisfiable because the
// corresponding layers are missing in createSpec.
continue
}
// Note, we run collection and row tests twice against the same hierarchy
// (batch and non-batch) even if they are mutable. This should be safe
// thanks to batch isolation.
clientCtx, cancelClientCtx := context.WithCancel(tu.NewCtx(ctx, rootp, fmt.Sprintf("client%d", i)))
switch layer := test.layer.(type) {
case serviceTest:
check(t, test, false, layer.f(clientCtx, s))
case databaseTest:
check(t, test, false, layer.f(clientCtx, d))
case batchDatabaseTest:
check(t, test, true, layer.f(clientCtx, dBatch))
case blobTest:
if layer.commit {
check(t, test, false, layer.f(clientCtx, d, blobCommitted))
} else {
check(t, test, false, layer.f(clientCtx, d, blobUncommitted))
}
case syncgroupTest:
check(t, test, false, layer.f(clientCtx, sg, c))
case collectionTest:
if !layer.noBatch {
check(t, test, true, layer.f(clientCtx, d, cBatch))
}
check(t, test, false, layer.f(clientCtx, d, c))
case rowTest:
if !layer.noBatch {
check(t, test, true, layer.f(clientCtx, rBatch))
}
check(t, test, false, layer.f(clientCtx, r))
default:
tu.Fatalf(t, "test %v: unknown test type", test)
}
cancelClientCtx()
}
}
// filterPermsPattern filters out (sets to '_') permissions in the pattern that
// cannot exist because the corresponding layer doesn't exist in createSpec.
func filterPermsPattern(createSpec, pattern string) string {
patternBytes := []byte(pattern)
if !strings.Contains(createSpec, csDatabase) {
patternBytes[1] = '_'
}
if !strings.Contains(createSpec, csCollection) {
patternBytes[2] = '_'
}
if !strings.Contains(createSpec, csSyncgroup) {
patternBytes[3] = '_'
}
return string(patternBytes)
}
// 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 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",
'W': "Write",
'A': "Admin",
}
for i, t := range tests {
if tag, ok := tagsMap[t.pattern[index]]; ok {
perms.Add(security.BlessingPattern(fmt.Sprintf("root:client%d", i)), tag)
}
}
return util.FilterTags(perms, allowTags...)
}
// makeErrorChecker builds a checkerFunc for TestErrorExistLeakSpec for the
// given createSpec.
func makeErrorChecker(createSpec string) checkerFunc {
return func(t *testing.T, test securitySpecTest, batch bool, gotErr error) {
filteredPattern := filterPermsPattern(createSpec, test.pattern)
if filteredPattern != test.pattern {
tu.Fatalf(t, "test didn't filter out pattern %s invalid for createSpec %s", test.pattern, createSpec)
}
assertErrorOneOf := func(wantErrs ...verror.ID) {
for _, wantErr := range wantErrs {
if verror.ErrorID(gotErr) == wantErr {
return
}
}
tu.Fatalf(t, "setup %s, test %v (batch: %v, filtered perms: %s) didn't fail with one of %v, got error: %v", createSpec, test, batch, filteredPattern, wantErrs, gotErr)
}
checkPerms := func(patterns ...string) bool {
for _, p := range patterns {
if isSubsetOf(filteredPattern, p) {
return true
}
}
return false
}
if test.checkLayer == clService {
// Service always exists, its methods only fail with ErrNoAccess.
assertErrorOneOf(verror.ErrNoAccess.ID)
return
}
if !checkPerms("XX__", "XR__", "XW__", "XA__", "R___", "W___") {
// Caller is not allowed to know if Database exists, so methods on
// Database and above fail with ErrNoExistOrNoAccess.
assertErrorOneOf(verror.ErrNoExistOrNoAccess.ID)
return
}
if !strings.Contains(createSpec, csDatabase) {
// Database does not exist, so methods on Database and above fail with
// ErrNoExist.
// Exists and Destroy (idempotent case) return nil error.
assertErrorOneOf(verror.ErrorID(nil), verror.ErrNoExist.ID)
return
}
if batch {
if !strings.Contains(createSpec, csBatch) {
// Client is allowed to know that Database exists, but the batch handle
// is invalid.
assertErrorOneOf(wire.ErrUnknownBatch.ID)
return
}
}
if test.checkLayer == clDatabase {
// Database exists and client is allowed to know it, so its methods fail
// with ErrNoAccess.
// Some of the Syncgroup Create and Join methods have patterns that do
// pass the Database ACL, but fail on Collection existence check or
// Syncgroup remote join check (after failing Syncgroup existence check).
assertErrorOneOf(verror.ErrNoAccess.ID, verror.ErrNoExist.ID, wire.ErrSyncgroupJoinFailed.ID)
return
}
if !checkPerms("X___") {
// Client cannot resolve through Service, so methods above Database fail
// with ErrNoAccess.
assertErrorOneOf(verror.ErrNoAccess.ID)
return
}
if test.checkLayer == clSyncgroup {
// Local Syncgroup method branch:
if !checkPerms("XX_R", "XX_A", "XR__", "XW__") {
// Caller is not allowed to know if Syncgroup exists, so methods on
// Syncgroup fail with ErrNoExistOrNoAccess.
assertErrorOneOf(verror.ErrNoExistOrNoAccess.ID)
return
}
if !strings.Contains(createSpec, csSyncgroup) {
// Syncgroup does not exist, so methods on Syncgroup fail with
// ErrNoExist.
assertErrorOneOf(verror.ErrNoExist.ID)
return
}
// Syncgroup exists and client is allowed to know it, so its methods fail
// with ErrNoAccess.
assertErrorOneOf(verror.ErrNoAccess.ID)
return
}
// Collection and Row method branch:
if !checkPerms("XXR_", "XXW_", "XXA_", "XR__", "XW__") {
// Caller is not allowed to know if Collection exists, so methods on
// Collection and above fail with ErrNoExistOrNoAccess.
assertErrorOneOf(verror.ErrNoExistOrNoAccess.ID)
return
}
if !strings.Contains(createSpec, csCollection) {
// Collection does not exist, so methods on Collection and above fail
// with ErrNoExist.
assertErrorOneOf(verror.ErrorID(nil), verror.ErrNoExist.ID)
return
}
if test.checkLayer == clCollection {
// Collection exists and client is allowed to know it, so its methods fail
// with ErrNoAccess.
assertErrorOneOf(verror.ErrNoAccess.ID)
return
}
if !checkPerms("XX__") {
// Client cannot resolve through Database, so methods above Collection
// fail with ErrNoAccess.
assertErrorOneOf(verror.ErrNoAccess.ID)
return
}
if !checkPerms("XXR_", "XXW_") {
// Caller is not allowed to know if Row exists, so methods on Row fail
// with ErrNoExistOrNoAccess.
assertErrorOneOf(verror.ErrNoExistOrNoAccess.ID)
return
}
// Note, since Exists is currently the only Row-checked method, the two
// checks below are unnecessary.
/*
if !strings.Contains(createSpec, csRow) {
// Row does not exist, so methods on Row fail with ErrNoExist.
assertErrorOneOf(verror.ErrNoExist.ID)
return
}
if test.checkLayer == clRow {
// Row exists and client is allowed to know it, so its methods fail
// with ErrNoAccess.
assertErrorOneOf(verror.ErrNoAccess.ID)
return
}
*/
// Blob layer has no permissions of its own, it is covered by Database
// permissions.
tu.Fatalf(t, "unknown permission check layer: %v", test.checkLayer)
}
}
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("...", string(access.Read)).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", nil)
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", 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},
[]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)
})
// Syncgroup ACL must allow Read access to client.
testPermsValidationOp(t, "sg.Create()",
[]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{
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)
})
// New syncgroup ACL must allow Read access to client.
testPermsValidationOp(t, "sg.SetSpec()",
[]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()},
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)
}
}
}