Tutorial: Writing policies for a simple photo-sharing service

Getting started
  • We will use Docker to run the server and the compiler.

docker pull ghcr.io/cerbos/cerbos:0.9.0
  • Create a file named config.yaml with the following contents:

---
server:
  httpListenAddr: ":3592"

storage:
  driver: "disk"
  disk:
    directory: /photo-share/policies
  • Create a directory named policies to hold the policies.

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

Album permissions matrix
Resource Action Allowed role Condition

album:object

create

user

delete

user

  • If user owns the album

moderator

  • If the album is flagged as inappropriate

share

user

  • If user owns the album

unshare

user

  • If user owns the album

view

user

  • If user owns the album

  • If the album is public

moderator

  • If the album is flagged as inappropriate

  • If the album is public

flag

user

  • If the album is public

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.9.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.9.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.9.0 \
    server --config=/photo-share/config.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.
Alicia trying to view her own album
cat <<EOF | curl --silent "http://localhost:3592/api/check?pretty" -d @-
{
  "requestId":  "test01",
  "includeMeta": true,
  "principal":  {
    "id":  "alicia",
    "policyVersion": "default",
    "roles":  ["user"]
  },
  "actions":  ["view"],
  "resource":  {
    "policyVersion": "default",
    "kind":  "album:object",
    "instances": {
      "XX125": {
        "attr":  {
          "owner":  "alicia",
          "id":  "XX125",
          "public": false,
          "flagged": false
        }
      }
    }
  }
}
EOF
{
  "requestId": "test01",
  "resourceInstances": {
    "XX125": {
      "actions": {
        "view": "EFFECT_ALLOW"
      }
    }
  },
  "meta": {
    "resourceInstances": {
      "XX125": {
        "actions": {
          "view": {
            "matchedPolicy": "resource.album_object.vdefault"
          }
        },
        "effectiveDerivedRoles": [
          "owner"
        ]
      }
    }
  }
}

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.

Writing tests to verify behaviour

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:
    kind: "album:object"
    attr:
      owner: "alicia"
      id: "XX125"
      public: false
      flagged: false

  alicia_public_album:
    kind: "album:object"
    attr:
      owner: "alicia"
      id: "XX525"
      public: true
      flagged: false

  alicia_flagged_album:
    kind: "album:object"
    attr:
      owner: "alicia"
      id: "XX666"
      public: true
      flagged: true

principals:
  alicia:
    id: "alicia"
    roles: ["user"]

  bradley:
    id: "bradley"
    roles: ["user"]

  maria:
    id: "maria"
    roles: ["moderator", "user"]

tests:
  - name: View private album
    input:
      requestId: "test01"
      actions: ["view"]
      resource: alicia_private_album
    expected:
      - principal: alicia
        actions:
          view: EFFECT_ALLOW

      - principal: bradley
        actions:
          view: EFFECT_DENY

      - principal: maria
        actions:
          view: EFFECT_DENY

  - name: View public album
    input:
      requestId: "test02"
      actions: ["view"]
      resource: alicia_public_album
    expected:
      - principal: alicia
        actions:
          view: EFFECT_ALLOW

      - principal: bradley
        actions:
          view: EFFECT_ALLOW

      - principal: maria
        actions:
          view: EFFECT_ALLOW

  - name: Delete unflagged album
    input:
      requestId: "test03"
      actions: ["delete"]
      resource: alicia_public_album
    expected:
      - principal: alicia
        actions:
          delete: EFFECT_ALLOW

      - principal: bradley
        actions:
          delete: EFFECT_DENY

      - principal: maria
        actions:
          delete: EFFECT_DENY

  - name: Delete flagged album
    input:
      requestId: "test04"
      actions: ["delete"]
      resource: alicia_flagged_album
    expected:
      - principal: alicia
        actions:
          delete: EFFECT_ALLOW

      - principal: bradley
        actions:
          delete: EFFECT_DENY

      - principal: maria
        actions:
          delete: EFFECT_ALLOW

Now run the compiler, pointing it to the tests directory.

docker run -it -v $(pwd):/photo-share ghcr.io/cerbos/cerbos:0.9.0 \
    compile --tests=/photo-share/tests /photo-share/policies
Test results
= AlbumObjectTestSuite (album_object_test.yaml)
== 'View private album' by principal 'alicia' [OK]
== 'View private album' by principal 'bradley' [OK]
== 'View private album' by principal 'maria' [OK]
== 'View public album' by principal 'alicia' [OK]
== 'View public album' by principal 'bradley' [OK]
== 'View public album' by principal 'maria' [OK]
== 'Delete unflagged album' by principal 'alicia' [OK]
== 'Delete unflagged album' by principal 'bradley' [OK]
== 'Delete unflagged album' by principal 'maria' [OK]
== 'Delete flagged album' by principal 'alicia' [OK]
== 'Delete flagged album' by principal 'bradley' [OK]
== 'Delete flagged album' by principal 'maria' [OK]

You can produce machine-readable output from the test command by using the --format=json flag.

docker run -it -v $(pwd):/photo-share ghcr.io/cerbos/cerbos:0.9.0 \
    compile --tests=/photo-share/tests --format=json /photo-share/policies

See Policy validation and testing documentation for more information.