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

Tutorial: Writing policies for a simple photo-sharing service

Getting started

  docker pull ghcr.io/cerbos/cerbos:0.42.0
  ---
  server:
    httpListenAddr: ":3592"

storage:
    driver: "disk"
    disk:
      directory: /photo-share/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:

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:

Resources and actions

In the Apatr application, the most obvious resource hierarchy is the following:

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.

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
    definitions:
      - name: owner
        parentRoles: ["user"]
        condition:
          match:
            expr: request.resource.attr.owner == request.principal.id

- name: abuse_moderator
        parentRoles: ["moderator"]
        condition:
          match:
            expr: request.resource.attr.flagged == true

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.42.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"
    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

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.42.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.42.0 \
      server --config=/photo-share/.cerbos.yaml

Alicia trying to view her own album

echo '{
  "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
        }
      }
    }
  ]
}' | curl --silent "http://localhost:3592/api/check/resources?pretty" -d @-
{
  "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.42.0 \
      compile --tests=/photo-share/tests /photo-share/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:

schemas: resourceSchema: ref: cerbos:///album_object.json


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.

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

Invalid request

echo '{
  "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"
        }
      }
    }
  ]
}' | curl --silent "http://localhost:3592/api/check/resources?pretty" -d @-
{
  "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"
          }
        }
      }
    }
  ]
}