Tutorial: Writing policies for a simple photo-sharing service :: Cerbos Authorization Management Platform // Documentation

Tutorial: Writing policies for a simple photo-sharing service

This documentation is for
a previous
version of Cerbos. Choose 0.53.0 from the version picker at the top right or navigate to https://docs.cerbos.dev for the latest version.

Getting started

    docker pull ghcr.io/cerbos/cerbos:0.50.0
    ```

- Create a file named `.cerbos.yaml` with the following contents:

```yaml
    ---
    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

| 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<br>  <br>- If the album is public |
| `moderator` | - If the album is flagged as inappropriate<br>  <br>- 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.

```yaml
    ---
    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.

```shell
    docker run -it -v $(pwd):/photo-share ghcr.io/cerbos/cerbos:0.50.0 \
        compile /photo-share/policies
    ```

## Resource policies

Let’s write a resource policy for the `album:object` resource.

```yaml
    ---
    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.

```shell
    docker run -it -v $(pwd):/photo-share ghcr.io/cerbos/cerbos:0.50.0 compile /photo-share/policies
    ```

Let’s start the server and try out a request.

```shell
    docker run -it -v $(pwd):/photo-share -p 3592:3592 ghcr.io/cerbos/cerbos:0.50.0 \
        server --config=/photo-share/.cerbos.yaml
    ```

## Alicia trying to view her own album

```shell
    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.

```shell
    docker run -it -v $(pwd):/photo-share ghcr.io/cerbos/cerbos:0.50.0 \
        compile --tests=/photo-share/tests /photo-share/policies
    ```

Test results └──AlbumObjectTestSuite (album_object_test.yaml) [18 OK]

18 tests executed [18 OK]


## 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.

```sh
    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:

```json
    {
      "$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:

```yaml
    ---
    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.

```yaml
    ---
    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.

```shell
    docker run -it -v $(pwd):/photo-share -p 3592:3592 ghcr.io/cerbos/cerbos:0.50.0 \
        server --config=/photo-share/.cerbos.yaml
    ```

Invalid request


```shell
    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
    ```

```json
    {
      "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"\
              }\
            }\
          }\
        }\
      ]
    }
    ```