From c7b8b93dd02d4b68b183cee8fc22131fd08c2d3b Mon Sep 17 00:00:00 2001 From: Shahzad Lone Date: Tue, 26 Nov 2024 09:42:08 -0500 Subject: [PATCH] feat: Add ability to add/delete relationship for all actors (#3254) ## Relevant issue(s) Resolves #3255 ## Description - Can target all actors using `"*"` to add or delete acp relationships. - All explicitly added relationships are unaffected upon revocation using `"*"` (they will keep access). ### For Reviewers - Commit by commit review should be easier. - [x] todo: Pushing the crashing gql tests fix once https://github.com/sourcenetwork/defradb/pull/3267 is merged ## 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? - Integration tests Specify the platform(s) on which this was tested: - Manjaro WSL2 --- acp/README.md | 40 ++ acp/acp_local.go | 36 +- acp/acp_source_hub.go | 64 +- cli/acp_relationship_add.go | 8 + client/db.go | 9 +- .../defradb_client_acp_relationship_add.md | 8 + tests/integration/acp.go | 10 +- .../add/with_target_all_actors_gql_test.go | 250 ++++++++ .../add/with_target_all_actors_test.go | 250 ++++++++ .../delete/with_target_all_actors_test.go | 548 ++++++++++++++++++ tests/integration/identity.go | 112 ++-- tests/integration/state.go | 4 +- tests/integration/test_case.go | 12 +- 13 files changed, 1278 insertions(+), 73 deletions(-) create mode 100644 tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_gql_test.go create mode 100644 tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_test.go create mode 100644 tests/integration/acp/relationship/doc_actor/delete/with_target_all_actors_test.go diff --git a/acp/README.md b/acp/README.md index 4e8d5b7f5b..3fedb5a274 100644 --- a/acp/README.md +++ b/acp/README.md @@ -631,6 +631,26 @@ Result: Error: document not found or not authorized to access ``` +Sometimes we might want to give a specific access (form a relationship) not just to one identity, but any identity. +In that case we can specify "*" instead of specifying an explicit `actor`: +```sh +defradb client acp relationship add \ +--collection Users \ +--docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \ +--relation reader \ +--actor "*" \ +--identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac +``` + +Result: +```json +{ + "ExistedAlready": false +} +``` + +**Note: specifying `*` does not overwrite any previous formed relationships, they will remain as is ** + ### Revoking Access To Private Documents To revoke access to a document for an actor, we must delete the relationship between the @@ -695,6 +715,26 @@ defradb client collection docIDs --identity 4d092126012ebaf56161716018a71630d994 **Result is empty from the above command** +We can also revoke the previously granted implicit relationship which gave all actors access using the "*" actor. +Similarly we can just specify "*" to revoke all access given to actors implicitly through this relationship: +```sh +defradb client acp relationship delete \ +--collection Users \ +--docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \ +--relation reader \ +--actor "*" \ +--identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac +``` + +Result: +```json +{ + "RecordFound": true +} +``` + +**Note: Deleting with`*` does not remove any explicitly formed relationships, they will remain as they were ** + ## DAC Usage HTTP: ### Authentication diff --git a/acp/acp_local.go b/acp/acp_local.go index a8a0d32290..99b2cb15f4 100644 --- a/acp/acp_local.go +++ b/acp/acp_local.go @@ -254,9 +254,25 @@ func (l *ACPLocal) AddActorRelationship( ctx = auth.InjectPrincipal(ctx, principal) + var newActorRelationship *types.Relationship + if targetActor == "*" { + newActorRelationship = types.NewAllActorsRelationship( + resourceName, + objectID, + relation, + ) + } else { + newActorRelationship = types.NewActorRelationship( + resourceName, + objectID, + relation, + targetActor, + ) + } + setRelationshipRequest := types.SetRelationshipRequest{ PolicyId: policyID, - Relationship: types.NewActorRelationship(resourceName, objectID, relation, targetActor), + Relationship: newActorRelationship, CreationTime: creationTime, } @@ -285,9 +301,25 @@ func (l *ACPLocal) DeleteActorRelationship( ctx = auth.InjectPrincipal(ctx, principal) + var newActorRelationship *types.Relationship + if targetActor == "*" { + newActorRelationship = types.NewAllActorsRelationship( + resourceName, + objectID, + relation, + ) + } else { + newActorRelationship = types.NewActorRelationship( + resourceName, + objectID, + relation, + targetActor, + ) + } + deleteRelationshipRequest := types.DeleteRelationshipRequest{ PolicyId: policyID, - Relationship: types.NewActorRelationship(resourceName, objectID, relation, targetActor), + Relationship: newActorRelationship, } deleteRelationshipResponse, err := l.engine.DeleteRelationship(ctx, &deleteRelationshipRequest) diff --git a/acp/acp_source_hub.go b/acp/acp_source_hub.go index edd6008b63..dd248c5db9 100644 --- a/acp/acp_source_hub.go +++ b/acp/acp_source_hub.go @@ -273,18 +273,28 @@ func (a *acpSourceHub) AddActorRelationship( creationTime *protoTypes.Timestamp, ) (bool, error) { msgSet := sourcehub.MsgSet{} + + var newActorRelationship *acptypes.Relationship + if targetActor == "*" { + newActorRelationship = acptypes.NewAllActorsRelationship( + resourceName, + objectID, + relation, + ) + } else { + newActorRelationship = acptypes.NewActorRelationship( + resourceName, + objectID, + relation, + targetActor, + ) + } + cmdMapper := msgSet.WithBearerPolicyCmd(&acptypes.MsgBearerPolicyCmd{ - Creator: a.signer.GetAccAddress(), - BearerToken: requester.BearerToken, - PolicyId: policyID, - Cmd: acptypes.NewSetRelationshipCmd( - acptypes.NewActorRelationship( - resourceName, - objectID, - relation, - targetActor, - ), - ), + Creator: a.signer.GetAccAddress(), + BearerToken: requester.BearerToken, + PolicyId: policyID, + Cmd: acptypes.NewSetRelationshipCmd(newActorRelationship), CreationTime: creationTime, }) tx, err := a.txBuilder.Build(ctx, a.signer, &msgSet) @@ -323,18 +333,28 @@ func (a *acpSourceHub) DeleteActorRelationship( creationTime *protoTypes.Timestamp, ) (bool, error) { msgSet := sourcehub.MsgSet{} + + var newActorRelationship *acptypes.Relationship + if targetActor == "*" { + newActorRelationship = acptypes.NewAllActorsRelationship( + resourceName, + objectID, + relation, + ) + } else { + newActorRelationship = acptypes.NewActorRelationship( + resourceName, + objectID, + relation, + targetActor, + ) + } + cmdMapper := msgSet.WithBearerPolicyCmd(&acptypes.MsgBearerPolicyCmd{ - Creator: a.signer.GetAccAddress(), - BearerToken: requester.BearerToken, - PolicyId: policyID, - Cmd: acptypes.NewDeleteRelationshipCmd( - acptypes.NewActorRelationship( - resourceName, - objectID, - relation, - targetActor, - ), - ), + Creator: a.signer.GetAccAddress(), + BearerToken: requester.BearerToken, + PolicyId: policyID, + Cmd: acptypes.NewDeleteRelationshipCmd(newActorRelationship), CreationTime: creationTime, }) diff --git a/cli/acp_relationship_add.go b/cli/acp_relationship_add.go index c0838a2ce2..0026e992f5 100644 --- a/cli/acp_relationship_add.go +++ b/cli/acp_relationship_add.go @@ -64,6 +64,14 @@ Example: Let another actor (4d092126012ebaf56161716018a71630d99443d9d5217e9d8502 --actor did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \ --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac +Example: Let all actors read a private document: + defradb client acp relationship add \ + --collection Users \ + --docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \ + --relation reader \ + --actor "*" \ + --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac + Example: Creating a dummy relationship does nothing (from database perspective): defradb client acp relationship add \ -c Users \ diff --git a/client/db.go b/client/db.go index e8942e8501..bfafb76942 100644 --- a/client/db.go +++ b/client/db.go @@ -113,7 +113,9 @@ type DB interface { // 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. + // Note: + // - The request actor must either be the owner or manager of the document. + // - If the target actor arg is "*", then the relationship applies to all actors implicitly. AddDocActorRelationship( ctx context.Context, collectionName string, @@ -128,7 +130,10 @@ type DB interface { // be true if the relationship record was found and deleted. Upon success the boolean value // will be false if the relationship record was not found (no-op). // - // Note: The request actor must either be the owner or manager of the document. + // Note: + // - The request actor must either be the owner or manager of the document. + // - If the target actor arg is "*", then the implicitly added relationship with all actors is + // removed, however this does not revoke access from actors that had explicit relationships. DeleteDocActorRelationship( ctx context.Context, collectionName string, diff --git a/docs/website/references/cli/defradb_client_acp_relationship_add.md b/docs/website/references/cli/defradb_client_acp_relationship_add.md index 1251ffb74e..f3313b45d4 100644 --- a/docs/website/references/cli/defradb_client_acp_relationship_add.md +++ b/docs/website/references/cli/defradb_client_acp_relationship_add.md @@ -30,6 +30,14 @@ Example: Let another actor (4d092126012ebaf56161716018a71630d99443d9d5217e9d8502 --actor did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \ --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac +Example: Let all actors read a private document: + defradb client acp relationship add \ + --collection Users \ + --docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \ + --relation reader \ + --actor "*" \ + --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac + Example: Creating a dummy relationship does nothing (from database perspective): defradb client acp relationship add \ -c Users \ diff --git a/tests/integration/acp.go b/tests/integration/acp.go index ce50637d4b..5983ad228a 100644 --- a/tests/integration/acp.go +++ b/tests/integration/acp.go @@ -88,7 +88,7 @@ type AddPolicy struct { Policy string // The policy creator identity, i.e. actor creating the policy. - Identity immutable.Option[identityRef] + Identity immutable.Option[identity] // The expected policyID generated based on the Policy loaded in to the ACP system. ExpectedPolicyID string @@ -159,13 +159,13 @@ type AddDocActorRelationship struct { // 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 NoIdentity() or leave default. - TargetIdentity immutable.Option[identityRef] + TargetIdentity immutable.Option[identity] // 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 NoIdentity() or leave default. - RequestorIdentity immutable.Option[identityRef] + RequestorIdentity immutable.Option[identity] // Result returns true if it was a no-op due to existing before, and false if a new relationship was made. ExpectedExistence bool @@ -251,13 +251,13 @@ type DeleteDocActorRelationship struct { // The target public identity, i.e. the identity of the actor with whom the relationship is with. // // This is a required field. To test the invalid usage of not having this arg, use NoIdentity() or leave default. - TargetIdentity immutable.Option[identityRef] + TargetIdentity immutable.Option[identity] // The requestor identity, i.e. identity of the actor deleting 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 NoIdentity() or leave default. - RequestorIdentity immutable.Option[identityRef] + RequestorIdentity immutable.Option[identity] // Result returns true if the relationship record was expected to be found and deleted, // and returns false if no matching relationship record was found (no-op). diff --git a/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_gql_test.go b/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_gql_test.go new file mode 100644 index 0000000000..c05380d8e0 --- /dev/null +++ b/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_gql_test.go @@ -0,0 +1,250 @@ +// 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_doc_actor_add + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerGivesOnlyReadAccessToAllActors_GQL_AllActorsCanReadButNotUpdateOrDelete(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives read access to all actors (gql), but the other actor can't update or delete", + + 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: testUtils.ClientIdentity(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: testUtils.ClientIdentity(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: testUtils.ClientIdentity(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: testUtils.ClientIdentity(2), + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.UpdateDoc{ // Since it can't read, it can't update either. + CollectionID: 0, + + Identity: testUtils.ClientIdentity(2), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + SkipLocalUpdateEvent: true, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: testUtils.ClientIdentity(1), + + TargetIdentity: testUtils.AllClientIdentities(), + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: testUtils.ClientIdentity(2), // Now any 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: testUtils.ClientIdentity(3), // Now any 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 doesn't mean they can update. + CollectionID: 0, + + Identity: testUtils.ClientIdentity(2), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.DeleteDoc{ // But doesn't mean they can delete. + CollectionID: 0, + + Identity: testUtils.ClientIdentity(2), + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_test.go b/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_test.go new file mode 100644 index 0000000000..4ee858345b --- /dev/null +++ b/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_test.go @@ -0,0 +1,250 @@ +// 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_doc_actor_add + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerGivesOnlyReadAccessToAllActors_AllActorsCanReadButNotUpdateOrDelete(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives read access to all actors, but the other actor can't update or delete", + + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }, + ), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: testUtils.ClientIdentity(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: testUtils.ClientIdentity(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: testUtils.ClientIdentity(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: testUtils.ClientIdentity(2), + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.UpdateDoc{ // Since it can't read, it can't update either. + CollectionID: 0, + + Identity: testUtils.ClientIdentity(2), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: testUtils.ClientIdentity(1), + + TargetIdentity: testUtils.AllClientIdentities(), + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: testUtils.ClientIdentity(2), // Now any 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: testUtils.ClientIdentity(3), // Now any 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 doesn't mean they can update. + CollectionID: 0, + + Identity: testUtils.ClientIdentity(2), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.DeleteDoc{ // But doesn't mean they can delete. + CollectionID: 0, + + Identity: testUtils.ClientIdentity(2), + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/doc_actor/delete/with_target_all_actors_test.go b/tests/integration/acp/relationship/doc_actor/delete/with_target_all_actors_test.go new file mode 100644 index 0000000000..14c0121a41 --- /dev/null +++ b/tests/integration/acp/relationship/doc_actor/delete/with_target_all_actors_test.go @@ -0,0 +1,548 @@ +// 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_doc_actor_delete + +import ( + "fmt" + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerRevokesAccessFromAllNonExplicitActors_ActorsCanNotReadAnymore(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner revokes read access from actors that were given read access implicitly", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: testUtils.ClientIdentity(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: testUtils.ClientIdentity(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: testUtils.ClientIdentity(1), + + TargetIdentity: testUtils.AllClientIdentities(), // Give implicit access to all identities. + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: testUtils.ClientIdentity(2), // Any 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: testUtils.ClientIdentity(3), // Any 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.DeleteDocActorRelationship{ // Revoke access from all actors, not explictly allowed. + RequestorIdentity: testUtils.ClientIdentity(1), + + TargetIdentity: testUtils.AllClientIdentities(), + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedRecordFound: true, + }, + + testUtils.Request{ + Identity: testUtils.ClientIdentity(2), // Can not read anymore + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents now + }, + }, + + testUtils.Request{ + Identity: testUtils.ClientIdentity(3), // Can not read anymore + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents now + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerRevokesAccessFromAllNonExplicitActors_ExplicitActorsCanStillRead(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner revokes read access from actors that were given read access implicitly", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: testUtils.ClientIdentity(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: testUtils.ClientIdentity(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: testUtils.ClientIdentity(1), + + TargetIdentity: testUtils.ClientIdentity(2), // Give access to this identity explictly before. + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: testUtils.ClientIdentity(1), + + TargetIdentity: testUtils.AllClientIdentities(), // Give implicit access to all identities. + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: testUtils.ClientIdentity(1), + + TargetIdentity: testUtils.ClientIdentity(4), // Give access to this identity explictly after. + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: testUtils.ClientIdentity(2), // Any 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: testUtils.ClientIdentity(3), // Any 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: testUtils.ClientIdentity(4), // Any 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: testUtils.ClientIdentity(5), // Any 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.DeleteDocActorRelationship{ // Revoke access from all actors, not explictly allowed. + RequestorIdentity: testUtils.ClientIdentity(1), + + TargetIdentity: testUtils.AllClientIdentities(), + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedRecordFound: true, + }, + + testUtils.Request{ + Identity: testUtils.ClientIdentity(3), // Can not read anymore, because it gained access implicitly. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents now + }, + }, + + testUtils.Request{ + Identity: testUtils.ClientIdentity(5), // Can not read anymore, because it gained access implicitly. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents now + }, + }, + + testUtils.Request{ + Identity: testUtils.ClientIdentity(2), // Can still read because it was given access explictly. + + 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: testUtils.ClientIdentity(4), // Can still read because it was given access explictly. + + 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) +} diff --git a/tests/integration/identity.go b/tests/integration/identity.go index 7c56d81375..d8efe506df 100644 --- a/tests/integration/identity.go +++ b/tests/integration/identity.go @@ -13,6 +13,7 @@ package tests import ( "context" "math/rand" + "strconv" "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/sourcenetwork/immutable" @@ -21,31 +22,56 @@ import ( acpIdentity "github.com/sourcenetwork/defradb/acp/identity" ) -// identityRef is a type that refers to a specific identity of a certain type. -type identityRef struct { - isClient bool - index int +type identityType int + +const ( + clientIdentityType identityType = iota + nodeIdentityType +) + +// identity helps specify identity type info and selector/index of identity to use in a test case. +type identity struct { + // type of identity + kind identityType + + // selector can be a valid identity index or a selecting pattern like "*". + // Note: "*" means to select all identities of the specified [kind] type. + selector string } // NoIdentity returns an reference to an identity that represents no identity. -func NoIdentity() immutable.Option[identityRef] { - return immutable.None[identityRef]() +func NoIdentity() immutable.Option[identity] { + return immutable.None[identity]() } -// ClientIdentity returns a reference to a user identity with a given index. -func ClientIdentity(index int) immutable.Option[identityRef] { - return immutable.Some(identityRef{ - isClient: true, - index: index, - }) +// AllClientIdentities returns user identity selector specified with the "*". +func AllClientIdentities() immutable.Option[identity] { + return immutable.Some( + identity{ + kind: clientIdentityType, + selector: "*", + }, + ) } -// NodeIdentity returns a reference to a node identity with a given index. -func NodeIdentity(index int) immutable.Option[identityRef] { - return immutable.Some(identityRef{ - isClient: false, - index: index, - }) +// ClientIdentity returns a user identity at the given index. +func ClientIdentity(indexSelector int) immutable.Option[identity] { + return immutable.Some( + identity{ + kind: clientIdentityType, + selector: strconv.Itoa(indexSelector), + }, + ) +} + +// ClientIdentity returns a node identity at the given index. +func NodeIdentity(indexSelector int) immutable.Option[identity] { + return immutable.Some( + identity{ + kind: nodeIdentityType, + selector: strconv.Itoa(indexSelector), + }, + ) } // identityHolder holds an identity and the generated tokens for each target node. @@ -66,30 +92,37 @@ func newIdentityHolder(ident acpIdentity.Identity) *identityHolder { // getIdentity returns the identity for the given reference. // If the identity does not exist, it will be generated. -func getIdentity(s *state, ref immutable.Option[identityRef]) acpIdentity.Identity { - if !ref.HasValue() { +func getIdentity(s *state, identity immutable.Option[identity]) acpIdentity.Identity { + if !identity.HasValue() { return acpIdentity.Identity{} } - return getIdentityHolder(s, ref.Value()).Identity + + // The selector must never be "*" here because this function returns a specific identity from the + // stored identities, if "*" string needs to be signaled to the acp module then it should be handled + // a call before this function. + if identity.Value().selector == "*" { + require.Fail(s.t, "Used the \"*\" selector for identity incorrectly.", s.testCase.Description) + } + return getIdentityHolder(s, identity.Value()).Identity } // getIdentityHolder returns the identity holder for the given reference. // If the identity does not exist, it will be generated. -func getIdentityHolder(s *state, ref identityRef) *identityHolder { - ident, ok := s.identities[ref] +func getIdentityHolder(s *state, identity identity) *identityHolder { + ident, ok := s.identities[identity] if ok { return ident } - s.identities[ref] = newIdentityHolder(generateIdentity(s)) - return s.identities[ref] + s.identities[identity] = newIdentityHolder(generateIdentity(s)) + return s.identities[identity] } // getIdentityForRequest returns the identity for the given reference and node index. // It prepares the identity for a request by generating a token if needed, i.e. it will // return an identity with [Identity.BearerToken] set. -func getIdentityForRequest(s *state, ref identityRef, nodeIndex int) acpIdentity.Identity { - identHolder := getIdentityHolder(s, ref) +func getIdentityForRequest(s *state, identity identity, nodeIndex int) acpIdentity.Identity { + identHolder := getIdentityHolder(s, identity) ident := identHolder.Identity token, ok := identHolder.NodeTokens[nodeIndex] @@ -129,19 +162,30 @@ func generateIdentity(s *state) acpIdentity.Identity { func getContextWithIdentity( ctx context.Context, s *state, - ref immutable.Option[identityRef], + identity immutable.Option[identity], nodeIndex int, ) context.Context { - if !ref.HasValue() { + if !identity.HasValue() { return ctx } - ident := getIdentityForRequest(s, ref.Value(), nodeIndex) - return acpIdentity.WithContext(ctx, immutable.Some(ident)) + return acpIdentity.WithContext( + ctx, + immutable.Some( + getIdentityForRequest( + s, + identity.Value(), + nodeIndex, + ), + ), + ) } -func getIdentityDID(s *state, ref immutable.Option[identityRef]) string { - if ref.HasValue() { - return getIdentity(s, ref).DID +func getIdentityDID(s *state, identity immutable.Option[identity]) string { + if identity.HasValue() { + if identity.Value().selector == "*" { + return identity.Value().selector + } + return getIdentity(s, identity).DID } return "" } diff --git a/tests/integration/state.go b/tests/integration/state.go index c495f80d9e..a1085b94b9 100644 --- a/tests/integration/state.go +++ b/tests/integration/state.go @@ -173,7 +173,7 @@ type state struct { // types. See [identRef]. // The map value is the identity holder that contains the identity itself and token // generated for different target nodes. See [identityHolder]. - identities map[identityRef]*identityHolder + identities map[identity]*identityHolder // The seed for the next identity generation. We want identities to be deterministic. nextIdentityGenSeed int @@ -237,7 +237,7 @@ func newState( clientType: clientType, txns: []datastore.Txn{}, allActionsDone: make(chan struct{}), - identities: map[identityRef]*identityHolder{}, + identities: map[identity]*identityHolder{}, subscriptionResultsChans: []chan func(){}, collectionNames: collectionNames, collectionIndexesByRoot: map[uint32]int{}, diff --git a/tests/integration/test_case.go b/tests/integration/test_case.go index a1ab291257..2e7eaaa5ea 100644 --- a/tests/integration/test_case.go +++ b/tests/integration/test_case.go @@ -297,7 +297,7 @@ type CreateDoc struct { // // Use `UserIdentity` to create a user identity and `NodeIdentity` to create a node identity. // Default value is `NoIdentity()`. - Identity immutable.Option[identityRef] + Identity immutable.Option[identity] // Specifies whether the document should be encrypted. IsDocEncrypted bool @@ -369,7 +369,7 @@ type DeleteDoc struct { // // Use `UserIdentity` to create a user identity and `NodeIdentity` to create a node identity. // Default value is `NoIdentity()`. - Identity immutable.Option[identityRef] + Identity immutable.Option[identity] // The collection in which this document should be deleted. CollectionID int @@ -402,7 +402,7 @@ type UpdateDoc struct { // // Use `UserIdentity` to create a user identity and `NodeIdentity` to create a node identity. // Default value is `NoIdentity()`. - Identity immutable.Option[identityRef] + Identity immutable.Option[identity] // The collection in which this document exists. CollectionID int @@ -445,7 +445,7 @@ type UpdateWithFilter struct { // // Use `UserIdentity` to create a user identity and `NodeIdentity` to create a node identity. // Default value is `NoIdentity()`. - Identity immutable.Option[identityRef] + Identity immutable.Option[identity] // The collection in which this document exists. CollectionID int @@ -602,7 +602,7 @@ type Request struct { // // Use `UserIdentity` to create a user identity and `NodeIdentity` to create a node identity. // Default value is `NoIdentity()`. - Identity immutable.Option[identityRef] + Identity immutable.Option[identity] // Used to identify the transaction for this to run against. Optional. TransactionID immutable.Option[int] @@ -805,7 +805,7 @@ type GetNodeIdentity struct { // // Use `UserIdentity` to create a user identity and `NodeIdentity` to create a node identity. // Default value is `NoIdentity()`. - ExpectedIdentity immutable.Option[identityRef] + ExpectedIdentity immutable.Option[identity] } // Wait is an action that will wait for the given duration.