Tutorial: Writing policies for a simple photo-sharing service
This documentation is for a previous version of Cerbos. Choose 0.39.0 from the version picker at the top right or navigate to https://docs.cerbos.dev for the latest version. |
You can find all the policies and tests used in this tutorial at https://github.com/cerbos/photo-share-tutorial. |
The Apatr application
Apatr is a simple photo-sharing service that allows users to upload their photos and optionally share them with the rest of the world. Users sign-up to the service either by creating their own user account on the website or by signing-in with an identity provider (IDP) like Google or Facebook. Apatr uses a third-party identity management tool to manage these accounts and authenticating users to the site. Once they are logged-in, users can do the following:
-
Create albums to organize their photos
-
Upload photos to albums
-
Share albums or individual photos with other Apatr users
-
Share albums or individual photos with the internet
Apatr also employs a team of moderators to investigate complaints and remove any illegal or offensive items from the site. To respect users privacy, moderators are only allowed to view photos or albums that are public or those that have been flagged as inappropriate by another user.
Apatr’s identity provider allows defining roles for users. The roles currently defined in this system are:
-
moderator
: Member of the moderator team -
user
: Authenticated users
Resources and actions
In the Apatr application, the most obvious resource hierarchy is the following:
-
Album
-
Photo
-
Caption
-
Comment
-
-
Description
-
-
User Profile
Resource | Action | Allowed role | Condition |
---|---|---|---|
album:object |
|
|
|
|
|
|
|
|
|
||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||
|
|
|
Derived roles
There are some recurring themes in the above permissions matrix.
-
People who have the
user
role can be either owners or viewers depending on the resource they are trying to access -
Moderators get extra capabilities when the content is flagged as inappropriate
These capabilities are determined based on contextual information. Let’s codify them so that they can be reused.
---
apiVersion: "api.cerbos.dev/v1"
description: |-
Common dynamic roles used within the Apatr app
derivedRoles:
name: apatr_common_roles (1)
definitions:
- name: owner (2)
parentRoles: ["user"] (3)
condition:
match:
expr: request.resource.attr.owner == request.principal.id (4)
- name: abuse_moderator
parentRoles: ["moderator"]
condition:
match:
expr: request.resource.attr.flagged == true
1 | Name that we will use to import this set of roles |
2 | Descriptive name for this derived role |
3 | The static roles (from the identity provider) to which this derived role applies to |
4 | An expression that is applied to the request to determine when this role becomes activated |
Save the above definition as apatr_common_roles.yaml
in the policies
directory.
Run the compiler to make sure that the contents of the file are valid.
docker run -it -v $(pwd):/photo-share ghcr.io/cerbos/cerbos:0.36.0 \
compile /photo-share/policies
Resource policies
Let’s write a resource policy for the album:object
resource.
---
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: "default" (1)
importDerivedRoles:
- apatr_common_roles (2)
resource: "album:object"
rules:
- actions: ['*']
effect: EFFECT_ALLOW
derivedRoles:
- owner
- actions: ['view', 'flag']
effect: EFFECT_ALLOW
roles:
- user
condition:
match:
expr: request.resource.attr.public == true
- actions: ['view', 'delete']
effect: EFFECT_ALLOW
derivedRoles:
- abuse_moderator
1 | You can have multiple policy versions for the same resource (e.g. production vs. staging). If the request does not explicitly specify the version, the default policy takes effect. |
2 | Import the roles we defined earlier |
Save the above policy definition as resource_album_object.yaml
inside the policies
directory.
Run the compiler to make sure that the contents of the policies directory are valid.
docker run -it -v $(pwd):/photo-share ghcr.io/cerbos/cerbos:0.36.0 compile /photo-share/policies
Let’s start the server and try out a request.
docker run -it -v $(pwd):/photo-share -p 3592:3592 ghcr.io/cerbos/cerbos:0.36.0 \
server --config=/photo-share/.cerbos.yaml
If you like to use Postman, Insomnia or any other software that supports OpenAPI, the Cerbos OpenAPI definitions can be downloaded by accessing http://localhost:3592/schema/swagger.json. |
cat <<EOF | curl --silent "http://localhost:3592/api/check/resources?pretty" -d @-
{
"requestId": "test01",
"includeMeta": true,
"principal": {
"id": "alicia",
"policyVersion": "default",
"roles": [
"user"
]
},
"resources": [
{
"actions": [
"view"
],
"resource": {
"id": "XX125",
"policyVersion": "default",
"kind": "album:object",
"attr": {
"owner": "alicia",
"public": false,
"flagged": false
}
}
}
]
}
EOF
{ "requestId": "test01", "results": [ { "resource": { "id": "XX125", "kind": "album:object", "policyVersion": "default" }, "actions": { "view": "EFFECT_ALLOW" }, "meta": { "actions": { "view": { "matchedPolicy": "resource.album_object.vdefault" } }, "effectiveDerivedRoles": [ "owner" ] } } ] }
Writing tests to verify behaviour
It’s not practical to start the server and manually make requests every time a policy is updated. So let’s write some tests instead.
Create a new directory named tests
and create a file named album_object_test.yaml
with the following contents.
---
name: AlbumObjectTestSuite
description: Tests for verifying the album:object resource policy
resources:
alicia_private_album:
id: "XX125"
kind: "album:object"
attr:
owner: "alicia"
public: false
flagged: false
alicia_public_album:
id: "XX525"
kind: "album:object"
attr:
owner: "alicia"
public: true
flagged: false
alicia_flagged_album:
id: "XX666"
kind: "album:object"
attr:
owner: "alicia"
public: true
flagged: true
principals:
alicia:
id: "alicia"
roles: ["user"]
bradley:
id: "bradley"
roles: ["user"]
maria:
id: "maria"
roles: ["moderator", "user"]
tests:
- name: View album
input: &testInput
principals:
- alicia
- bradley
- maria
actions:
- view
resources:
- alicia_private_album
- alicia_public_album
- alicia_flagged_album
expected:
- &viewExp
principal: alicia
resource: alicia_private_album
actions:
view: EFFECT_ALLOW
- <<: *viewExp
resource: alicia_public_album
- <<: *viewExp
resource: alicia_flagged_album
- <<: *viewExp
principal: bradley
resource: alicia_public_album
- <<: *viewExp
principal: bradley
resource: alicia_flagged_album
- <<: *viewExp
principal: maria
resource: alicia_public_album
- <<: *viewExp
principal: maria
resource: alicia_flagged_album
- name: Delete album
input:
<<: *testInput
actions:
- delete
expected:
- &deleteExp
principal: alicia
resource: alicia_private_album
actions:
delete: EFFECT_ALLOW
- <<: *deleteExp
resource: alicia_public_album
- <<: *deleteExp
resource: alicia_flagged_album
- <<: *deleteExp
principal: maria
resource: alicia_flagged_album
Now run the compiler, pointing it to the tests
directory.
docker run -it -v $(pwd):/photo-share ghcr.io/cerbos/cerbos:0.36.0 \
compile --tests=/photo-share/tests /photo-share/policies
Test results └──AlbumObjectTestSuite (album_object_test.yaml) [18 OK] 18 tests executed [18 OK]
See https://github.com/cerbos/photo-share-tutorial for an example of using Cerbos GitHub Actions in a CI workflow to compile and test policies. |
Using schemas to enforce type safety [Optional]
The derived roles and resource policy rules we defined above rely on certain attributes being present in the attr
sections of the incoming request. To ensure that API requests are strictly-typed and contain required attributes, we can define schemas for the principal and resource attributes sections.
Create a new directory named _schemas
inside the policies
directory.
mkdir policies/_schemas
Let’s add a JSON schema defining the data types and required fields for album:object
resources. Create a file named album_object.json
inside the policies/_schemas
directory with the following contents:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"owner": {
"type": "string"
},
"public": {
"type": "boolean"
},
"flagged": {
"type": "boolean"
}
},
"required": [
"owner"
]
}
Now update policies/resource_album_object.yaml
to add the reference to the schema:
---
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: "default"
importDerivedRoles:
- apatr_common_roles
resource: "album:object"
rules:
- actions: ['*']
effect: EFFECT_ALLOW
derivedRoles:
- owner
- actions: ['view', 'flag']
effect: EFFECT_ALLOW
roles:
- user
condition:
match:
expr: request.resource.attr.public == true
- actions: ['view', 'delete']
effect: EFFECT_ALLOW
derivedRoles:
- abuse_moderator
schemas:
resourceSchema:
ref: cerbos:///album_object.json (1)
1 | Defines the schema to use for validating resource attributes |
Update .cerbos.yaml
to enable schema enforcement.
---
server:
httpListenAddr: ":3592"
storage:
driver: "disk"
disk:
directory: /photo-share/policies
schema:
enforcement: reject
Now start the server again and send a request that does not conform to the schema. The server response should contain a list of validation errors.
docker run -it -v $(pwd):/photo-share -p 3592:3592 ghcr.io/cerbos/cerbos:0.36.0 \
server --config=/photo-share/.cerbos.yaml
cat <<EOF | curl --silent "http://localhost:3592/api/check/resources?pretty" -d @-
{
"requestId": "test02",
"includeMeta": true,
"principal": {
"id": "alicia",
"policyVersion": "default",
"roles": [
"user"
]
},
"resources": [
{
"actions": [
"view"
],
"resource": {
"id": "XX125",
"policyVersion": "default",
"kind": "album:object",
"attr": {
"public": "false",
"flagged": "false"
}
}
}
]
}
EOF
{ "requestId": "test02", "results": [ { "resource": { "id": "XX125", "kind": "album:object", "policyVersion": "default" }, "actions": { "view": "EFFECT_DENY" }, "validationErrors": [ { "message": "missing properties: 'owner'", "source": "SOURCE_RESOURCE" }, { "path": "/public", "message": "expected boolean, but got string", "source": "SOURCE_RESOURCE" }, { "path": "/flagged", "message": "expected boolean, but got string", "source": "SOURCE_RESOURCE" } ], "meta": { "actions": { "view": { "matchedPolicy": "resource.album_object.vdefault" } } } } ] }