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 <name>,<key>=<val>,...;<name>,... + --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 <name>,<key>=<val>,...;<name>,... + --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)