前言

最近因为业务中需要用到权限管理和用户授权,我调研了多个开源解决方案,以及自建权限管理的方案,最终找到了这款开源的用户授权解决方案 Cerbos

Cerbos 基于 Go 语言开发,支持 RESTful 和 gRPC 方式调用,并且提供了常见语言的 SDK,但这不是选择它的主要原因,因为在其他的开源项目中也都有一定的支持。最终让我在一众开源解决方案中选择它的原因是:

  • 无状态的授权即服务
  • 支持多种后端存储(用于存储授权策略)
  • 简单方便的部署和用户友好的文档

对比

调研的过程中我翻阅了很多开源项目的文档及其源码,其中包括:

Project Language Stateless Library Service Dynamic Policy
Oso Rust Yes Yes No No
OPA Go Yes Yes Yes No
OPAL Python Yes No Yes Yes
Ladon Go Yes Yes No Yes
Permify Go No No Yes Yes
OpenFGA Go No No Yes Yes
Authzed Go No No Yes Yes
Pomerium Go No No Yes Yes
Shield Go No No Yes Yes
Speedle Go No No Yes Yes
Cerbos Go Yes No Yes Yes

为什么我比较看中无状态呢?因为这种方式对于开发者而言最为简单,无需在其他数据库中维护用户和资源的关系或者用户和角色或权限的关系。而支持无状态的也就只有 Oso、OPA、OPAL、Ladon 和 Cerbos,而 Oso 则是只能作为库来使用,并且不支持 PHP 等语言;

OPA 则可以作为 Go 项目中的库来使用,也可以作为单独的服务来使用,但是问题就是不支持动态更新策略,但是 OPA 的策略编写语法最为强大和灵活!

OPAL 则是在 OPA 之上又实用 Python 进行了一层封装,通过 Python 开发的服务,来实现动态更新 Policy 文件。

最后就是 Cerbos 了,它支持本地存储、Git、SQLite 和 MySQL 等常见数据库,只是用于存储策略和角色定义!并且策略的定义支持 YAML 格式或 JSON 格式,这对于想通过 Admin API 来管理策略和 Role 就方便很多了!

配置

---
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: git@gitlab.example.com: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

部署

docker-compose.yaml 配置如下:

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

编写策略

policies 目录的结构如下:

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

policies/_schemas/customer.json 内容如下:

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

注意:_schemas 必须在 policies 的根目录下!!!

policies/crm/resources/customer/customer.yaml 配置文件内容如下:

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

policies/crm/resources/customer/derivedRoles.yaml 内容如下:

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

上面的策略是对于 CRM 中的客户资源进行限制:

  • ADMIN 可以执行所有 Action,
  • 普通的 usersales 则可以查看和创建客户信息
  • owner 则可以对客户信息进行 updatedeletetransfertracking,但是必须满足用户没有 trackerID 或 trackerID 是自己
  • derivedRoles.yaml 中的配置则允许衍生角色,当客户的 trackerID 等于自己的 ID 时,则对于当前客户资源来说,该用户则是 owner 权限

到这里,用户授权模型就搭建完成了。后面会尝试在其他项目中实现接入用户授权逻辑!

I hope this is helpful, Happy hacking…