From 568a7c8a96b4cfff1c41d0fe9d0d94a12e7f1a4d Mon Sep 17 00:00:00 2001 From: Shahzad Lone Date: Wed, 7 Aug 2024 04:42:54 -0400 Subject: [PATCH] PR(MAIN): Implement local & sourcehub acp doc sharing --- acp/acp.go | 16 ++++ acp/acp_local.go | 31 +++++++ acp/acp_source_hub.go | 50 +++++++++++ acp/errors.go | 70 ++++++++++++--- acp/source_hub_client.go | 100 +++++++++++++++++++++- cli/acp_relationship.go | 25 ++++++ cli/acp_relationship_add.go | 157 ++++++++++++++++++++++++++++++++++ cli/cli.go | 6 ++ client/db.go | 17 ++++ client/errors.go | 37 ++++---- http/client_acp.go | 50 +++++++++++ http/handler_acp.go | 46 ++++++++++ internal/db/db.go | 49 ++++++++++- tests/clients/cli/wrapper.go | 28 ++++++ tests/clients/http/wrapper.go | 16 ++++ 15 files changed, 666 insertions(+), 32 deletions(-) create mode 100644 cli/acp_relationship.go create mode 100644 cli/acp_relationship_add.go diff --git a/acp/acp.go b/acp/acp.go index 973181ae91..c7ae5936e6 100644 --- a/acp/acp.go +++ b/acp/acp.go @@ -99,6 +99,22 @@ type ACP interface { docID string, ) (bool, error) + // AddDocActorRelationship creates a relationship between document and the target actor. + // + // If failure occurs, the result will return an error. Upon success the boolean value will + // be true if the relationship already existed (no-op), and false if a new relationship was made. + // + // Note: The request actor must either be the owner or manager of the document. + AddDocActorRelationship( + ctx context.Context, + policyID string, + resourceName string, + docID string, + relation string, + requestActor identity.Identity, + targetActor string, + ) (bool, error) + // SupportsP2P returns true if the implementation supports ACP across a peer network. SupportsP2P() bool } diff --git a/acp/acp_local.go b/acp/acp_local.go index 97e7a67cce..6e85ac9313 100644 --- a/acp/acp_local.go +++ b/acp/acp_local.go @@ -236,3 +236,34 @@ func (l *ACPLocal) VerifyAccessRequest( return resp.Valid, nil } + +func (l *ACPLocal) AddActorRelationship( + ctx context.Context, + policyID string, + resourceName string, + objectID string, + relation string, + requester identity.Identity, + targetActor string, + creationTime *protoTypes.Timestamp, +) (bool, error) { + principal, err := auth.NewDIDPrincipal(requester.DID) + if err != nil { + return false, newErrInvalidActorID(err, requester.DID) + } + + ctx = auth.InjectPrincipal(ctx, principal) + + setRelationshipRequest := types.SetRelationshipRequest{ + PolicyId: policyID, + Relationship: types.NewActorRelationship(resourceName, objectID, relation, targetActor), + CreationTime: creationTime, + } + + setRelationshipResponse, err := l.engine.SetRelationship(ctx, &setRelationshipRequest) + if err != nil { + return false, err + } + + return setRelationshipResponse.RecordExisted, nil +} diff --git a/acp/acp_source_hub.go b/acp/acp_source_hub.go index 4dfb26c090..d0c4fb6b89 100644 --- a/acp/acp_source_hub.go +++ b/acp/acp_source_hub.go @@ -261,3 +261,53 @@ func (a *acpSourceHub) VerifyAccessRequest( func (a *acpSourceHub) Close() error { return nil } + +func (a *acpSourceHub) AddActorRelationship( + ctx context.Context, + policyID string, + resourceName string, + objectID string, + relation string, + requester identity.Identity, + targetActor string, + creationTime *protoTypes.Timestamp, +) (bool, error) { + msgSet := sourcehub.MsgSet{} + cmdMapper := msgSet.WithBearerPolicyCmd(&acptypes.MsgBearerPolicyCmd{ + Creator: a.signer.GetAccAddress(), + BearerToken: requester.BearerToken, + PolicyId: policyID, + Cmd: acptypes.NewSetRelationshipCmd( + acptypes.NewActorRelationship( + resourceName, + objectID, + relation, + targetActor, + ), + ), + CreationTime: creationTime, + }) + tx, err := a.txBuilder.Build(ctx, a.signer, &msgSet) + if err != nil { + return false, err + } + resp, err := a.client.BroadcastTx(ctx, tx) + if err != nil { + return false, err + } + + result, err := a.client.AwaitTx(ctx, resp.TxHash) + if err != nil { + return false, err + } + if result.Error() != nil { + return false, result.Error() + } + + cmdResult, err := cmdMapper.Map(result.TxPayload()) + if err != nil { + return false, err + } + + return cmdResult.GetResult().GetSetRelationshipResult().RecordExisted, nil +} diff --git a/acp/errors.go b/acp/errors.go index 5ff4eee302..4511dabcc6 100644 --- a/acp/errors.go +++ b/acp/errors.go @@ -15,12 +15,14 @@ import ( ) const ( - errInitializationOfACPFailed = "initialization of acp failed" - errStartingACPInEmptyPath = "starting acp in an empty path" - errFailedToAddPolicyWithACP = "failed to add policy with acp" - errFailedToRegisterDocWithACP = "failed to register document with acp" - errFailedToCheckIfDocIsRegisteredWithACP = "failed to check if doc is registered with acp" - errFailedToVerifyDocAccessWithACP = "failed to verify doc access with acp" + errInitializationOfACPFailed = "initialization of acp failed" + errStartingACPInEmptyPath = "starting acp in an empty path" + errFailedToAddPolicyWithACP = "failed to add policy with acp" + errFailedToRegisterDocWithACP = "failed to register document with acp" + errFailedToCheckIfDocIsRegisteredWithACP = "failed to check if doc is registered with acp" + errFailedToVerifyDocAccessWithACP = "failed to verify doc access with acp" + errFailedToAddDocActorRelationshipWithACP = "failed to add document actor relationship with acp" + errMissingRequiredArgToAddDocActorRelationship = "missing a required argument needed to add doc actor relationship" errObjectDidNotRegister = "no-op while registering object (already exists or error) with acp" errNoPolicyArgs = "missing policy arguments, must have both id and resource" @@ -40,12 +42,13 @@ const ( ) var ( - ErrInitializationOfACPFailed = errors.New(errInitializationOfACPFailed) - ErrFailedToAddPolicyWithACP = errors.New(errFailedToAddPolicyWithACP) - ErrFailedToRegisterDocWithACP = errors.New(errFailedToRegisterDocWithACP) - ErrFailedToCheckIfDocIsRegisteredWithACP = errors.New(errFailedToCheckIfDocIsRegisteredWithACP) - ErrFailedToVerifyDocAccessWithACP = errors.New(errFailedToVerifyDocAccessWithACP) - ErrPolicyDoesNotExistWithACP = errors.New(errPolicyDoesNotExistWithACP) + ErrInitializationOfACPFailed = errors.New(errInitializationOfACPFailed) + ErrFailedToAddPolicyWithACP = errors.New(errFailedToAddPolicyWithACP) + ErrFailedToRegisterDocWithACP = errors.New(errFailedToRegisterDocWithACP) + ErrFailedToCheckIfDocIsRegisteredWithACP = errors.New(errFailedToCheckIfDocIsRegisteredWithACP) + ErrFailedToVerifyDocAccessWithACP = errors.New(errFailedToVerifyDocAccessWithACP) + ErrFailedToAddDocActorRelationshipWithACP = errors.New(errFailedToAddDocActorRelationshipWithACP) + ErrPolicyDoesNotExistWithACP = errors.New(errPolicyDoesNotExistWithACP) ErrResourceDoesNotExistOnTargetPolicy = errors.New(errResourceDoesNotExistOnTargetPolicy) @@ -139,6 +142,29 @@ func NewErrFailedToVerifyDocAccessWithACP( ) } +func NewErrFailedToAddDocActorRelationshipWithACP( + inner error, + Type string, + policyID string, + resourceName string, + docID string, + relation string, + requestActor string, + targetActor string, +) error { + return errors.Wrap( + errFailedToAddDocActorRelationshipWithACP, + inner, + errors.NewKV("Type", Type), + errors.NewKV("PolicyID", policyID), + errors.NewKV("ResourceName", resourceName), + errors.NewKV("DocID", docID), + errors.NewKV("Relation", relation), + errors.NewKV("RequestActor", requestActor), + errors.NewKV("TargetActor", targetActor), + ) +} + func newErrPolicyDoesNotExistWithACP( inner error, policyID string, @@ -209,6 +235,26 @@ func newErrExprOfRequiredPermissionHasInvalidChar( ) } +func NewErrMissingRequiredArgToAddDocActorRelationship( + policyID string, + resourceName string, + docID string, + relation string, + requestActor string, + targetActor string, +) error { + return errors.New( + errMissingRequiredArgToAddDocActorRelationship, + errors.NewKV("PolicyID", policyID), + errors.NewKV("ResourceName", resourceName), + errors.NewKV("DocID", docID), + errors.NewKV("Relation", relation), + errors.NewKV("RequestActor", requestActor), + errors.NewKV("TargetActor", targetActor), + ) +} + + func newErrInvalidActorID( inner error, id string, diff --git a/acp/source_hub_client.go b/acp/source_hub_client.go index 0bf344afb8..61cd7bb10b 100644 --- a/acp/source_hub_client.go +++ b/acp/source_hub_client.go @@ -85,6 +85,27 @@ type sourceHubClient interface { docID string, ) (bool, error) + // AddActorRelationship creates a relationship within a policy which ties the target actor + // with the specified object, which means that the set of high level rules defined in the + // policy will now apply to target actor as well. + // + // If failure occurs, the result will return an error. Upon success the boolean value will + // be true if the relationship with actor already existed (no-op), and false if a new + // relationship was made. + // + // Note: The requester identity must either be the owner of the object (being shared) or + // the manager (i.e. the relation has `manages` defined in the policy). + AddActorRelationship( + ctx context.Context, + policyID string, + resourceName string, + objectID string, + relation string, + requester identity.Identity, + targetActor string, + creationTime *protoTypes.Timestamp, + ) (bool, error) + // Close closes any resources in use by acp. Close() error } @@ -129,7 +150,7 @@ func (a *sourceHubBridge) Start(ctx context.Context) error { func (a *sourceHubBridge) AddPolicy(ctx context.Context, creator identity.Identity, policy string) (string, error) { // Having a creator identity is a MUST requirement for adding a policy. - if creator.DID == "" { + if creator.DID == "" { // TODO-ACP: FIX TO CHECK ALL THE Identity is not empty return "", ErrPolicyCreatorMustNotBeEmpty } @@ -335,6 +356,83 @@ func (a *sourceHubBridge) CheckDocAccess( } } +func (a *sourceHubBridge) AddDocActorRelationship( + ctx context.Context, + policyID string, + resourceName string, + docID string, + relation string, + requestActor identity.Identity, + targetActor string, +) (bool, error) { + // Must have all required args. + if policyID == "" || + resourceName == "" || + docID == "" || + relation == "" || + requestActor == (identity.Identity{}) || + targetActor == "" { + return false, NewErrMissingRequiredArgToAddDocActorRelationship( + policyID, + resourceName, + docID, + relation, + requestActor.DID, + targetActor, + ) + } + + exists, err := a.client.AddActorRelationship( + ctx, + policyID, + resourceName, + docID, + relation, + requestActor, + targetActor, + protoTypes.TimestampNow(), + ) + + if err != nil { + return false, NewErrFailedToAddDocActorRelationshipWithACP( + err, + "Local", + policyID, + resourceName, + docID, + relation, + requestActor.DID, + targetActor, + ) + } + + if exists { + log.InfoContext( + ctx, + "Document and actor already have that relationship [no-op]", + 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), + ) + return true, nil + } else { + log.InfoContext( + ctx, + "Document and actor have now formed that relationship", + 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), + ) + return false, nil + } +} + func (a *sourceHubBridge) SupportsP2P() bool { _, ok := a.client.(*acpSourceHub) return ok diff --git a/cli/acp_relationship.go b/cli/acp_relationship.go new file mode 100644 index 0000000000..9965d457a7 --- /dev/null +++ b/cli/acp_relationship.go @@ -0,0 +1,25 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package cli + +import ( + "github.com/spf13/cobra" +) + +func MakeACPRelationshipCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "relation", + Short: "Interact with the acp relationship features of DefraDB instance", + Long: `Interact with the acp relationship features of DefraDB instance`, + } + + return cmd +} diff --git a/cli/acp_relationship_add.go b/cli/acp_relationship_add.go new file mode 100644 index 0000000000..6a233e04ea --- /dev/null +++ b/cli/acp_relationship_add.go @@ -0,0 +1,157 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package cli + +import ( + "github.com/spf13/cobra" +) + +func MakeACPRelationshipAddCommand() *cobra.Command { + const ( + collectionFlagLong string = "collection" + collectionFlagShort string = "c" + + relationFlagLong string = "relation" + relationFlagShort string = "r" + + targetActorFlagLong string = "actor" + targetActorFlagShort string = "a" + + docIDFlag string = "docID" + ) + + var ( + collectionArg string + relationArg string + targetActorArg string + docIDArg string + ) + + var cmd = &cobra.Command{ + Use: "add [-i --identity] [policy]", + Short: "Add new relationship", + Long: `Add new relationship + +Notes: + - ACP must be available (i.e. ACP can not be disabled). + - The target document must be registered with ACP already (policy & resource specified). + - The requesting identity MUST either be the owner OR the manager (manages the relation) of the resource. + - If the specified relation was not granted the miminum DPI permissions (read or write) within the policy, + and a relationship is formed, the subject/actor will still not be able to access (read or write) the resource. + - Learn more about [ACP & DPI Rules](/acp/README.md) + +Consider the following 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 +' + +defradb client ... --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac + + +Example: Let another actor read my private document: + defradb client acp relationship add --collection User --docID bae-91171025-ed21-50e3-b0dc-e31bccdfa1ab \ + --relation reader --actor did:key:z6MkkHsQbp3tXECqmUJoCJwyuxSKn1BDF1RHzwDGg9tHbXKw \ + --identity 028d53f37a19afb9a0dbc5b4be30c65731479ee8cfa0c9bc8f8bf198cc3c075f + +Example: Create a dummy relation that doesn't do anything (from database prespective): + defradb client acp relationship add -c User --docID bae-91171025-ed21-50e3-b0dc-e31bccdfa1ab -r dummy \ + -a did:key:z6MkkHsQbp3tXECqmUJoCJwyuxSKn1BDF1RHzwDGg9tHbXKw \ + -i 028d53f37a19afb9a0dbc5b4be30c65731479ee8cfa0c9bc8f8bf198cc3c075f + +`, + RunE: func(cmd *cobra.Command, args []string) error { + db := mustGetContextDB(cmd) + exists, err := db.AddDocActorRelationship( + cmd.Context(), + collectionArg, + docIDArg, + relationArg, + targetActorArg, + ) + + if err != nil { + return err + } + + return writeJSON(cmd, exists) + }, + } + + cmd.Flags().StringVarP( + &collectionArg, + collectionFlagLong, + collectionFlagShort, + "", + "Collection that has the resource and policy for object", + ) + cmd.MarkFlagRequired(collectionFlagLong) + + cmd.Flags().StringVarP( + &relationArg, + relationFlagLong, + relationFlagShort, + "", + "Relation that needs to be set for the relationship", + ) + cmd.MarkFlagRequired(relationFlagLong) + + cmd.Flags().StringVarP( + &targetActorArg, + targetActorFlagLong, + targetActorFlagShort, + "", + "Actor to add relationship with", + ) + cmd.MarkFlagRequired(targetActorFlagLong) + + cmd.Flags().StringVarP( + &docIDArg, + docIDFlag, + "", + "", + "Document Identifier (ObjectID) to make relationship for", + ) + cmd.MarkFlagRequired(docIDFlag) + + return cmd +} diff --git a/cli/cli.go b/cli/cli.go index 9a95bb1bd8..6fbe706891 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -67,9 +67,15 @@ func NewDefraCommand() *cobra.Command { MakeACPPolicyAddCommand(), ) + acp_relationship := MakeACPRelationshipCommand() + acp_relationship.AddCommand( + MakeACPRelationshipAddCommand(), + ) + acp := MakeACPCommand() acp.AddCommand( acp_policy, + acp_relationship, ) view := MakeViewCommand() diff --git a/client/db.go b/client/db.go index ad2229cdb0..f5c6defc51 100644 --- a/client/db.go +++ b/client/db.go @@ -99,6 +99,23 @@ type DB interface { // // Note: A policy can not be added without the creatorID (identity). AddPolicy(ctx context.Context, policy string) (AddPolicyResult, error) + + // AddDocActorRelationship creates a relationship between document and the target actor. + // + // Moreover, since collection has the policy and resource information we don't need to + // explicitly have the user provide them as arguments. + // + // If failure occurs, the result will return an error. Upon success the boolean value will + // be true if the relationship already existed (no-op), and false if a new relationship was made. + // + // Note: The request actor must either be the owner or manager of the document. + AddDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, + ) (bool, error) } // Store contains the core DefraDB read-write operations. diff --git a/client/errors.go b/client/errors.go index dac8ebcc87..ec426ec47d 100644 --- a/client/errors.go +++ b/client/errors.go @@ -39,24 +39,25 @@ const ( // This list is incomplete and undefined errors may also be returned. // Errors returned from this package may be tested against these errors with errors.Is. var ( - ErrFieldNotExist = errors.New(errFieldNotExist) - ErrUnexpectedType = errors.New(errUnexpectedType) - ErrFailedToUnmarshalCollection = errors.New(errFailedToUnmarshalCollection) - ErrOperationNotPermittedOnNamelessCols = errors.New(errOperationNotPermittedOnNamelessCols) - ErrFieldNotObject = errors.New("trying to access field on a non object type") - ErrValueTypeMismatch = errors.New("value does not match indicated type") - ErrDocumentNotFoundOrNotAuthorized = errors.New("document not found or not authorized to access") - ErrPolicyAddFailureNoACP = errors.New("failure adding policy because ACP was not available") - ErrInvalidUpdateTarget = errors.New("the target document to update is of invalid type") - ErrInvalidUpdater = errors.New("the updater of a document is of invalid type") - ErrInvalidDeleteTarget = errors.New("the target document to delete is of invalid type") - ErrMalformedDocID = errors.New("malformed document ID, missing either version or cid") - ErrInvalidDocIDVersion = errors.New("invalid document ID version") - ErrInvalidJSONPayload = errors.New(errInvalidJSONPayload) - ErrCanNotNormalizeValue = errors.New(errCanNotNormalizeValue) - ErrCanNotTurnNormalValueIntoArray = errors.New(errCanNotTurnNormalValueIntoArray) - ErrCanNotMakeNormalNilFromFieldKind = errors.New(errCanNotMakeNormalNilFromFieldKind) - ErrCollectionNotFound = errors.New(errCollectionNotFound) + ErrFieldNotExist = errors.New(errFieldNotExist) + ErrUnexpectedType = errors.New(errUnexpectedType) + ErrFailedToUnmarshalCollection = errors.New(errFailedToUnmarshalCollection) + ErrOperationNotPermittedOnNamelessCols = errors.New(errOperationNotPermittedOnNamelessCols) + ErrFieldNotObject = errors.New("trying to access field on a non object type") + ErrValueTypeMismatch = errors.New("value does not match indicated type") + ErrDocumentNotFoundOrNotAuthorized = errors.New("document not found or not authorized to access") + ErrACPOperationButACPNotAvailable = errors.New("operation requires ACP, but ACP not available") + ErrACPOperationButCollectionHasNoPolicy = errors.New("ACP operation, but collection has no policy") + ErrInvalidUpdateTarget = errors.New("the target document to update is of invalid type") + ErrInvalidUpdater = errors.New("the updater of a document is of invalid type") + ErrInvalidDeleteTarget = errors.New("the target document to delete is of invalid type") + ErrMalformedDocID = errors.New("malformed document ID, missing either version or cid") + ErrInvalidDocIDVersion = errors.New("invalid document ID version") + ErrInvalidJSONPayload = errors.New(errInvalidJSONPayload) + ErrCanNotNormalizeValue = errors.New(errCanNotNormalizeValue) + ErrCanNotTurnNormalValueIntoArray = errors.New(errCanNotTurnNormalValueIntoArray) + ErrCanNotMakeNormalNilFromFieldKind = errors.New(errCanNotMakeNormalNilFromFieldKind) + ErrCollectionNotFound = errors.New(errCollectionNotFound) ) // NewErrFieldNotExist returns an error indicating that the given field does not exist. diff --git a/http/client_acp.go b/http/client_acp.go index a0140cf437..64041d03fc 100644 --- a/http/client_acp.go +++ b/http/client_acp.go @@ -11,7 +11,9 @@ package http import ( + "bytes" "context" + "encoding/json" "net/http" "strings" @@ -42,3 +44,51 @@ func (c *Client) AddPolicy( return policyResult, nil } + +type addDocActorRelationshipRequest struct { + CollectionName string + DocID string + Relation string + TargetActor string +} + +func (c *Client) AddDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, +) (bool, error) { + methodURL := c.http.baseURL.JoinPath("acp", "relationship") + + body, err := json.Marshal( + addDocActorRelationshipRequest{ + CollectionName: collectionName, + DocID: docID, + Relation: relation, + TargetActor: targetActor, + }, + ) + + if err != nil { + return false, err + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + methodURL.String(), + bytes.NewBuffer(body), + ) + + if err != nil { + return false, err + } + + var exists bool + if err := c.http.requestJson(req, &exists); err != nil { + return false, err + } + + return exists, nil +} diff --git a/http/handler_acp.go b/http/handler_acp.go index c3c5985c71..d9239ef2c4 100644 --- a/http/handler_acp.go +++ b/http/handler_acp.go @@ -46,6 +46,36 @@ func (s *acpHandler) AddPolicy(rw http.ResponseWriter, req *http.Request) { responseJSON(rw, http.StatusOK, addPolicyResult) } +func (s *acpHandler) AddDocActorRelationship(rw http.ResponseWriter, req *http.Request) { + db, ok := req.Context().Value(dbContextKey).(client.DB) + if !ok { + responseJSON(rw, http.StatusBadRequest, errorResponse{NewErrFailedToGetContext("db")}) + return + } + + var message addDocActorRelationshipRequest + err := requestJSON(req, &message) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + + exists, err := db.AddDocActorRelationship( + req.Context(), + message.CollectionName, + message.DocID, + message.Relation, + message.TargetActor, + ) + if err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + + // rw.WriteHeader(http.StatusOK) + responseJSON(rw, http.StatusOK, exists) +} + func (h *acpHandler) bindRoutes(router *Router) { successResponse := &openapi3.ResponseRef{ Ref: "#/components/responses/success", @@ -69,5 +99,21 @@ func (h *acpHandler) bindRoutes(router *Router) { Value: acpAddPolicyRequest, } + acpAddDocActorRelationshipRequest := openapi3.NewRequestBody(). + WithRequired(true). + WithContent(openapi3.NewContentWithSchema(openapi3.NewStringSchema(), []string{"text/plain"})) + + acpAddDocActorRelationship := openapi3.NewOperation() + acpAddDocActorRelationship.OperationID = "add relationship" + acpAddDocActorRelationship.Description = "Add an actor relationship using acp system" + acpAddDocActorRelationship.Tags = []string{"acp_relationship"} + acpAddDocActorRelationship.Responses = openapi3.NewResponses() + acpAddDocActorRelationship.Responses.Set("200", successResponse) + acpAddDocActorRelationship.Responses.Set("400", errorResponse) + acpAddDocActorRelationship.RequestBody = &openapi3.RequestBodyRef{ + Value: acpAddDocActorRelationshipRequest, + } + router.AddRoute("/acp/policy", http.MethodPost, acpAddPolicy, h.AddPolicy) + router.AddRoute("/acp/relationship", http.MethodPost, acpAddDocActorRelationship, h.AddDocActorRelationship) } diff --git a/internal/db/db.go b/internal/db/db.go index 81ec48e199..96ebd1bfd5 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -31,6 +31,7 @@ import ( "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/event" "github.com/sourcenetwork/defradb/internal/core" + "github.com/sourcenetwork/defradb/internal/db/permission" "github.com/sourcenetwork/defradb/internal/request/graphql" ) @@ -180,8 +181,9 @@ func (db *db) AddPolicy( policy string, ) (client.AddPolicyResult, error) { if !db.acp.HasValue() { - return client.AddPolicyResult{}, client.ErrPolicyAddFailureNoACP + return client.AddPolicyResult{}, client.ErrACPOperationButACPNotAvailable } + identity := GetContextIdentity(ctx) policyID, err := db.acp.Value().AddPolicy( @@ -196,6 +198,51 @@ func (db *db) AddPolicy( return client.AddPolicyResult{PolicyID: policyID}, nil } +func (db *db) AddDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, +) (bool, error) { + if !db.acp.HasValue() { + return false, client.ErrACPOperationButACPNotAvailable + } + + if collectionName == "" { + return false, client.ErrOperationNotPermittedOnNamelessCols + } + + collection, err := db.GetCollectionByName(ctx, collectionName) + if err != nil { + return false, err + } + + policyID, resourceName, hasPolicy := permission.IsPermissioned(collection) + if !hasPolicy { + return false, client.ErrACPOperationButCollectionHasNoPolicy + } + + identity := GetContextIdentity(ctx) + + exists, err := db.acp.Value().AddDocActorRelationship( + ctx, + policyID, + resourceName, + docID, + relation, + identity.Value(), + targetActor, + ) + + if err != nil { + return false, err + } + + return exists, nil + +} + // Initialize is called when a database is first run and creates all the db global meta data // like Collection ID counters. func (db *db) initialize(ctx context.Context) error { diff --git a/tests/clients/cli/wrapper.go b/tests/clients/cli/wrapper.go index 14e4df7cc4..3ecb58f08d 100644 --- a/tests/clients/cli/wrapper.go +++ b/tests/clients/cli/wrapper.go @@ -195,6 +195,34 @@ func (w *Wrapper) AddPolicy( return addPolicyResult, err } +func (w *Wrapper) AddDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, +) (bool, error) { + args := []string{ + "client", "acp", "relationship", "add", + "--collection", collectionName, + "--docID", docID, + "--relation", relation, + "--actor", targetActor, + } + + data, err := w.cmd.execute(ctx, args) + if err != nil { + return false, err + } + + var exists bool + if err := json.Unmarshal(data, &exists); err != nil { + return false, err + } + + return exists, err +} + func (w *Wrapper) AddSchema(ctx context.Context, schema string) ([]client.CollectionDescription, error) { args := []string{"client", "schema", "add"} args = append(args, schema) diff --git a/tests/clients/http/wrapper.go b/tests/clients/http/wrapper.go index 76e31d9cbd..239af77653 100644 --- a/tests/clients/http/wrapper.go +++ b/tests/clients/http/wrapper.go @@ -105,6 +105,22 @@ func (w *Wrapper) AddPolicy( return w.client.AddPolicy(ctx, policy) } +func (w *Wrapper) AddDocActorRelationship( + ctx context.Context, + collectionName string, + docID string, + relation string, + targetActor string, +) (bool, error) { + return w.client.AddDocActorRelationship( + ctx, + collectionName, + docID, + relation, + targetActor, + ) +} + func (w *Wrapper) PatchSchema( ctx context.Context, patch string,