diff --git a/acp/README.md b/acp/README.md index 4c2c73907a..4e8d5b7f5b 100644 --- a/acp/README.md +++ b/acp/README.md @@ -631,6 +631,70 @@ Result: Error: document not found or not authorized to access ``` +### Revoking Access To Private Documents + +To revoke access to a document for an actor, we must delete the relationship between the +actor and the document. Inorder to delete the relationship we require all of the following: + +1) Target DocID: The docID of the document we want to delete 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 deleted for. +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 relationship record was not found, then it will be a no-op. + +Consider the same policy and added relationship from the previous example in the section above where we learnt +how to share the document with other actors. + +We made the document accessible to an actor 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 +} +``` + +Similarly, inorder to revoke access to a document we have the following command to delete the relationship: +```sh +defradb client acp relationship delete \ +--collection Users \ +--docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \ +--relation reader \ +--actor did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \ +--identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac +``` + +Result: +```json +{ + "RecordFound": true +} +``` + +**Note: If the same relationship is deleted again (or a record for a relationship does not exist) then the `RecordFound` +would be false, indicating no-op** + +Now the other actor can no longer read: +```sh +defradb client collection docIDs --identity 4d092126012ebaf56161716018a71630d99443d9d5217e9d8502bb5c5456f2c5 +``` + +**Result is empty from the above command** + ## DAC Usage HTTP: ### Authentication diff --git a/acp/acp.go b/acp/acp.go index c7ae5936e6..d30c45d128 100644 --- a/acp/acp.go +++ b/acp/acp.go @@ -115,6 +115,23 @@ type ACP interface { targetActor string, ) (bool, error) + // DeleteDocActorRelationship deletes 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 record was found, and deleted. Upon success the boolean + // value will be false if the relationship record was not found (no-op). + // + // Note: The request actor must either be the owner or manager of the document. + DeleteDocActorRelationship( + 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 6e85ac9313..a8a0d32290 100644 --- a/acp/acp_local.go +++ b/acp/acp_local.go @@ -267,3 +267,33 @@ func (l *ACPLocal) AddActorRelationship( return setRelationshipResponse.RecordExisted, nil } + +func (l *ACPLocal) DeleteActorRelationship( + 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) + + deleteRelationshipRequest := types.DeleteRelationshipRequest{ + PolicyId: policyID, + Relationship: types.NewActorRelationship(resourceName, objectID, relation, targetActor), + } + + deleteRelationshipResponse, err := l.engine.DeleteRelationship(ctx, &deleteRelationshipRequest) + if err != nil { + return false, err + } + + return deleteRelationshipResponse.RecordFound, nil +} diff --git a/acp/acp_local_test.go b/acp/acp_local_test.go index 7b30b44cbb..fce65e9974 100644 --- a/acp/acp_local_test.go +++ b/acp/acp_local_test.go @@ -854,6 +854,223 @@ func Test_LocalACP_PersistentMemory_AddDocActorRelationship_FalseIfExistsBeforeT require.Nil(t, errClose) } +func Test_LocalACP_InMemory_DeleteDocActorRelationship_TrueIfFoundAndDeletedFalseOtherwise(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) + + // 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) + + // 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) + + // Delete other identity's access by removing their relationship. + foundRecord, errDeleteDocActorRelationship := localACP.DeleteDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + identity1, + identity2.DID, + ) + require.Nil(t, errDeleteDocActorRelationship) + require.True(t, foundRecord) + + // Deleting same relationship again should be no-op. + foundRecord, errDeleteDocActorRelationship = localACP.DeleteDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + identity1, + identity2.DID, + ) + require.Nil(t, errDeleteDocActorRelationship) + require.False(t, foundRecord) // Is a no-op + + // Other identity now has no access again. + hasAccess, errCheckDocAccess = localACP.CheckDocAccess( + ctx, + ReadPermission, + identity2.DID, + validPolicyID, + "users", + "documentID_XYZ", + ) + require.Nil(t, errCheckDocAccess) + require.False(t, hasAccess) + + errClose := localACP.Close() + require.Nil(t, errClose) +} + +func Test_LocalACP_PersistentMemory_DeleteDocActorRelationship_TrueIfFoundAndDeletedFalseOtherwise(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) + + // 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) + + // 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) + + // Delete other identity's access by removing their relationship. + foundRecord, errDeleteDocActorRelationship := localACP.DeleteDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + identity1, + identity2.DID, + ) + require.Nil(t, errDeleteDocActorRelationship) + require.True(t, foundRecord) + + // Deleting same relationship again should be no-op. + foundRecord, errDeleteDocActorRelationship = localACP.DeleteDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + identity1, + identity2.DID, + ) + require.Nil(t, errDeleteDocActorRelationship) + require.False(t, foundRecord) // Is a no-op + + // Other identity now has no access again. + hasAccess, errCheckDocAccess = localACP.CheckDocAccess( + ctx, + ReadPermission, + identity2.DID, + validPolicyID, + "users", + "documentID_XYZ", + ) + require.Nil(t, errCheckDocAccess) + require.False(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 no access. + hasAccess, errCheckDocAccess = localACP.CheckDocAccess( + ctx, + ReadPermission, + identity2.DID, + validPolicyID, + "users", + "documentID_XYZ", + ) + require.Nil(t, errCheckDocAccess) + require.False(t, hasAccess) + + errClose = localACP.Close() + require.Nil(t, errClose) +} + func Test_LocalACP_InMemory_AddPolicy_InvalidCreatorIDReturnsError(t *testing.T) { ctx := context.Background() localACP := NewLocalACP() @@ -1024,3 +1241,82 @@ func Test_LocalACP_Persistent_AddDocActorRelationship_InvalidIdentitiesReturnErr err = localACP.Close() require.NoError(t, err) } + +func Test_LocalACP_InMemory_DeleteDocActorRelationship_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.DeleteDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + invalidIdentity, + identity2.DID, + ) + require.False(t, exists) + require.ErrorIs(t, err, ErrInvalidActorID) + + // Invalid target actor. + exists, err = localACP.DeleteDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + identity1, + invalidIdentity.DID, + ) + require.False(t, exists) + require.ErrorIs(t, err, ErrFailedToDeleteDocActorRelationshipWithACP) + + err = localACP.Close() + require.NoError(t, err) +} + +func Test_LocalACP_Persistent_DeleteDocActorRelationship_InvalidIdentitiesReturnError(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) + + // Invalid requesting identity. + exists, err := localACP.DeleteDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + invalidIdentity, + identity2.DID, + ) + require.False(t, exists) + require.ErrorIs(t, err, ErrInvalidActorID) + + // Invalid target actor. + exists, err = localACP.DeleteDocActorRelationship( + ctx, + validPolicyID, + "users", + "documentID_XYZ", + "reader", + identity1, + invalidIdentity.DID, + ) + require.False(t, exists) + require.ErrorIs(t, err, ErrFailedToDeleteDocActorRelationshipWithACP) + + err = localACP.Close() + require.NoError(t, err) +} diff --git a/acp/acp_source_hub.go b/acp/acp_source_hub.go index d0c4fb6b89..edd6008b63 100644 --- a/acp/acp_source_hub.go +++ b/acp/acp_source_hub.go @@ -311,3 +311,56 @@ func (a *acpSourceHub) AddActorRelationship( return cmdResult.GetResult().GetSetRelationshipResult().RecordExisted, nil } + +func (a *acpSourceHub) DeleteActorRelationship( + 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.NewDeleteRelationshipCmd( + 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().GetDeleteRelationshipResult().GetRecordFound(), nil +} diff --git a/acp/errors.go b/acp/errors.go index e0717f15dd..72fbc00b95 100644 --- a/acp/errors.go +++ b/acp/errors.go @@ -15,14 +15,16 @@ 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" - errFailedToAddDocActorRelationshipWithACP = "failed to add document actor relationship with acp" - errMissingRequiredArgToAddDocActorRelationship = "missing a required argument needed to add doc actor relationship" + 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" + errFailedToDeleteDocActorRelationshipWithACP = "failed to delete document actor relationship with acp" + errMissingReqArgToAddDocActorRelationship = "missing a required argument needed to add doc actor relationship" + errMissingReqArgToDeleteDocActorRelationship = "missing a required argument needed to delete 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" @@ -42,13 +44,14 @@ const ( ) var ( - 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) + ErrInitializationOfACPFailed = errors.New(errInitializationOfACPFailed) + ErrFailedToAddPolicyWithACP = errors.New(errFailedToAddPolicyWithACP) + ErrFailedToRegisterDocWithACP = errors.New(errFailedToRegisterDocWithACP) + ErrFailedToCheckIfDocIsRegisteredWithACP = errors.New(errFailedToCheckIfDocIsRegisteredWithACP) + ErrFailedToVerifyDocAccessWithACP = errors.New(errFailedToVerifyDocAccessWithACP) + ErrFailedToAddDocActorRelationshipWithACP = errors.New(errFailedToAddDocActorRelationshipWithACP) + ErrFailedToDeleteDocActorRelationshipWithACP = errors.New(errFailedToDeleteDocActorRelationshipWithACP) + ErrPolicyDoesNotExistWithACP = errors.New(errPolicyDoesNotExistWithACP) ErrResourceDoesNotExistOnTargetPolicy = errors.New(errResourceDoesNotExistOnTargetPolicy) @@ -165,6 +168,29 @@ func NewErrFailedToAddDocActorRelationshipWithACP( ) } +func NewErrFailedToDeleteDocActorRelationshipWithACP( + inner error, + Type string, + policyID string, + resourceName string, + docID string, + relation string, + requestActor string, + targetActor string, +) error { + return errors.Wrap( + errFailedToDeleteDocActorRelationshipWithACP, + 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, @@ -244,7 +270,26 @@ func NewErrMissingRequiredArgToAddDocActorRelationship( targetActor string, ) error { return errors.New( - errMissingRequiredArgToAddDocActorRelationship, + errMissingReqArgToAddDocActorRelationship, + 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 NewErrMissingRequiredArgToDeleteDocActorRelationship( + policyID string, + resourceName string, + docID string, + relation string, + requestActor string, + targetActor string, +) error { + return errors.New( + errMissingReqArgToDeleteDocActorRelationship, errors.NewKV("PolicyID", policyID), errors.NewKV("ResourceName", resourceName), errors.NewKV("DocID", docID), diff --git a/acp/source_hub_client.go b/acp/source_hub_client.go index 0bfbae72b1..b211214d9f 100644 --- a/acp/source_hub_client.go +++ b/acp/source_hub_client.go @@ -106,6 +106,27 @@ type sourceHubClient interface { creationTime *protoTypes.Timestamp, ) (bool, error) + // DeleteActorRelationship deletes 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 for that relation no-longer will apply to target actor anymore. + // + // If failure occurs, the result will return an error. Upon success the boolean value will + // be true if the relationship record was found and deleted. Upon success the boolean value + // will be false if the relationship record was not found (no-op). + // + // Note: The 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). + DeleteActorRelationship( + 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 } @@ -420,6 +441,70 @@ func (a *sourceHubBridge) AddDocActorRelationship( return exists, nil } +func (a *sourceHubBridge) DeleteDocActorRelationship( + 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, NewErrMissingRequiredArgToDeleteDocActorRelationship( + policyID, + resourceName, + docID, + relation, + requestActor.DID, + targetActor, + ) + } + + recordFound, err := a.client.DeleteActorRelationship( + ctx, + policyID, + resourceName, + docID, + relation, + requestActor, + targetActor, + protoTypes.TimestampNow(), + ) + + if err != nil { + return false, NewErrFailedToDeleteDocActorRelationshipWithACP( + err, + "Local", + policyID, + resourceName, + docID, + relation, + requestActor.DID, + targetActor, + ) + } + + log.InfoContext( + ctx, + "Document and actor relationship delete", + 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("RecordFound", recordFound), + ) + + return recordFound, nil +} + func (a *sourceHubBridge) SupportsP2P() bool { _, ok := a.client.(*acpSourceHub) return ok diff --git a/cli/acp_relationship_add.go b/cli/acp_relationship_add.go index 9733732af8..59b5c3cd32 100644 --- a/cli/acp_relationship_add.go +++ b/cli/acp_relationship_add.go @@ -117,11 +117,10 @@ Example: Creating a dummy relationship does nothing (from database prespective): ) _ = cmd.MarkFlagRequired(targetActorFlagLong) - cmd.Flags().StringVarP( + cmd.Flags().StringVar( &docIDArg, docIDFlag, "", - "", "Document Identifier (ObjectID) to make relationship for", ) _ = cmd.MarkFlagRequired(docIDFlag) diff --git a/cli/acp_relationship_delete.go b/cli/acp_relationship_delete.go new file mode 100644 index 0000000000..7e0852e301 --- /dev/null +++ b/cli/acp_relationship_delete.go @@ -0,0 +1,121 @@ +// 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 MakeACPRelationshipDeleteCommand() *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: "delete [--docID] [-c --collection] [-r --relation] [-a --actor] [-i --identity]", + Short: "Delete relationship", + Long: `Delete relationship + +To revoke access to a document for an actor, we must delete the relationship between the +actor and the document. Inorder to delete the relationship we require all of the following: + +1) Target DocID: The docID of the document we want to delete 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 deleted for. +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 relationship record was not found, then it will be a no-op. + - Learn more about [ACP & DPI Rules](/acp/README.md) + +Example: Let another actor (4d092126012ebaf56161716018a71630d99443d9d5217e9d8502bb5c5456f2c5) read a private document: + defradb client acp relationship delete \ + --collection Users \ + --docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \ + --relation reader \ + --actor did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \ + --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac +`, + RunE: func(cmd *cobra.Command, args []string) error { + db := mustGetContextDB(cmd) + deleteDocActorRelationshipResult, err := db.DeleteDocActorRelationship( + cmd.Context(), + collectionArg, + docIDArg, + relationArg, + targetActorArg, + ) + + if err != nil { + return err + } + + return writeJSON(cmd, deleteDocActorRelationshipResult) + }, + } + + 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 deleted within the relationship", + ) + _ = cmd.MarkFlagRequired(relationFlagLong) + + cmd.Flags().StringVarP( + &targetActorArg, + targetActorFlagLong, + targetActorFlagShort, + "", + "Actor to delete relationship for", + ) + _ = cmd.MarkFlagRequired(targetActorFlagLong) + + cmd.Flags().StringVar( + &docIDArg, + docIDFlag, + "", + "Document Identifier (ObjectID) to delete relationship for", + ) + _ = cmd.MarkFlagRequired(docIDFlag) + + return cmd +} diff --git a/cli/cli.go b/cli/cli.go index 61d1fd51cf..f6950225a6 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -70,6 +70,7 @@ func NewDefraCommand() *cobra.Command { acp_relationship := MakeACPRelationshipCommand() acp_relationship.AddCommand( MakeACPRelationshipAddCommand(), + MakeACPRelationshipDeleteCommand(), ) acp := MakeACPCommand() diff --git a/client/acp.go b/client/acp.go index 7795369c8f..19d330fe02 100644 --- a/client/acp.go +++ b/client/acp.go @@ -36,3 +36,10 @@ type AddDocActorRelationshipResult struct { // it is false if a new relationship was created. ExistedAlready bool } + +// DeleteDocActorRelationshipResult wraps the result of making a document-actor relationship. +type DeleteDocActorRelationshipResult struct { + // RecordFound is true if the relationship record was found, and + // is false if the relationship record was not found (no-op). + RecordFound bool +} diff --git a/client/db.go b/client/db.go index e28d21df02..4838773dde 100644 --- a/client/db.go +++ b/client/db.go @@ -120,6 +120,21 @@ type DB interface { relation string, targetActor string, ) (AddDocActorRelationshipResult, error) + + // DeleteDocActorRelationship deletes 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 record was found and deleted. Upon success the boolean value + // will be false if the relationship record was not found (no-op). + // + // Note: The request actor must either be the owner or manager of the document. + DeleteDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, + ) (DeleteDocActorRelationshipResult, error) } // Store contains the core DefraDB read-write operations. diff --git a/client/mocks/db.go b/client/mocks/db.go index 1297870e15..73cf4b3665 100644 --- a/client/mocks/db.go +++ b/client/mocks/db.go @@ -492,6 +492,66 @@ func (_c *DB_Close_Call) RunAndReturn(run func()) *DB_Close_Call { return _c } +// DeleteDocActorRelationship provides a mock function with given fields: ctx, collectionName, docID, relation, targetActor +func (_m *DB) DeleteDocActorRelationship(ctx context.Context, collectionName string, docID string, relation string, targetActor string) (client.DeleteDocActorRelationshipResult, error) { + ret := _m.Called(ctx, collectionName, docID, relation, targetActor) + + if len(ret) == 0 { + panic("no return value specified for DeleteDocActorRelationship") + } + + var r0 client.DeleteDocActorRelationshipResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) (client.DeleteDocActorRelationshipResult, error)); ok { + return rf(ctx, collectionName, docID, relation, targetActor) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) client.DeleteDocActorRelationshipResult); ok { + r0 = rf(ctx, collectionName, docID, relation, targetActor) + } else { + r0 = ret.Get(0).(client.DeleteDocActorRelationshipResult) + } + + 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_DeleteDocActorRelationship_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteDocActorRelationship' +type DB_DeleteDocActorRelationship_Call struct { + *mock.Call +} + +// DeleteDocActorRelationship is a helper method to define mock.On call +// - ctx context.Context +// - collectionName string +// - docID string +// - relation string +// - targetActor string +func (_e *DB_Expecter) DeleteDocActorRelationship(ctx interface{}, collectionName interface{}, docID interface{}, relation interface{}, targetActor interface{}) *DB_DeleteDocActorRelationship_Call { + return &DB_DeleteDocActorRelationship_Call{Call: _e.mock.On("DeleteDocActorRelationship", ctx, collectionName, docID, relation, targetActor)} +} + +func (_c *DB_DeleteDocActorRelationship_Call) Run(run func(ctx context.Context, collectionName string, docID string, relation string, targetActor string)) *DB_DeleteDocActorRelationship_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_DeleteDocActorRelationship_Call) Return(_a0 client.DeleteDocActorRelationshipResult, _a1 error) *DB_DeleteDocActorRelationship_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DB_DeleteDocActorRelationship_Call) RunAndReturn(run func(context.Context, string, string, string, string) (client.DeleteDocActorRelationshipResult, error)) *DB_DeleteDocActorRelationship_Call { + _c.Call.Return(run) + return _c +} + // DeleteReplicator provides a mock function with given fields: ctx, rep func (_m *DB) DeleteReplicator(ctx context.Context, rep client.Replicator) error { ret := _m.Called(ctx, rep) diff --git a/docs/website/references/cli/defradb_client_acp_relationship.md b/docs/website/references/cli/defradb_client_acp_relationship.md index 4c204d0ccd..2518f6c3ed 100644 --- a/docs/website/references/cli/defradb_client_acp_relationship.md +++ b/docs/website/references/cli/defradb_client_acp_relationship.md @@ -38,4 +38,5 @@ Interact with the acp relationship features of DefraDB instance * [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 +* [defradb client acp relationship delete](defradb_client_acp_relationship_delete.md) - Delete relationship diff --git a/docs/website/references/cli/defradb_client_acp_relationship_delete.md b/docs/website/references/cli/defradb_client_acp_relationship_delete.md new file mode 100644 index 0000000000..501f5fb242 --- /dev/null +++ b/docs/website/references/cli/defradb_client_acp_relationship_delete.md @@ -0,0 +1,73 @@ +## defradb client acp relationship delete + +Delete relationship + +### Synopsis + +Delete relationship + +To revoke access to a document for an actor, we must delete the relationship between the +actor and the document. Inorder to delete the relationship we require all of the following: + +1) Target DocID: The docID of the document we want to delete 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 deleted for. +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 relationship record was not found, then it will be a no-op. + - Learn more about [ACP & DPI Rules](/acp/README.md) + +Example: Let another actor (4d092126012ebaf56161716018a71630d99443d9d5217e9d8502bb5c5456f2c5) read a private document: + defradb client acp relationship delete \ + --collection Users \ + --docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \ + --relation reader \ + --actor did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \ + --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac + + +``` +defradb client acp relationship delete [--docID] [-c --collection] [-r --relation] [-a --actor] [-i --identity] [flags] +``` + +### Options + +``` + -a, --actor string Actor to delete relationship for + -c, --collection string Collection that has the resource and policy for object + --docID string Document Identifier (ObjectID) to delete relationship for + -h, --help help for delete + -r, --relation string Relation that needs to be deleted within the relationship +``` + +### Options inherited from parent commands + +``` + -i, --identity string Hex formatted private key used to authenticate with ACP + --keyring-backend string Keyring backend to use. Options are file or system (default "file") + --keyring-namespace string Service name to use when using the system backend (default "defradb") + --keyring-path string Path to store encrypted keys when using the file backend (default "keys") + --log-format string Log format to use. Options are text or json (default "text") + --log-level string Log level to use. Options are debug, info, error, fatal (default "info") + --log-output string Log output path. Options are stderr or stdout. (default "stderr") + --log-overrides string Logger config overrides. Format ,=,...;,... + --log-source Include source location in logs + --log-stacktrace Include stacktrace in error and fatal logs + --no-keyring Disable the keyring and generate ephemeral keys + --no-log-color Disable colored log output + --rootdir string Directory for persistent data (default: $HOME/.defradb) + --secret-file string Path to the file containing secrets (default ".env") + --source-hub-address string The SourceHub address authorized by the client to make SourceHub transactions on behalf of the actor + --tx uint Transaction ID + --url string URL of HTTP endpoint to listen on or connect to (default "127.0.0.1:9181") +``` + +### SEE ALSO + +* [defradb client acp relationship](defradb_client_acp_relationship.md) - Interact with the acp relationship features of DefraDB instance + diff --git a/docs/website/references/http/openapi.json b/docs/website/references/http/openapi.json index c0a7898364..77168f7e93 100644 --- a/docs/website/references/http/openapi.json +++ b/docs/website/references/http/openapi.json @@ -27,6 +27,64 @@ } }, "schemas": { + "acp_policy_add_result": { + "properties": { + "PolicyID": { + "type": "string" + } + }, + "type": "object" + }, + "acp_relationship_add_request": { + "properties": { + "CollectionName": { + "type": "string" + }, + "DocID": { + "type": "string" + }, + "Relation": { + "type": "string" + }, + "TargetActor": { + "type": "string" + } + }, + "type": "object" + }, + "acp_relationship_add_result": { + "properties": { + "ExistedAlready": { + "type": "boolean" + } + }, + "type": "object" + }, + "acp_relationship_delete_request": { + "properties": { + "CollectionName": { + "type": "string" + }, + "DocID": { + "type": "string" + }, + "Relation": { + "type": "string" + }, + "TargetActor": { + "type": "string" + } + }, + "type": "object" + }, + "acp_relationship_delete_result": { + "properties": { + "RecordFound": { + "type": "boolean" + } + }, + "type": "object" + }, "add_view_request": { "properties": { "Query": { @@ -574,7 +632,14 @@ }, "responses": { "200": { - "$ref": "#/components/responses/success" + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/acp_policy_add_result" + } + } + }, + "description": "Add acp policy result" }, "400": { "$ref": "#/components/responses/error" @@ -589,14 +654,49 @@ } }, "/acp/relationship": { + "delete": { + "description": "Delete an actor relationship using acp system", + "operationId": "delete relationship", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/acp_relationship_delete_request" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/acp_relationship_delete_result" + } + } + }, + "description": "Delete acp relationship result" + }, + "400": { + "$ref": "#/components/responses/error" + }, + "default": { + "description": "" + } + }, + "tags": [ + "acp_relationship" + ] + }, "post": { "description": "Add an actor relationship using acp system", "operationId": "add relationship", "requestBody": { "content": { - "text/plain": { + "application/json": { "schema": { - "type": "string" + "$ref": "#/components/schemas/acp_relationship_add_request" } } }, @@ -604,7 +704,14 @@ }, "responses": { "200": { - "$ref": "#/components/responses/success" + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/acp_relationship_add_result" + } + } + }, + "description": "Add acp relationship result" }, "400": { "$ref": "#/components/responses/error" diff --git a/http/client_acp.go b/http/client_acp.go index d4f1ed02e5..91a2f502b2 100644 --- a/http/client_acp.go +++ b/http/client_acp.go @@ -92,3 +92,51 @@ func (c *Client) AddDocActorRelationship( return addDocActorRelResult, nil } + +type deleteDocActorRelationshipRequest struct { + CollectionName string + DocID string + Relation string + TargetActor string +} + +func (c *Client) DeleteDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, +) (client.DeleteDocActorRelationshipResult, error) { + methodURL := c.http.baseURL.JoinPath("acp", "relationship") + + body, err := json.Marshal( + deleteDocActorRelationshipRequest{ + CollectionName: collectionName, + DocID: docID, + Relation: relation, + TargetActor: targetActor, + }, + ) + + if err != nil { + return client.DeleteDocActorRelationshipResult{}, err + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodDelete, + methodURL.String(), + bytes.NewBuffer(body), + ) + + if err != nil { + return client.DeleteDocActorRelationshipResult{}, err + } + + var deleteDocActorRelResult client.DeleteDocActorRelationshipResult + if err := c.http.requestJson(req, &deleteDocActorRelResult); err != nil { + return client.DeleteDocActorRelationshipResult{}, err + } + + return deleteDocActorRelResult, nil +} diff --git a/http/handler_acp.go b/http/handler_acp.go index e9bdf2ce0e..d359d5085e 100644 --- a/http/handler_acp.go +++ b/http/handler_acp.go @@ -75,24 +75,70 @@ func (s *acpHandler) AddDocActorRelationship(rw http.ResponseWriter, req *http.R responseJSON(rw, http.StatusOK, addDocActorRelResult) } -func (h *acpHandler) bindRoutes(router *Router) { - successResponse := &openapi3.ResponseRef{ - Ref: "#/components/responses/success", +func (s *acpHandler) DeleteDocActorRelationship(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 deleteDocActorRelationshipRequest + err := requestJSON(req, &message) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + + deleteDocActorRelResult, err := db.DeleteDocActorRelationship( + req.Context(), + message.CollectionName, + message.DocID, + message.Relation, + message.TargetActor, + ) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return } + + responseJSON(rw, http.StatusOK, deleteDocActorRelResult) +} + +func (h *acpHandler) bindRoutes(router *Router) { errorResponse := &openapi3.ResponseRef{ Ref: "#/components/responses/error", } + acpPolicyAddResultSchema := &openapi3.SchemaRef{ + Ref: "#/components/schemas/acp_policy_add_result", + } + + acpRelationshipAddRequestSchema := &openapi3.SchemaRef{ + Ref: "#/components/schemas/acp_relationship_add_request", + } + acpRelationshipAddResultSchema := &openapi3.SchemaRef{ + Ref: "#/components/schemas/acp_relationship_add_result", + } + + acpRelationshipDeleteRequestSchema := &openapi3.SchemaRef{ + Ref: "#/components/schemas/acp_relationship_delete_request", + } + acpRelationshipDeleteResultSchema := &openapi3.SchemaRef{ + Ref: "#/components/schemas/acp_relationship_delete_result", + } + acpAddPolicyRequest := openapi3.NewRequestBody(). WithRequired(true). WithContent(openapi3.NewContentWithSchema(openapi3.NewStringSchema(), []string{"text/plain"})) - + acpPolicyAddResult := openapi3.NewResponse(). + WithDescription("Add acp policy result"). + WithJSONSchemaRef(acpPolicyAddResultSchema) acpAddPolicy := openapi3.NewOperation() acpAddPolicy.OperationID = "add policy" acpAddPolicy.Description = "Add a policy using acp system" acpAddPolicy.Tags = []string{"acp_policy"} acpAddPolicy.Responses = openapi3.NewResponses() - acpAddPolicy.Responses.Set("200", successResponse) + acpAddPolicy.AddResponse(200, acpPolicyAddResult) acpAddPolicy.Responses.Set("400", errorResponse) acpAddPolicy.RequestBody = &openapi3.RequestBodyRef{ Value: acpAddPolicyRequest, @@ -100,19 +146,39 @@ func (h *acpHandler) bindRoutes(router *Router) { acpAddDocActorRelationshipRequest := openapi3.NewRequestBody(). WithRequired(true). - WithContent(openapi3.NewContentWithSchema(openapi3.NewStringSchema(), []string{"text/plain"})) - + WithContent(openapi3.NewContentWithJSONSchemaRef(acpRelationshipAddRequestSchema)) + acpAddDocActorRelationshipResult := openapi3.NewResponse(). + WithDescription("Add acp relationship result"). + WithJSONSchemaRef(acpRelationshipAddResultSchema) 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.AddResponse(200, acpAddDocActorRelationshipResult) acpAddDocActorRelationship.Responses.Set("400", errorResponse) acpAddDocActorRelationship.RequestBody = &openapi3.RequestBodyRef{ Value: acpAddDocActorRelationshipRequest, } + acpDeleteDocActorRelationshipRequest := openapi3.NewRequestBody(). + WithRequired(true). + WithContent(openapi3.NewContentWithJSONSchemaRef(acpRelationshipDeleteRequestSchema)) + acpDeleteDocActorRelationshipResult := openapi3.NewResponse(). + WithDescription("Delete acp relationship result"). + WithJSONSchemaRef(acpRelationshipDeleteResultSchema) + acpDeleteDocActorRelationship := openapi3.NewOperation() + acpDeleteDocActorRelationship.OperationID = "delete relationship" + acpDeleteDocActorRelationship.Description = "Delete an actor relationship using acp system" + acpDeleteDocActorRelationship.Tags = []string{"acp_relationship"} + acpDeleteDocActorRelationship.Responses = openapi3.NewResponses() + acpDeleteDocActorRelationship.AddResponse(200, acpDeleteDocActorRelationshipResult) + acpDeleteDocActorRelationship.Responses.Set("400", errorResponse) + acpDeleteDocActorRelationship.RequestBody = &openapi3.RequestBodyRef{ + Value: acpDeleteDocActorRelationshipRequest, + } + router.AddRoute("/acp/policy", http.MethodPost, acpAddPolicy, h.AddPolicy) router.AddRoute("/acp/relationship", http.MethodPost, acpAddDocActorRelationship, h.AddDocActorRelationship) + router.AddRoute("/acp/relationship", http.MethodDelete, acpDeleteDocActorRelationship, h.DeleteDocActorRelationship) } diff --git a/http/openapi.go b/http/openapi.go index 0bb5f71743..0a62b6ebac 100644 --- a/http/openapi.go +++ b/http/openapi.go @@ -20,27 +20,32 @@ import ( // openApiSchemas is a mapping of types to auto generate schemas for. var openApiSchemas = map[string]any{ - "error": &errorResponse{}, - "create_tx": &CreateTxResponse{}, - "collection_update": &CollectionUpdateRequest{}, - "collection_delete": &CollectionDeleteRequest{}, - "peer_info": &peer.AddrInfo{}, - "graphql_request": &GraphQLRequest{}, - "backup_config": &client.BackupConfig{}, - "collection": &client.CollectionDescription{}, - "schema": &client.SchemaDescription{}, - "collection_definition": &client.CollectionDefinition{}, - "index": &client.IndexDescription{}, - "delete_result": &client.DeleteResult{}, - "update_result": &client.UpdateResult{}, - "lens_config": &client.LensConfig{}, - "replicator": &client.Replicator{}, - "ccip_request": &CCIPRequest{}, - "ccip_response": &CCIPResponse{}, - "patch_schema_request": &patchSchemaRequest{}, - "add_view_request": &addViewRequest{}, - "migrate_request": &migrateRequest{}, - "set_migration_request": &setMigrationRequest{}, + "error": &errorResponse{}, + "create_tx": &CreateTxResponse{}, + "collection_update": &CollectionUpdateRequest{}, + "collection_delete": &CollectionDeleteRequest{}, + "peer_info": &peer.AddrInfo{}, + "graphql_request": &GraphQLRequest{}, + "backup_config": &client.BackupConfig{}, + "collection": &client.CollectionDescription{}, + "schema": &client.SchemaDescription{}, + "collection_definition": &client.CollectionDefinition{}, + "index": &client.IndexDescription{}, + "delete_result": &client.DeleteResult{}, + "update_result": &client.UpdateResult{}, + "lens_config": &client.LensConfig{}, + "replicator": &client.Replicator{}, + "ccip_request": &CCIPRequest{}, + "ccip_response": &CCIPResponse{}, + "patch_schema_request": &patchSchemaRequest{}, + "add_view_request": &addViewRequest{}, + "migrate_request": &migrateRequest{}, + "set_migration_request": &setMigrationRequest{}, + "acp_policy_add_result": &client.AddPolicyResult{}, + "acp_relationship_add_request": &addDocActorRelationshipRequest{}, + "acp_relationship_add_result": &client.AddDocActorRelationshipResult{}, + "acp_relationship_delete_request": &deleteDocActorRelationshipRequest{}, + "acp_relationship_delete_result": &client.DeleteDocActorRelationshipResult{}, } func NewOpenAPISpec() (*openapi3.T, error) { diff --git a/internal/db/db.go b/internal/db/db.go index 73165c239a..d5872cef0c 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -248,6 +248,46 @@ func (db *db) AddDocActorRelationship( return client.AddDocActorRelationshipResult{ExistedAlready: exists}, nil } +func (db *db) DeleteDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, +) (client.DeleteDocActorRelationshipResult, error) { + if !db.acp.HasValue() { + return client.DeleteDocActorRelationshipResult{}, client.ErrACPOperationButACPNotAvailable + } + + collection, err := db.GetCollectionByName(ctx, collectionName) + if err != nil { + return client.DeleteDocActorRelationshipResult{}, err + } + + policyID, resourceName, hasPolicy := permission.IsPermissioned(collection) + if !hasPolicy { + return client.DeleteDocActorRelationshipResult{}, client.ErrACPOperationButCollectionHasNoPolicy + } + + identity := GetContextIdentity(ctx) + + recordFound, err := db.acp.Value().DeleteDocActorRelationship( + ctx, + policyID, + resourceName, + docID, + relation, + identity.Value(), + targetActor, + ) + + if err != nil { + return client.DeleteDocActorRelationshipResult{}, err + } + + return client.DeleteDocActorRelationshipResult{RecordFound: recordFound}, 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/tests/clients/cli/wrapper_acp.go b/tests/clients/cli/wrapper_acp.go index f76aad3cdf..06c356afaa 100644 --- a/tests/clients/cli/wrapper_acp.go +++ b/tests/clients/cli/wrapper_acp.go @@ -64,3 +64,31 @@ func (w *Wrapper) AddDocActorRelationship( return exists, err } + +func (w *Wrapper) DeleteDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, +) (client.DeleteDocActorRelationshipResult, error) { + args := []string{ + "client", "acp", "relationship", "delete", + "--collection", collectionName, + "--docID", docID, + "--relation", relation, + "--actor", targetActor, + } + + data, err := w.cmd.execute(ctx, args) + if err != nil { + return client.DeleteDocActorRelationshipResult{}, err + } + + var exists client.DeleteDocActorRelationshipResult + if err := json.Unmarshal(data, &exists); err != nil { + return client.DeleteDocActorRelationshipResult{}, err + } + + return exists, err +} diff --git a/tests/clients/http/wrapper.go b/tests/clients/http/wrapper.go index 81ed74b095..ae6cd61529 100644 --- a/tests/clients/http/wrapper.go +++ b/tests/clients/http/wrapper.go @@ -121,6 +121,22 @@ func (w *Wrapper) AddDocActorRelationship( ) } +func (w *Wrapper) DeleteDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, +) (client.DeleteDocActorRelationshipResult, error) { + return w.client.DeleteDocActorRelationship( + 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 a8f41e5f41..d98fe08a3f 100644 --- a/tests/integration/acp.go +++ b/tests/integration/acp.go @@ -117,14 +117,14 @@ func addPolicyACP( ctx := db.SetContextIdentity(s.ctx, identity) policyResult, err := node.AddPolicy(ctx, action.Policy) - if err == nil { + expectedErrorRaised := AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) + + if !expectedErrorRaised { require.Equal(s.t, action.ExpectedError, "") require.Equal(s.t, action.ExpectedPolicyID, policyResult.PolicyID) } - expectedErrorRaised := AssertError(s.t, s.testCase.Description, err, action.ExpectedError) - assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) - // The policy should only be added to a SourceHub chain once - there is no need to loop through // the nodes. if acpType == SourceHubACPType { @@ -236,13 +236,13 @@ func addDocActorRelationshipACP( targetIdentity, ) - if err == nil { + expectedErrorRaised := AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) + + if !expectedErrorRaised { 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 @@ -293,14 +293,192 @@ func addDocActorRelationshipACP( targetIdentity, ) - if err == nil { + expectedErrorRaised := AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) + + if !expectedErrorRaised { require.Equal(s.t, action.ExpectedError, "") require.Equal(s.t, action.ExpectedExistence, exists.ExistedAlready) } + // The relationship should only be added to a SourceHub chain once - there is no need to loop through + // the nodes. + if acpType == SourceHubACPType { + break + } + } + } +} + +// DeleteDocActorRelationship will attempt to delete a relationship between a document and an actor. +type DeleteDocActorRelationship struct { + // NodeID may hold the ID (index) of the node we want to delete doc actor relationship on. + // + // If a value is not provided the relationship will be deleted on all nodes, unless testing with + // sourcehub ACP, in which case the relationship will only be deleted once. + NodeID immutable.Option[int] + + // The collection in which the target document we want to delete 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 within the relationship we want to delete (should be defined in the policy). + // + // This is a required field. + Relation string + + // The target public identity, i.e. the identity of the actor with whom the relationship is with. + // + // This is a required field. To test the invalid usage of not having this arg, use -1 index. + TargetIdentity int + + // The requestor identity, i.e. identity of the actor deleting the relationship. + // Note: This identity must either own or have managing access defined in the policy. + // + // This is a required field. To test the invalid usage of not having this arg, use -1 index. + RequestorIdentity int + + // Result returns true if the relationship record was expected to be found and deleted, + // and returns false if no matching relationship record was found (no-op). + ExpectedRecordFound 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 deleteDocActorRelationshipACP( + s *state, + action DeleteDocActorRelationship, +) { + 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) + + deleteDocActorRelationshipResult, err := node.DeleteDocActorRelationship( + ctx, + collectionName, + docID, + action.Relation, + targetIdentity, + ) + + expectedErrorRaised := AssertError(s.t, s.testCase.Description, err, action.ExpectedError) + assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) + + if !expectedErrorRaised { + require.Equal(s.t, action.ExpectedError, "") + require.Equal(s.t, action.ExpectedRecordFound, deleteDocActorRelationshipResult.RecordFound) + } + } 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) + + deleteDocActorRelationshipResult, err := node.DeleteDocActorRelationship( + ctx, + collectionName, + docID, + action.Relation, + targetIdentity, + ) + expectedErrorRaised := AssertError(s.t, s.testCase.Description, err, action.ExpectedError) assertExpectedErrorRaised(s.t, s.testCase.Description, action.ExpectedError, expectedErrorRaised) + if !expectedErrorRaised { + require.Equal(s.t, action.ExpectedError, "") + require.Equal(s.t, action.ExpectedRecordFound, deleteDocActorRelationshipResult.RecordFound) + } + // The relationship should only be added to a SourceHub chain once - there is no need to loop through // the nodes. if acpType == SourceHubACPType { @@ -314,7 +492,10 @@ func setupSourceHub(s *state) ([]node.ACPOpt, error) { var isACPTest bool for _, a := range s.testCase.Actions { switch a.(type) { - case AddPolicy, AddDocActorRelationship: + case + AddPolicy, + AddDocActorRelationship, + DeleteDocActorRelationship: isACPTest = true } } 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 index fe06e10061..cdefe70a46 100644 --- a/tests/integration/acp/p2p/replicator_with_doc_actor_relationship_test.go +++ b/tests/integration/acp/p2p/replicator_with_doc_actor_relationship_test.go @@ -212,6 +212,76 @@ func TestACP_P2PReplicatorWithPermissionedCollectionCreateDocActorRelationship_S }, }, }, + + testUtils.DeleteDocActorRelationship{ + NodeID: immutable.Some(1), + + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedRecordFound: true, + }, + + testUtils.DeleteDocActorRelationship{ + NodeID: immutable.Some(0), // Note: Different node than the previous + + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedRecordFound: false, // Making the same relation through any node should be a no-op + }, + + testUtils.Request{ + // Ensure that the document is now inaccessible on all nodes to the actor we revoked access from. + Identity: immutable.Some(2), + + Request: ` + query { + Users { + name + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + + 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", + }, + }, + }, + }, }, } 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 index a55c5a333e..b9f3f8edd3 100644 --- a/tests/integration/acp/p2p/subscribe_with_doc_actor_relationship_test.go +++ b/tests/integration/acp/p2p/subscribe_with_doc_actor_relationship_test.go @@ -218,6 +218,76 @@ func TestACP_P2PSubscribeAddGetSingleWithPermissionedCollectionCreateDocActorRel }, }, }, + + testUtils.DeleteDocActorRelationship{ + NodeID: immutable.Some(1), + + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedRecordFound: true, + }, + + testUtils.DeleteDocActorRelationship{ + NodeID: immutable.Some(0), // Note: Different node than the previous + + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedRecordFound: false, // Making the same relation through any node should be a no-op + }, + + testUtils.Request{ + // Ensure that the document is now inaccessible on all nodes to the actor we revoked access from. + Identity: immutable.Some(2), + + Request: ` + query { + Users { + name + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + + 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", + }, + }, + }, + }, }, } diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_invalid_test.go b/tests/integration/acp/relationship/doc_actor/add/invalid_test.go similarity index 99% rename from tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_invalid_test.go rename to tests/integration/acp/relationship/doc_actor/add/invalid_test.go index cc0e0dac69..d9f96d9c21 100644 --- a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_invalid_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/invalid_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_acp_relationship_add_docactor +package test_acp_relationship_doc_actor_add import ( "fmt" diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_delete_test.go b/tests/integration/acp/relationship/doc_actor/add/with_delete_test.go similarity index 99% rename from tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_delete_test.go rename to tests/integration/acp/relationship/doc_actor/add/with_delete_test.go index 9be3ace27d..c87c3c0a8f 100644 --- a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_delete_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_delete_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_acp_relationship_add_docactor +package test_acp_relationship_doc_actor_add import ( "fmt" diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_dummy_relation_test.go b/tests/integration/acp/relationship/doc_actor/add/with_dummy_relation_test.go similarity index 99% rename from tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_dummy_relation_test.go rename to tests/integration/acp/relationship/doc_actor/add/with_dummy_relation_test.go index 66e17ba00a..79cc4639e2 100644 --- a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_dummy_relation_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_dummy_relation_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_acp_relationship_add_docactor +package test_acp_relationship_doc_actor_add import ( "fmt" diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_gql_test.go b/tests/integration/acp/relationship/doc_actor/add/with_manager_gql_test.go similarity index 99% rename from tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_gql_test.go rename to tests/integration/acp/relationship/doc_actor/add/with_manager_gql_test.go index 9c2280d6ce..1881979c32 100644 --- a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_gql_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_manager_gql_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_acp_relationship_add_docactor +package test_acp_relationship_doc_actor_add import ( "fmt" diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_test.go b/tests/integration/acp/relationship/doc_actor/add/with_manager_test.go similarity index 99% rename from tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_test.go rename to tests/integration/acp/relationship/doc_actor/add/with_manager_test.go index 4467aa1af9..f07971589c 100644 --- a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_manager_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_manager_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_acp_relationship_add_docactor +package test_acp_relationship_doc_actor_add import ( "fmt" 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/doc_actor/add/with_no_policy_on_collection_test.go similarity index 96% rename from tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_collection_with_no_policy_test.go rename to tests/integration/acp/relationship/doc_actor/add/with_no_policy_on_collection_test.go index a614ef3ce9..a7ad53db41 100644 --- a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_collection_with_no_policy_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_no_policy_on_collection_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_acp_relationship_add_docactor +package test_acp_relationship_doc_actor_add import ( "testing" 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/doc_actor/add/with_only_write_gql_test.go similarity index 98% rename from tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_only_write_gql_test.go rename to tests/integration/acp/relationship/doc_actor/add/with_only_write_gql_test.go index e3f3e62050..36bf181478 100644 --- a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_only_write_gql_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_only_write_gql_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_acp_relationship_add_docactor +package test_acp_relationship_doc_actor_add import ( "fmt" diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_only_write_test.go b/tests/integration/acp/relationship/doc_actor/add/with_only_write_test.go similarity index 99% rename from tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_only_write_test.go rename to tests/integration/acp/relationship/doc_actor/add/with_only_write_test.go index e052d19afd..09703f93aa 100644 --- a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_only_write_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_only_write_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_acp_relationship_add_docactor +package test_acp_relationship_doc_actor_add import ( "fmt" diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_public_document_test.go b/tests/integration/acp/relationship/doc_actor/add/with_public_document_test.go similarity index 98% rename from tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_public_document_test.go rename to tests/integration/acp/relationship/doc_actor/add/with_public_document_test.go index e134a821e4..30c299e222 100644 --- a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_public_document_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_public_document_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_acp_relationship_add_docactor +package test_acp_relationship_doc_actor_add import ( "fmt" diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_gql_test.go b/tests/integration/acp/relationship/doc_actor/add/with_reader_gql_test.go similarity index 98% rename from tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_gql_test.go rename to tests/integration/acp/relationship/doc_actor/add/with_reader_gql_test.go index 02a637833f..e40661cede 100644 --- a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_gql_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_reader_gql_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_acp_relationship_add_docactor +package test_acp_relationship_doc_actor_add import ( "fmt" diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_test.go b/tests/integration/acp/relationship/doc_actor/add/with_reader_test.go similarity index 99% rename from tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_test.go rename to tests/integration/acp/relationship/doc_actor/add/with_reader_test.go index 70a7676a96..bac553d553 100644 --- a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_reader_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_reader_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_acp_relationship_add_docactor +package test_acp_relationship_doc_actor_add import ( "fmt" diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_gql_test.go b/tests/integration/acp/relationship/doc_actor/add/with_update_gql_test.go similarity index 99% rename from tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_gql_test.go rename to tests/integration/acp/relationship/doc_actor/add/with_update_gql_test.go index dcfda587e8..d265b448c3 100644 --- a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_gql_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_update_gql_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_acp_relationship_add_docactor +package test_acp_relationship_doc_actor_add import ( "fmt" diff --git a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_test.go b/tests/integration/acp/relationship/doc_actor/add/with_update_test.go similarity index 99% rename from tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_test.go rename to tests/integration/acp/relationship/doc_actor/add/with_update_test.go index 79d727a690..de98f32b53 100644 --- a/tests/integration/acp/relationship/add_doc_actor_test/add_doc_actor_with_update_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_update_test.go @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -package test_acp_relationship_add_docactor +package test_acp_relationship_doc_actor_add import ( "fmt" diff --git a/tests/integration/acp/relationship/doc_actor/delete/invalid_test.go b/tests/integration/acp/relationship/doc_actor/delete/invalid_test.go new file mode 100644 index 0000000000..41cb6e4921 --- /dev/null +++ b/tests/integration/acp/relationship/doc_actor/delete/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_doc_actor_delete + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_DeleteDocActorRelationshipMissingDocID_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, delete 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.DeleteDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: -1, + + Relation: "reader", + + ExpectedError: "missing a required argument needed to delete doc actor relationship.", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_DeleteDocActorRelationshipMissingCollection_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, delete 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.DeleteDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: -1, + + DocID: 0, + + Relation: "reader", + + ExpectedError: "collection name can't be empty", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_DeleteDocActorRelationshipMissingRelationName_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, delete 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.DeleteDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "", + + ExpectedError: "missing a required argument needed to delete doc actor relationship.", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_DeleteDocActorRelationshipMissingTargetActorName_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, delete 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.DeleteDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: -1, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedError: "missing a required argument needed to delete doc actor relationship.", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_DeleteDocActorRelationshipMissingReqestingIdentityName_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, delete 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.DeleteDocActorRelationship{ + RequestorIdentity: -1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedError: "missing a required argument needed to delete doc actor relationship.", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/doc_actor/delete/with_delete_test.go b/tests/integration/acp/relationship/doc_actor/delete/with_delete_test.go new file mode 100644 index 0000000000..d931a7049b --- /dev/null +++ b/tests/integration/acp/relationship/doc_actor/delete/with_delete_test.go @@ -0,0 +1,252 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_doc_actor_delete + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerRevokesDeleteWriteAccess_OtherActorCanNoLongerDelete(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner revokes write(delete) access from another actor, they can not delete anymore", + + 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, + ), + }, + + // Creating two documents because need one to do the test on after one is deleted. + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + testUtils.CreateDoc{ + Identity: immutable.Some(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad Lone", + "age": 28 + } + `, + }, + + // Give access to the other actor to delete and read both documents. + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedExistence: false, + }, + testUtils.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 1, + + Relation: "writer", + + ExpectedExistence: false, + }, + + // Now the other identity can read both and delete both of those documents + testUtils.Request{ + Identity: immutable.Some(2), // This identity can read. + + Request: ` + query { + Users { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + "age": int64(28), + }, + { + "name": "Shahzad Lone", + "age": int64(28), + }, + }, + }, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), // This identity can also delete. + + DocID: 1, + }, + + testUtils.DeleteDocActorRelationship{ // Revoke access from being able to delete (and read) the document. + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedRecordFound: true, + }, + + // The other identity can neither delete nor read the other document anymore. + testUtils.Request{ + Identity: immutable.Some(2), + + Request: ` + query { + Users { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't read the document anymore + }, + }, + + testUtils.DeleteDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", // Can't delete the document anymore. + }, + + // Ensure document was not accidentally deleted using owner identity. + testUtils.Request{ + Identity: immutable.Some(1), + + Request: ` + query { + Users { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/doc_actor/delete/with_dummy_relation_test.go b/tests/integration/acp/relationship/doc_actor/delete/with_dummy_relation_test.go new file mode 100644 index 0000000000..190850dfdd --- /dev/null +++ b/tests/integration/acp/relationship/doc_actor/delete/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_doc_actor_delete + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_DeleteDocActorRelationshipWithDummyRelationDefinedOnPolicy_NothingChanges(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, delete 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.DeleteDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "dummy", // Doesn't mean anything to the database. + + ExpectedRecordFound: 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_DeleteDocActorRelationshipWithDummyRelationNotDefinedOnPolicy_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, delete 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.DeleteDocActorRelationship{ + 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 delete 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/doc_actor/delete/with_manager_test.go b/tests/integration/acp/relationship/doc_actor/delete/with_manager_test.go new file mode 100644 index 0000000000..fd841c562a --- /dev/null +++ b/tests/integration/acp/relationship/doc_actor/delete/with_manager_test.go @@ -0,0 +1,534 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_doc_actor_delete + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_ManagerRevokesReadAccess_OtherActorCanNoLongerRead(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, manager revokes read access, other actor that can read before no longer 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{ // Owner makes admin / manager + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ // Owner gives an actor read access + RequestorIdentity: 1, + + TargetIdentity: 3, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: immutable.Some(3), // The other actor can read + + Request: ` + query { + Users { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.DeleteDocActorRelationship{ // Admin revokes access of the other actor that could read. + RequestorIdentity: 2, + + TargetIdentity: 3, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedRecordFound: true, + }, + + // The other actor can no longer read. + testUtils.Request{ + Identity: immutable.Some(3), + + Request: ` + query { + Users { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerRevokesManagersAccess_ManagerCanNoLongerManageOthers(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner revokes manager's access, manager can not longer manage others.", + + 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{ // Owner makes admin / manager + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedExistence: false, + }, + + testUtils.AddDocActorRelationship{ // Manager gives an actor read access + 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 { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.DeleteDocActorRelationship{ // Admin revokes access of the admin. + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedRecordFound: true, + }, + + testUtils.AddDocActorRelationship{ // Manager can no longer grant read access. + RequestorIdentity: 2, + + TargetIdentity: 4, // This identity has no access previously. + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedError: "failed to add document actor relationship with acp", + }, + + testUtils.Request{ + Identity: immutable.Some(4), // The other actor can ofcourse still not read. + + Request: ` + query { + Users { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_AdminTriesToRevokeOwnersAccess_NotAllowedError(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, admin tries to revoke owner's access, not allowed 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{ // Owner makes admin / manager + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedExistence: false, + }, + + testUtils.DeleteDocActorRelationship{ // Admin tries to revoke owners `owner` relation. + RequestorIdentity: 2, + + TargetIdentity: 1, + + CollectionID: 0, + + DocID: 0, + + Relation: "owner", + + ExpectedError: "cannot delete an owner relationship", + }, + + testUtils.DeleteDocActorRelationship{ // Owner can still perform owner operations, like restrict admin. + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedRecordFound: true, + }, + + testUtils.Request{ + Identity: immutable.Some(1), // The owner can still read + + Request: ` + query { + Users { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/doc_actor/delete/with_no_policy_on_collection_test.go b/tests/integration/acp/relationship/doc_actor/delete/with_no_policy_on_collection_test.go new file mode 100644 index 0000000000..3039d32e5f --- /dev/null +++ b/tests/integration/acp/relationship/doc_actor/delete/with_no_policy_on_collection_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_doc_actor_delete + +import ( + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_DeleteDocActorRelationshipWithCollectionThatHasNoPolicy_NotAllowedError(t *testing.T) { + test := testUtils.TestCase{ + + Description: "Test acp, delete 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.DeleteDocActorRelationship{ + 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/doc_actor/delete/with_public_document_test.go b/tests/integration/acp/relationship/doc_actor/delete/with_public_document_test.go new file mode 100644 index 0000000000..fa071c6806 --- /dev/null +++ b/tests/integration/acp/relationship/doc_actor/delete/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_doc_actor_delete + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_DeleteDocActorRelationshipWithPublicDocument_CanAlreadyAccess_Error(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, delete 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.DeleteDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedError: "failed to delete document actor relationship with acp", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/doc_actor/delete/with_reader_test.go b/tests/integration/acp/relationship/doc_actor/delete/with_reader_test.go new file mode 100644 index 0000000000..58b74e4dc1 --- /dev/null +++ b/tests/integration/acp/relationship/doc_actor/delete/with_reader_test.go @@ -0,0 +1,314 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_doc_actor_delete + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerRevokesReadAccessTwice_ShowThatTheRecordWasNotFoundSecondTime(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner revokes read access twice, second time is 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.AddDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.DeleteDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedRecordFound: true, + }, + + testUtils.DeleteDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedRecordFound: false, // is a no-op + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerRevokesGivenReadAccess_OtherActorCanNoLongerRead(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner revokes read access from another actor, they can not read anymore", + + 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), // 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.DeleteDocActorRelationship{ + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedRecordFound: true, + }, + + testUtils.Request{ + Identity: immutable.Some(2), // This identity can not read anymore. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents now + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/doc_actor/delete/with_self_test.go b/tests/integration/acp/relationship/doc_actor/delete/with_self_test.go new file mode 100644 index 0000000000..563359fcd4 --- /dev/null +++ b/tests/integration/acp/relationship/doc_actor/delete/with_self_test.go @@ -0,0 +1,272 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_doc_actor_delete + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_AdminTriesToRevokeItsOwnAccess_NotAllowedError(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, admin tries to revoke it's own access, not allowed 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{ // Owner makes admin / manager + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedExistence: false, + }, + + testUtils.DeleteDocActorRelationship{ // Admin tries to revoke it's own relation. + RequestorIdentity: 2, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "admin", + + ExpectedError: "failed to delete document actor relationship with acp", + }, + + testUtils.AddDocActorRelationship{ // Admin can still perform admin operations. + RequestorIdentity: 2, + + TargetIdentity: 3, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerTriesToRevokeItsOwnAccess_NotAllowedError(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner tries to revoke it's own access, not allowed 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.DeleteDocActorRelationship{ // Owner tries to revoke it's own relation. + RequestorIdentity: 1, + + TargetIdentity: 1, + + CollectionID: 0, + + DocID: 0, + + Relation: "owner", + + ExpectedError: "failed to delete document actor relationship with acp", + }, + + testUtils.AddDocActorRelationship{ // Owner can still perform admin operations. + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/doc_actor/delete/with_update_test.go b/tests/integration/acp/relationship/doc_actor/delete/with_update_test.go new file mode 100644 index 0000000000..e51edc22ca --- /dev/null +++ b/tests/integration/acp/relationship/doc_actor/delete/with_update_test.go @@ -0,0 +1,458 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package test_acp_relationship_doc_actor_delete + +import ( + "fmt" + "testing" + + "github.com/sourcenetwork/immutable" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestACP_OwnerRevokesUpdateWriteAccess_OtherActorCanNoLongerUpdate(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner revokes write(update) access from another actor, they can not update anymore", + + 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 + } + `, + }, + + // Give access to the other actor to update and read the document. + 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 update. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + }, + + // Ensure the other identity can read and update the document. + testUtils.Request{ + Identity: immutable.Some(2), // This identity can also read. + + Request: ` + query { + Users { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad Lone", + "age": int64(28), + }, + }, + }, + }, + + testUtils.DeleteDocActorRelationship{ // Revoke access from being able to update (and read) the document. + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedRecordFound: true, + }, + + // The other identity can neither update nor read the other document anymore. + testUtils.Request{ + Identity: immutable.Some(2), + + Request: ` + query { + Users { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't read the document anymore + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Update Not Possible" + } + `, + + ExpectedError: "document not found or not authorized to access", // Can't update the document anymore. + }, + + // Ensure document was not accidentally updated using owner identity. + testUtils.Request{ + Identity: immutable.Some(1), + + Request: ` + query { + Users { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad Lone", + "age": int64(28), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestACP_OwnerRevokesUpdateWriteAccess_GQL_OtherActorCanNoLongerUpdate(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner revokes write(update) access from another actor, they can not update anymore (gql)", + + SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ + // GQL mutation will return no error. + 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 + } + `, + }, + + // Give access to the other actor to update and read the document. + 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 update. + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + }, + + // Ensure the other identity can read and update the document. + testUtils.Request{ + Identity: immutable.Some(2), // This identity can also read. + + Request: ` + query { + Users { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad Lone", + "age": int64(28), + }, + }, + }, + }, + + testUtils.DeleteDocActorRelationship{ // Revoke access from being able to update (and read) the document. + RequestorIdentity: 1, + + TargetIdentity: 2, + + CollectionID: 0, + + DocID: 0, + + Relation: "writer", + + ExpectedRecordFound: true, + }, + + // The other identity can neither update nor read the other document anymore. + testUtils.Request{ + Identity: immutable.Some(2), + + Request: ` + query { + Users { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't read the document anymore + }, + }, + + testUtils.UpdateDoc{ + CollectionID: 0, + + Identity: immutable.Some(2), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Update Not Possible" + } + `, + + SkipLocalUpdateEvent: true, + }, + + // Ensure document was not accidentally updated using owner identity. + testUtils.Request{ + Identity: immutable.Some(1), + + Request: ` + query { + Users { + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "name": "Shahzad Lone", + "age": int64(28), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/utils.go b/tests/integration/utils.go index eb0128ab00..6aac10e5e4 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -343,6 +343,9 @@ func performAction( case AddDocActorRelationship: addDocActorRelationshipACP(s, action) + case DeleteDocActorRelationship: + deleteDocActorRelationshipACP(s, action) + case CreateDoc: createDoc(s, action)