Skip to content

Commit

Permalink
feat: Ability to relate private documents to actors (#2907)
Browse files Browse the repository at this point in the history
## Relevant issue(s)
Resolves #2762

## Description
This PR introduces the ability to make use of the `relation`s defined
within a policy to create relationships between an actor and a document
within a collection. For users sake, I have made the clients (http, and
cli) not consume the `policyID` and `resource` name but instead a
`docID` and `collection name`, since the collection will have the policy
and resource information available we can fetch that and make lives
easier for the users.

This PR also makes use of the `manages` feature we have had in our
policy. The manages essentially defines who can make the relationship
manipulation requests.

There are a lot of tests in this PR due to a lot of edge cases I wanted
to have tested specific to `manger`, and ensuring `write` and `read`
permissions don't leak (i.e. are accidently granted).

## CLI Demo
The following lets the target actor be able to now read the private
document:

```bash
defradb client acp relationship add \
--collection Users \
--docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \
--relation reader \
--actor did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \
--identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Result:
```json
{
  "ExistedAlready": false // <-------------- Indicates a new relationship was formed
}
```


### Future (out-of-scope of this PR):
- Most of write tests will split into `delete` and `update` in #2905 
- Ability to revoke or delete relation coming in #2906 
- Decide on the `can't write if no read permission` in #2992 
- Move acp logic to a shared repo:
#2980


## How has this been tested?
- Integration tests

Specify the platform(s) on which this was tested:
- Manjaro WSL2
  • Loading branch information
shahzadlone authored Oct 2, 2024
1 parent 3101c61 commit c986312
Show file tree
Hide file tree
Showing 47 changed files with 8,010 additions and 102 deletions.
203 changes: 203 additions & 0 deletions acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,209 @@ Error:

### Execute Explain example (coming soon)

### Sharing Private Documents With Others

To share a document (or grant a more restricted access) with another actor, we must add a relationship between the
actor and the document. Inorder to make the relationship we require all of the following:

1) **Target DocID**: The `docID` of the document we want to make a relationship for.
2) **Collection Name**: The name of the collection that has the `Target DocID`.
3) **Relation Name**: The type of relation (name must be defined within the linked policy on collection).
4) **Target Identity**: The identity of the actor the relationship is being made with.
5) **Requesting Identity**: The identity of the actor that is making the request.

Note:
- ACP must be available (i.e. ACP can not be disabled).
- The collection with the target document must have a valid policy and resource linked.
- The target document must be registered with ACP already (private document).
- The requesting identity MUST either be the owner OR the manager (manages the relation) of the resource.
- If the specified relation was not granted the miminum DPI permissions (read or write) within the policy,
and a relationship is formed, the subject/actor will still not be able to access (read or write) the resource.
- If the relationship already exists, then it will just be a no-op.

Consider the following policy that we have under `examples/dpi_policy/user_dpi_policy_with_manages.yml`:

```yaml
name: An Example Policy

description: A Policy

actor:
name: actor

resources:
users:
permissions:
read:
expr: owner + reader + writer

write:
expr: owner + writer

nothing:
expr: dummy

relations:
owner:
types:
- actor

reader:
types:
- actor

writer:
types:
- actor

admin:
manages:
- reader
types:
- actor

dummy:
types:
- actor
```
Add the policy:
```sh
defradb client acp policy add -f examples/dpi_policy/user_dpi_policy_with_manages.yml \
--identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Result:
```json
{
"PolicyID": "ec11b7e29a4e195f95787e2ec9b65af134718d16a2c9cd655b5e04562d1cabf9"
}
```

Add schema, linking to the users resource and our policyID:
```sh
defradb client schema add '
type Users @policy(
id: "ec11b7e29a4e195f95787e2ec9b65af134718d16a2c9cd655b5e04562d1cabf9",
resource: "users"
) {
name: String
age: Int
}
'
```

Result:
```json
[
{
"Name": "Users",
"ID": 1,
"RootID": 1,
"SchemaVersionID": "bafkreihhd6bqrjhl5zidwztgxzeseveplv3cj3fwtn3unjkdx7j2vr2vrq",
"Sources": [],
"Fields": [
{
"Name": "_docID",
"ID": 0,
"Kind": null,
"RelationName": null,
"DefaultValue": null
},
{
"Name": "age",
"ID": 1,
"Kind": null,
"RelationName": null,
"DefaultValue": null
},
{
"Name": "name",
"ID": 2,
"Kind": null,
"RelationName": null,
"DefaultValue": null
}
],
"Indexes": [],
"Policy": {
"ID": "ec11b7e29a4e195f95787e2ec9b65af134718d16a2c9cd655b5e04562d1cabf9",
"ResourceName": "users"
},
"IsMaterialized": true
}
]
```

Create a private document:
```sh
defradb client collection create --name Users '[{ "name": "SecretShahzadLone" }]' \
--identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Only the owner can see it:
```sh
defradb client collection docIDs --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Result:
```json
{
"docID": "bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c",
"error": ""
}
```

Another actor can not:
```sh
defradb client collection docIDs --identity 4d092126012ebaf56161716018a71630d99443d9d5217e9d8502bb5c5456f2c5
```

**Result is empty from the above command**


Now let's make the other actor a reader of the document by adding a relationship:
```sh
defradb client acp relationship add \
--collection Users \
--docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \
--relation reader \
--actor did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \
--identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Result:
```json
{
"ExistedAlready": false
}
```

**Note: If the same relationship is created again the `ExistedAlready` would then be true, indicating no-op**

Now the other actor can read:
```sh
defradb client collection docIDs --identity 4d092126012ebaf56161716018a71630d99443d9d5217e9d8502bb5c5456f2c5
```

Result:
```json
{
"docID": "bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c",
"error": ""
}
```

But, they still can not perform an update as they were only granted a read permission (through `reader` relation):
```sh
defradb client collection update --name Users --docID "bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c" \
--identity 4d092126012ebaf56161716018a71630d99443d9d5217e9d8502bb5c5456f2c5 '{ "name": "SecretUpdatedShahzad" }'
```

Result:
```sh
Error: document not found or not authorized to access
```

## DAC Usage HTTP:

Expand Down
16 changes: 16 additions & 0 deletions acp/acp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
31 changes: 31 additions & 0 deletions acp/acp_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading

0 comments on commit c986312

Please sign in to comment.