Introduction

Recently, due to business needs for permission management and user authorization, I researched multiple open source solutions as well as custom-built permission management options, and ultimately found this open source user authorization solution called Cerbos.

Cerbos is developed in Go, supports both RESTful and gRPC calls, and provides SDKs for common languages. However, these weren’t the main reasons for choosing it, as other open source projects also offer similar support. What ultimately made me choose Cerbos among many open source solutions were:

  • Stateless authorization as a service
  • Support for multiple backend storage options (for storing authorization policies)
  • Simple deployment and user-friendly documentation

Comparison

During my research, I reviewed documentation and source code from many open source projects, including:

ProjectLanguageStatelessLibraryServiceDynamic Policy
OsoRustYesYesNoNo
OPAGoYesYesYesNo
OPALPythonYesNoYesYes
LadonGoYesYesNoYes
PermifyGoNoNoYesYes
OpenFGAGoNoNoYesYes
AuthzedGoNoNoYesYes
PomeriumGoNoNoYesYes
ShieldGoNoNoYesYes
SpeedleGoNoNoYesYes
WarrantGoNONoYesYes
CerbosGoYesNoYesYes

Why do I value statelessness? Because this approach is simplest for developers, eliminating the need to maintain relationships between users and resources or users and roles/permissions in other databases. Only Oso, OPA, OPAL, Ladon, and Cerbos support statelessness. Oso can only be used as a library and doesn’t support languages like PHP.

OPA can be used as a library in Go projects or as a standalone service, but it doesn’t support dynamic policy updates. However, OPA’s policy writing syntax is the most powerful and flexible!

OPAL adds a Python wrapper on top of OPA, using a Python-developed service to implement dynamic Policy file updates.

Finally, there’s Cerbos, which supports local storage, Git, SQLite, MySQL, and other common databases, but only for storing policies and role definitions! The policies can be defined in YAML or JSON format, making it much more convenient for those who want to manage policies and roles through an Admin API!

Configuration

---
server:
  cors:
    allowedOrigins:
      - example.com
    allowedHeaders:
      - X-RequestID
  adminAPI:
    enabled: true
    adminCredentials:
      username: {ADMIN_USER}
      passwordHash: {BASE64_ENCODE_HASH_PASS}
  httpListenAddr: ":3592"
  grpcListenAddr: ":3593"
  metricsEnabled: true
  logRequestPayloads: true

storage:
  driver: disk
  git:
    protocol: ssh
    url: [email protected]:services/policies.git
    subDir: policies
    branch: master
    checkoutDir: /etc/cerbos/policies
    updatePollInterval: 60s
    ssh:
      user: git
      privateKeyFile: ${HOME}/.ssh/id_rsa
  sqlite3:
    dsn: ":memory:"
  disk:
    directory: /policies
    watchForChanges: true

telemetry:
  disabled: true

engine:
  defaultPolicyVersion: "default"

auxData:
  jwt:
    keySets:
      - id: zitadel
        remote:
          url: https://passport.example.com/.well-known/openid-configuration

schema:
  enforcement: reject

Deployment

docker-compose.yaml configuration is as follows:

services:
  cerbos:
    image: ghcr.io/cerbos/cerbos:latest
    restart: always
    container_name: cerbos
    ports:
      - 0.0.0.0:3592:3592
      - 0.0.0.0:3593:3593
    command: ["server", "--config=/config/config.yaml"]
    volumes:
      - ./cerbos/config:/config
      - ./cerbos/policies:/policies

networks:
  services:
    name: services
    external: true

Writing Policies

The structure of the policies directory is as follows:

.
├── _schemas
│   └── customer.json
└── crm
    ├── principals
    └── resources
        └── customer
            ├── customer.yaml
            └── derivedRoles.yaml

The content of policies/_schemas/customer.json is as follows:

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
        "trackerID": {
            "type": "string"
        }
    },
    "required": [
        "trackerID"
    ]
}

Note: _schemas must be in the root directory of policies!!!

The content of the policies/crm/resources/customer/customer.yaml configuration file is as follows:

---
apiVersion: 'api.cerbos.dev/v1'
resourcePolicy:
  version: 'default'
  resource: 'crm:customer'
  importDerivedRoles:
    - customer_owner_role
  rules:
    - actions:
        - '*'
      roles:
        - admin
      effect: EFFECT_ALLOW

    - actions:
        - view
        - create
      roles:
        - user
        - sales
      effect: EFFECT_ALLOW

    - actions:
        - update
        - delete
        - transfer
        - tracking
      effect: EFFECT_ALLOW
      roles:
        - owner
      condition:
        match:
          any:
            of:
              - expr: request.resource.attr.trackerID == ""
              - expr: request.resource.attr.trackerID == request.principal.id
  schemas:
    resourceSchema:
      ref: "cerbos:///customer.json"

The content of policies/crm/resources/customer/derivedRoles.yaml is as follows:

---
apiVersion: "api.cerbos.dev/v1"
description: "Common dynamic roles used within the CRM"
variables:
  flagged_resource: request.resource.attr.flagged
derivedRoles:
  name: customer_owner_role
  definitions:
    - name: owner
      parentRoles: ["user"]
      condition:
        match:
          expr: request.resource.attr.trackerID == request.principal.id

The above policy restricts customer resources in the CRM:

  • ADMIN can perform all Actions
  • Regular user and sales roles can view and create customer information
  • owner can update, delete, transfer, and tracking customer information, but only if the user has no trackerID or the trackerID is their own
  • The configuration in derivedRoles.yaml allows derived roles - when a customer’s trackerID equals the user’s ID, that user has owner permissions for that customer resource

At this point, the user authorization model is complete. I’ll try to implement user authorization logic in other projects in the future!

I hope this is helpful, Happy hacking…