syncbase: Tests for preventing errors leaking existence.
Also improved resource cleanup in permissions tests.
Change-Id: I4f591a64d8aaa874f4e7dd03333c66842da7795b
diff --git a/syncbase/permissions_internal_test.go b/syncbase/permissions_internal_test.go
new file mode 100644
index 0000000..6447eef
--- /dev/null
+++ b/syncbase/permissions_internal_test.go
@@ -0,0 +1,10 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package syncbase
+
+func NewNonexistentBatchForTest(db Database) BatchDatabase {
+ d := db.(*database)
+ return newBatch(d.parentFullName, d.id, "sdeadbeefdeadbeef") // likely doesn't exist
+}
diff --git a/syncbase/permissions_test.go b/syncbase/permissions_test.go
index b8ffd4f..2837648 100644
--- a/syncbase/permissions_test.go
+++ b/syncbase/permissions_test.go
@@ -7,6 +7,7 @@
import (
"fmt"
"reflect"
+ "strings"
"testing"
"time"
@@ -21,6 +22,7 @@
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 {
@@ -64,17 +66,31 @@
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
- pattern string
+ layer interface{}
+ name string
+ checkLayer checkLayer
+ pattern string
}
type securitySpecTestGroup struct {
- layer interface{}
- name string
- patterns []string
- mutating bool
+ layer interface{}
+ name string
+ checkLayer checkLayer
+ patterns []string
+ mutating bool
}
// TODO(rogulenko): Add more test groups.
@@ -85,40 +101,45 @@
_, err := wire.ServiceClient(s.FullName()).DevModeGetTime(ctx)
return err
}},
- name: "service.DevModeGetTime",
- patterns: []string{"A___"},
+ 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",
- patterns: []string{"A___"},
- mutating: true,
+ 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",
- patterns: []string{"A___"},
+ 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",
- patterns: []string{"A___"},
- mutating: true,
+ 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",
- patterns: []string{"R___"},
+ name: "service.ListDatabases",
+ checkLayer: clService,
+ patterns: []string{"R___"},
},
// Database tests.
@@ -126,74 +147,86 @@
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",
- patterns: []string{"W___"},
- mutating: true,
+ 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",
- patterns: []string{"XA__", "A___"},
- mutating: true,
+ 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",
- patterns: []string{"XX__", "XR__", "XW__", "XA__", "R___", "W___"},
+ 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",
- patterns: []string{"XA__"},
+ 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",
- patterns: []string{"XA__"},
- mutating: true,
+ 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",
- patterns: []string{"XR__"},
+ 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",
- patterns: []string{"XR__"},
+ 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",
- patterns: []string{"XX__", "XR__", "XW__", "XA__"},
+ 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",
- patterns: []string{"XX__", "XR__", "XW__", "XA__"},
+ name: "database.GetResumeMarker",
+ checkLayer: clDatabase,
+ patterns: []string{"XX__", "XR__", "XW__", "XA__"},
},
// TODO(ivanpi): Test Exec.
// TODO(ivanpi): Test RPC-only methods such as Glob.
@@ -203,25 +236,28 @@
layer: batchDatabaseTest{f: func(ctx *context.T, b syncbase.BatchDatabase) error {
return b.Commit(ctx)
}},
- name: "database.Commit",
- patterns: []string{"XX__", "XR__", "XW__", "XA__"},
- mutating: true,
+ 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",
- patterns: []string{"XX__", "XR__", "XW__", "XA__"},
- mutating: true,
+ 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",
- patterns: []string{"XR__"},
+ name: "database.ListCollections - batch",
+ checkLayer: clDatabase,
+ patterns: []string{"XR__"},
},
// TODO(ivanpi): Test Exec.
@@ -234,46 +270,58 @@
}
return err
}},
- name: "database.GetSchemaMetadata",
- patterns: []string{"XR__"},
+ 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",
- patterns: []string{"XA__"},
- mutating: true,
+ 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",
- patterns: []string{"XA__"},
- mutating: true,
+ 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",
- patterns: []string{"XA__"},
- mutating: true,
+ 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
}
- go s.RecvStream().Advance()
- <-time.After(1 * time.Second)
- return s.RecvStream().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",
- patterns: []string{"XA__"},
- mutating: true,
+ name: "database.StartConflictResolver",
+ checkLayer: clDatabase,
+ patterns: []string{"XA__"},
+ mutating: true,
},
// Blob manager tests.
@@ -282,9 +330,10 @@
_, err := d.CreateBlob(ctx)
return err
}},
- name: "database.CreateBlob",
- patterns: []string{"XW__"},
- mutating: true,
+ 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 {
@@ -297,33 +346,37 @@
}
return w.Close()
}},
- name: "blob.Put",
- patterns: []string{"XW__"},
- mutating: true,
+ 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",
- patterns: []string{"XW__"},
- mutating: true,
+ 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",
- patterns: []string{"XX__", "XR__", "XW__", "XA__"},
+ 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",
- patterns: []string{"XX__", "XR__", "XW__", "XA__"},
- mutating: true,
+ 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 {
@@ -331,11 +384,13 @@
if err != nil {
return err
}
+ defer r.Cancel()
r.Advance()
return r.Err()
}},
- name: "blob.Get",
- patterns: []string{"XX__", "XR__", "XW__", "XA__"},
+ 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 {
@@ -343,36 +398,41 @@
if err != nil {
return err
}
+ defer s.Cancel()
s.Advance()
return s.Err()
}},
- name: "blob.Fetch",
- patterns: []string{"XX__", "XR__", "XW__", "XA__"},
- mutating: true,
+ 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",
- patterns: []string{"XW__"},
- mutating: true,
+ 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",
- patterns: []string{"XX__", "XR__", "XW__", "XA__"},
- mutating: true,
+ 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",
- patterns: []string{"XX__", "XR__", "XW__", "XA__"},
- mutating: true,
+ name: "blob.Keep",
+ checkLayer: clDatabase,
+ patterns: []string{"XX__", "XR__", "XW__", "XA__"},
+ mutating: true,
},
// TODO(ivanpi): Test Syncbase-to-Syncbase blob RPCs.
@@ -382,8 +442,9 @@
_, err := d.ListSyncgroups(ctx)
return err
}},
- name: "database.ListSyncgroups",
- patterns: []string{"XR__"},
+ name: "database.ListSyncgroups",
+ checkLayer: clDatabase,
+ patterns: []string{"XR__"},
},
{
layer: collectionTest{noBatch: true, f: func(ctx *context.T, d syncbase.Database, c syncbase.Collection) error {
@@ -392,50 +453,56 @@
Collections: []wire.Id{c.Id()},
}, wire.SyncgroupMemberInfo{})
}},
- name: "syncgroup.Create",
- patterns: []string{"XWR_"},
- mutating: true,
+ 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)",
- patterns: []string{"XWRR"},
- mutating: true,
+ 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",
- patterns: []string{"XW__"},
- mutating: true,
+ 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",
- patterns: []string{"XW_A"},
- mutating: true,
+ 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",
- patterns: []string{"XX_A"},
- mutating: true,
+ 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",
- patterns: []string{"XX_R"},
+ name: "syncgroup.GetSpec",
+ checkLayer: clSyncgroup,
+ patterns: []string{"XX_R"},
},
{
layer: syncgroupTest{f: func(ctx *context.T, sg syncbase.Syncgroup, c syncbase.Collection) error {
@@ -444,17 +511,19 @@
Collections: []wire.Id{c.Id()},
}, "")
}},
- name: "syncgroup.SetSpec",
- patterns: []string{"XX_A"},
- mutating: true,
+ 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",
- patterns: []string{"XX_R"},
+ name: "syncgroup.GetMembers",
+ checkLayer: clSyncgroup,
+ patterns: []string{"XX_R"},
},
// TODO(ivanpi): Test Syncbase-to-Syncbase sync RPCs.
@@ -463,58 +532,66 @@
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",
- patterns: []string{"XW__"},
- mutating: true,
+ 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",
- patterns: []string{"XXA_", "XA__"},
- mutating: true,
+ 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",
- patterns: []string{"XXR_", "XXW_", "XXA_", "XR__", "XW__"},
+ 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",
- patterns: []string{"XXA_"},
+ 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",
- patterns: []string{"XXA_"},
- mutating: true,
+ 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",
- patterns: []string{"XXR_"},
+ 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",
- patterns: []string{"XXW_"},
- mutating: true,
+ 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.)
@@ -524,33 +601,37 @@
_, err := r.Exists(ctx)
return err
}},
- name: "row.Exists",
- patterns: []string{"XXR_", "XXW_"},
+ 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",
- patterns: []string{"XXR_"},
+ 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",
- patterns: []string{"XXW_"},
- mutating: true,
+ 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",
- patterns: []string{"XXW_"},
- mutating: true,
+ name: "row.Delete",
+ checkLayer: clCollection,
+ patterns: []string{"XXW_"},
+ mutating: true,
},
}
@@ -597,19 +678,69 @@
tu.Fatalf(t, "test %v (batch: %v) didn't fail with ErrNoAccess or ErrNoExistOrNoAccess error: %v", test, batch, err)
}
}
- runTests(t, checkDenied, deniedTests...)
+ 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, checkAllowed, staticAllowedTests...)
+ runTests(t, csAll, checkAllowed, staticAllowedTests...)
for _, test := range mutatingAllowedTests {
- runTests(t, checkAllowed, test)
+ 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.
@@ -620,9 +751,10 @@
// Fill in allowed test slices.
for pattern := range allowedPatterns {
test := securitySpecTest{
- layer: testGroup.layer,
- name: testGroup.name,
- pattern: pattern,
+ layer: testGroup.layer,
+ name: testGroup.name,
+ checkLayer: testGroup.checkLayer,
+ pattern: pattern,
}
if testGroup.mutating {
mutatingAllowedTests = append(mutatingAllowedTests, test)
@@ -637,7 +769,7 @@
for i := 0; i < len(pattern); i++ {
if pattern[i] != '_' {
patternBytes := []byte(pattern)
- for _, c := range []byte{'X', 'R', 'W', 'A', '_'} {
+ for _, c := range []byte{'_', 'X', 'R', 'W', 'A'} {
patternBytes[i] = c
if !isSubsetOfAny(string(patternBytes), allowedPatterns) {
deniedPatterns[string(patternBytes)] = true
@@ -649,9 +781,10 @@
// Fill in denied test slice.
for pattern := range deniedPatterns {
test := securitySpecTest{
- layer: testGroup.layer,
- name: testGroup.name,
- pattern: pattern,
+ layer: testGroup.layer,
+ name: testGroup.name,
+ checkLayer: testGroup.checkLayer,
+ pattern: pattern,
}
deniedTests = append(deniedTests, test)
}
@@ -659,24 +792,45 @@
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 {
- isSubset := true
- for i := 0; i < len(p); i++ {
- if p[i] != '_' && p[i] != needle[i] {
- isSubset = false
- break
- }
- }
- if isSubset {
- // All perms in needle correspond to the same perm or '_' in p.
+ if isSubsetOf(needle, p) {
return true
}
}
return false
}
-func runTests(t *testing.T, check func(t *testing.T, test securitySpecTest, batch bool, err error), tests ...securitySpecTest) {
+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...)
@@ -686,51 +840,82 @@
// 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 err := d.Create(adminCtx, databasePerms); err != nil {
- tu.Fatalf(t, "d.Create failed: %v", err)
+ 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 err := c.Create(adminCtx, collectionPerms); err != nil {
- tu.Fatalf(t, "c.Create failed: %v", err)
+ 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 err := r.Put(adminCtx, "value"); err != nil {
- tu.Fatalf(t, "r.Put failed: %v", err)
+ 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"})
- sgSpec := wire.SyncgroupSpec{
- Perms: sgPerms,
- Collections: []wire.Id{c.Id()},
+ 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)
+ }
}
- 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)
+ }
}
- 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)
- }
- dBatch, err := d.BeginBatch(adminCtx, wire.BatchOptions{})
- if err != nil {
- tu.Fatalf(t, "d.BeginBatch 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 {
- clientCtx := tu.NewCtx(ctx, rootp, fmt.Sprintf("client%d", i))
+ 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))
@@ -759,9 +944,26 @@
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].
@@ -781,6 +983,154 @@
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