From 20596e3f6beaa51e70457dca45a7276a5439cd03 Mon Sep 17 00:00:00 2001 From: Shahzad Lone Date: Thu, 26 Sep 2024 03:28:43 -0400 Subject: [PATCH 1/8] fix(i): Sort out invalid testing framework node indexing (#3068) ## Relevant issue(s) Resolves #3065 ## Description The main bug was only visible on sourcehub acp using http, due to the identity being copied with the audience value of another node's host failing authentication (the bearer tokens should be unique using correct node's audience). The biggest issue was the way we use `getNodes` and `getNodeCollections`. I would be in favor of completely removing them as they are more troublesome than the utility they provide. - First commit documents the bug - Some utility functions were overwriting and producing the wrong node index - Forbidden bug happening on sourcehub<>http test run: https://github.com/sourcenetwork/defradb/actions/runs/10930293192/job/30342883535?pr=2907 ### Future: - Resolve import/export documentation and implementation if different (#3067) - Should likely clean this test utils up and make helper methods to avoid code duplication (https://github.com/sourcenetwork/defradb/issues/3069) - Likely should remove all usages of `getNodeCollections` (https://github.com/sourcenetwork/defradb/issues/3069) - Likely should remove all usages of `getNodes` (https://github.com/sourcenetwork/defradb/issues/3069) ## How has this been tested? - Very painfully haha, had to install Linux bare-metal to investigate the first bug (sourcehub doesn't build on wsl for me) that was only occurring on sourcehub acp using http, due to the identity being copied with the audience value of another node the way we use `getNodes` and `getNodeCollections` --- tests/integration/acp/p2p/create_test.go | 132 ++++++++++ tests/integration/acp/p2p/delete_test.go | 159 ++++++++++++ tests/integration/acp/p2p/update_test.go | 171 +++++++++++++ tests/integration/test_case.go | 6 +- tests/integration/utils.go | 304 ++++++++++++++++++----- 5 files changed, 712 insertions(+), 60 deletions(-) create mode 100644 tests/integration/acp/p2p/create_test.go create mode 100644 tests/integration/acp/p2p/delete_test.go create mode 100644 tests/integration/acp/p2p/update_test.go diff --git a/tests/integration/acp/p2p/create_test.go b/tests/integration/acp/p2p/create_test.go new file mode 100644 index 0000000000..8775a553d7 --- /dev/null +++ b/tests/integration/acp/p2p/create_test.go @@ -0,0 +1,132 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_p2p + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_P2PCreatePrivateDocumentsOnDifferentNodes_SourceHubACP(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, p2p create private documents on different nodes, with source-hub", + + SupportedACPTypes: immutable.Some( + []testUtils.ACPType{ + testUtils.SourceHubACPType, + }, + ), + + Actions: []any{ + testUtils.RandomNetworkingConfig(), + + testUtils.RandomNetworkingConfig(), + + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + NodeID: immutable.Some(0), + + CollectionID: 0, + + DocMap: map[string]any{ + "name": "Shahzad", + }, + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + NodeID: immutable.Some(1), + + CollectionID: 0, + + DocMap: map[string]any{ + "name": "Shahzad Lone", + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/p2p/delete_test.go b/tests/integration/acp/p2p/delete_test.go new file mode 100644 index 0000000000..59cae4cde9 --- /dev/null +++ b/tests/integration/acp/p2p/delete_test.go @@ -0,0 +1,159 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_p2p + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_P2PDeletePrivateDocumentsOnDifferentNodes_SourceHubACP(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, p2p delete private documents on different nodes, with source-hub", + + SupportedACPTypes: immutable.Some( + []testUtils.ACPType{ + testUtils.SourceHubACPType, + }, + ), + + Actions: []any{ + testUtils.RandomNetworkingConfig(), + + testUtils.RandomNetworkingConfig(), + + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.ConfigureReplicator{ + SourceNodeID: 0, + TargetNodeID: 1, + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + NodeID: immutable.Some(0), + + CollectionID: 0, + + DocMap: map[string]any{ + "name": "Shahzad", + }, + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + NodeID: immutable.Some(1), + + CollectionID: 0, + + DocMap: map[string]any{ + "name": "Shahzad Lone", + }, + }, + + testUtils.WaitForSync{}, + + testUtils.DeleteDoc{ + Identity: immutable.Some(1), + + NodeID: immutable.Some(0), + + CollectionID: 0, + + DocID: 0, + }, + + testUtils.DeleteDoc{ + Identity: immutable.Some(1), + + NodeID: immutable.Some(1), + + CollectionID: 0, + + DocID: 1, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/p2p/update_test.go b/tests/integration/acp/p2p/update_test.go new file mode 100644 index 0000000000..339babee10 --- /dev/null +++ b/tests/integration/acp/p2p/update_test.go @@ -0,0 +1,171 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_p2p + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_P2PUpdatePrivateDocumentsOnDifferentNodes_SourceHubACP(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, p2p update private documents on different nodes, with source-hub", + + SupportedACPTypes: immutable.Some( + []testUtils.ACPType{ + testUtils.SourceHubACPType, + }, + ), + + Actions: []any{ + testUtils.RandomNetworkingConfig(), + + testUtils.RandomNetworkingConfig(), + + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.ConfigureReplicator{ + SourceNodeID: 0, + TargetNodeID: 1, + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + NodeID: immutable.Some(0), + + CollectionID: 0, + + DocMap: map[string]any{ + "name": "Shahzad", + }, + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + NodeID: immutable.Some(1), + + CollectionID: 0, + + DocMap: map[string]any{ + "name": "Shahzad Lone", + }, + }, + + testUtils.WaitForSync{}, + + testUtils.UpdateDoc{ + Identity: immutable.Some(1), + + NodeID: immutable.Some(0), + + CollectionID: 0, + + DocID: 0, + + Doc: ` + { + "name": "ShahzadLone" + } + `, + }, + + testUtils.UpdateDoc{ + Identity: immutable.Some(1), + + NodeID: immutable.Some(1), + + CollectionID: 0, + + DocID: 1, + + Doc: ` + { + "name": "ShahzadLone" + } + `, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/test_case.go b/tests/integration/test_case.go index 9b0bce913b..f102294e97 100644 --- a/tests/integration/test_case.go +++ b/tests/integration/test_case.go @@ -718,7 +718,8 @@ type ClientIntrospectionRequest struct { type BackupExport struct { // NodeID may hold the ID (index) of a node to generate the backup from. // - // If a value is not provided the indexes will be retrieved from the first nodes. + // If a value is not provided the backup export will be done for all the nodes. + // todo: https://github.com/sourcenetwork/defradb/issues/3067 NodeID immutable.Option[int] // The backup configuration. @@ -738,7 +739,8 @@ type BackupExport struct { type BackupImport struct { // NodeID may hold the ID (index) of a node to generate the backup from. // - // If a value is not provided the indexes will be retrieved from the first nodes. + // If a value is not provided the backup import will be done for all the nodes. + // todo: https://github.com/sourcenetwork/defradb/issues/3067 NodeID immutable.Option[int] // The backup file path. diff --git a/tests/integration/utils.go b/tests/integration/utils.go index 85ba2f870d..e6ab296140 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -572,6 +572,12 @@ func getNodes(nodeID immutable.Option[int], nodes []clients.Client) []clients.Cl // // If nodeID has a value it will return collections for that node only, otherwise all collections across all // nodes will be returned. +// +// WARNING: +// The caller must not assume the returned collections are in order of the node index if the specified +// index is greater than 0. For example if requesting collections with nodeID=2 then the resulting output +// will contain only one element (at index 0) that will be the collections of the respective node, the +// caller might accidentally assume that these collections belong to node 0. func getNodeCollections(nodeID immutable.Option[int], collections [][]client.Collection) [][]client.Collection { if !nodeID.HasValue() { return collections @@ -931,11 +937,12 @@ func getIndexes( } var expectedErrorRaised bool - actionNodes := getNodes(action.NodeID, s.nodes) - for nodeID, collections := range getNodeCollections(action.NodeID, s.collections) { - err := withRetry( - actionNodes, - nodeID, + + if action.NodeID.HasValue() { + nodeID := action.NodeID.Value() + collections := s.collections[nodeID] + err := withRetryOnNode( + s.nodes[nodeID], func() error { actualIndexes, err := collections[action.CollectionID].GetIndexes(s.ctx) if err != nil { @@ -950,6 +957,25 @@ func getIndexes( ) expectedErrorRaised = expectedErrorRaised || AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } else { + for nodeID, collections := range s.collections { + err := withRetryOnNode( + s.nodes[nodeID], + func() error { + actualIndexes, err := collections[action.CollectionID].GetIndexes(s.ctx) + if err != nil { + return err + } + + assertIndexesListsEqual(action.ExpectedIndexes, + actualIndexes, s.t, s.testCase.Description) + + return nil + }, + ) + expectedErrorRaised = expectedErrorRaised || + AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } } assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) @@ -1206,18 +1232,43 @@ func createDoc( var expectedErrorRaised bool var docIDs []client.DocID - actionNodes := getNodes(action.NodeID, s.nodes) - for nodeID, collections := range getNodeCollections(action.NodeID, s.collections) { - err := withRetry( - actionNodes, - nodeID, + + if action.NodeID.HasValue() { + actionNode := s.nodes[action.NodeID.Value()] + collections := s.collections[action.NodeID.Value()] + err := withRetryOnNode( + actionNode, func() error { var err error - docIDs, err = mutation(s, action, actionNodes[nodeID], nodeID, collections[action.CollectionID]) + docIDs, err = mutation( + s, + action, + actionNode, + action.NodeID.Value(), + collections[action.CollectionID], + ) return err }, ) expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } else { + for nodeID, collections := range s.collections { + err := withRetryOnNode( + s.nodes[nodeID], + func() error { + var err error + docIDs, err = mutation( + s, + action, + s.nodes[nodeID], + nodeID, + collections[action.CollectionID], + ) + return err + }, + ) + expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } } assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) @@ -1389,20 +1440,34 @@ func deleteDoc( docID := s.docIDs[action.CollectionID][action.DocID] var expectedErrorRaised bool - actionNodes := getNodes(action.NodeID, s.nodes) - for nodeID, collections := range getNodeCollections(action.NodeID, s.collections) { + + if action.NodeID.HasValue() { + nodeID := action.NodeID.Value() + actionNode := s.nodes[nodeID] + collections := s.collections[nodeID] identity := getIdentity(s, nodeID, action.Identity) ctx := db.SetContextIdentity(s.ctx, identity) - - err := withRetry( - actionNodes, - nodeID, + err := withRetryOnNode( + actionNode, func() error { _, err := collections[action.CollectionID].Delete(ctx, docID) return err }, ) expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } else { + for nodeID, collections := range s.collections { + identity := getIdentity(s, nodeID, action.Identity) + ctx := db.SetContextIdentity(s.ctx, identity) + err := withRetryOnNode( + s.nodes[nodeID], + func() error { + _, err := collections[action.CollectionID].Delete(ctx, docID) + return err + }, + ) + expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } } assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) @@ -1433,16 +1498,41 @@ func updateDoc( } var expectedErrorRaised bool - actionNodes := getNodes(action.NodeID, s.nodes) - for nodeID, collections := range getNodeCollections(action.NodeID, s.collections) { - err := withRetry( - actionNodes, - nodeID, + + if action.NodeID.HasValue() { + nodeID := action.NodeID.Value() + collections := s.collections[nodeID] + actionNode := s.nodes[nodeID] + err := withRetryOnNode( + actionNode, func() error { - return mutation(s, action, actionNodes[nodeID], nodeID, collections[action.CollectionID]) + return mutation( + s, + action, + actionNode, + nodeID, + collections[action.CollectionID], + ) }, ) expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } else { + for nodeID, collections := range s.collections { + actionNode := s.nodes[nodeID] + err := withRetryOnNode( + actionNode, + func() error { + return mutation( + s, + action, + actionNode, + nodeID, + collections[action.CollectionID], + ) + }, + ) + expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } } assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) @@ -1531,14 +1621,13 @@ func updateDocViaGQL( func updateWithFilter(s *state, action UpdateWithFilter) { var res *client.UpdateResult var expectedErrorRaised bool - actionNodes := getNodes(action.NodeID, s.nodes) - for nodeID, collections := range getNodeCollections(action.NodeID, s.collections) { + if action.NodeID.HasValue() { + nodeID := action.NodeID.Value() + collections := s.collections[nodeID] identity := getIdentity(s, nodeID, action.Identity) ctx := db.SetContextIdentity(s.ctx, identity) - - err := withRetry( - actionNodes, - nodeID, + err := withRetryOnNode( + s.nodes[nodeID], func() error { var err error res, err = collections[action.CollectionID].UpdateWithFilter(ctx, action.Filter, action.Updater) @@ -1546,6 +1635,20 @@ func updateWithFilter(s *state, action UpdateWithFilter) { }, ) expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } else { + for nodeID, collections := range s.collections { + identity := getIdentity(s, nodeID, action.Identity) + ctx := db.SetContextIdentity(s.ctx, identity) + err := withRetryOnNode( + s.nodes[nodeID], + func() error { + var err error + res, err = collections[action.CollectionID].UpdateWithFilter(ctx, action.Filter, action.Updater) + return err + }, + ) + expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } } assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) @@ -1562,11 +1665,15 @@ func createIndex( ) { if action.CollectionID >= len(s.indexes) { // Expand the slice if required, so that the index can be accessed by collection index - s.indexes = append(s.indexes, - make([][][]client.IndexDescription, action.CollectionID-len(s.indexes)+1)...) + s.indexes = append( + s.indexes, + make([][][]client.IndexDescription, action.CollectionID-len(s.indexes)+1)..., + ) } - actionNodes := getNodes(action.NodeID, s.nodes) - for nodeID, collections := range getNodeCollections(action.NodeID, s.collections) { + + if action.NodeID.HasValue() { + nodeID := action.NodeID.Value() + collections := s.collections[nodeID] indexDesc := client.IndexDescription{ Name: action.IndexName, } @@ -1584,23 +1691,64 @@ func createIndex( }) } } + indexDesc.Unique = action.Unique - err := withRetry( - actionNodes, - nodeID, + err := withRetryOnNode( + s.nodes[nodeID], func() error { desc, err := collections[action.CollectionID].CreateIndex(s.ctx, indexDesc) if err != nil { return err } - s.indexes[nodeID][action.CollectionID] = - append(s.indexes[nodeID][action.CollectionID], desc) + s.indexes[nodeID][action.CollectionID] = append( + s.indexes[nodeID][action.CollectionID], + desc, + ) return nil }, ) if AssertError(s.t, s.testCase.Description, err, action.ExpectedError) { return } + } else { + for nodeID, collections := range s.collections { + indexDesc := client.IndexDescription{ + Name: action.IndexName, + } + if action.FieldName != "" { + indexDesc.Fields = []client.IndexedFieldDescription{ + { + Name: action.FieldName, + }, + } + } else if len(action.Fields) > 0 { + for i := range action.Fields { + indexDesc.Fields = append(indexDesc.Fields, client.IndexedFieldDescription{ + Name: action.Fields[i].Name, + Descending: action.Fields[i].Descending, + }) + } + } + + indexDesc.Unique = action.Unique + err := withRetryOnNode( + s.nodes[nodeID], + func() error { + desc, err := collections[action.CollectionID].CreateIndex(s.ctx, indexDesc) + if err != nil { + return err + } + s.indexes[nodeID][action.CollectionID] = append( + s.indexes[nodeID][action.CollectionID], + desc, + ) + return nil + }, + ) + if AssertError(s.t, s.testCase.Description, err, action.ExpectedError) { + return + } + } } assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, false) @@ -1612,21 +1760,38 @@ func dropIndex( action DropIndex, ) { var expectedErrorRaised bool - actionNodes := getNodes(action.NodeID, s.nodes) - for nodeID, collections := range getNodeCollections(action.NodeID, s.collections) { + + if action.NodeID.HasValue() { + nodeID := action.NodeID.Value() + collections := s.collections[nodeID] + indexName := action.IndexName if indexName == "" { indexName = s.indexes[nodeID][action.CollectionID][action.IndexID].Name } - err := withRetry( - actionNodes, - nodeID, + err := withRetryOnNode( + s.nodes[nodeID], func() error { return collections[action.CollectionID].DropIndex(s.ctx, indexName) }, ) expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } else { + for nodeID, collections := range s.collections { + indexName := action.IndexName + if indexName == "" { + indexName = s.indexes[nodeID][action.CollectionID][action.IndexID].Name + } + + err := withRetryOnNode( + s.nodes[nodeID], + func() error { + return collections[action.CollectionID].DropIndex(s.ctx, indexName) + }, + ) + expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } } assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) @@ -1642,11 +1807,12 @@ func backupExport( } var expectedErrorRaised bool - actionNodes := getNodes(action.NodeID, s.nodes) - for nodeID, node := range actionNodes { - err := withRetry( - actionNodes, - nodeID, + + if action.NodeID.HasValue() { + nodeID := action.NodeID.Value() + node := s.nodes[nodeID] + err := withRetryOnNode( + node, func() error { return node.BasicExport(s.ctx, &action.Config) }, ) expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) @@ -1654,7 +1820,20 @@ func backupExport( if !expectedErrorRaised { assertBackupContent(s.t, action.ExpectedContent, action.Config.Filepath) } + } else { + for _, node := range s.nodes { + err := withRetryOnNode( + node, + func() error { return node.BasicExport(s.ctx, &action.Config) }, + ) + expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + + if !expectedErrorRaised { + assertBackupContent(s.t, action.ExpectedContent, action.Config.Filepath) + } + } } + assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) } @@ -1672,31 +1851,40 @@ func backupImport( _ = os.WriteFile(action.Filepath, []byte(action.ImportContent), 0664) var expectedErrorRaised bool - actionNodes := getNodes(action.NodeID, s.nodes) - for nodeID, node := range actionNodes { - err := withRetry( - actionNodes, - nodeID, + + if action.NodeID.HasValue() { + nodeID := action.NodeID.Value() + node := s.nodes[nodeID] + err := withRetryOnNode( + node, func() error { return node.BasicImport(s.ctx, action.Filepath) }, ) expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } else { + for _, node := range s.nodes { + err := withRetryOnNode( + node, + func() error { return node.BasicImport(s.ctx, action.Filepath) }, + ) + expectedErrorRaised = AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + } } + assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) } -// withRetry attempts to perform the given action, retrying up to a DB-defined +// withRetryOnNode attempts to perform the given action, retrying up to a DB-defined // maximum attempt count if a transaction conflict error is returned. // // If a P2P-sync commit for the given document is already in progress this // Save call can fail as the transaction will conflict. We dont want to worry // about this in our tests so we just retry a few times until it works (or the // retry limit is breached - important incase this is a different error) -func withRetry( - nodes []clients.Client, - nodeID int, +func withRetryOnNode( + node clients.Client, action func() error, ) error { - for i := 0; i < nodes[nodeID].MaxTxnRetries(); i++ { + for i := 0; i < node.MaxTxnRetries(); i++ { err := action() if errors.Is(err, datastore.ErrTxnConflict) { time.Sleep(100 * time.Millisecond) From cf6154319a8a8e699eb9bb54287bd95c50d378c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:11:24 -0400 Subject: [PATCH 2/8] bot: Bump @typescript-eslint/parser from 8.6.0 to 8.7.0 in /playground (#3060) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.6.0 to 8.7.0.
Release notes

Sourced from @​typescript-eslint/parser's releases.

v8.7.0

8.7.0 (2024-09-23)

🚀 Features

  • eslint-plugin: [no-unsafe-call] check calls of Function (#10010)
  • eslint-plugin: [consistent-type-exports] check export * exports to see if all exported members are types (#10006)

🩹 Fixes

  • eslint-plugin: properly coerce all types to string in getStaticMemberAccessValue (#10004)
  • eslint-plugin: [no-deprecated] report on imported deprecated variables (#9987)
  • eslint-plugin: [no-confusing-non-null-assertion] check !in and !instanceof (#9994)
  • types: add NewExpression as a parent of SpreadElement (#10024)
  • utils: add missing entries to the RuleListener selectors list (#9992)

❤️ Thank You

You can read about our versioning strategy and releases on our website.

Changelog

Sourced from @​typescript-eslint/parser's changelog.

8.7.0 (2024-09-23)

This was a version bump only for parser to align it with other projects, there were no code changes.

You can read about our versioning strategy and releases on our website.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@typescript-eslint/parser&package-manager=npm_and_yarn&previous-version=8.6.0&new-version=8.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Shahzad Lone --- playground/package-lock.json | 91 ++++++++++++++++++++++++++++++++---- playground/package.json | 2 +- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/playground/package-lock.json b/playground/package-lock.json index b36597dc6f..3b27f0ff32 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -19,7 +19,7 @@ "@types/react-dom": "^18.3.0", "@types/swagger-ui-react": "^4.18.3", "@typescript-eslint/eslint-plugin": "^8.6.0", - "@typescript-eslint/parser": "^8.6.0", + "@typescript-eslint/parser": "^8.7.0", "@vitejs/plugin-react-swc": "^3.7.0", "eslint": "^9.11.1", "eslint-plugin-react-hooks": "^4.6.2", @@ -2550,15 +2550,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz", - "integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz", + "integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.6.0", - "@typescript-eslint/types": "8.6.0", - "@typescript-eslint/typescript-estree": "8.6.0", - "@typescript-eslint/visitor-keys": "8.6.0", + "@typescript-eslint/scope-manager": "8.7.0", + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/typescript-estree": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0", "debug": "^4.3.4" }, "engines": { @@ -2577,6 +2577,81 @@ } } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz", + "integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz", + "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz", + "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz", + "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.7.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz", diff --git a/playground/package.json b/playground/package.json index eb14b3e0aa..39a56dd3f9 100644 --- a/playground/package.json +++ b/playground/package.json @@ -21,7 +21,7 @@ "@types/react-dom": "^18.3.0", "@types/swagger-ui-react": "^4.18.3", "@typescript-eslint/eslint-plugin": "^8.6.0", - "@typescript-eslint/parser": "^8.6.0", + "@typescript-eslint/parser": "^8.7.0", "@vitejs/plugin-react-swc": "^3.7.0", "eslint": "^9.11.1", "eslint-plugin-react-hooks": "^4.6.2", From 54c99e7c057857f832b72a17c2ec7adcb9f71f0e Mon Sep 17 00:00:00 2001 From: AndrewSisley Date: Thu, 26 Sep 2024 13:21:46 -0400 Subject: [PATCH 3/8] fix(i): Validate nearby relation fields in SchemaPatch (#3077) ## Relevant issue(s) Resolves #3074 ## Description Correctly handles validation of nearby relation fields in schema patch. Previously the equality check failed to account for `Kind` being a pointer and thus always flagged relation fields as having mutated. This was likely introduced in https://github.com/sourcenetwork/defradb/pull/2961 when `Kind` became a pointer. Collections referenced by relation fields were also not included in the validation set, causing the rules to think that the related object did not exist. --- internal/db/definition_validation.go | 4 +- internal/db/schema.go | 25 +++++++--- .../updates/add/field/with_relation_test.go | 46 +++++++++++++++++++ 3 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 tests/integration/schema/updates/add/field/with_relation_test.go diff --git a/internal/db/definition_validation.go b/internal/db/definition_validation.go index 47c222bf9c..2d178f1e07 100644 --- a/internal/db/definition_validation.go +++ b/internal/db/definition_validation.go @@ -818,7 +818,9 @@ func validateFieldNotMutated( for _, newField := range newSchema.Fields { oldField, exists := oldFieldsByName[newField.Name] - if exists && oldField != newField { + + // DeepEqual is temporary, as this validation is temporary + if exists && !reflect.DeepEqual(oldField, newField) { return NewErrCannotMutateField(newField.Name) } } diff --git a/internal/db/schema.go b/internal/db/schema.go index d9b9a4055c..f1906feb54 100644 --- a/internal/db/schema.go +++ b/internal/db/schema.go @@ -371,6 +371,11 @@ func (db *db) updateSchema( proposedDescriptionsByName[schema.Name] = schema } + allExistingCols, err := db.getCollections(ctx, client.CollectionFetchOptions{}) + if err != nil { + return err + } + for _, schema := range proposedDescriptionsByName { previousSchema := existingSchemaByName[schema.Name] @@ -488,17 +493,25 @@ func (db *db) updateSchema( return err } - allExistingCols, err := db.getCollections(ctx, client.CollectionFetchOptions{}) - if err != nil { - return err - } - oldDefs := make([]client.CollectionDefinition, 0, len(allExistingCols)) for _, col := range allExistingCols { oldDefs = append(oldDefs, col.Definition()) } - err = db.validateSchemaUpdate(ctx, oldDefs, definitions) + defNames := make(map[string]struct{}, len(definitions)) + for _, def := range definitions { + defNames[def.GetName()] = struct{}{} + } + + newDefs := make([]client.CollectionDefinition, 0, len(definitions)) + newDefs = append(newDefs, definitions...) + for _, existing := range allExistingCols { + if _, ok := defNames[existing.Definition().GetName()]; !ok { + newDefs = append(newDefs, existing.Definition()) + } + } + + err = db.validateSchemaUpdate(ctx, oldDefs, newDefs) if err != nil { return err } diff --git a/tests/integration/schema/updates/add/field/with_relation_test.go b/tests/integration/schema/updates/add/field/with_relation_test.go new file mode 100644 index 0000000000..9adb5659ac --- /dev/null +++ b/tests/integration/schema/updates/add/field/with_relation_test.go @@ -0,0 +1,46 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package field + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +// This test ensures that nearby relation fields are not failing validation during a schema patch. +func TestSchemaUpdatesAddField_DoesNotAffectExistingRelation(t *testing.T) { + test := testUtils.TestCase{ + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Book { + name: String + author: Author + } + + type Author { + name: String + books: [Book] + } + `, + }, + testUtils.SchemaPatch{ + Patch: ` + [ + { "op": "add", "path": "/Book/Fields/-", "value": {"Name": "rating", "Kind": 4} } + ] + `, + }, + }, + } + testUtils.ExecuteTestCase(t, test) +} From 88972df0fdd9dcd4872bab6d96a01cba67e2bdf3 Mon Sep 17 00:00:00 2001 From: Keenan Nemetz Date: Fri, 27 Sep 2024 13:01:12 -0700 Subject: [PATCH 4/8] feat: GraphQL upsert mutation (#3075) ## Relevant issue(s) Resolves #783 ## Description This PR adds a new upsert GraphQL mutation operation. ## Tasks - [x] I made sure the code is well commented, particularly hard-to-understand areas. - [x] I made sure the repository-held documentation is changed accordingly. - [x] I made sure the pull request title adheres to the conventional commit style (the subset used in the project can be found in [tools/configs/chglog/config.yml](tools/configs/chglog/config.yml)). - [x] I made sure to discuss its limitations such as threats to validity, vulnerability to mistake and misuse, robustness to invalidation of assumptions, resource requirements, ... ## How has this been tested? Added integration tests. Specify the platform(s) on which this was tested: - MacOS --- client/request/consts.go | 2 + client/request/mutation.go | 11 +- internal/planner/create.go | 6 +- internal/planner/errors.go | 1 + internal/planner/explain.go | 3 + internal/planner/mapper/mapper.go | 3 +- internal/planner/mapper/mutation.go | 8 +- internal/planner/operations.go | 1 + internal/planner/planner.go | 6 + internal/planner/update.go | 6 +- internal/planner/upsert.go | 174 +++++++++ internal/request/graphql/parser/mutation.go | 161 ++++++--- .../request/graphql/schema/descriptions.go | 12 + internal/request/graphql/schema/generate.go | 13 +- tests/integration/explain.go | 1 + .../integration/explain/debug/upsert_test.go | 64 ++++ .../explain/default/upsert_test.go | 104 ++++++ .../explain/execute/upsert_test.go | 125 +++++++ .../mutation/upsert/simple_test.go | 330 ++++++++++++++++++ 19 files changed, 962 insertions(+), 69 deletions(-) create mode 100644 internal/planner/upsert.go create mode 100644 tests/integration/explain/debug/upsert_test.go create mode 100644 tests/integration/explain/default/upsert_test.go create mode 100644 tests/integration/explain/execute/upsert_test.go create mode 100644 tests/integration/mutation/upsert/simple_test.go diff --git a/client/request/consts.go b/client/request/consts.go index 157cab8b5f..0e27eaeb3d 100644 --- a/client/request/consts.go +++ b/client/request/consts.go @@ -21,6 +21,8 @@ const ( Cid = "cid" Input = "input" + CreateInput = "create" + UpdateInput = "update" FieldName = "field" FieldIDName = "fieldId" ShowDeleted = "showDeleted" diff --git a/client/request/mutation.go b/client/request/mutation.go index 146a7ac8b5..c91b5868b6 100644 --- a/client/request/mutation.go +++ b/client/request/mutation.go @@ -17,6 +17,7 @@ const ( CreateObjects UpdateObjects DeleteObjects + UpsertObjects ) // ObjectMutation is a field on the `mutation` operation of a graphql request. It includes @@ -36,11 +37,11 @@ type ObjectMutation struct { // Collection is the target collection name. Collection string - // Input is the array of json representations of the fieldName-value pairs of document - // properties to mutate. - // - // This is ignored for [DeleteObjects] mutations. - Input []map[string]any + // CreateInput is the array of maps of fields and values used for a create mutation. + CreateInput []map[string]any + + // UpdateInput is a map of fields and values used for an update mutation. + UpdateInput map[string]any // Encrypt is a boolean flag that indicates whether the input data should be encrypted. Encrypt bool diff --git a/internal/planner/create.go b/internal/planner/create.go index 0c36658a14..18365f966d 100644 --- a/internal/planner/create.go +++ b/internal/planner/create.go @@ -65,7 +65,7 @@ func docIDsToSpans(ids []string, desc client.CollectionDescription) core.Spans { return core.NewSpans(spans...) } -func documentsToDocIDs(docs []*client.Document) []string { +func documentsToDocIDs(docs ...*client.Document) []string { docIDs := make([]string, len(docs)) for i, doc := range docs { docIDs[i] = doc.ID().String() @@ -96,7 +96,7 @@ func (n *createNode) Next() (bool, error) { return false, err } - n.results.Spans(docIDsToSpans(documentsToDocIDs(n.docs), n.collection.Description())) + n.results.Spans(docIDsToSpans(documentsToDocIDs(n.docs...), n.collection.Description())) err = n.results.Init() if err != nil { @@ -151,7 +151,7 @@ func (p *Planner) CreateDocs(parsed *mapper.Mutation) (planNode, error) { // create a mutation createNode. create := &createNode{ p: p, - input: parsed.Input, + input: parsed.CreateInput, results: results, docMapper: docMapper{parsed.DocumentMapping}, } diff --git a/internal/planner/errors.go b/internal/planner/errors.go index 54db7a7c79..295196d4d5 100644 --- a/internal/planner/errors.go +++ b/internal/planner/errors.go @@ -34,6 +34,7 @@ var ( ErrMissingChildValue = errors.New("expected child value, however none was yielded") ErrUnknownRelationType = errors.New("failed sub selection, unknown relation type") ErrUnknownExplainRequestType = errors.New("can not explain request of unknown type") + ErrUpsertMultipleDocuments = errors.New("cannot upsert multiple matching documents") ) func NewErrUnknownDependency(name string) error { diff --git a/internal/planner/explain.go b/internal/planner/explain.go index 1f17968489..76679a85e3 100644 --- a/internal/planner/explain.go +++ b/internal/planner/explain.go @@ -47,6 +47,7 @@ var ( _ explainablePlanNode = (*topLevelNode)(nil) _ explainablePlanNode = (*typeIndexJoin)(nil) _ explainablePlanNode = (*updateNode)(nil) + _ explainablePlanNode = (*upsertNode)(nil) ) const ( @@ -54,6 +55,8 @@ const ( collectionIDLabel = "collectionID" collectionNameLabel = "collectionName" inputLabel = "input" + createInputLabel = "create" + updateInputLabel = "update" fieldNameLabel = "fieldName" filterLabel = "filter" joinRootLabel = "root" diff --git a/internal/planner/mapper/mapper.go b/internal/planner/mapper/mapper.go index 68a7924806..8aeab3c22e 100644 --- a/internal/planner/mapper/mapper.go +++ b/internal/planner/mapper/mapper.go @@ -1245,7 +1245,8 @@ func toMutation( return &Mutation{ Select: *underlyingSelect, Type: MutationType(mutationRequest.Type), - Input: mutationRequest.Input, + CreateInput: mutationRequest.CreateInput, + UpdateInput: mutationRequest.UpdateInput, Encrypt: mutationRequest.Encrypt, EncryptFields: mutationRequest.EncryptFields, }, nil diff --git a/internal/planner/mapper/mutation.go b/internal/planner/mapper/mutation.go index 6c4ab4c56f..4644efb2ab 100644 --- a/internal/planner/mapper/mutation.go +++ b/internal/planner/mapper/mutation.go @@ -17,6 +17,7 @@ const ( CreateObjects UpdateObjects DeleteObjects + UpsertObjects ) // Mutation represents a request to mutate data stored in Defra. @@ -27,8 +28,11 @@ type Mutation struct { // The type of mutation. For example a create request. Type MutationType - // Input is the array of maps of fields and values used for the mutation. - Input []map[string]any + // CreateInput is the array of maps of fields and values used for a create mutation. + CreateInput []map[string]any + + // UpdateInput is a map of fields and values used for an update mutation. + UpdateInput map[string]any // Encrypt is a flag to indicate if the input data should be encrypted. Encrypt bool diff --git a/internal/planner/operations.go b/internal/planner/operations.go index e08ebae5c3..6cbf7c24d4 100644 --- a/internal/planner/operations.go +++ b/internal/planner/operations.go @@ -31,6 +31,7 @@ var ( _ planNode = (*typeJoinMany)(nil) _ planNode = (*typeJoinOne)(nil) _ planNode = (*updateNode)(nil) + _ planNode = (*upsertNode)(nil) _ planNode = (*valuesNode)(nil) _ planNode = (*viewNode)(nil) _ planNode = (*lensNode)(nil) diff --git a/internal/planner/planner.go b/internal/planner/planner.go index 4423183d75..fb5ce5812a 100644 --- a/internal/planner/planner.go +++ b/internal/planner/planner.go @@ -121,6 +121,9 @@ func (p *Planner) newObjectMutationPlan(stmt *mapper.Mutation) (planNode, error) case mapper.DeleteObjects: return p.DeleteDocs(stmt) + case mapper.UpsertObjects: + return p.UpsertDocs(stmt) + default: return nil, client.NewErrUnhandledType("mutation", stmt.Type) } @@ -184,6 +187,9 @@ func (p *Planner) expandPlan(planNode planNode, parentPlan *selectTopNode) error case *deleteNode: return p.expandPlan(n.source, parentPlan) + case *upsertNode: + return p.expandPlan(n.source, parentPlan) + case *viewNode: return p.expandPlan(n.source, parentPlan) diff --git a/internal/planner/update.go b/internal/planner/update.go index 2f282af292..e707065022 100644 --- a/internal/planner/update.go +++ b/internal/planner/update.go @@ -163,15 +163,11 @@ func (p *Planner) UpdateDocs(parsed *mapper.Mutation) (planNode, error) { p: p, filter: parsed.Filter, docIDs: parsed.DocIDs.Value(), + input: parsed.UpdateInput, isUpdating: true, docMapper: docMapper{parsed.DocumentMapping}, } - // update mutation only supports a single input - if len(parsed.Input) > 0 { - update.input = parsed.Input[0] - } - // get collection col, err := p.db.GetCollectionByName(p.ctx, parsed.Name) if err != nil { diff --git a/internal/planner/upsert.go b/internal/planner/upsert.go new file mode 100644 index 0000000000..4f12395284 --- /dev/null +++ b/internal/planner/upsert.go @@ -0,0 +1,174 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package planner + +import ( + "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/client/request" + "github.com/sourcenetwork/defradb/internal/core" + "github.com/sourcenetwork/defradb/internal/planner/mapper" +) + +type upsertNode struct { + documentIterator + docMapper + + p *Planner + collection client.Collection + filter *mapper.Filter + createInput map[string]any + updateInput map[string]any + isInitialized bool + source planNode +} + +// Next only returns once. +func (n *upsertNode) Next() (bool, error) { + if !n.isInitialized { + next, err := n.source.Next() + if err != nil { + return false, err + } + if next { + n.currentValue = n.source.Value() + // make sure multiple documents do not match + next, err := n.source.Next() + if err != nil { + return false, err + } + if next { + return false, ErrUpsertMultipleDocuments + } + docID, err := client.NewDocIDFromString(n.currentValue.GetID()) + if err != nil { + return false, err + } + doc, err := n.collection.Get(n.p.ctx, docID, false) + if err != nil { + return false, err + } + for k, v := range n.updateInput { + if err := doc.Set(k, v); err != nil { + return false, err + } + } + err = n.collection.Update(n.p.ctx, doc) + if err != nil { + return false, err + } + } else { + doc, err := client.NewDocFromMap(n.createInput, n.collection.Definition()) + if err != nil { + return false, err + } + err = n.collection.Create(n.p.ctx, doc) + if err != nil { + return false, err + } + n.source.Spans(docIDsToSpans(documentsToDocIDs(doc), n.collection.Description())) + } + err = n.source.Init() + if err != nil { + return false, err + } + n.isInitialized = true + } + next, err := n.source.Next() + if err != nil { + return false, err + } + if !next { + return false, nil + } + n.currentValue = n.source.Value() + return true, nil +} + +func (n *upsertNode) Kind() string { + return "upsertNode" +} + +func (n *upsertNode) Spans(spans core.Spans) { + n.source.Spans(spans) +} + +func (n *upsertNode) Init() error { + return n.source.Init() +} + +func (n *upsertNode) Start() error { + return n.source.Start() +} + +func (n *upsertNode) Close() error { + return n.source.Close() +} + +func (n *upsertNode) Source() planNode { + return n.source +} + +func (n *upsertNode) simpleExplain() (map[string]any, error) { + simpleExplainMap := map[string]any{} + + // Add the filter attribute + simpleExplainMap[filterLabel] = n.filter.ToMap(n.documentMapping) + + // Add the attribute that represents the values to create or update. + simpleExplainMap[updateInputLabel] = n.updateInput + simpleExplainMap[createInputLabel] = n.createInput + + return simpleExplainMap, nil +} + +// Explain method returns a map containing all attributes of this node that +// are to be explained, subscribes / opts-in this node to be an explainablePlanNode. +func (n *upsertNode) Explain(explainType request.ExplainType) (map[string]any, error) { + switch explainType { + case request.SimpleExplain: + return n.simpleExplain() + + case request.ExecuteExplain: + return map[string]any{}, nil + + default: + return nil, ErrUnknownExplainRequestType + } +} + +func (p *Planner) UpsertDocs(parsed *mapper.Mutation) (planNode, error) { + upsert := &upsertNode{ + p: p, + filter: parsed.Filter, + updateInput: parsed.UpdateInput, + docMapper: docMapper{parsed.DocumentMapping}, + } + + if len(parsed.CreateInput) > 0 { + upsert.createInput = parsed.CreateInput[0] + } + + // get collection + col, err := p.db.GetCollectionByName(p.ctx, parsed.Name) + if err != nil { + return nil, err + } + upsert.collection = col + + // create the results Select node + resultsNode, err := p.Select(&parsed.Select) + if err != nil { + return nil, err + } + upsert.source = resultsNode + + return upsert, nil +} diff --git a/internal/request/graphql/parser/mutation.go b/internal/request/graphql/parser/mutation.go index 71942f1223..a5b0a62ea8 100644 --- a/internal/request/graphql/parser/mutation.go +++ b/internal/request/graphql/parser/mutation.go @@ -20,14 +20,6 @@ import ( "github.com/sourcenetwork/defradb/client/request" ) -var ( - mutationNameToType = map[string]request.MutationType{ - "create": request.CreateObjects, - "update": request.UpdateObjects, - "delete": request.DeleteObjects, - } -) - // parseMutationOperationDefinition parses the individual GraphQL // 'mutation' operations, which there may be multiple of. func parseMutationOperationDefinition( @@ -64,9 +56,6 @@ func parseMutation(exe *gql.ExecutionContext, parent *gql.Object, field *ast.Fie }, } - fieldDef := gql.GetFieldDef(exe.Schema, parent, mut.Name) - arguments := gql.GetArgumentValues(fieldDef.Args, field.Arguments, exe.VariableValues) - // parse the mutation type // mutation names are either generated from a type // which means they are in the form name_type, where @@ -77,11 +66,6 @@ func parseMutation(exe *gql.ExecutionContext, parent *gql.Object, field *ast.Fie // get back one of our defined types. mutNameParts := strings.Split(mut.Name, "_") typeStr := mutNameParts[0] - var ok bool - mut.Type, ok = mutationNameToType[typeStr] - if !ok { - return nil, ErrUnknownMutationName - } if len(mutNameParts) > 1 { // only generated object mutations // reconstruct the name. @@ -92,41 +76,61 @@ func parseMutation(exe *gql.ExecutionContext, parent *gql.Object, field *ast.Fie mut.Collection = strings.Join(mutNameParts[1:], "_") } - for _, argument := range field.Arguments { - name := argument.Name.Value - value := arguments[name] + fieldDef := gql.GetFieldDef(exe.Schema, parent, mut.Name) + arguments := gql.GetArgumentValues(fieldDef.Args, field.Arguments, exe.VariableValues) - switch name { - case request.Input: - switch v := value.(type) { - case []any: - // input for create is a list - inputs := make([]map[string]any, len(v)) - for i, v := range v { - inputs[i] = v.(map[string]any) - } - mut.Input = inputs + switch typeStr { + case "create": + mut.Type = request.CreateObjects + parseCreateMutationArgs(mut, arguments) - case map[string]any: - // input for update is an object - mut.Input = []map[string]any{v} - } + case "update": + mut.Type = request.UpdateObjects + parseUpdateMutationArgs(mut, arguments) - case request.FilterClause: - if v, ok := value.(map[string]any); ok { - mut.Filter = immutable.Some(request.Filter{Conditions: v}) - } + case "delete": + mut.Type = request.DeleteObjects + parseDeleteMutationArgs(mut, arguments) - case request.DocIDArgName: + case "upsert": + mut.Type = request.UpsertObjects + parseUpsertMutationArgs(mut, arguments) + + default: + return nil, ErrUnknownMutationName + } + + // if theres no field selections, just return + if field.SelectionSet == nil { + return mut, nil + } + + fieldObject, err := typeFromFieldDef(fieldDef) + if err != nil { + return nil, err + } + + mut.Fields, err = parseSelectFields(exe, fieldObject, field.SelectionSet) + if err != nil { + return nil, err + } + + return mut, err +} + +func parseCreateMutationArgs(mut *request.ObjectMutation, args map[string]any) { + for name, value := range args { + switch name { + case request.Input: v, ok := value.([]any) if !ok { continue // value is nil } - docIDs := make([]string, len(v)) + inputs := make([]map[string]any, len(v)) for i, v := range v { - docIDs[i] = v.(string) + inputs[i] = v.(map[string]any) } - mut.DocIDs = immutable.Some(docIDs) + mut.CreateInput = inputs case request.EncryptDocArgName: if v, ok := value.(bool); ok { @@ -145,21 +149,74 @@ func parseMutation(exe *gql.ExecutionContext, parent *gql.Object, field *ast.Fie mut.EncryptFields = fields } } +} - // if theres no field selections, just return - if field.SelectionSet == nil { - return mut, nil - } +func parseDeleteMutationArgs(mut *request.ObjectMutation, args map[string]any) { + for name, value := range args { + switch name { + case request.DocIDArgName: + v, ok := value.([]any) + if !ok { + continue // value is nil + } + docIDs := make([]string, len(v)) + for i, v := range v { + docIDs[i] = v.(string) + } + mut.DocIDs = immutable.Some(docIDs) - fieldObject, err := typeFromFieldDef(fieldDef) - if err != nil { - return nil, err + case request.FilterClause: + if v, ok := value.(map[string]any); ok { + mut.Filter = immutable.Some(request.Filter{Conditions: v}) + } + } } +} - mut.Fields, err = parseSelectFields(exe, fieldObject, field.SelectionSet) - if err != nil { - return nil, err +func parseUpdateMutationArgs(mut *request.ObjectMutation, args map[string]any) { + for name, value := range args { + switch name { + case request.Input: + if v, ok := value.(map[string]any); ok { + mut.UpdateInput = v + } + + case request.DocIDArgName: + v, ok := value.([]any) + if !ok { + continue // value is nil + } + docIDs := make([]string, len(v)) + for i, v := range v { + docIDs[i] = v.(string) + } + mut.DocIDs = immutable.Some(docIDs) + + case request.FilterClause: + if v, ok := value.(map[string]any); ok { + mut.Filter = immutable.Some(request.Filter{Conditions: v}) + } + } } +} - return mut, err +func parseUpsertMutationArgs(mut *request.ObjectMutation, args map[string]any) { + for name, value := range args { + switch name { + case request.CreateInput: + if v, ok := value.(map[string]any); ok { + mut.CreateInput = []map[string]any{v} + } + + case request.UpdateInput: + if v, ok := value.(map[string]any); ok { + mut.UpdateInput = v + } + + case request.FilterClause: + if v, ok := value.(map[string]any); ok { + mut.Filter = immutable.Some(request.Filter{Conditions: v}) + } + } + } } diff --git a/internal/request/graphql/schema/descriptions.go b/internal/request/graphql/schema/descriptions.go index 9fe841c466..b667410c2c 100644 --- a/internal/request/graphql/schema/descriptions.go +++ b/internal/request/graphql/schema/descriptions.go @@ -104,6 +104,13 @@ An optional value that specifies as to whether deleted documents may be createDocumentDescription string = ` Creates one or more documents of this type using the data provided. ` + upsertDocumentDescription string = ` +Update or create a document in this collection using the data provided. The provided filter + must match at most one document. The matching document will be updated with the provided + update input, or if no matching document is found, a new document will be created with the + provided create input. + +NOTE: It is highly recommended to create an index on the fields used to filter.` updateDocumentsDescription string = ` Updates documents in this collection using the data provided. Only documents matching any provided criteria will be updated, if no criteria are provided @@ -123,6 +130,11 @@ An optional set of docID values that will limit the update to documents An optional filter for this update that will limit the update to the documents matching the given criteria. If no matching documents are found, the operation will succeed, but no documents will be updated. +` + upsertFilterArgDescription string = ` +A required filter for this upsert that must match one or zero documents. + If a matching document is found it will be updated, otherwise a new + document will be created. ` deleteDocumentsDescription string = ` Deletes documents in this collection matching any provided criteria. If no diff --git a/internal/request/graphql/schema/generate.go b/internal/request/graphql/schema/generate.go index b0f0163f06..c2850f79ce 100644 --- a/internal/request/graphql/schema/generate.go +++ b/internal/request/graphql/schema/generate.go @@ -1082,7 +1082,18 @@ func (g *Generator) GenerateMutationInputForGQLType(obj *gql.Object) ([]*gql.Fie }, } - return []*gql.Field{create, update, delete}, nil + upsert := &gql.Field{ + Name: "upsert_" + obj.Name(), + Description: upsertDocumentDescription, + Type: gql.NewList(obj), + Args: gql.FieldConfigArgument{ + request.FilterClause: schemaTypes.NewArgConfig(gql.NewNonNull(filterInput), upsertFilterArgDescription), + request.CreateInput: schemaTypes.NewArgConfig(gql.NewNonNull(mutationInput), "Create field values"), + request.UpdateInput: schemaTypes.NewArgConfig(gql.NewNonNull(mutationInput), "Update field values"), + }, + } + + return []*gql.Field{create, update, delete, upsert}, nil } func (g *Generator) genTypeFieldsEnum(obj *gql.Object) *gql.Enum { diff --git a/tests/integration/explain.go b/tests/integration/explain.go index 7bc7f9074a..325e69b3f7 100644 --- a/tests/integration/explain.go +++ b/tests/integration/explain.go @@ -54,6 +54,7 @@ var ( "typeJoinMany": {}, "typeJoinOne": {}, "updateNode": {}, + "upsertNode": {}, "valuesNode": {}, "viewNode": {}, "lensNode": {}, diff --git a/tests/integration/explain/debug/upsert_test.go b/tests/integration/explain/debug/upsert_test.go new file mode 100644 index 0000000000..5c14b2328a --- /dev/null +++ b/tests/integration/explain/debug/upsert_test.go @@ -0,0 +1,64 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_explain_debug + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" + explainUtils "github.com/sourcenetwork/defradb/tests/integration/explain" +) + +var upsertPattern = dataMap{ + "explain": dataMap{ + "operationNode": []dataMap{ + { + "upsertNode": dataMap{ + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "scanNode": dataMap{}, + }, + }, + }, + }, + }, + }, +} + +func TestDebugExplainMutationRequest_WithUpsert_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (debug) mutation request with upsert.", + + Actions: []any{ + explainUtils.SchemaForExplainTests, + + testUtils.ExplainRequest{ + + Request: `mutation @explain(type: debug) { + upsert_Author( + filter: {name: {_eq: "Bob"}}, + update: {age: 59}, + create: {name: "Bob", age: 59} + ) { + _docID + name + age + } + }`, + + ExpectedPatterns: upsertPattern, + }, + }, + } + + explainUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/explain/default/upsert_test.go b/tests/integration/explain/default/upsert_test.go new file mode 100644 index 0000000000..7cc38294e8 --- /dev/null +++ b/tests/integration/explain/default/upsert_test.go @@ -0,0 +1,104 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_explain_default + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" + explainUtils "github.com/sourcenetwork/defradb/tests/integration/explain" +) + +var upsertPattern = dataMap{ + "explain": dataMap{ + "operationNode": []dataMap{ + { + "upsertNode": dataMap{ + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "scanNode": dataMap{}, + }, + }, + }, + }, + }, + }, +} + +func TestDefaultExplainMutationRequest_WithUpsert_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (default) mutation request with upsert.", + + Actions: []any{ + explainUtils.SchemaForExplainTests, + + testUtils.ExplainRequest{ + + Request: `mutation @explain { + upsert_Author( + filter: {name: {_eq: "Bob"}}, + create: {name: "Bob", age: 59}, + update: {age: 59} + ) { + _docID + name + age + } + }`, + + ExpectedPatterns: upsertPattern, + + ExpectedTargets: []testUtils.PlanNodeTargetCase{ + { + TargetNodeName: "upsertNode", + IncludeChildNodes: false, + ExpectedAttributes: dataMap{ + "create": dataMap{ + "name": "Bob", + "age": int32(59), + }, + "update": dataMap{ + "age": int32(59), + }, + "filter": dataMap{ + "name": dataMap{ + "_eq": "Bob", + }, + }, + }, + }, + { + TargetNodeName: "scanNode", + IncludeChildNodes: true, // should be last node, so will have no child nodes. + ExpectedAttributes: dataMap{ + "collectionID": "3", + "collectionName": "Author", + "filter": dataMap{ + "name": dataMap{ + "_eq": "Bob", + }, + }, + "spans": []dataMap{ + { + "end": "/4", + "start": "/3", + }, + }, + }, + }, + }, + }, + }, + } + + explainUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/explain/execute/upsert_test.go b/tests/integration/explain/execute/upsert_test.go new file mode 100644 index 0000000000..65b2abeed1 --- /dev/null +++ b/tests/integration/explain/execute/upsert_test.go @@ -0,0 +1,125 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_explain_execute + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" + explainUtils "github.com/sourcenetwork/defradb/tests/integration/explain" +) + +func TestExecuteExplainMutationRequest_WithUpsertAndMatchingFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) mutation request with upsert and matching filter.", + + Actions: []any{ + explainUtils.SchemaForExplainTests, + + // Addresses + create2AddressDocuments(), + + testUtils.ExplainRequest{ + Request: `mutation @explain(type: execute) { + upsert_ContactAddress( + filter: {city: {_eq: "Waterloo"}}, + create: {city: "Waterloo", country: "USA"}, + update: {country: "USA"} + ) { + country + city + } + }`, + + ExpectedFullGraph: dataMap{ + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 1, + "planExecutions": uint64(2), + "operationNode": []dataMap{ + { + "upsertNode": dataMap{ + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(4), + "filterMatches": uint64(2), + "scanNode": dataMap{ + "iterations": uint64(4), + "docFetches": uint64(4), + "fieldFetches": uint64(6), + "indexFetches": uint64(0), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + explainUtils.ExecuteTestCase(t, test) +} + +func TestExecuteExplainMutationRequest_WithUpsertAndNoMatchingFilter_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Explain (execute) mutation request with upsert and no matching filter.", + + Actions: []any{ + explainUtils.SchemaForExplainTests, + + testUtils.ExplainRequest{ + Request: `mutation @explain(type: execute) { + upsert_ContactAddress( + filter: {city: {_eq: "Waterloo"}}, + create: {city: "Waterloo", country: "USA"}, + update: {country: "USA"} + ) { + country + city + } + }`, + + ExpectedFullGraph: dataMap{ + "explain": dataMap{ + "executionSuccess": true, + "sizeOfResult": 1, + "planExecutions": uint64(2), + "operationNode": []dataMap{ + { + "upsertNode": dataMap{ + "selectTopNode": dataMap{ + "selectNode": dataMap{ + "iterations": uint64(3), + "filterMatches": uint64(1), + "scanNode": dataMap{ + "iterations": uint64(3), + "docFetches": uint64(1), + "fieldFetches": uint64(2), + "indexFetches": uint64(0), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + explainUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/mutation/upsert/simple_test.go b/tests/integration/mutation/upsert/simple_test.go new file mode 100644 index 0000000000..ba2ac7fe58 --- /dev/null +++ b/tests/integration/mutation/upsert/simple_test.go @@ -0,0 +1,330 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package upsert + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestMutationUpsertSimple_WithNoFilterMatch_CreatesNewDoc(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple upsert mutation with no filter match", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + age: Int + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Alice", + "age": 40 + }`, + }, + testUtils.Request{ + Request: `mutation { + upsert_Users( + filter: {name: {_eq: "Bob"}}, + create: {name: "Bob", age: 40}, + update: {age: 40} + ) { + name + age + } + }`, + Results: map[string]any{ + "upsert_Users": []map[string]any{ + { + "name": "Bob", + "age": int64(40), + }, + }, + }, + }, + testUtils.Request{ + Request: `query { + Users { + name + age + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Bob", + "age": int64(40), + }, + { + "name": "Alice", + "age": int64(40), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestMutationUpsertSimple_WithFilterMatch_UpdatesDoc(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple upsert mutation with filter match", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + age: Int + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Alice", + "age": 40 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Bob", + "age": 30 + }`, + }, + testUtils.Request{ + Request: `mutation { + upsert_Users( + filter: {name: {_eq: "Bob"}}, + create: {name: "Bob", age: 40}, + update: {age: 40} + ) { + name + age + } + }`, + Results: map[string]any{ + "upsert_Users": []map[string]any{ + { + "name": "Bob", + "age": int64(40), + }, + }, + }, + }, + testUtils.Request{ + Request: `query { + Users { + name + age + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Alice", + "age": int64(40), + }, + { + "name": "Bob", + "age": int64(40), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestMutationUpsertSimple_WithFilterMatchMultiple_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple upsert mutation with multiple filter matches", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + age: Int + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Bob", + "age": 30 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Alice", + "age": 40 + }`, + }, + testUtils.Request{ + Request: `mutation { + upsert_Users( + filter: {}, + create: {name: "Alice", age: 40}, + update: {age: 50} + ) { + name + age + } + }`, + ExpectedError: `cannot upsert multiple matching documents`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestMutationUpsertSimple_WithNullCreateInput_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple upsert mutation with null create input", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + age: Int + } + `, + }, + testUtils.Request{ + Request: `mutation { + upsert_Users( + filter: {}, + create: null, + update: {age: 50} + ) { + name + age + } + }`, + ExpectedError: `Argument "create" has invalid value `, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestMutationUpsertSimple_WithNullUpdateInput_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple upsert mutation with null update input", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + age: Int + } + `, + }, + testUtils.Request{ + Request: `mutation { + upsert_Users( + filter: {}, + create: {name: "Alice", age: 40}, + update: null, + ) { + name + age + } + }`, + ExpectedError: `Argument "update" has invalid value `, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestMutationUpsertSimple_WithNullFilterInput_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple upsert mutation with null filter input", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + age: Int + } + `, + }, + testUtils.Request{ + Request: `mutation { + upsert_Users( + filter: null, + create: {name: "Alice", age: 40}, + update: {age: 50} + ) { + name + age + } + }`, + ExpectedError: `Argument "filter" has invalid value `, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestMutationUpsertSimple_WithUniqueCompositeIndexAndDuplicateUpdate_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple upsert mutation with unique composite index and update", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users @index(includes: [{name: "name"}, {name: "age"}], unique: true) { + name: String + age: Int + } + `, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Alice", + "age": 40 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "name": "Bob", + "age": 50 + }`, + }, + testUtils.Request{ + Request: `mutation { + upsert_Users( + filter: {name: {_eq: "Bob"}}, + create: {name: "Alice", age: 40}, + update: {name: "Alice", age: 40} + ) { + name + age + } + }`, + ExpectedError: `can not index a doc's field(s) that violates unique index`, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} From adf0c11d930ed1cecf935a62841f66575d99abca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:14:51 -0400 Subject: [PATCH 5/8] bot: Update dependencies (bulk dependabot PRs) 30-09-2024 (#3088) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ This PR was created by combining the following PRs: #3084 bot: Bump @vitejs/plugin-react-swc from 3.7.0 to 3.7.1 in /playground #3083 bot: Bump @types/react from 18.3.8 to 18.3.10 in /playground #3082 bot: Bump vite from 5.4.7 to 5.4.8 in /playground #3081 bot: Bump @typescript-eslint/eslint-plugin from 8.6.0 to 8.7.0 in /playground --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- playground/package-lock.json | 267 ++++++++++++----------------------- playground/package.json | 8 +- 2 files changed, 93 insertions(+), 182 deletions(-) diff --git a/playground/package-lock.json b/playground/package-lock.json index 3b27f0ff32..a08f776df2 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -15,17 +15,17 @@ "swagger-ui-react": "^5.17.14" }, "devDependencies": { - "@types/react": "^18.3.8", + "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", "@types/swagger-ui-react": "^4.18.3", - "@typescript-eslint/eslint-plugin": "^8.6.0", + "@typescript-eslint/eslint-plugin": "^8.7.0", "@typescript-eslint/parser": "^8.7.0", - "@vitejs/plugin-react-swc": "^3.7.0", + "@vitejs/plugin-react-swc": "^3.7.1", "eslint": "^9.11.1", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.12", "typescript": "^5.6.2", - "vite": "^5.4.7" + "vite": "^5.4.8" } }, "node_modules/@babel/runtime": { @@ -2171,12 +2171,11 @@ } }, "node_modules/@swc/core": { - "version": "1.7.14", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.14.tgz", - "integrity": "sha512-9aeXeifnyuvc2pcuuhPQgVUwdpGEzZ+9nJu0W8/hNl/aESFsJGR5i9uQJRGu0atoNr01gK092fvmqMmQAPcKow==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.26.tgz", + "integrity": "sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==", "dev": true, "hasInstallScript": true, - "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.12" @@ -2189,16 +2188,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.7.14", - "@swc/core-darwin-x64": "1.7.14", - "@swc/core-linux-arm-gnueabihf": "1.7.14", - "@swc/core-linux-arm64-gnu": "1.7.14", - "@swc/core-linux-arm64-musl": "1.7.14", - "@swc/core-linux-x64-gnu": "1.7.14", - "@swc/core-linux-x64-musl": "1.7.14", - "@swc/core-win32-arm64-msvc": "1.7.14", - "@swc/core-win32-ia32-msvc": "1.7.14", - "@swc/core-win32-x64-msvc": "1.7.14" + "@swc/core-darwin-arm64": "1.7.26", + "@swc/core-darwin-x64": "1.7.26", + "@swc/core-linux-arm-gnueabihf": "1.7.26", + "@swc/core-linux-arm64-gnu": "1.7.26", + "@swc/core-linux-arm64-musl": "1.7.26", + "@swc/core-linux-x64-gnu": "1.7.26", + "@swc/core-linux-x64-musl": "1.7.26", + "@swc/core-win32-arm64-msvc": "1.7.26", + "@swc/core-win32-ia32-msvc": "1.7.26", + "@swc/core-win32-x64-msvc": "1.7.26" }, "peerDependencies": { "@swc/helpers": "*" @@ -2210,14 +2209,13 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.7.14", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.14.tgz", - "integrity": "sha512-V0OUXjOH+hdGxDYG8NkQzy25mKOpcNKFpqtZEzLe5V/CpLJPnpg1+pMz70m14s9ZFda9OxsjlvPbg1FLUwhgIQ==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.26.tgz", + "integrity": "sha512-FF3CRYTg6a7ZVW4yT9mesxoVVZTrcSWtmZhxKCYJX9brH4CS/7PRPjAKNk6kzWgWuRoglP7hkjQcd6EpMcZEAw==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -2227,14 +2225,13 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.7.14", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.14.tgz", - "integrity": "sha512-9iFvUnxG6FC3An5ogp5jbBfQuUmTTwy8KMB+ZddUoPB3NR1eV+Y9vOh/tfWcenSJbgOKDLgYC5D/b1mHAprsrQ==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.26.tgz", + "integrity": "sha512-az3cibZdsay2HNKmc4bjf62QVukuiMRh5sfM5kHR/JMTrLyS6vSw7Ihs3UTkZjUxkLTT8ro54LI6sV6sUQUbLQ==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -2244,14 +2241,13 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.7.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.14.tgz", - "integrity": "sha512-zGJsef9qPivKSH8Vv4F/HiBXBTHZ5Hs3ZjVGo/UIdWPJF8fTL9OVADiRrl34Q7zOZEtGXRwEKLUW1SCQcbDvZA==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.26.tgz", + "integrity": "sha512-VYPFVJDO5zT5U3RpCdHE5v1gz4mmR8BfHecUZTmD2v1JeFY6fv9KArJUpjrHEEsjK/ucXkQFmJ0jaiWXmpOV9Q==", "cpu": [ "arm" ], "dev": true, - "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -2261,14 +2257,13 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.7.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.14.tgz", - "integrity": "sha512-AxV3MPsoI7i4B8FXOew3dx3N8y00YoJYvIPfxelw07RegeCEH3aHp2U2DtgbP/NV1ugZMx0TL2Z2DEvocmA51g==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.26.tgz", + "integrity": "sha512-YKevOV7abpjcAzXrhsl+W48Z9mZvgoVs2eP5nY+uoMAdP2b3GxC0Df1Co0I90o2lkzO4jYBpTMcZlmUXLdXn+Q==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -2278,14 +2273,13 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.7.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.14.tgz", - "integrity": "sha512-JDLdNjUj3zPehd4+DrQD8Ltb3B5lD8D05IwePyDWw+uR/YPc7w/TX1FUVci5h3giJnlMCJRvi1IQYV7K1n7KtQ==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.26.tgz", + "integrity": "sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -2295,14 +2289,13 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.7.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.14.tgz", - "integrity": "sha512-Siy5OvPCLLWmMdx4msnEs8HvEVUEigSn0+3pbLjv78iwzXd0qSBNHUPZyC1xeurVaUbpNDxZTpPRIwpqNE2+Og==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.26.tgz", + "integrity": "sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -2312,14 +2305,13 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.7.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.14.tgz", - "integrity": "sha512-FtEGm9mwtRYQNK43WMtUIadxHs/ja2rnDurB99os0ZoFTGG2IHuht2zD97W0wB8JbqEabT1XwSG9Y5wmN+ciEQ==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.26.tgz", + "integrity": "sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -2329,14 +2321,13 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.7.14", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.14.tgz", - "integrity": "sha512-Jp8KDlfq7Ntt2/BXr0y344cYgB1zf0DaLzDZ1ZJR6rYlAzWYSccLYcxHa97VGnsYhhPspMpmCvHid97oe2hl4A==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.26.tgz", + "integrity": "sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -2346,14 +2337,13 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.7.14", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.14.tgz", - "integrity": "sha512-I+cFsXF0OU0J9J4zdWiQKKLURO5dvCujH9Jr8N0cErdy54l9d4gfIxdctfTF+7FyXtWKLTCkp+oby9BQhkFGWA==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.26.tgz", + "integrity": "sha512-9YngxNcG3177GYdsTum4V98Re+TlCeJEP4kEwEg9EagT5s3YejYdKwVAkAsJszzkXuyRDdnHUpYbTrPG6FiXrQ==", "cpu": [ "ia32" ], "dev": true, - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -2363,14 +2353,13 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.7.14", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.14.tgz", - "integrity": "sha512-NNrprQCK6d28mG436jVo2TD+vACHseUECacEBGZ9Ef0qfOIWS1XIt2MisQKG0Oea2VvLFl6tF/V4Lnx/H0Sn3Q==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.26.tgz", + "integrity": "sha512-VR+hzg9XqucgLjXxA13MtV5O3C0bK0ywtLIBw/+a+O+Oc6mxFWHtdUeXDbIi5AiPbn0fjgVJMqYnyjGyyX8u0w==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -2383,15 +2372,13 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, - "license": "Apache-2.0" + "dev": true }, "node_modules/@swc/types": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" } @@ -2467,9 +2454,9 @@ } }, "node_modules/@types/react": { - "version": "18.3.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz", - "integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==", + "version": "18.3.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz", + "integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==", "devOptional": true, "dependencies": { "@types/prop-types": "*", @@ -2517,16 +2504,16 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz", - "integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz", + "integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.6.0", - "@typescript-eslint/type-utils": "8.6.0", - "@typescript-eslint/utils": "8.6.0", - "@typescript-eslint/visitor-keys": "8.6.0", + "@typescript-eslint/scope-manager": "8.7.0", + "@typescript-eslint/type-utils": "8.7.0", + "@typescript-eslint/utils": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2577,7 +2564,7 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "node_modules/@typescript-eslint/scope-manager": { "version": "8.7.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz", "integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==", @@ -2594,89 +2581,14 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz", - "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz", - "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.7.0", - "@typescript-eslint/visitor-keys": "8.7.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz", - "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.7.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz", - "integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.6.0", - "@typescript-eslint/visitor-keys": "8.6.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz", - "integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz", + "integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.6.0", - "@typescript-eslint/utils": "8.6.0", + "@typescript-eslint/typescript-estree": "8.7.0", + "@typescript-eslint/utils": "8.7.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2694,9 +2606,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz", - "integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz", + "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2707,13 +2619,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz", - "integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz", + "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.6.0", - "@typescript-eslint/visitor-keys": "8.6.0", + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2735,15 +2647,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz", - "integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz", + "integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.6.0", - "@typescript-eslint/types": "8.6.0", - "@typescript-eslint/typescript-estree": "8.6.0" + "@typescript-eslint/scope-manager": "8.7.0", + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/typescript-estree": "8.7.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2757,12 +2669,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz", - "integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz", + "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/types": "8.7.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2774,13 +2686,12 @@ } }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz", - "integrity": "sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.1.tgz", + "integrity": "sha512-vgWOY0i1EROUK0Ctg1hwhtC3SdcDjZcdit4Ups4aPkDcB1jYhmo+RMYWY87cmXMhvtD5uf8lV89j2w16vkdSVg==", "dev": true, - "license": "MIT", "dependencies": { - "@swc/core": "^1.5.7" + "@swc/core": "^1.7.26" }, "peerDependencies": { "vite": "^4 || ^5" @@ -6009,9 +5920,9 @@ "optional": true }, "node_modules/vite": { - "version": "5.4.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz", - "integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==", + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", "dev": true, "dependencies": { "esbuild": "^0.21.3", diff --git a/playground/package.json b/playground/package.json index 39a56dd3f9..658c97c8d2 100644 --- a/playground/package.json +++ b/playground/package.json @@ -17,16 +17,16 @@ "swagger-ui-react": "^5.17.14" }, "devDependencies": { - "@types/react": "^18.3.8", + "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", "@types/swagger-ui-react": "^4.18.3", - "@typescript-eslint/eslint-plugin": "^8.6.0", + "@typescript-eslint/eslint-plugin": "^8.7.0", "@typescript-eslint/parser": "^8.7.0", - "@vitejs/plugin-react-swc": "^3.7.0", + "@vitejs/plugin-react-swc": "^3.7.1", "eslint": "^9.11.1", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.12", "typescript": "^5.6.2", - "vite": "^5.4.7" + "vite": "^5.4.8" } } From 6b1f97fb6dedfbf46f087a38bad6206a4925f72e Mon Sep 17 00:00:00 2001 From: Keenan Nemetz Date: Mon, 30 Sep 2024 09:59:19 -0700 Subject: [PATCH 6/8] fix(i): Null compound filter panic (#3080) ## Relevant issue(s) Resolves #3079 ## Description This PR fixes an issue where null values within compound filters would panic. ## Tasks - [x] I made sure the code is well commented, particularly hard-to-understand areas. - [x] I made sure the repository-held documentation is changed accordingly. - [x] I made sure the pull request title adheres to the conventional commit style (the subset used in the project can be found in [tools/configs/chglog/config.yml](tools/configs/chglog/config.yml)). - [x] I made sure to discuss its limitations such as threats to validity, vulnerability to mistake and misuse, robustness to invalidation of assumptions, resource requirements, ... ## How has this been tested? Added integration tests Specify the platform(s) on which this was tested: - MacOS --- internal/connor/and.go | 3 + internal/connor/or.go | 5 +- internal/planner/mapper/mapper.go | 15 +- internal/planner/mapper/targetable.go | 11 +- internal/request/graphql/schema/generate.go | 6 +- .../query/simple/with_null_input_test.go | 210 ++++++++++++++++++ tests/integration/schema/default_fields.go | 8 +- tests/integration/schema/filter_test.go | 8 +- 8 files changed, 246 insertions(+), 20 deletions(-) diff --git a/internal/connor/and.go b/internal/connor/and.go index be2e097309..d302617dc8 100644 --- a/internal/connor/and.go +++ b/internal/connor/and.go @@ -16,6 +16,9 @@ func and(condition, data any) (bool, error) { } return true, nil + case nil: + return true, nil + default: return false, client.NewErrUnhandledType("condition", cn) } diff --git a/internal/connor/or.go b/internal/connor/or.go index 6273155e7d..c15e27393f 100644 --- a/internal/connor/or.go +++ b/internal/connor/or.go @@ -14,8 +14,11 @@ func or(condition, data any) (bool, error) { return true, nil } } - return false, nil + + case nil: + return true, nil + default: return false, client.NewErrUnhandledType("condition", cn) } diff --git a/internal/planner/mapper/mapper.go b/internal/planner/mapper/mapper.go index 8aeab3c22e..dfadd2f06c 100644 --- a/internal/planner/mapper/mapper.go +++ b/internal/planner/mapper/mapper.go @@ -964,9 +964,12 @@ func resolveInnerFilterDependencies( ) ([]Requestable, error) { newFields := []Requestable{} - for key := range source { + for key, value := range source { if key == request.FilterOpAnd || key == request.FilterOpOr { - compoundFilter := source[key].([]any) + if value == nil { + continue + } + compoundFilter := value.([]any) for _, innerFilter := range compoundFilter { innerFields, err := resolveInnerFilterDependencies( ctx, @@ -987,7 +990,10 @@ func resolveInnerFilterDependencies( } continue } else if key == request.FilterOpNot { - notFilter := source[key].(map[string]any) + if value == nil { + continue + } + notFilter := value.(map[string]any) innerFields, err := resolveInnerFilterDependencies( ctx, store, @@ -1044,8 +1050,7 @@ func resolveInnerFilterDependencies( newFields = append(newFields, childSelect) } - childSource := source[key] - childFilter, isChildFilter := childSource.(map[string]any) + childFilter, isChildFilter := value.(map[string]any) if !isChildFilter { // If the filter is not a child filter then the will be no inner dependencies to add and // we can continue. diff --git a/internal/planner/mapper/targetable.go b/internal/planner/mapper/targetable.go index a45e99a516..f85e6c8016 100644 --- a/internal/planner/mapper/targetable.go +++ b/internal/planner/mapper/targetable.go @@ -126,7 +126,10 @@ func filterObjectToMap(mapping *core.DocumentMapping, obj map[connor.FilterKey]a case *Operator: switch keyType.Operation { case request.FilterOpAnd, request.FilterOpOr: - v := v.([]any) + v, ok := v.([]any) + if !ok { + continue // value is nil + } logicMapEntries := make([]any, len(v)) for i, item := range v { itemMap := item.(map[connor.FilterKey]any) @@ -134,8 +137,10 @@ func filterObjectToMap(mapping *core.DocumentMapping, obj map[connor.FilterKey]a } outmap[keyType.Operation] = logicMapEntries case request.FilterOpNot: - itemMap := v.(map[connor.FilterKey]any) - outmap[keyType.Operation] = filterObjectToMap(mapping, itemMap) + itemMap, ok := v.(map[connor.FilterKey]any) + if ok { + outmap[keyType.Operation] = filterObjectToMap(mapping, itemMap) + } default: outmap[keyType.Operation] = v } diff --git a/internal/request/graphql/schema/generate.go b/internal/request/graphql/schema/generate.go index c2850f79ce..85491f5ee1 100644 --- a/internal/request/graphql/schema/generate.go +++ b/internal/request/graphql/schema/generate.go @@ -1138,11 +1138,11 @@ func (g *Generator) genTypeFilterArgInput(obj *gql.Object) *gql.InputObject { fields["_and"] = &gql.InputObjectFieldConfig{ Description: schemaTypes.AndOperatorDescription, - Type: gql.NewList(selfRefType), + Type: gql.NewList(gql.NewNonNull(selfRefType)), } fields["_or"] = &gql.InputObjectFieldConfig{ Description: schemaTypes.OrOperatorDescription, - Type: gql.NewList(selfRefType), + Type: gql.NewList(gql.NewNonNull(selfRefType)), } fields["_not"] = &gql.InputObjectFieldConfig{ Description: schemaTypes.NotOperatorDescription, @@ -1220,7 +1220,7 @@ func (g *Generator) genLeafFilterArgInput(obj gql.Type) *gql.InputObject { fields := gql.InputObjectConfigFieldMap{} compoundListType := &gql.InputObjectFieldConfig{ - Type: gql.NewList(selfRefType), + Type: gql.NewList(gql.NewNonNull(selfRefType)), } fields["_and"] = compoundListType diff --git a/tests/integration/query/simple/with_null_input_test.go b/tests/integration/query/simple/with_null_input_test.go index 9f11d14a8e..f81219051b 100644 --- a/tests/integration/query/simple/with_null_input_test.go +++ b/tests/integration/query/simple/with_null_input_test.go @@ -334,3 +334,213 @@ func TestQuerySimple_WithNullShowDeleted_Succeeds(t *testing.T) { executeTestCase(t, test) } + +func TestQuerySimple_WithFilterWithNullOr_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query, with filter with null or", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "Name": "John" + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {_or: null}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "John", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQuerySimple_WithFilterWithNullOrElement_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query, with filter with null or element", + Actions: []any{ + testUtils.Request{ + Request: `query { + Users(filter: {_or: [null]}) { + Name + } + }`, + ExpectedError: `Expected "UsersFilterArg!", found null`, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQuerySimple_WithFilterWithNullOrField_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query, with filter with or with null field", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "Name": null + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {_or: [{Name: null}]}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": nil, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQuerySimple_WithFilterWithNullAnd_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query, with filter with null and", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "Name": "John" + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {_and: null}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "John", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQuerySimple_WithFilterWithNullAndElement_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query, with filter with null and element", + Actions: []any{ + testUtils.Request{ + Request: `query { + Users(filter: {_and: [null]}) { + Name + } + }`, + ExpectedError: `Expected "UsersFilterArg!", found null`, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQuerySimple_WithFilterWithNullAndField_ReturnsError(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query, with filter with and with null field", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "Name": null + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {_and: [{Name: null}]}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": nil, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQuerySimple_WithFilterWithNullNot_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query, with filter with null not", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "Name": "John" + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {_not: null}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "John", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} + +func TestQuerySimple_WithFilterWithNullNotField_Succeeds(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query, with filter with null not field", + Actions: []any{ + testUtils.CreateDoc{ + Doc: `{ + "Name": "John" + }`, + }, + testUtils.Request{ + Request: `query { + Users(filter: {_not: {Name: null}}) { + Name + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "John", + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/schema/default_fields.go b/tests/integration/schema/default_fields.go index 51b224bd93..6462ef6066 100644 --- a/tests/integration/schema/default_fields.go +++ b/tests/integration/schema/default_fields.go @@ -215,14 +215,14 @@ func buildFilterArg(objectName string, fields []argDef) Field { inputFields := []any{ makeInputObject("_and", nil, map[string]any{ - "kind": "INPUT_OBJECT", - "name": filterArgName, + "kind": "NON_NULL", + "name": nil, }), makeInputObject("_docID", "IDOperatorBlock", nil), makeInputObject("_not", filterArgName, nil), makeInputObject("_or", nil, map[string]any{ - "kind": "INPUT_OBJECT", - "name": filterArgName, + "kind": "NON_NULL", + "name": nil, }), } diff --git a/tests/integration/schema/filter_test.go b/tests/integration/schema/filter_test.go index bf7617a3b6..c3dc47d668 100644 --- a/tests/integration/schema/filter_test.go +++ b/tests/integration/schema/filter_test.go @@ -71,7 +71,7 @@ func TestFilterForSimpleSchema(t *testing.T) { "type": map[string]any{ "name": nil, "ofType": map[string]any{ - "name": "UsersFilterArg", + "name": nil, }, }, }, @@ -94,7 +94,7 @@ func TestFilterForSimpleSchema(t *testing.T) { "type": map[string]any{ "name": nil, "ofType": map[string]any{ - "name": "UsersFilterArg", + "name": nil, }, }, }, @@ -203,7 +203,7 @@ func TestFilterForOneToOneSchema(t *testing.T) { "type": map[string]any{ "name": nil, "ofType": map[string]any{ - "name": "BookFilterArg", + "name": nil, }, }, }, @@ -226,7 +226,7 @@ func TestFilterForOneToOneSchema(t *testing.T) { "type": map[string]any{ "name": nil, "ofType": map[string]any{ - "name": "BookFilterArg", + "name": nil, }, }, }, From 3101c612d084e0978621d3fcd290f9d0c4efc881 Mon Sep 17 00:00:00 2001 From: Shahzad Lone Date: Mon, 30 Sep 2024 14:46:17 -0400 Subject: [PATCH 7/8] ci(i): Fix vulnerabilities scan by ignoring x/crises (#3091) ## Relevant issue(s) Resolves #3090 ## Description - Hacky fix for the vulnerability scanner until they improve the tool (or we solve the vulnerability). - Make the vulnerability scan fail if new vulnerabilities are introduced and pass if previous known ones remain. ## How has this been tested? - Tried introducing a vul and it works and ignores the current `x/crisis` one - Run: https://github.com/sourcenetwork/defradb/actions/runs/11110357130/job/30867868823?pr=3091 --- .github/workflows/check-vulnerabilities.yml | 28 ++++++++++++++++++--- Makefile | 5 ++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-vulnerabilities.yml b/.github/workflows/check-vulnerabilities.yml index 5ebb3192b1..82d05e80ca 100644 --- a/.github/workflows/check-vulnerabilities.yml +++ b/.github/workflows/check-vulnerabilities.yml @@ -30,10 +30,32 @@ jobs: runs-on: ubuntu-latest steps: - - name: Run govulncheck - uses: golang/govulncheck-action@v1 + - name: Checkout code into the directory + uses: actions/checkout@v4 + + - name: Setup Go environment explicitly + uses: actions/setup-go@v5 with: go-version-file: 'go.mod' check-latest: true cache: false - go-package: ./... + + - name: Install govulncheck + run: make deps:vulncheck + + - name: Run govulncheck scan + run: govulncheck -C . -format text ./... | tee govulncheck.txt + + - name: Check if only known vulnerabilities were found (there are new vulnerabilities if this fails) + run: cat govulncheck.txt | grep "Your code is affected by 2 vulnerabilities from 1 module." + + # Use the steps below once the x/crisis (crisis.init) bug is fixed or if the + # ability to silence is implemented: https://github.com/golang/go/issues/61211 + #steps: + # - name: Run govulncheck + # uses: golang/govulncheck-action@v1 + # with: + # go-version-file: 'go.mod' + # check-latest: true + # cache: false + # go-package: ./... diff --git a/Makefile b/Makefile index c84a0a118e..b5fb0ea43a 100644 --- a/Makefile +++ b/Makefile @@ -135,6 +135,10 @@ else $(info YAML linter 'yamllint' already installed.) endif +.PHONY: deps\:vulncheck +deps\:vulncheck: + go install golang.org/x/vuln/cmd/govulncheck@latest + .PHONY: deps\:lint deps\:lint: @$(MAKE) deps:lint-go && \ @@ -172,6 +176,7 @@ deps: $(MAKE) deps:bench && \ $(MAKE) deps:chglog && \ $(MAKE) deps:lint && \ + $(MAKE) deps:vulncheck && \ $(MAKE) deps:test && \ $(MAKE) deps:mocks From c9863123596e71b90ce2ad3ec5a1f97986e94d18 Mon Sep 17 00:00:00 2001 From: Shahzad Lone Date: Tue, 1 Oct 2024 20:22:08 -0400 Subject: [PATCH 8/8] feat: Ability to relate private documents to actors (#2907) ## Relevant issue(s) Resolves #2762 ## Description This PR introduces the ability to make use of the `relation`s defined within a policy to create relationships between an actor and a document within a collection. For users sake, I have made the clients (http, and cli) not consume the `policyID` and `resource` name but instead a `docID` and `collection name`, since the collection will have the policy and resource information available we can fetch that and make lives easier for the users. This PR also makes use of the `manages` feature we have had in our policy. The manages essentially defines who can make the relationship manipulation requests. There are a lot of tests in this PR due to a lot of edge cases I wanted to have tested specific to `manger`, and ensuring `write` and `read` permissions don't leak (i.e. are accidently granted). ## CLI Demo The following lets the target actor be able to now read the private document: ```bash defradb client acp relationship add \ --collection Users \ --docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \ --relation reader \ --actor did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \ --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac ``` Result: ```json { "ExistedAlready": false // <-------------- Indicates a new relationship was formed } ``` ### Future (out-of-scope of this PR): - Most of write tests will split into `delete` and `update` in #2905 - Ability to revoke or delete relation coming in #2906 - Decide on the `can't write if no read permission` in #2992 - Move acp logic to a shared repo: https://github.com/sourcenetwork/defradb/issues/2980 ## How has this been tested? - Integration tests Specify the platform(s) on which this was tested: - Manjaro WSL2 --- acp/README.md | 203 +++ acp/acp.go | 16 + acp/acp_local.go | 31 + acp/acp_local_test.go | 286 +++- acp/acp_source_hub.go | 50 + acp/errors.go | 69 +- acp/source_hub_client.go | 85 ++ cli/acp_relationship.go | 25 + cli/acp_relationship_add.go | 130 ++ cli/cli.go | 12 +- client/{policy.go => acp.go} | 7 + client/db.go | 14 + client/errors.go | 39 +- client/mocks/db.go | 60 + .../references/cli/defradb_client_acp.md | 1 + .../cli/defradb_client_acp_relationship.md | 41 + .../defradb_client_acp_relationship_add.md | 81 ++ docs/website/references/http/openapi.json | 30 + examples/dpi_policy/user_dpi_policy.json | 1 + examples/dpi_policy/user_dpi_policy.yml | 2 + .../user_dpi_policy_with_manages.yml | 49 + http/client_acp.go | 50 + http/handler_acp.go | 45 + internal/db/db.go | 44 +- internal/db/permission/check.go | 2 +- internal/db/permission/permission.go | 4 +- internal/db/permission/register.go | 2 +- tests/clients/cli/wrapper.go | 20 - tests/clients/cli/wrapper_acp.go | 66 + tests/clients/http/wrapper.go | 16 + tests/integration/acp.go | 257 +++- ...icator_with_doc_actor_relationship_test.go | 219 +++ ...scribe_with_doc_actor_relationship_test.go | 225 +++ ...oc_actor_collection_with_no_policy_test.go | 66 + .../add_doc_actor_invalid_test.go | 545 +++++++ .../add_doc_actor_with_delete_test.go | 505 +++++++ .../add_doc_actor_with_dummy_relation_test.go | 302 ++++ .../add_doc_actor_with_manager_gql_test.go | 604 ++++++++ .../add_doc_actor_with_manager_test.go | 1286 +++++++++++++++++ .../add_doc_actor_with_only_write_gql_test.go | 198 +++ .../add_doc_actor_with_only_write_test.go | 359 +++++ ...add_doc_actor_with_public_document_test.go | 147 ++ .../add_doc_actor_with_reader_gql_test.go | 204 +++ .../add_doc_actor_with_reader_test.go | 810 +++++++++++ .../add_doc_actor_with_update_gql_test.go | 360 +++++ .../add_doc_actor_with_update_test.go | 541 +++++++ tests/integration/utils.go | 3 + 47 files changed, 8010 insertions(+), 102 deletions(-) create mode 100644 cli/acp_relationship.go create mode 100644 cli/acp_relationship_add.go rename client/{policy.go => acp.go} (80%) create mode 100644 docs/website/references/cli/defradb_client_acp_relationship.md create mode 100644 docs/website/references/cli/defradb_client_acp_relationship_add.md create mode 100644 examples/dpi_policy/user_dpi_policy_with_manages.yml create mode 100644 tests/clients/cli/wrapper_acp.go create mode 100644 tests/integration/acp/p2p/replicator_with_doc_actor_relationship_test.go create mode 100644 tests/integration/acp/p2p/subscribe_with_doc_actor_relationship_test.go create mode 100644 tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_collection_with_no_policy_test.go create mode 100644 tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_invalid_test.go create mode 100644 tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_delete_test.go create mode 100644 tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_dummy_relation_test.go create mode 100644 tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_gql_test.go create mode 100644 tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_test.go create mode 100644 tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_only_write_gql_test.go create mode 100644 tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_only_write_test.go create mode 100644 tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_public_document_test.go create mode 100644 tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_gql_test.go create mode 100644 tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_test.go create mode 100644 tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_gql_test.go create mode 100644 tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_test.go diff --git a/acp/README.md b/acp/README.md index 54c479b5ea..4c2c73907a 100644 --- a/acp/README.md +++ b/acp/README.md @@ -427,6 +427,209 @@ Error: ### Execute Explain example (coming soon) +### Sharing Private Documents With Others + +To share a document (or grant a more restricted access) with another actor, we must add a relationship between the +actor and the document. Inorder to make the relationship we require all of the following: + +1) **Target DocID**: The `docID` of the document we want to make a relationship for. +2) **Collection Name**: The name of the collection that has the `Target DocID`. +3) **Relation Name**: The type of relation (name must be defined within the linked policy on collection). +4) **Target Identity**: The identity of the actor the relationship is being made with. +5) **Requesting Identity**: The identity of the actor that is making the request. + +Note: + - ACP must be available (i.e. ACP can not be disabled). + - The collection with the target document must have a valid policy and resource linked. + - The target document must be registered with ACP already (private document). + - The requesting identity MUST either be the owner OR the manager (manages the relation) of the resource. + - If the specified relation was not granted the miminum DPI permissions (read or write) within the policy, + and a relationship is formed, the subject/actor will still not be able to access (read or write) the resource. + - If the relationship already exists, then it will just be a no-op. + +Consider the following policy that we have under `examples/dpi_policy/user_dpi_policy_with_manages.yml`: + +```yaml +name: An Example Policy + +description: A Policy + +actor: + name: actor + +resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor +``` + +Add the policy: +```sh +defradb client acp policy add -f examples/dpi_policy/user_dpi_policy_with_manages.yml \ +--identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac +``` + +Result: +```json +{ + "PolicyID": "ec11b7e29a4e195f95787e2ec9b65af134718d16a2c9cd655b5e04562d1cabf9" +} +``` + +Add schema, linking to the users resource and our policyID: +```sh +defradb client schema add ' +type Users @policy( + id: "ec11b7e29a4e195f95787e2ec9b65af134718d16a2c9cd655b5e04562d1cabf9", + resource: "users" +) { + name: String + age: Int +} +' +``` + +Result: +```json +[ + { + "Name": "Users", + "ID": 1, + "RootID": 1, + "SchemaVersionID": "bafkreihhd6bqrjhl5zidwztgxzeseveplv3cj3fwtn3unjkdx7j2vr2vrq", + "Sources": [], + "Fields": [ + { + "Name": "_docID", + "ID": 0, + "Kind": null, + "RelationName": null, + "DefaultValue": null + }, + { + "Name": "age", + "ID": 1, + "Kind": null, + "RelationName": null, + "DefaultValue": null + }, + { + "Name": "name", + "ID": 2, + "Kind": null, + "RelationName": null, + "DefaultValue": null + } + ], + "Indexes": [], + "Policy": { + "ID": "ec11b7e29a4e195f95787e2ec9b65af134718d16a2c9cd655b5e04562d1cabf9", + "ResourceName": "users" + }, + "IsMaterialized": true + } +] +``` + +Create a private document: +```sh +defradb client collection create --name Users '[{ "name": "SecretShahzadLone" }]' \ +--identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac +``` + +Only the owner can see it: +```sh +defradb client collection docIDs --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac +``` + +Result: +```json +{ + "docID": "bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c", + "error": "" +} +``` + +Another actor can not: +```sh +defradb client collection docIDs --identity 4d092126012ebaf56161716018a71630d99443d9d5217e9d8502bb5c5456f2c5 +``` + +**Result is empty from the above command** + + +Now let's make the other actor a reader of the document by adding a relationship: +```sh +defradb client acp relationship add \ +--collection Users \ +--docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \ +--relation reader \ +--actor did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \ +--identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac +``` + +Result: +```json +{ + "ExistedAlready": false +} +``` + +**Note: If the same relationship is created again the `ExistedAlready` would then be true, indicating no-op** + +Now the other actor can read: +```sh +defradb client collection docIDs --identity 4d092126012ebaf56161716018a71630d99443d9d5217e9d8502bb5c5456f2c5 +``` + +Result: +```json +{ + "docID": "bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c", + "error": "" +} +``` + +But, they still can not perform an update as they were only granted a read permission (through `reader` relation): +```sh +defradb client collection update --name Users --docID "bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c" \ +--identity 4d092126012ebaf56161716018a71630d99443d9d5217e9d8502bb5c5456f2c5 '{ "name": "SecretUpdatedShahzad" }' +``` + +Result: +```sh +Error: document not found or not authorized to access +``` ## DAC Usage HTTP: diff --git a/acp/acp.go b/acp/acp.go index 973181ae91..c7ae5936e6 100644 --- a/acp/acp.go +++ b/acp/acp.go @@ -99,6 +99,22 @@ type ACP interface { docID string, ) (bool, error) + // AddDocActorRelationship creates a relationship between document and the target actor. + // + // If failure occurs, the result will return an error. Upon success the boolean value will + // be true if the relationship already existed (no-op), and false if a new relationship was made. + // + // Note: The request actor must either be the owner or manager of the document. + AddDocActorRelationship( + ctx context.Context, + policyID string, + resourceName string, + docID string, + relation string, + requestActor identity.Identity, + targetActor string, + ) (bool, error) + // SupportsP2P returns true if the implementation supports ACP across a peer network. SupportsP2P() bool } diff --git a/acp/acp_local.go b/acp/acp_local.go index 97e7a67cce..6e85ac9313 100644 --- a/acp/acp_local.go +++ b/acp/acp_local.go @@ -236,3 +236,34 @@ func (l *ACPLocal) VerifyAccessRequest( return resp.Valid, nil } + +func (l *ACPLocal) AddActorRelationship( + ctx context.Context, + policyID string, + resourceName string, + objectID string, + relation string, + requester identity.Identity, + targetActor string, + creationTime *protoTypes.Timestamp, +) (bool, error) { + principal, err := auth.NewDIDPrincipal(requester.DID) + if err != nil { + return false, newErrInvalidActorID(err, requester.DID) + } + + ctx = auth.InjectPrincipal(ctx, principal) + + setRelationshipRequest := types.SetRelationshipRequest{ + PolicyId: policyID, + Relationship: types.NewActorRelationship(resourceName, objectID, relation, targetActor), + CreationTime: creationTime, + } + + setRelationshipResponse, err := l.engine.SetRelationship(ctx, &setRelationshipRequest) + if err != nil { + return false, err + } + + return setRelationshipResponse.RecordExisted, nil +} diff --git a/acp/acp_local_test.go b/acp/acp_local_test.go index 9dbf0b36e8..7b30b44cbb 100644 --- a/acp/acp_local_test.go +++ b/acp/acp_local_test.go @@ -663,6 +663,197 @@ func Test_LocalACP_PersistentMemory_CheckDocAccess_TrueIfHaveAccessFalseIfNotErr require.Nil(t, errClose) } +func Test_LocalACP_InMemory_AddDocActorRelationship_FalseIfExistsBeforeTrueIfNoOp(t *testing.T) { + ctx := context.Background() + localACP := NewLocalACP() + + localACP.Init(ctx, "") + errStart := localACP.Start(ctx) + require.Nil(t, errStart) + + policyID, errAddPolicy := localACP.AddPolicy( + ctx, + identity1, + validPolicy, + ) + require.Nil(t, errAddPolicy) + require.Equal( + t, + validPolicyID, + policyID, + ) + + // Register a document. + errRegisterDoc := localACP.RegisterDocObject( + ctx, + identity1, + validPolicyID, + "users", + "documentID_XYZ", + ) + require.Nil(t, errRegisterDoc) + + // Other identity does not have access yet. + hasAccess, errCheckDocAccess := localACP.CheckDocAccess( + ctx, + ReadPermission, + identity2.DID, + validPolicyID, + "users", + "documentID_XYZ", + ) + require.Nil(t, errCheckDocAccess) + require.False(t, hasAccess) + + // Grant other identity access. + exists, errAddDocActorRelationship := localACP.AddDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + identity1, + identity2.DID, + ) + require.Nil(t, errAddDocActorRelationship) + require.False(t, exists) + + // Granting again will be no-op + exists, errAddDocActorRelationship = localACP.AddDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + identity1, + identity2.DID, + ) + require.Nil(t, errAddDocActorRelationship) + require.True(t, exists) // Exists already this time + + // Now the other identity has access. + hasAccess, errCheckDocAccess = localACP.CheckDocAccess( + ctx, + ReadPermission, + identity2.DID, + validPolicyID, + "users", + "documentID_XYZ", + ) + require.Nil(t, errCheckDocAccess) + require.True(t, hasAccess) + + errClose := localACP.Close() + require.Nil(t, errClose) +} + +func Test_LocalACP_PersistentMemory_AddDocActorRelationship_FalseIfExistsBeforeTrueIfNoOp(t *testing.T) { + acpPath := t.TempDir() + require.NotEqual(t, "", acpPath) + + ctx := context.Background() + localACP := NewLocalACP() + + localACP.Init(ctx, acpPath) + errStart := localACP.Start(ctx) + require.Nil(t, errStart) + + policyID, errAddPolicy := localACP.AddPolicy( + ctx, + identity1, + validPolicy, + ) + require.Nil(t, errAddPolicy) + require.Equal( + t, + validPolicyID, + policyID, + ) + + // Register a document. + errRegisterDoc := localACP.RegisterDocObject( + ctx, + identity1, + validPolicyID, + "users", + "documentID_XYZ", + ) + require.Nil(t, errRegisterDoc) + + // Other identity does not have access yet. + hasAccess, errCheckDocAccess := localACP.CheckDocAccess( + ctx, + ReadPermission, + identity2.DID, + validPolicyID, + "users", + "documentID_XYZ", + ) + require.Nil(t, errCheckDocAccess) + require.False(t, hasAccess) + + // Grant other identity access. + exists, errAddDocActorRelationship := localACP.AddDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + identity1, + identity2.DID, + ) + require.Nil(t, errAddDocActorRelationship) + require.False(t, exists) + + // Granting again will be no-op + exists, errAddDocActorRelationship = localACP.AddDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + identity1, + identity2.DID, + ) + require.Nil(t, errAddDocActorRelationship) + require.True(t, exists) // Exists already this time + + // Now the other identity has access. + hasAccess, errCheckDocAccess = localACP.CheckDocAccess( + ctx, + ReadPermission, + identity2.DID, + validPolicyID, + "users", + "documentID_XYZ", + ) + require.Nil(t, errCheckDocAccess) + require.True(t, hasAccess) + + // Should continue having their correct behaviour and access even after a restart. + errClose := localACP.Close() + require.Nil(t, errClose) + + localACP.Init(ctx, acpPath) + errStart = localACP.Start(ctx) + require.Nil(t, errStart) + + // Now check again after the restart that the second identity still has access. + hasAccess, errCheckDocAccess = localACP.CheckDocAccess( + ctx, + ReadPermission, + identity2.DID, + validPolicyID, + "users", + "documentID_XYZ", + ) + require.Nil(t, errCheckDocAccess) + require.True(t, hasAccess) + + errClose = localACP.Close() + require.Nil(t, errClose) +} + func Test_LocalACP_InMemory_AddPolicy_InvalidCreatorIDReturnsError(t *testing.T) { ctx := context.Background() localACP := NewLocalACP() @@ -684,6 +875,30 @@ func Test_LocalACP_InMemory_AddPolicy_InvalidCreatorIDReturnsError(t *testing.T) require.NoError(t, err) } +func Test_LocalACP_Persistent_AddPolicy_InvalidCreatorIDReturnsError(t *testing.T) { + acpPath := t.TempDir() + require.NotEqual(t, "", acpPath) + + ctx := context.Background() + localACP := NewLocalACP() + + localACP.Init(ctx, acpPath) + err := localACP.Start(ctx) + require.Nil(t, err) + + policyID, err := localACP.AddPolicy( + ctx, + invalidIdentity, + validPolicy, + ) + + require.ErrorIs(t, err, ErrInvalidActorID) + require.Empty(t, policyID) + + err = localACP.Close() + require.NoError(t, err) +} + func Test_LocalACP_InMemory_RegisterObject_InvalidCreatorIDReturnsError(t *testing.T) { ctx := context.Background() localACP := NewLocalACP() @@ -706,7 +921,7 @@ func Test_LocalACP_InMemory_RegisterObject_InvalidCreatorIDReturnsError(t *testi require.NoError(t, err) } -func Test_LocalACP_Persistent_AddPolicy_InvalidCreatorIDReturnsError(t *testing.T) { +func Test_LocalACP_Persistent_RegisterObject_InvalidCreatorIDReturnsError(t *testing.T) { acpPath := t.TempDir() require.NotEqual(t, "", acpPath) @@ -717,20 +932,59 @@ func Test_LocalACP_Persistent_AddPolicy_InvalidCreatorIDReturnsError(t *testing. err := localACP.Start(ctx) require.Nil(t, err) - policyID, err := localACP.AddPolicy( + err = localACP.RegisterDocObject( ctx, invalidIdentity, - validPolicy, + validPolicyID, + "users", + "documentID_XYZ", ) require.ErrorIs(t, err, ErrInvalidActorID) - require.Empty(t, policyID) err = localACP.Close() require.NoError(t, err) } -func Test_LocalACP_Persistent_RegisterObject_InvalidCreatorIDReturnsError(t *testing.T) { +func Test_LocalACP_InMemory_AddDocActorRelationship_InvalidIdentitiesReturnError(t *testing.T) { + ctx := context.Background() + localACP := NewLocalACP() + + localACP.Init(ctx, "") + err := localACP.Start(ctx) + require.Nil(t, err) + + // Invalid requesting identity. + exists, err := localACP.AddDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + invalidIdentity, + identity2.DID, + ) + require.False(t, exists) + require.ErrorIs(t, err, ErrInvalidActorID) + + // Invalid target actor. + exists, err = localACP.AddDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + identity1, + invalidIdentity.DID, + ) + require.False(t, exists) + require.ErrorIs(t, err, ErrFailedToAddDocActorRelationshipWithACP) + + err = localACP.Close() + require.NoError(t, err) +} + +func Test_LocalACP_Persistent_AddDocActorRelationship_InvalidIdentitiesReturnError(t *testing.T) { acpPath := t.TempDir() require.NotEqual(t, "", acpPath) @@ -741,16 +995,32 @@ func Test_LocalACP_Persistent_RegisterObject_InvalidCreatorIDReturnsError(t *tes err := localACP.Start(ctx) require.Nil(t, err) - err = localACP.RegisterDocObject( + // Invalid requesting identity. + exists, err := localACP.AddDocActorRelationship( ctx, - invalidIdentity, validPolicyID, "users", "documentID_XYZ", + "reader", + invalidIdentity, + identity2.DID, ) - + require.False(t, exists) require.ErrorIs(t, err, ErrInvalidActorID) + // Invalid target actor. + exists, err = localACP.AddDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + identity1, + invalidIdentity.DID, + ) + require.False(t, exists) + require.ErrorIs(t, err, ErrFailedToAddDocActorRelationshipWithACP) + err = localACP.Close() require.NoError(t, err) } diff --git a/acp/acp_source_hub.go b/acp/acp_source_hub.go index 4dfb26c090..d0c4fb6b89 100644 --- a/acp/acp_source_hub.go +++ b/acp/acp_source_hub.go @@ -261,3 +261,53 @@ func (a *acpSourceHub) VerifyAccessRequest( func (a *acpSourceHub) Close() error { return nil } + +func (a *acpSourceHub) AddActorRelationship( + ctx context.Context, + policyID string, + resourceName string, + objectID string, + relation string, + requester identity.Identity, + targetActor string, + creationTime *protoTypes.Timestamp, +) (bool, error) { + msgSet := sourcehub.MsgSet{} + cmdMapper := msgSet.WithBearerPolicyCmd(&acptypes.MsgBearerPolicyCmd{ + Creator: a.signer.GetAccAddress(), + BearerToken: requester.BearerToken, + PolicyId: policyID, + Cmd: acptypes.NewSetRelationshipCmd( + acptypes.NewActorRelationship( + resourceName, + objectID, + relation, + targetActor, + ), + ), + CreationTime: creationTime, + }) + tx, err := a.txBuilder.Build(ctx, a.signer, &msgSet) + if err != nil { + return false, err + } + resp, err := a.client.BroadcastTx(ctx, tx) + if err != nil { + return false, err + } + + result, err := a.client.AwaitTx(ctx, resp.TxHash) + if err != nil { + return false, err + } + if result.Error() != nil { + return false, result.Error() + } + + cmdResult, err := cmdMapper.Map(result.TxPayload()) + if err != nil { + return false, err + } + + return cmdResult.GetResult().GetSetRelationshipResult().RecordExisted, nil +} diff --git a/acp/errors.go b/acp/errors.go index 5ff4eee302..e0717f15dd 100644 --- a/acp/errors.go +++ b/acp/errors.go @@ -15,12 +15,14 @@ import ( ) const ( - errInitializationOfACPFailed = "initialization of acp failed" - errStartingACPInEmptyPath = "starting acp in an empty path" - errFailedToAddPolicyWithACP = "failed to add policy with acp" - errFailedToRegisterDocWithACP = "failed to register document with acp" - errFailedToCheckIfDocIsRegisteredWithACP = "failed to check if doc is registered with acp" - errFailedToVerifyDocAccessWithACP = "failed to verify doc access with acp" + errInitializationOfACPFailed = "initialization of acp failed" + errStartingACPInEmptyPath = "starting acp in an empty path" + errFailedToAddPolicyWithACP = "failed to add policy with acp" + errFailedToRegisterDocWithACP = "failed to register document with acp" + errFailedToCheckIfDocIsRegisteredWithACP = "failed to check if doc is registered with acp" + errFailedToVerifyDocAccessWithACP = "failed to verify doc access with acp" + errFailedToAddDocActorRelationshipWithACP = "failed to add document actor relationship with acp" + errMissingRequiredArgToAddDocActorRelationship = "missing a required argument needed to add doc actor relationship" errObjectDidNotRegister = "no-op while registering object (already exists or error) with acp" errNoPolicyArgs = "missing policy arguments, must have both id and resource" @@ -40,12 +42,13 @@ const ( ) var ( - ErrInitializationOfACPFailed = errors.New(errInitializationOfACPFailed) - ErrFailedToAddPolicyWithACP = errors.New(errFailedToAddPolicyWithACP) - ErrFailedToRegisterDocWithACP = errors.New(errFailedToRegisterDocWithACP) - ErrFailedToCheckIfDocIsRegisteredWithACP = errors.New(errFailedToCheckIfDocIsRegisteredWithACP) - ErrFailedToVerifyDocAccessWithACP = errors.New(errFailedToVerifyDocAccessWithACP) - ErrPolicyDoesNotExistWithACP = errors.New(errPolicyDoesNotExistWithACP) + ErrInitializationOfACPFailed = errors.New(errInitializationOfACPFailed) + ErrFailedToAddPolicyWithACP = errors.New(errFailedToAddPolicyWithACP) + ErrFailedToRegisterDocWithACP = errors.New(errFailedToRegisterDocWithACP) + ErrFailedToCheckIfDocIsRegisteredWithACP = errors.New(errFailedToCheckIfDocIsRegisteredWithACP) + ErrFailedToVerifyDocAccessWithACP = errors.New(errFailedToVerifyDocAccessWithACP) + ErrFailedToAddDocActorRelationshipWithACP = errors.New(errFailedToAddDocActorRelationshipWithACP) + ErrPolicyDoesNotExistWithACP = errors.New(errPolicyDoesNotExistWithACP) ErrResourceDoesNotExistOnTargetPolicy = errors.New(errResourceDoesNotExistOnTargetPolicy) @@ -139,6 +142,29 @@ func NewErrFailedToVerifyDocAccessWithACP( ) } +func NewErrFailedToAddDocActorRelationshipWithACP( + inner error, + Type string, + policyID string, + resourceName string, + docID string, + relation string, + requestActor string, + targetActor string, +) error { + return errors.Wrap( + errFailedToAddDocActorRelationshipWithACP, + inner, + errors.NewKV("Type", Type), + errors.NewKV("PolicyID", policyID), + errors.NewKV("ResourceName", resourceName), + errors.NewKV("DocID", docID), + errors.NewKV("Relation", relation), + errors.NewKV("RequestActor", requestActor), + errors.NewKV("TargetActor", targetActor), + ) +} + func newErrPolicyDoesNotExistWithACP( inner error, policyID string, @@ -209,6 +235,25 @@ func newErrExprOfRequiredPermissionHasInvalidChar( ) } +func NewErrMissingRequiredArgToAddDocActorRelationship( + policyID string, + resourceName string, + docID string, + relation string, + requestActor string, + targetActor string, +) error { + return errors.New( + errMissingRequiredArgToAddDocActorRelationship, + errors.NewKV("PolicyID", policyID), + errors.NewKV("ResourceName", resourceName), + errors.NewKV("DocID", docID), + errors.NewKV("Relation", relation), + errors.NewKV("RequestActor", requestActor), + errors.NewKV("TargetActor", targetActor), + ) +} + func newErrInvalidActorID( inner error, id string, diff --git a/acp/source_hub_client.go b/acp/source_hub_client.go index 0bf344afb8..0bfbae72b1 100644 --- a/acp/source_hub_client.go +++ b/acp/source_hub_client.go @@ -85,6 +85,27 @@ type sourceHubClient interface { docID string, ) (bool, error) + // AddActorRelationship creates a relationship within a policy which ties the target actor + // with the specified object, which means that the set of high level rules defined in the + // policy will now apply to target actor as well. + // + // If failure occurs, the result will return an error. Upon success the boolean value will + // be true if the relationship with actor already existed (no-op), and false if a new + // relationship was made. + // + // Note: The requester identity must either be the owner of the object (being shared) or + // the manager (i.e. the relation has `manages` defined in the policy). + AddActorRelationship( + ctx context.Context, + policyID string, + resourceName string, + objectID string, + relation string, + requester identity.Identity, + targetActor string, + creationTime *protoTypes.Timestamp, + ) (bool, error) + // Close closes any resources in use by acp. Close() error } @@ -335,6 +356,70 @@ func (a *sourceHubBridge) CheckDocAccess( } } +func (a *sourceHubBridge) AddDocActorRelationship( + ctx context.Context, + policyID string, + resourceName string, + docID string, + relation string, + requestActor identity.Identity, + targetActor string, +) (bool, error) { + if policyID == "" || + resourceName == "" || + docID == "" || + relation == "" || + requestActor == (identity.Identity{}) || + targetActor == "" { + return false, NewErrMissingRequiredArgToAddDocActorRelationship( + policyID, + resourceName, + docID, + relation, + requestActor.DID, + targetActor, + ) + } + + exists, err := a.client.AddActorRelationship( + ctx, + policyID, + resourceName, + docID, + relation, + requestActor, + targetActor, + protoTypes.TimestampNow(), + ) + + if err != nil { + return false, NewErrFailedToAddDocActorRelationshipWithACP( + err, + "Local", + policyID, + resourceName, + docID, + relation, + requestActor.DID, + targetActor, + ) + } + + log.InfoContext( + ctx, + "Document and actor relationship set", + corelog.Any("PolicyID", policyID), + corelog.Any("ResourceName", resourceName), + corelog.Any("DocID", docID), + corelog.Any("Relation", relation), + corelog.Any("RequestActor", requestActor.DID), + corelog.Any("TargetActor", targetActor), + corelog.Any("Existed", exists), + ) + + return exists, nil +} + func (a *sourceHubBridge) SupportsP2P() bool { _, ok := a.client.(*acpSourceHub) return ok diff --git a/cli/acp_relationship.go b/cli/acp_relationship.go new file mode 100644 index 0000000000..a2a5f3cb64 --- /dev/null +++ b/cli/acp_relationship.go @@ -0,0 +1,25 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package cli + +import ( + "github.com/spf13/cobra" +) + +func MakeACPRelationshipCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "relationship", + Short: "Interact with the acp relationship features of DefraDB instance", + Long: `Interact with the acp relationship features of DefraDB instance`, + } + + return cmd +} diff --git a/cli/acp_relationship_add.go b/cli/acp_relationship_add.go new file mode 100644 index 0000000000..9733732af8 --- /dev/null +++ b/cli/acp_relationship_add.go @@ -0,0 +1,130 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package cli + +import ( + "github.com/spf13/cobra" +) + +func MakeACPRelationshipAddCommand() *cobra.Command { + const ( + collectionFlagLong string = "collection" + collectionFlagShort string = "c" + + relationFlagLong string = "relation" + relationFlagShort string = "r" + + targetActorFlagLong string = "actor" + targetActorFlagShort string = "a" + + docIDFlag string = "docID" + ) + + var ( + collectionArg string + relationArg string + targetActorArg string + docIDArg string + ) + + var cmd = &cobra.Command{ + Use: "add [--docID] [-c --collection] [-r --relation] [-a --actor] [-i --identity]", + Short: "Add new relationship", + Long: `Add new relationship + +To share a document (or grant a more restricted access) with another actor, we must add a relationship between the +actor and the document. Inorder to make the relationship we require all of the following: +1) Target DocID: The docID of the document we want to make a relationship for. +2) Collection Name: The name of the collection that has the Target DocID. +3) Relation Name: The type of relation (name must be defined within the linked policy on collection). +4) Target Identity: The identity of the actor the relationship is being made with. +5) Requesting Identity: The identity of the actor that is making the request. + +Notes: + - ACP must be available (i.e. ACP can not be disabled). + - The target document must be registered with ACP already (policy & resource specified). + - The requesting identity MUST either be the owner OR the manager (manages the relation) of the resource. + - If the specified relation was not granted the miminum DPI permissions (read or write) within the policy, + and a relationship is formed, the subject/actor will still not be able to access (read or write) the resource. + - Learn more about [ACP & DPI Rules](/acp/README.md) + +Example: Let another actor (4d092126012ebaf56161716018a71630d99443d9d5217e9d8502bb5c5456f2c5) read a private document: + defradb client acp relationship add \ + --collection Users \ + --docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \ + --relation reader \ + --actor did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \ + --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac + +Example: Creating a dummy relationship does nothing (from database prespective): + defradb client acp relationship add \ + -c Users \ + --docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \ + -r dummy \ + -a did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \ + -i e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac +`, + RunE: func(cmd *cobra.Command, args []string) error { + db := mustGetContextDB(cmd) + exists, err := db.AddDocActorRelationship( + cmd.Context(), + collectionArg, + docIDArg, + relationArg, + targetActorArg, + ) + + if err != nil { + return err + } + + return writeJSON(cmd, exists) + }, + } + + cmd.Flags().StringVarP( + &collectionArg, + collectionFlagLong, + collectionFlagShort, + "", + "Collection that has the resource and policy for object", + ) + _ = cmd.MarkFlagRequired(collectionFlagLong) + + cmd.Flags().StringVarP( + &relationArg, + relationFlagLong, + relationFlagShort, + "", + "Relation that needs to be set for the relationship", + ) + _ = cmd.MarkFlagRequired(relationFlagLong) + + cmd.Flags().StringVarP( + &targetActorArg, + targetActorFlagLong, + targetActorFlagShort, + "", + "Actor to add relationship with", + ) + _ = cmd.MarkFlagRequired(targetActorFlagLong) + + cmd.Flags().StringVarP( + &docIDArg, + docIDFlag, + "", + "", + "Document Identifier (ObjectID) to make relationship for", + ) + _ = cmd.MarkFlagRequired(docIDFlag) + + return cmd +} diff --git a/cli/cli.go b/cli/cli.go index 4453cbaafb..61d1fd51cf 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -62,14 +62,20 @@ func NewDefraCommand() *cobra.Command { schema_migrate, ) - policy := MakeACPPolicyCommand() - policy.AddCommand( + acp_policy := MakeACPPolicyCommand() + acp_policy.AddCommand( MakeACPPolicyAddCommand(), ) + acp_relationship := MakeACPRelationshipCommand() + acp_relationship.AddCommand( + MakeACPRelationshipAddCommand(), + ) + acp := MakeACPCommand() acp.AddCommand( - policy, + acp_policy, + acp_relationship, ) view := MakeViewCommand() diff --git a/client/policy.go b/client/acp.go similarity index 80% rename from client/policy.go rename to client/acp.go index 5b877696c2..7795369c8f 100644 --- a/client/policy.go +++ b/client/acp.go @@ -29,3 +29,10 @@ type AddPolicyResult struct { // upon successful creation of a policy. PolicyID string } + +// AddDocActorRelationshipResult wraps the result of making a document-actor relationship. +type AddDocActorRelationshipResult struct { + // ExistedAlready is true if the relationship existed already (no-op), and + // it is false if a new relationship was created. + ExistedAlready bool +} diff --git a/client/db.go b/client/db.go index b8f5e91e35..e28d21df02 100644 --- a/client/db.go +++ b/client/db.go @@ -106,6 +106,20 @@ type DB interface { // // Note: A policy can not be added without the creatorID (identity). AddPolicy(ctx context.Context, policy string) (AddPolicyResult, error) + + // AddDocActorRelationship creates a relationship between document and the target actor. + // + // If failure occurs, the result will return an error. Upon success the boolean value will + // be true if the relationship already existed (no-op), and false if a new relationship was made. + // + // Note: The request actor must either be the owner or manager of the document. + AddDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, + ) (AddDocActorRelationshipResult, error) } // Store contains the core DefraDB read-write operations. diff --git a/client/errors.go b/client/errors.go index 866ad98ec4..ceb526b35e 100644 --- a/client/errors.go +++ b/client/errors.go @@ -41,25 +41,26 @@ const ( // This list is incomplete and undefined errors may also be returned. // Errors returned from this package may be tested against these errors with errors.Is. var ( - ErrFieldNotExist = errors.New(errFieldNotExist) - ErrUnexpectedType = errors.New(errUnexpectedType) - ErrFailedToUnmarshalCollection = errors.New(errFailedToUnmarshalCollection) - ErrOperationNotPermittedOnNamelessCols = errors.New(errOperationNotPermittedOnNamelessCols) - ErrFieldNotObject = errors.New("trying to access field on a non object type") - ErrValueTypeMismatch = errors.New("value does not match indicated type") - ErrDocumentNotFoundOrNotAuthorized = errors.New("document not found or not authorized to access") - ErrPolicyAddFailureNoACP = errors.New("failure adding policy because ACP was not available") - ErrInvalidUpdateTarget = errors.New("the target document to update is of invalid type") - ErrInvalidUpdater = errors.New("the updater of a document is of invalid type") - ErrInvalidDeleteTarget = errors.New("the target document to delete is of invalid type") - ErrMalformedDocID = errors.New("malformed document ID, missing either version or cid") - ErrInvalidDocIDVersion = errors.New("invalid document ID version") - ErrInvalidJSONPayload = errors.New(errInvalidJSONPayload) - ErrCanNotNormalizeValue = errors.New(errCanNotNormalizeValue) - ErrCanNotTurnNormalValueIntoArray = errors.New(errCanNotTurnNormalValueIntoArray) - ErrCanNotMakeNormalNilFromFieldKind = errors.New(errCanNotMakeNormalNilFromFieldKind) - ErrCollectionNotFound = errors.New(errCollectionNotFound) - ErrFailedToParseKind = errors.New(errFailedToParseKind) + ErrFieldNotExist = errors.New(errFieldNotExist) + ErrUnexpectedType = errors.New(errUnexpectedType) + ErrFailedToUnmarshalCollection = errors.New(errFailedToUnmarshalCollection) + ErrOperationNotPermittedOnNamelessCols = errors.New(errOperationNotPermittedOnNamelessCols) + ErrFieldNotObject = errors.New("trying to access field on a non object type") + ErrValueTypeMismatch = errors.New("value does not match indicated type") + ErrDocumentNotFoundOrNotAuthorized = errors.New("document not found or not authorized to access") + ErrACPOperationButACPNotAvailable = errors.New("operation requires ACP, but ACP not available") + ErrACPOperationButCollectionHasNoPolicy = errors.New("operation requires ACP, but collection has no policy") + ErrInvalidUpdateTarget = errors.New("the target document to update is of invalid type") + ErrInvalidUpdater = errors.New("the updater of a document is of invalid type") + ErrInvalidDeleteTarget = errors.New("the target document to delete is of invalid type") + ErrMalformedDocID = errors.New("malformed document ID, missing either version or cid") + ErrInvalidDocIDVersion = errors.New("invalid document ID version") + ErrInvalidJSONPayload = errors.New(errInvalidJSONPayload) + ErrCanNotNormalizeValue = errors.New(errCanNotNormalizeValue) + ErrCanNotTurnNormalValueIntoArray = errors.New(errCanNotTurnNormalValueIntoArray) + ErrCanNotMakeNormalNilFromFieldKind = errors.New(errCanNotMakeNormalNilFromFieldKind) + ErrCollectionNotFound = errors.New(errCollectionNotFound) + ErrFailedToParseKind = errors.New(errFailedToParseKind) ) // NewErrFieldNotExist returns an error indicating that the given field does not exist. diff --git a/client/mocks/db.go b/client/mocks/db.go index 8923e63d78..1297870e15 100644 --- a/client/mocks/db.go +++ b/client/mocks/db.go @@ -35,6 +35,66 @@ func (_m *DB) EXPECT() *DB_Expecter { return &DB_Expecter{mock: &_m.Mock} } +// AddDocActorRelationship provides a mock function with given fields: ctx, collectionName, docID, relation, targetActor +func (_m *DB) AddDocActorRelationship(ctx context.Context, collectionName string, docID string, relation string, targetActor string) (client.AddDocActorRelationshipResult, error) { + ret := _m.Called(ctx, collectionName, docID, relation, targetActor) + + if len(ret) == 0 { + panic("no return value specified for AddDocActorRelationship") + } + + var r0 client.AddDocActorRelationshipResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) (client.AddDocActorRelationshipResult, error)); ok { + return rf(ctx, collectionName, docID, relation, targetActor) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) client.AddDocActorRelationshipResult); ok { + r0 = rf(ctx, collectionName, docID, relation, targetActor) + } else { + r0 = ret.Get(0).(client.AddDocActorRelationshipResult) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) error); ok { + r1 = rf(ctx, collectionName, docID, relation, targetActor) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DB_AddDocActorRelationship_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddDocActorRelationship' +type DB_AddDocActorRelationship_Call struct { + *mock.Call +} + +// AddDocActorRelationship is a helper method to define mock.On call +// - ctx context.Context +// - collectionName string +// - docID string +// - relation string +// - targetActor string +func (_e *DB_Expecter) AddDocActorRelationship(ctx interface{}, collectionName interface{}, docID interface{}, relation interface{}, targetActor interface{}) *DB_AddDocActorRelationship_Call { + return &DB_AddDocActorRelationship_Call{Call: _e.mock.On("AddDocActorRelationship", ctx, collectionName, docID, relation, targetActor)} +} + +func (_c *DB_AddDocActorRelationship_Call) Run(run func(ctx context.Context, collectionName string, docID string, relation string, targetActor string)) *DB_AddDocActorRelationship_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) + }) + return _c +} + +func (_c *DB_AddDocActorRelationship_Call) Return(_a0 client.AddDocActorRelationshipResult, _a1 error) *DB_AddDocActorRelationship_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_AddDocActorRelationship_Call) RunAndReturn(run func(context.Context, string, string, string, string) (client.AddDocActorRelationshipResult, error)) *DB_AddDocActorRelationship_Call { + _c.Call.Return(run) + return _c +} + // AddP2PCollections provides a mock function with given fields: ctx, collectionIDs func (_m *DB) AddP2PCollections(ctx context.Context, collectionIDs []string) error { ret := _m.Called(ctx, collectionIDs) diff --git a/docs/website/references/cli/defradb_client_acp.md b/docs/website/references/cli/defradb_client_acp.md index 5a9c9aef80..d2ffce5036 100644 --- a/docs/website/references/cli/defradb_client_acp.md +++ b/docs/website/references/cli/defradb_client_acp.md @@ -42,4 +42,5 @@ Learn more about [ACP](/acp/README.md) * [defradb client](defradb_client.md) - Interact with a DefraDB node * [defradb client acp policy](defradb_client_acp_policy.md) - Interact with the acp policy features of DefraDB instance +* [defradb client acp relationship](defradb_client_acp_relationship.md) - Interact with the acp relationship features of DefraDB instance diff --git a/docs/website/references/cli/defradb_client_acp_relationship.md b/docs/website/references/cli/defradb_client_acp_relationship.md new file mode 100644 index 0000000000..4c204d0ccd --- /dev/null +++ b/docs/website/references/cli/defradb_client_acp_relationship.md @@ -0,0 +1,41 @@ +## defradb client acp relationship + +Interact with the acp relationship features of DefraDB instance + +### Synopsis + +Interact with the acp relationship features of DefraDB instance + +### Options + +``` + -h, --help help for relationship +``` + +### Options inherited from parent commands + +``` + -i, --identity string Hex formatted private key used to authenticate with ACP + --keyring-backend string Keyring backend to use. Options are file or system (default "file") + --keyring-namespace string Service name to use when using the system backend (default "defradb") + --keyring-path string Path to store encrypted keys when using the file backend (default "keys") + --log-format string Log format to use. Options are text or json (default "text") + --log-level string Log level to use. Options are debug, info, error, fatal (default "info") + --log-output string Log output path. Options are stderr or stdout. (default "stderr") + --log-overrides string Logger config overrides. Format ,=,...;,... + --log-source Include source location in logs + --log-stacktrace Include stacktrace in error and fatal logs + --no-keyring Disable the keyring and generate ephemeral keys + --no-log-color Disable colored log output + --rootdir string Directory for persistent data (default: $HOME/.defradb) + --secret-file string Path to the file containing secrets (default ".env") + --source-hub-address string The SourceHub address authorized by the client to make SourceHub transactions on behalf of the actor + --tx uint Transaction ID + --url string URL of HTTP endpoint to listen on or connect to (default "127.0.0.1:9181") +``` + +### SEE ALSO + +* [defradb client acp](defradb_client_acp.md) - Interact with the access control system of a DefraDB node +* [defradb client acp relationship add](defradb_client_acp_relationship_add.md) - Add new relationship + diff --git a/docs/website/references/cli/defradb_client_acp_relationship_add.md b/docs/website/references/cli/defradb_client_acp_relationship_add.md new file mode 100644 index 0000000000..ba5647c163 --- /dev/null +++ b/docs/website/references/cli/defradb_client_acp_relationship_add.md @@ -0,0 +1,81 @@ +## defradb client acp relationship add + +Add new relationship + +### Synopsis + +Add new relationship + +To share a document (or grant a more restricted access) with another actor, we must add a relationship between the +actor and the document. Inorder to make the relationship we require all of the following: +1) Target DocID: The docID of the document we want to make a relationship for. +2) Collection Name: The name of the collection that has the Target DocID. +3) Relation Name: The type of relation (name must be defined within the linked policy on collection). +4) Target Identity: The identity of the actor the relationship is being made with. +5) Requesting Identity: The identity of the actor that is making the request. + +Notes: + - ACP must be available (i.e. ACP can not be disabled). + - The target document must be registered with ACP already (policy & resource specified). + - The requesting identity MUST either be the owner OR the manager (manages the relation) of the resource. + - If the specified relation was not granted the miminum DPI permissions (read or write) within the policy, + and a relationship is formed, the subject/actor will still not be able to access (read or write) the resource. + - Learn more about [ACP & DPI Rules](/acp/README.md) + +Example: Let another actor (4d092126012ebaf56161716018a71630d99443d9d5217e9d8502bb5c5456f2c5) read a private document: + defradb client acp relationship add \ + --collection Users \ + --docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \ + --relation reader \ + --actor did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \ + --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac + +Example: Creating a dummy relationship does nothing (from database prespective): + defradb client acp relationship add \ + -c Users \ + --docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \ + -r dummy \ + -a did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \ + -i e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac + + +``` +defradb client acp relationship add [--docID] [-c --collection] [-r --relation] [-a --actor] [-i --identity] [flags] +``` + +### Options + +``` + -a, --actor string Actor to add relationship with + -c, --collection string Collection that has the resource and policy for object + --docID string Document Identifier (ObjectID) to make relationship for + -h, --help help for add + -r, --relation string Relation that needs to be set for the relationship +``` + +### Options inherited from parent commands + +``` + -i, --identity string Hex formatted private key used to authenticate with ACP + --keyring-backend string Keyring backend to use. Options are file or system (default "file") + --keyring-namespace string Service name to use when using the system backend (default "defradb") + --keyring-path string Path to store encrypted keys when using the file backend (default "keys") + --log-format string Log format to use. Options are text or json (default "text") + --log-level string Log level to use. Options are debug, info, error, fatal (default "info") + --log-output string Log output path. Options are stderr or stdout. (default "stderr") + --log-overrides string Logger config overrides. Format ,=,...;,... + --log-source Include source location in logs + --log-stacktrace Include stacktrace in error and fatal logs + --no-keyring Disable the keyring and generate ephemeral keys + --no-log-color Disable colored log output + --rootdir string Directory for persistent data (default: $HOME/.defradb) + --secret-file string Path to the file containing secrets (default ".env") + --source-hub-address string The SourceHub address authorized by the client to make SourceHub transactions on behalf of the actor + --tx uint Transaction ID + --url string URL of HTTP endpoint to listen on or connect to (default "127.0.0.1:9181") +``` + +### SEE ALSO + +* [defradb client acp relationship](defradb_client_acp_relationship.md) - Interact with the acp relationship features of DefraDB instance + diff --git a/docs/website/references/http/openapi.json b/docs/website/references/http/openapi.json index 6b7686c7c1..c0a7898364 100644 --- a/docs/website/references/http/openapi.json +++ b/docs/website/references/http/openapi.json @@ -588,6 +588,36 @@ ] } }, + "/acp/relationship": { + "post": { + "description": "Add an actor relationship using acp system", + "operationId": "add relationship", + "requestBody": { + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "200": { + "$ref": "#/components/responses/success" + }, + "400": { + "$ref": "#/components/responses/error" + }, + "default": { + "description": "" + } + }, + "tags": [ + "acp_relationship" + ] + } + }, "/backup/export": { "post": { "description": "Export a database backup to file", diff --git a/examples/dpi_policy/user_dpi_policy.json b/examples/dpi_policy/user_dpi_policy.json index 74028d8ee6..96c794b490 100644 --- a/examples/dpi_policy/user_dpi_policy.json +++ b/examples/dpi_policy/user_dpi_policy.json @@ -1,4 +1,5 @@ { + "name": "An Example Policy", "description": "A Valid Defra Policy Interface (DPI)", "actor": { "name": "actor" diff --git a/examples/dpi_policy/user_dpi_policy.yml b/examples/dpi_policy/user_dpi_policy.yml index fafae06957..1b1df1e0b9 100644 --- a/examples/dpi_policy/user_dpi_policy.yml +++ b/examples/dpi_policy/user_dpi_policy.yml @@ -7,6 +7,8 @@ # # Learn more about the DefraDB Policy Interface [DPI](/acp/README.md) +name: An Example Policy + description: A Valid DefraDB Policy Interface (DPI) actor: diff --git a/examples/dpi_policy/user_dpi_policy_with_manages.yml b/examples/dpi_policy/user_dpi_policy_with_manages.yml new file mode 100644 index 0000000000..4667660136 --- /dev/null +++ b/examples/dpi_policy/user_dpi_policy_with_manages.yml @@ -0,0 +1,49 @@ +# The below policy contains an example with valid DPI compliant resource that can be linked to a collection +# object during the schema add command to have access control enabled for documents of that collection. +# +# This policy specifically has the manages attribute defined under admin relation which gives admin +# of a resource, the ability to add/remove relationships with `reader` relation name. +# +# Learn more about the DefraDB Policy Interface [DPI](/acp/README.md) + +name: An Example Policy + +description: A Policy + +actor: + name: actor + +resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor diff --git a/http/client_acp.go b/http/client_acp.go index a0140cf437..d4f1ed02e5 100644 --- a/http/client_acp.go +++ b/http/client_acp.go @@ -11,7 +11,9 @@ package http import ( + "bytes" "context" + "encoding/json" "net/http" "strings" @@ -42,3 +44,51 @@ func (c *Client) AddPolicy( return policyResult, nil } + +type addDocActorRelationshipRequest struct { + CollectionName string + DocID string + Relation string + TargetActor string +} + +func (c *Client) AddDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, +) (client.AddDocActorRelationshipResult, error) { + methodURL := c.http.baseURL.JoinPath("acp", "relationship") + + body, err := json.Marshal( + addDocActorRelationshipRequest{ + CollectionName: collectionName, + DocID: docID, + Relation: relation, + TargetActor: targetActor, + }, + ) + + if err != nil { + return client.AddDocActorRelationshipResult{}, err + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + methodURL.String(), + bytes.NewBuffer(body), + ) + + if err != nil { + return client.AddDocActorRelationshipResult{}, err + } + + var addDocActorRelResult client.AddDocActorRelationshipResult + if err := c.http.requestJson(req, &addDocActorRelResult); err != nil { + return client.AddDocActorRelationshipResult{}, err + } + + return addDocActorRelResult, nil +} diff --git a/http/handler_acp.go b/http/handler_acp.go index c3c5985c71..e9bdf2ce0e 100644 --- a/http/handler_acp.go +++ b/http/handler_acp.go @@ -46,6 +46,35 @@ func (s *acpHandler) AddPolicy(rw http.ResponseWriter, req *http.Request) { responseJSON(rw, http.StatusOK, addPolicyResult) } +func (s *acpHandler) AddDocActorRelationship(rw http.ResponseWriter, req *http.Request) { + db, ok := req.Context().Value(dbContextKey).(client.DB) + if !ok { + responseJSON(rw, http.StatusBadRequest, errorResponse{NewErrFailedToGetContext("db")}) + return + } + + var message addDocActorRelationshipRequest + err := requestJSON(req, &message) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + + addDocActorRelResult, err := db.AddDocActorRelationship( + req.Context(), + message.CollectionName, + message.DocID, + message.Relation, + message.TargetActor, + ) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + + responseJSON(rw, http.StatusOK, addDocActorRelResult) +} + func (h *acpHandler) bindRoutes(router *Router) { successResponse := &openapi3.ResponseRef{ Ref: "#/components/responses/success", @@ -69,5 +98,21 @@ func (h *acpHandler) bindRoutes(router *Router) { Value: acpAddPolicyRequest, } + acpAddDocActorRelationshipRequest := openapi3.NewRequestBody(). + WithRequired(true). + WithContent(openapi3.NewContentWithSchema(openapi3.NewStringSchema(), []string{"text/plain"})) + + acpAddDocActorRelationship := openapi3.NewOperation() + acpAddDocActorRelationship.OperationID = "add relationship" + acpAddDocActorRelationship.Description = "Add an actor relationship using acp system" + acpAddDocActorRelationship.Tags = []string{"acp_relationship"} + acpAddDocActorRelationship.Responses = openapi3.NewResponses() + acpAddDocActorRelationship.Responses.Set("200", successResponse) + acpAddDocActorRelationship.Responses.Set("400", errorResponse) + acpAddDocActorRelationship.RequestBody = &openapi3.RequestBodyRef{ + Value: acpAddDocActorRelationshipRequest, + } + router.AddRoute("/acp/policy", http.MethodPost, acpAddPolicy, h.AddPolicy) + router.AddRoute("/acp/relationship", http.MethodPost, acpAddDocActorRelationship, h.AddDocActorRelationship) } diff --git a/internal/db/db.go b/internal/db/db.go index d88c5920bc..73165c239a 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -31,6 +31,7 @@ import ( "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/event" "github.com/sourcenetwork/defradb/internal/core" + "github.com/sourcenetwork/defradb/internal/db/permission" "github.com/sourcenetwork/defradb/internal/request/graphql" ) @@ -190,8 +191,9 @@ func (db *db) AddPolicy( policy string, ) (client.AddPolicyResult, error) { if !db.acp.HasValue() { - return client.AddPolicyResult{}, client.ErrPolicyAddFailureNoACP + return client.AddPolicyResult{}, client.ErrACPOperationButACPNotAvailable } + identity := GetContextIdentity(ctx) policyID, err := db.acp.Value().AddPolicy( @@ -206,6 +208,46 @@ func (db *db) AddPolicy( return client.AddPolicyResult{PolicyID: policyID}, nil } +func (db *db) AddDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, +) (client.AddDocActorRelationshipResult, error) { + if !db.acp.HasValue() { + return client.AddDocActorRelationshipResult{}, client.ErrACPOperationButACPNotAvailable + } + + collection, err := db.GetCollectionByName(ctx, collectionName) + if err != nil { + return client.AddDocActorRelationshipResult{}, err + } + + policyID, resourceName, hasPolicy := permission.IsPermissioned(collection) + if !hasPolicy { + return client.AddDocActorRelationshipResult{}, client.ErrACPOperationButCollectionHasNoPolicy + } + + identity := GetContextIdentity(ctx) + + exists, err := db.acp.Value().AddDocActorRelationship( + ctx, + policyID, + resourceName, + docID, + relation, + identity.Value(), + targetActor, + ) + + if err != nil { + return client.AddDocActorRelationshipResult{}, err + } + + return client.AddDocActorRelationshipResult{ExistedAlready: exists}, nil +} + // Initialize is called when a database is first run and creates all the db global meta data // like Collection ID counters. func (db *db) initialize(ctx context.Context) error { diff --git a/internal/db/permission/check.go b/internal/db/permission/check.go index 9d3d8a587b..b19500f41b 100644 --- a/internal/db/permission/check.go +++ b/internal/db/permission/check.go @@ -43,7 +43,7 @@ func CheckAccessOfDocOnCollectionWithACP( ) (bool, error) { // Even if acp exists, but there is no policy on the collection (unpermissioned collection) // then we still have unrestricted access. - policyID, resourceName, hasPolicy := isPermissioned(collection) + policyID, resourceName, hasPolicy := IsPermissioned(collection) if !hasPolicy { return true, nil } diff --git a/internal/db/permission/permission.go b/internal/db/permission/permission.go index 3b365cba75..a91d346a6f 100644 --- a/internal/db/permission/permission.go +++ b/internal/db/permission/permission.go @@ -14,13 +14,13 @@ import ( "github.com/sourcenetwork/defradb/client" ) -// isPermissioned returns true if the collection has a policy, otherwise returns false. +// IsPermissioned returns true if the collection has a policy, otherwise returns false. // // This tells us if access control is enabled for this collection or not. // // When there is a policy, in addition to returning true in the last return value, the // first returned value is policyID, second is the resource name. -func isPermissioned(collection client.Collection) (string, string, bool) { +func IsPermissioned(collection client.Collection) (string, string, bool) { policy := collection.Definition().Description.Policy if policy.HasValue() && policy.Value().ID != "" && diff --git a/internal/db/permission/register.go b/internal/db/permission/register.go index dedbdd8d63..5e03967fb4 100644 --- a/internal/db/permission/register.go +++ b/internal/db/permission/register.go @@ -37,7 +37,7 @@ func RegisterDocOnCollectionWithACP( docID string, ) error { // An identity exists and the collection has a policy. - if policyID, resourceName, hasPolicy := isPermissioned(collection); hasPolicy && identity.HasValue() { + if policyID, resourceName, hasPolicy := IsPermissioned(collection); hasPolicy && identity.HasValue() { return acpSystem.RegisterDocObject( ctx, identity.Value(), diff --git a/tests/clients/cli/wrapper.go b/tests/clients/cli/wrapper.go index 7a2f28fd4a..b3261f09a8 100644 --- a/tests/clients/cli/wrapper.go +++ b/tests/clients/cli/wrapper.go @@ -175,26 +175,6 @@ func (w *Wrapper) BasicExport(ctx context.Context, config *client.BackupConfig) return err } -func (w *Wrapper) AddPolicy( - ctx context.Context, - policy string, -) (client.AddPolicyResult, error) { - args := []string{"client", "acp", "policy", "add"} - args = append(args, policy) - - data, err := w.cmd.execute(ctx, args) - if err != nil { - return client.AddPolicyResult{}, err - } - - var addPolicyResult client.AddPolicyResult - if err := json.Unmarshal(data, &addPolicyResult); err != nil { - return client.AddPolicyResult{}, err - } - - return addPolicyResult, err -} - func (w *Wrapper) AddSchema(ctx context.Context, schema string) ([]client.CollectionDescription, error) { args := []string{"client", "schema", "add"} args = append(args, schema) diff --git a/tests/clients/cli/wrapper_acp.go b/tests/clients/cli/wrapper_acp.go new file mode 100644 index 0000000000..f76aad3cdf --- /dev/null +++ b/tests/clients/cli/wrapper_acp.go @@ -0,0 +1,66 @@ +// Copyright 2023 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package cli + +import ( + "context" + "encoding/json" + + "github.com/sourcenetwork/defradb/client" +) + +func (w *Wrapper) AddPolicy( + ctx context.Context, + policy string, +) (client.AddPolicyResult, error) { + args := []string{"client", "acp", "policy", "add"} + args = append(args, policy) + + data, err := w.cmd.execute(ctx, args) + if err != nil { + return client.AddPolicyResult{}, err + } + + var addPolicyResult client.AddPolicyResult + if err := json.Unmarshal(data, &addPolicyResult); err != nil { + return client.AddPolicyResult{}, err + } + + return addPolicyResult, err +} + +func (w *Wrapper) AddDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, +) (client.AddDocActorRelationshipResult, error) { + args := []string{ + "client", "acp", "relationship", "add", + "--collection", collectionName, + "--docID", docID, + "--relation", relation, + "--actor", targetActor, + } + + data, err := w.cmd.execute(ctx, args) + if err != nil { + return client.AddDocActorRelationshipResult{}, err + } + + var exists client.AddDocActorRelationshipResult + if err := json.Unmarshal(data, &exists); err != nil { + return client.AddDocActorRelationshipResult{}, err + } + + return exists, err +} diff --git a/tests/clients/http/wrapper.go b/tests/clients/http/wrapper.go index 2b84bfc701..81ed74b095 100644 --- a/tests/clients/http/wrapper.go +++ b/tests/clients/http/wrapper.go @@ -105,6 +105,22 @@ func (w *Wrapper) AddPolicy( return w.client.AddPolicy(ctx, policy) } +func (w *Wrapper) AddDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, +) (client.AddDocActorRelationshipResult, error) { + return w.client.AddDocActorRelationship( + ctx, + collectionName, + docID, + relation, + targetActor, + ) +} + func (w *Wrapper) PatchSchema( ctx context.Context, patch string, diff --git a/tests/integration/acp.go b/tests/integration/acp.go index a6efd64110..a8f41e5f41 100644 --- a/tests/integration/acp.go +++ b/tests/integration/acp.go @@ -133,14 +133,192 @@ func addPolicyACP( } } +// AddDocActorRelationship will attempt to create a new relationship for a document with an actor. +type AddDocActorRelationship struct { + // NodeID may hold the ID (index) of the node we want to add doc actor relationship on. + // + // If a value is not provided the relationship will be added in all nodes, unless testing with + // sourcehub ACP, in which case the relationship will only be defined once. + NodeID immutable.Option[int] + + // The collection in which this document we want to add a relationship for exists. + // + // This is a required field. To test the invalid usage of not having this arg, use -1 index. + CollectionID int + + // The index-identifier of the document within the collection. This is based on + // the order in which it was created, not the ordering of the document within the + // database. + // + // This is a required field. To test the invalid usage of not having this arg, use -1 index. + DocID int + + // The name of the relation to set between document and target actor (should be defined in the policy). + // + // This is a required field. + Relation string + + // The target public identity, i.e. the identity of the actor to tie the document's relation with. + // + // This is a required field. To test the invalid usage of not having this arg, use -1 index. + TargetIdentity int + + // The requestor identity, i.e. identity of the actor creating the relationship. + // Note: This identity must either own or have managing access defined in the policy. + // + // This is a required field. To test the invalid usage of not having this arg, use -1 index. + RequestorIdentity int + + // Result returns true if it was a no-op due to existing before, and false if a new relationship was made. + ExpectedExistence bool + + // Any error expected from the action. Optional. + // + // String can be a partial, and the test will pass if an error is returned that + // contains this string. + ExpectedError string +} + +func addDocActorRelationshipACP( + s *state, + action AddDocActorRelationship, +) { + if action.NodeID.HasValue() { + nodeID := action.NodeID.Value() + collections := s.collections[nodeID] + node := s.nodes[nodeID] + + var collectionName string + if action.CollectionID == -1 { + collectionName = "" + } else { + collection := collections[action.CollectionID] + if !collection.Description().Name.HasValue() { + require.Fail(s.t, "Expected non-empty collection name, but it was empty.", s.testCase.Description) + } + collectionName = collection.Description().Name.Value() + } + + var docID string + if action.DocID == -1 || action.CollectionID == -1 { + docID = "" + } else { + docID = s.docIDs[action.CollectionID][action.DocID].String() + } + + var targetIdentity string + if action.TargetIdentity == -1 { + targetIdentity = "" + } else { + optionalTargetIdentity := getIdentity(s, nodeID, immutable.Some(action.TargetIdentity)) + if !optionalTargetIdentity.HasValue() { + require.Fail(s.t, "Expected non-empty target identity, but it was empty.", s.testCase.Description) + } + targetIdentity = optionalTargetIdentity.Value().DID + } + + var requestorIdentity immutable.Option[acpIdentity.Identity] + if action.RequestorIdentity == -1 { + requestorIdentity = acpIdentity.None + } else { + requestorIdentity = getIdentity(s, nodeID, immutable.Some(action.RequestorIdentity)) + if !requestorIdentity.HasValue() { + require.Fail(s.t, "Expected non-empty requestor identity, but it was empty.", s.testCase.Description) + } + } + ctx := db.SetContextIdentity(s.ctx, requestorIdentity) + + exists, err := node.AddDocActorRelationship( + ctx, + collectionName, + docID, + action.Relation, + targetIdentity, + ) + + if err == nil { + require.Equal(s.t, action.ExpectedError, "") + require.Equal(s.t, action.ExpectedExistence, exists.ExistedAlready) + } + + expectedErrorRaised := AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) + } else { + for i, node := range getNodes(action.NodeID, s.nodes) { + var collectionName string + if action.CollectionID == -1 { + collectionName = "" + } else { + collection := s.collections[i][action.CollectionID] + if !collection.Description().Name.HasValue() { + require.Fail(s.t, "Expected non-empty collection name, but it was empty.", s.testCase.Description) + } + collectionName = collection.Description().Name.Value() + } + + var docID string + if action.DocID == -1 || action.CollectionID == -1 { + docID = "" + } else { + docID = s.docIDs[action.CollectionID][action.DocID].String() + } + + var targetIdentity string + if action.TargetIdentity == -1 { + targetIdentity = "" + } else { + optionalTargetIdentity := getIdentity(s, i, immutable.Some(action.TargetIdentity)) + if !optionalTargetIdentity.HasValue() { + require.Fail(s.t, "Expected non-empty target identity, but it was empty.", s.testCase.Description) + } + targetIdentity = optionalTargetIdentity.Value().DID + } + + var requestorIdentity immutable.Option[acpIdentity.Identity] + if action.RequestorIdentity == -1 { + requestorIdentity = acpIdentity.None + } else { + requestorIdentity = getIdentity(s, i, immutable.Some(action.RequestorIdentity)) + if !requestorIdentity.HasValue() { + require.Fail(s.t, "Expected non-empty requestor identity, but it was empty.", s.testCase.Description) + } + } + ctx := db.SetContextIdentity(s.ctx, requestorIdentity) + + exists, err := node.AddDocActorRelationship( + ctx, + collectionName, + docID, + action.Relation, + targetIdentity, + ) + + if err == nil { + require.Equal(s.t, action.ExpectedError, "") + require.Equal(s.t, action.ExpectedExistence, exists.ExistedAlready) + } + + expectedErrorRaised := AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) + + // The relationship should only be added to a SourceHub chain once - there is no need to loop through + // the nodes. + if acpType == SourceHubACPType { + break + } + } + } +} + func setupSourceHub(s *state) ([]node.ACPOpt, error) { var isACPTest bool for _, a := range s.testCase.Actions { - if _, ok := a.(AddPolicy); ok { + switch a.(type) { + case AddPolicy, AddDocActorRelationship: isACPTest = true - break } } + if !isACPTest { // Spinning up SourceHub instances is a bit slow, so we should be quite aggressive in trimming down the // runtime of the test suite when SourceHub ACP is selected. @@ -405,6 +583,37 @@ func crossLock(port uint16) (func(), error) { nil } +// Generate the keys using the index as the seed so that multiple +// runs yield the same private key. This is important for stuff like +// the change detector. +func generateIdentity(s *state, seedIndex int, nodeIndex int) (acpIdentity.Identity, error) { + var audience immutable.Option[string] + switch client := s.nodes[nodeIndex].(type) { + case *http.Wrapper: + audience = immutable.Some(strings.TrimPrefix(client.Host(), "http://")) + case *cli.Wrapper: + audience = immutable.Some(strings.TrimPrefix(client.Host(), "http://")) + } + + source := rand.NewSource(int64(seedIndex)) + r := rand.New(source) + + privateKey, err := secp256k1.GeneratePrivateKeyFromRand(r) + require.NoError(s.t, err) + + identity, err := acpIdentity.FromPrivateKey( + privateKey, + authTokenExpiration, + audience, + immutable.Some(s.sourcehubAddress), + // Creating and signing the bearer token is slow, so we skip it if it not + // required. + !(acpType == SourceHubACPType || audience.HasValue()), + ) + + return identity, err +} + func getIdentity(s *state, nodeIndex int, index immutable.Option[int]) immutable.Option[acpIdentity.Identity] { if !index.HasValue() { return immutable.None[acpIdentity.Identity]() @@ -419,40 +628,18 @@ func getIdentity(s *state, nodeIndex int, index immutable.Option[int]) immutable if len(nodeIdentities) <= index.Value() { identities := make([]acpIdentity.Identity, index.Value()+1) - copy(identities, nodeIdentities) - nodeIdentities = identities - s.identities[nodeIndex] = nodeIdentities - - var audience immutable.Option[string] - switch client := s.nodes[nodeIndex].(type) { - case *http.Wrapper: - audience = immutable.Some(strings.TrimPrefix(client.Host(), "http://")) - case *cli.Wrapper: - audience = immutable.Some(strings.TrimPrefix(client.Host(), "http://")) + // Fill any empty identities up to the index. + for i := range identities { + if i < len(nodeIdentities) && nodeIdentities[i] != (acpIdentity.Identity{}) { + identities[i] = nodeIdentities[i] + continue + } + newIdentity, err := generateIdentity(s, i, nodeIndex) + require.NoError(s.t, err) + identities[i] = newIdentity } - - // Generate the keys using the index as the seed so that multiple - // runs yield the same private key. This is important for stuff like - // the change detector. - source := rand.NewSource(int64(index.Value())) - r := rand.New(source) - - privateKey, err := secp256k1.GeneratePrivateKeyFromRand(r) - require.NoError(s.t, err) - - identity, err := acpIdentity.FromPrivateKey( - privateKey, - authTokenExpiration, - audience, - immutable.Some(s.sourcehubAddress), - // Creating and signing the bearer token is slow, so we skip it if it not - // required. - !(acpType == SourceHubACPType || audience.HasValue()), - ) - require.NoError(s.t, err) - - nodeIdentities[index.Value()] = identity - return immutable.Some(identity) + s.identities[nodeIndex] = identities + return immutable.Some(identities[index.Value()]) } else { return immutable.Some(nodeIdentities[index.Value()]) } diff --git a/tests/integration/acp/p2p/replicator_with_doc_actor_relationship_test.go b/tests/integration/acp/p2p/replicator_with_doc_actor_relationship_test.go new file mode 100644 index 0000000000..fe06e10061 --- /dev/null +++ b/tests/integration/acp/p2p/replicator_with_doc_actor_relationship_test.go @@ -0,0 +1,219 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_p2p + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_P2PReplicatorWithPermissionedCollectionCreateDocActorRelationship_SourceHubACP(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, p2p replicator with collection that has a policy, create a new doc-actor relationship", + + SupportedACPTypes: immutable.Some( + []testUtils.ACPType{ + testUtils.SourceHubACPType, + }, + ), + + Actions: []any{ + testUtils.RandomNetworkingConfig(), + + testUtils.RandomNetworkingConfig(), + + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.ConfigureReplicator{ + SourceNodeID: 0, + + TargetNodeID: 1, + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + NodeID: immutable.Some(0), + + CollectionID: 0, + + DocMap: map[string]any{ + "name": "Shahzad", + }, + }, + + testUtils.WaitForSync{}, + + testUtils.Request{ + // Ensure that the document is hidden on all nodes to an unauthorized actor + Identity: immutable.Some(2), + + Request: ` + query { + Users { + name + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + + testUtils.AddDocActorRelationship{ + NodeID: immutable.Some(0), + + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ + NodeID: immutable.Some(1), // Note: Different node than the previous + + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: true, // Making the same relation through any node should be a no-op + }, + + testUtils.Request{ + // Ensure that the document is now accessible on all nodes to the newly authorized actor. + Identity: immutable.Some(2), + + Request: ` + query { + Users { + name + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + + testUtils.Request{ + // Ensure that the document is still accessible on all nodes to the owner. + Identity: immutable.Some(1), + + Request: ` + query { + Users { + name + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/p2p/subscribe_with_doc_actor_relationship_test.go b/tests/integration/acp/p2p/subscribe_with_doc_actor_relationship_test.go new file mode 100644 index 0000000000..a55c5a333e --- /dev/null +++ b/tests/integration/acp/p2p/subscribe_with_doc_actor_relationship_test.go @@ -0,0 +1,225 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_p2p + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_P2PSubscribeAddGetSingleWithPermissionedCollectionCreateDocActorRelationship_SourceHubACP(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, p2p subscribe collection that has a policy, and create a new doc-actor relationship", + + SupportedACPTypes: immutable.Some( + []testUtils.ACPType{ + testUtils.SourceHubACPType, + }, + ), + + Actions: []any{ + testUtils.RandomNetworkingConfig(), + + testUtils.RandomNetworkingConfig(), + + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.ConnectPeers{ + SourceNodeID: 1, + + TargetNodeID: 0, + }, + + testUtils.SubscribeToCollection{ + NodeID: 1, + + CollectionIDs: []int{0}, + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + NodeID: immutable.Some(0), + + CollectionID: 0, + + DocMap: map[string]any{ + "name": "Shahzad", + }, + }, + + testUtils.WaitForSync{}, + + testUtils.Request{ + // Ensure that the document is hidden on all nodes to an unauthorized actor + Identity: immutable.Some(2), + + Request: ` + query { + Users { + name + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + + testUtils.AddDocActorRelationship{ + NodeID: immutable.Some(0), + + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ + NodeID: immutable.Some(1), // Note: Different node than the previous + + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: true, // Making the same relation through any node should be a no-op + }, + + testUtils.Request{ + // Ensure that the document is now accessible on all nodes to the newly authorized actor. + Identity: immutable.Some(2), + + Request: ` + query { + Users { + name + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + + testUtils.Request{ + // Ensure that the document is still accessible on all nodes to the owner. + Identity: immutable.Some(1), + + Request: ` + query { + Users { + name + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_collection_with_no_policy_test.go b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_collection_with_no_policy_test.go new file mode 100644 index 0000000000..a614ef3ce9 --- /dev/null +++ b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_collection_with_no_policy_test.go @@ -0,0 +1,66 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_add_docactor + +import ( + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_AddDocActorRelationshipWithCollectionThatHasNoPolicy_NotAllowedError(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Test acp, add doc actor relationship on a collection with no policy, not allowed error", + + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type Users { + name: String + age: Int + } + `, + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedError: "operation requires ACP, but collection has no policy", // Everything is public anyway + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_invalid_test.go b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_invalid_test.go new file mode 100644 index 0000000000..cc0e0dac69 --- /dev/null +++ b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_invalid_test.go @@ -0,0 +1,545 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_add_docactor + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_AddDocActorRelationshipMissingDocID_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, add doc actor relationship with docID missing, return error", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: -1, + + Relation: "reader", + + ExpectedError: "missing a required argument needed to add doc actor relationship.", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_AddDocActorRelationshipMissingCollection_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, add doc actor relationship with collection missing, return error", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: -1, + + DocID: 0, + + Relation: "reader", + + ExpectedError: "collection name can't be empty", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_AddDocActorRelationshipMissingRelationName_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, add doc actor relationship with relation name missing, return error", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "", + + ExpectedError: "missing a required argument needed to add doc actor relationship.", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_AddDocActorRelationshipMissingTargetActorName_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, add doc actor relationship with target actor missing, return error", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: -1, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedError: "missing a required argument needed to add doc actor relationship.", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_AddDocActorRelationshipMissingReqestingIdentityName_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, add doc actor relationship with requesting identity missing, return error", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: -1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedError: "missing a required argument needed to add doc actor relationship.", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_delete_test.go b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_delete_test.go new file mode 100644 index 0000000000..9be3ace27d --- /dev/null +++ b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_delete_test.go @@ -0,0 +1,505 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_add_docactor + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerGivesDeleteWriteAccessToAnotherActorTwice_ShowThatTheRelationshipAlreadyExists(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives write(delete) access to another actor twice, no-op", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can not delete yet. + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: true, // is a no-op + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerGivesDeleteWriteAccessToAnotherActor_OtherActorCanDelete(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives write(delete) access to another actor", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can not delete yet. + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can now read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can now delete. + + DocID: 0, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Check if actually deleted. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerGivesDeleteWriteAccessToAnotherActor_OtherActorCanDeleteSoCanTheOwner(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives write(delete) access to another actor, both can read", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(1), // Owner can still also delete (ownership not transferred) + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(1), // Owner can still also delete. + + DocID: 0, + }, + + testUtils.Request{ + Identity: immutable.Some(1), // Check if actually deleted. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_dummy_relation_test.go b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_dummy_relation_test.go new file mode 100644 index 0000000000..66e17ba00a --- /dev/null +++ b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_dummy_relation_test.go @@ -0,0 +1,302 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_add_docactor + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_AddDocActorRelationshipWithDummyRelationDefinedOnPolicy_NothingChanges(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, add doc actor relationship with a dummy relation defined on policy, nothing happens", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents + }, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "dummy", // Doesn't mean anything to the database. + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can still not read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_AddDocActorRelationshipWithDummyRelationNotDefinedOnPolicy_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, add doc actor relationship with an invalid relation (not defined on policy), error", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents + }, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "NotOnPolicy", // Doesn't mean anything to the database and not on policy either. + + ExpectedError: "failed to add document actor relationship with acp", + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can still not read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_gql_test.go b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_gql_test.go new file mode 100644 index 0000000000..9c2280d6ce --- /dev/null +++ b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_gql_test.go @@ -0,0 +1,604 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_add_docactor + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerMakesAManagerThatGivesItSelfReadAndWriteAccess_GQL_ManagerCanReadAndWrite(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner makes a manager that gives itself read and write access", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used so test that separately. + testUtils.GQLRequestMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + - writer + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity (to be manager) can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager can't update yet. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + SkipLocalUpdateEvent: true, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager can't delete yet. + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ // Make admin / manager + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ // Manager makes itself a writer + RequestorIdentity: 2, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + // Note: It is not neccesary to make itself a reader, as becoming a writer allows reading. + testUtils.AddDocActorRelationship{ // Manager makes itself a reader + RequestorIdentity: 2, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager can now update. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Manager can read now + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad Lone", + "age": int64(28), + }, + }, + }, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager can now delete. + + DocID: 0, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Make sure manager was able to delete the document. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerMakesManagerButManagerCanNotPerformOperations_GQL_ManagerCantReadOrWrite(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner makes a manager, manager can't read or write", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used so test that separately. + testUtils.GQLRequestMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ // Make admin / manager + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Manager can not read + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager can not update. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + SkipLocalUpdateEvent: true, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager can not delete. + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ // Manager can manage only. + RequestorIdentity: 2, + + TargetIdentity: 3, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_ManagerAddsRelationshipWithRelationItDoesNotManageAccordingToPolicy_GQL_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, manager adds relationship with relation it does not manage according to policy, error", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used so test that separately. + testUtils.GQLRequestMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ // Make admin / manager + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ // Admin tries to make another actor a writer + RequestorIdentity: 2, + + TargetIdentity: 3, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedError: "acp protocol violation", + }, + + testUtils.Request{ + Identity: immutable.Some(3), // The other actor can't read + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(3), // The other actor can not update + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + SkipLocalUpdateEvent: true, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(3), // The other actor can not delete + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_test.go b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_test.go new file mode 100644 index 0000000000..4467aa1af9 --- /dev/null +++ b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_test.go @@ -0,0 +1,1286 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_add_docactor + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_ManagerGivesReadAccessToAnotherActor_OtherActorCanRead(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives read access to another actor", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(3), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.AddDocActorRelationship{ // Make admin / manager + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ // Admin makes another actor a reader + RequestorIdentity: 2, + + TargetIdentity: 3, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(3), // The other actor can read + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(3), // The other actor can not update + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(3), // The other actor can not delete + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_ManagerGivesWriteAccessToAnotherActor_OtherActorCanWrite(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives write access to another actor", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - writer + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(3), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.AddDocActorRelationship{ // Make admin / manager + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ // Admin makes another actor a writer + RequestorIdentity: 2, + + TargetIdentity: 3, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(3), // The other actor can update + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(3), // The other actor can read + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad Lone", // Updated name + "age": int64(28), + }, + }, + }, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(3), // The other actor can delete + + DocID: 0, + }, + + testUtils.Request{ + Identity: immutable.Some(3), + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ // Check actually deleted + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerMakesAManagerThatGivesItSelfReadAccess_ManagerCanRead(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner makes a manager that gives itself read access", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity (to be manager) can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.AddDocActorRelationship{ // Make admin / manager + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ // Manager makes itself a reader + RequestorIdentity: 2, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Manager can read now + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager still can't update + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager still can't delete + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerMakesAManagerThatGivesItSelfReadAndWriteAccess_ManagerCanReadAndWrite(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner makes a manager that gives itself read and write access", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + - writer + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity (to be manager) can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager can't update yet. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager can't delete yet. + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ // Make admin / manager + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ // Manager makes itself a writer + RequestorIdentity: 2, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + // Note: It is not neccesary to make itself a reader, as becoming a writer allows reading. + testUtils.AddDocActorRelationship{ // Manager makes itself a reader + RequestorIdentity: 2, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager can now update. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Manager can read now + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad Lone", + "age": int64(28), + }, + }, + }, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager can now delete. + + DocID: 0, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Make sure manager was able to delete the document. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_ManagerAddsRelationshipWithRelationItDoesNotManageAccordingToPolicy_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, manager adds relationship with relation it does not manage according to policy, error", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ // Make admin / manager + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ // Admin tries to make another actor a writer + RequestorIdentity: 2, + + TargetIdentity: 3, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedError: "acp protocol violation", + }, + + testUtils.Request{ + Identity: immutable.Some(3), // The other actor can't read + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(3), // The other actor can not update + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(3), // The other actor can not delete + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerMakesManagerButManagerCanNotPerformOperations_ManagerCantReadOrWrite(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner makes a manager, manager can't read or write", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ // Make admin / manager + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Manager can not read + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager can not update. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // Manager can not delete. + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ // Manager can manage only. + RequestorIdentity: 2, + + TargetIdentity: 3, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_CantMakeRelationshipIfNotOwnerOrManager_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, cant make relation if identity doesn't own or manage object, return error", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 2, // This identity can not manage as not an admin yet + + TargetIdentity: 3, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + + ExpectedError: "failed to add document actor relationship with acp", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_only_write_gql_test.go b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_only_write_gql_test.go new file mode 100644 index 0000000000..e3f3e62050 --- /dev/null +++ b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_only_write_gql_test.go @@ -0,0 +1,198 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_add_docactor + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerGivesUpdateWriteAccessToAnotherActorWithoutExplicitReadPerm_GQL_OtherActorCantUpdate(t *testing.T) { + expectedPolicyID := "0a243b1e61f990bccde41db7e81a915ffa1507c1403ae19727ce764d3b08846b" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives write(update) access to another actor, without explicit read permission", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used so test that separately. + testUtils.GQLRequestMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can not update yet. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + SkipLocalUpdateEvent: true, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can still not update. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + SkipLocalUpdateEvent: true, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can still not read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_only_write_test.go b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_only_write_test.go new file mode 100644 index 0000000000..e052d19afd --- /dev/null +++ b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_only_write_test.go @@ -0,0 +1,359 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_add_docactor + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerGivesUpdateWriteAccessToAnotherActorWithoutExplicitReadPerm_OtherActorCantUpdate(t *testing.T) { + expectedPolicyID := "0a243b1e61f990bccde41db7e81a915ffa1507c1403ae19727ce764d3b08846b" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives write(update) access to another actor, without explicit read permission", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can not update yet. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can still not update. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can still not read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerGivesDeleteWriteAccessToAnotherActorWithoutExplicitReadPerm_OtherActorCantDelete(t *testing.T) { + expectedPolicyID := "0a243b1e61f990bccde41db7e81a915ffa1507c1403ae19727ce764d3b08846b" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives write(delete) access to another actor, without explicit read permission", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can not delete yet. + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can still not read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can still not delete. + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_public_document_test.go b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_public_document_test.go new file mode 100644 index 0000000000..e134a821e4 --- /dev/null +++ b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_public_document_test.go @@ -0,0 +1,147 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_add_docactor + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_AddDocActorRelationshipWithPublicDocument_CanAlreadyAccess_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, add doc actor relationship on a public document, return error", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ // Note: Is a public document (without an identity). + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Can read as it is a public document + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedError: "failed to add document actor relationship with acp", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_gql_test.go b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_gql_test.go new file mode 100644 index 0000000000..02a637833f --- /dev/null +++ b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_gql_test.go @@ -0,0 +1,204 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_add_docactor + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerGivesOnlyReadAccessToAnotherActor_GQL_OtherActorCanReadButNotUpdate(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives read access to another actor, but the other actor can't update", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used so test that separately. + testUtils.GQLRequestMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.UpdateDoc{ // Since it can't read, it can't update either. + CollectionID: 0, + + Identity: immutable.Some(2), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + SkipLocalUpdateEvent: true, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Now this identity can read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.UpdateDoc{ // But this actor still can't update. + CollectionID: 0, + + Identity: immutable.Some(2), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_test.go b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_test.go new file mode 100644 index 0000000000..70a7676a96 --- /dev/null +++ b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_test.go @@ -0,0 +1,810 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_add_docactor + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerGivesReadAccessToAnotherActorTwice_ShowThatTheRelationshipAlreadyExists(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives read access to another actor twice, no-op", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: true, // is a no-op + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerGivesReadAccessToAnotherActor_OtherActorCanRead(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives read access to another actor", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Now this identity can read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +// Note: Testing that owner can still read after the relationship was formed is to ensure +// that no transfer of ownership has taken place. +func TestACP_OwnerGivesReadAccessToAnotherActor_OtherActorCanReadSoCanTheOwner(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives read access to another actor, both can read", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Now this identity can read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.Request{ + Identity: immutable.Some(1), // And so can the owner (ownership not transferred). + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerGivesOnlyReadAccessToAnotherActor_OtherActorCanReadButNotUpdate(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives read access to another actor, but the other actor can't update", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.UpdateDoc{ // Since it can't read, it can't update either. + CollectionID: 0, + + Identity: immutable.Some(2), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Now this identity can read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.UpdateDoc{ // But this actor still can't update. + CollectionID: 0, + + Identity: immutable.Some(2), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerGivesOnlyReadAccessToAnotherActor_OtherActorCanReadButNotDelete(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives read access to another actor, but the other actor can't delete", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.DeleteDoc{ // Since it can't read, it can't delete either. + CollectionID: 0, + + Identity: immutable.Some(2), + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Now this identity can read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.DeleteDoc{ // But this actor still can't delete. + CollectionID: 0, + + Identity: immutable.Some(2), + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_gql_test.go b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_gql_test.go new file mode 100644 index 0000000000..dcfda587e8 --- /dev/null +++ b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_gql_test.go @@ -0,0 +1,360 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_add_docactor + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerGivesUpdateWriteAccessToAnotherActorTwice_GQL_ShowThatTheRelationshipAlreadyExists(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives write(update) access to another actor twice, no-op", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used so test that separately. + testUtils.GQLRequestMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can not update yet. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + SkipLocalUpdateEvent: true, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: true, // is a no-op + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerGivesUpdateWriteAccessToAnotherActor_GQL_OtherActorCanUpdate(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives write(update) access to another actor", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used so test that separately. + testUtils.GQLRequestMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can not update yet. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + SkipLocalUpdateEvent: true, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can now update. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can now also read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad Lone", // Note: updated name + "age": int64(28), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_test.go b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_test.go new file mode 100644 index 0000000000..79d727a690 --- /dev/null +++ b/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_test.go @@ -0,0 +1,541 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_add_docactor + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerGivesUpdateWriteAccessToAnotherActorTwice_ShowThatTheRelationshipAlreadyExists(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives write(update) access to another actor twice, no-op", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can not update yet. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: true, // is a no-op + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerGivesUpdateWriteAccessToAnotherActor_OtherActorCanUpdate(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives write(update) access to another actor", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read yet. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can not update yet. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can now update. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can now also read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad Lone", // Note: updated name + "age": int64(28), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerGivesUpdateWriteAccessToAnotherActor_OtherActorCanUpdateSoCanTheOwner(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives write(update) access to another actor, both can read", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: immutable.Some(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can now update. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can now also read. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad Lone", // Note: updated name + "age": int64(28), + }, + }, + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(1), // Owner can still also update (ownership not transferred) + + DocID: 0, + + Doc: ` + { + "name": "Lone" + } + `, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // Owner can still also read (ownership not transferred) + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Lone", // Note: updated name + "age": int64(28), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/utils.go b/tests/integration/utils.go index e6ab296140..eb0128ab00 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -340,6 +340,9 @@ func performAction( case AddPolicy: addPolicyACP(s, action) + case AddDocActorRelationship: + addDocActorRelationshipACP(s, action) + case CreateDoc: createDoc(s, action)