Skip to content

SolaTyolo/opa-role

Repository files navigation

lowcode-role

Authorization control plane built on DSL + OPA: configure roles and policies via HTTP API, compile to a Rego bundle, and let OPA make allow/deny decisions. Does not generate PostgreSQL RLS SQL; policies take effect at the application layer through /v1/authorize.

Architecture

┌─────────────┐     CRUD      ┌──────────────────┐
│  Admin /    │ ────────────► │  role-server     │
│  Client     │              │  (:8080)         │
└─────────────┘               └────────┬─────────┘
                                       │ publish bundle
                                       ▼
                              ┌──────────────────┐
                              │  .bundle/out     │
                              │  main.rego       │
                              │  generated.rego  │
                              │  role_grants.json│
                              └────────┬─────────┘
                                       │ -w watch
                                       ▼
┌─────────────┐   authorize   ┌──────────────────┐
│  App        │ ────────────► │  OPA (:8181)     │
└─────────────┘               └──────────────────┘
                                       ▲
                              ┌────────┴─────────┐
                              │  PostgreSQL      │
                              │  roles/policies  │
                              └──────────────────┘

Two authorization layers:

Layer Config Purpose
Static ACL roles.static_permissions Coarse RBAC, e.g. orders:select, *:*
DSL policies policies (kind: dsl) Table / row / column / CRUD fine-grained rules

After publish (POST /v1/releases), no restart is required; OPA hot-reloads the bundle with -w. See docs/architecture.md for store modes, flows, and deployment patterns.

Quick start

Prerequisites

  • Go 1.25+
  • Docker (Postgres + OPA)
  • Optional: opa CLI (for opa check on publish)

Run locally

make docker-up    # Postgres + OPA (deploy/docker-compose.dev.yml)
make run          # role-server :8080

Smoke test

make smoke
make smoke-file   # ROLE_STORE=file, no Postgres

Configure roles, DSL policies, publish bundles, and test authorize via the HTTP API.

Store modes

ROLE_STORE Backend PostgreSQL Typical use
db (default) GORM + DATABASE_DIALECT (postgres, mysql, sqlite) Required for postgres only Online CRUD, audit, multi-user admin
file ROLE_CONFIG_DIR JSON files Not used GitOps, local demo, no database

SQL backend (ROLE_STORE=db) uses GORM with AutoMigrate on startup. Set DATABASE_DIALECT + DATABASE_URL:

# PostgreSQL (default)
DATABASE_DIALECT=postgres DATABASE_URL='postgres://...' make run

# MySQL
DATABASE_DIALECT=mysql DATABASE_URL='user:pass@tcp(127.0.0.1:3306)/lowcode_role?charset=utf8mb4&parseTime=True&loc=Local' make run

# SQLite (local file, no Postgres)
DATABASE_DIALECT=sqlite DATABASE_URL='./role.db' make run
# or: make run-sqlite

File mode (ROLE_STORE=file):

make docker-up          # OPA only (no Postgres required for the server)
make run-file           # role-server reads ./config
make smoke-file         # file-mode smoke (no Postgres)
# Edit config/roles.json and config/policies/*.json, then:
curl -X POST http://127.0.0.1:8080/v1/releases
# Or set ROLE_CONFIG_WATCH=1 for auto-publish

See config/README.md for the file layout. Write APIs (create/patch/delete role or policy) return 405 in file mode; edit files and call POST /v1/releases to reload.

Environment variables

Variable Default Description
ROLE_STORE db db or file
DATABASE_DIALECT postgres When ROLE_STORE=db: postgres, mysql, or sqlite
ROLE_CONFIG_DIR ./config File store root (roles.json, policies/)
DATABASE_URL postgres://... Connection string when ROLE_STORE=db (SQLite: file path, e.g. ./role.db)
LISTEN_ADDR :8080 HTTP listen address
OPA_BASE_URL http://127.0.0.1:8181 OPA decision API
BUNDLE_OUT_DIR ./.bundle/out Bundle output directory
BASE_REGO_PATH ./rego/role/main.rego Base static ACL Rego
OPA_EXECUTABLE opa Local opa check binary
ROLE_CACHE_TTL_MS 200 Authorize decision cache TTL
ADMIN_TOKEN (empty) When set, write APIs require Authorization: Bearer … or X-Admin-Token
RESOLVE_ROLES_FROM_STORE false Merge principal_roles / principals.json into user.roles on authorize
ROLE_CONFIG_WATCH false File mode: auto-publish when config/ changes

DSL format

Policy type: kind: "dsl". body.version is 1 (flat conditions) or 2 (condition groups + refs).

JSON Schema:

{
  "version": 1,
  "resource": {
    "type": "db",
    "schema": "public",
    "table": "orders"
  },
  "operations": {
    "select": {
      "when": [
        {
          "left": "input.request.resource.row.user_id",
          "op": "eq",
          "right": "input.user.sub"
        }
      ]
    },
    "insert": {
      "check": [
        {
          "left": "input.request.resource.row.user_id",
          "op": "eq",
          "right": "input.user.sub"
        }
      ]
    }
  },
  "fields": {
    "amount":  { "select": true, "insert": false },
    "user_id": { "select": true, "insert": true }
  }
}
Field Description
operations.select/delete.when Row-level filter (read/delete)
operations.insert/update.check Write validation
operations.update.when Row filter on update
fields.{col}.{select|insert|update} Column-level ACL

Condition op supports: eq, neq, in. right may be a Rego reference (input.*) or a literal.

Policy lifecycle

1. POST /v1/policies              Create DSL (status=draft)
2. POST /v1/roles/{id}/policies   Attach to role
3. POST /v1/policies/{id}/compile   Compile preview (optional)
4. PATCH /v1/policies/{id}        status=published
5. POST /v1/releases              Publish bundle → OPA hot reload

Draft policies are not published; only published + role-bound policies enter the bundle.

API overview

Method Path Auth Description
GET /healthz Health: store + OPA
GET /metrics Prometheus
GET /v1/releases/current Current revision (store field included)
GET /v1/roles, GET /v1/policies List (read-only in file mode)
POST /v1/authorize Authorization decision
POST /v1/authorize/batch Batch authorize (max 100)
POST /v1/dsl/validate Validate DSL body against schema
POST /v1/dsl/compile-preview Preview compile (no save)
GET /v1/bundles/role OPA bundle tarball (gzip); ETag = digest
GET /v1/dsl/schema DSL JSON Schema (?version=1 default, ?version=2)
GET /v1/export Admin Export snapshot (file-store JSON shape)
POST /v1/releases Admin Publish bundle
POST/PATCH/DELETE /v1/roles, /v1/policies, bindings Admin CRUD (405 in file mode)

Request header X-Actor is used for audit; X-Role-Revision skips stale cache entries.

See docs/idp-roles.md for mapping IdP claims → user.roles.

Authorize request format

{
  "user": {
    "sub": "user-001",
    "roles": ["authenticated"]
  },
  "request": {
    "action": "select",
    "resource": {
      "type": "db",
      "schema": "public",
      "table": "orders",
      "row": { "user_id": "user-001", "amount": 100 },
      "fields": ["amount", "status"]
    }
  }
}

Response: { "allow": true, "cache": "hit|miss", "revision": 1 }

Note: /v1/authorize does not query the store by default; callers pass user.roles. Set RESOLVE_ROLES_FROM_STORE=true to merge bindings from principal_roles / principals.json.

Project layout

cmd/server/           Entry point
internal/
  api/                REST API
  lowcode/            DSL v1 → Rego compiler
  bundle/             Bundle publish (atomic swap)
  opa/                OPA HTTP client
  db/                 GORM models + AutoMigrate (postgres/mysql/sqlite)
  store/              Store interface (GORM + file)
  revision/           pg_notify revision sync (postgres)
  watch/              file-mode config hot reload
  cache/              Decision TTL cache
rego/role/            Static ACL base Rego (`package role`)
config/               File store example (ROLE_STORE=file)
scripts/              k6 load tests
deploy/               Production Docker Compose stack

Development

make tidy
make test
make build
make k6          # requires k6

Deployment

Production Docker Compose stack: deploy/README.md

docker compose -f deploy/docker-compose.yml up -d --build

Cursor / AI assistance

Translations

License

Private / internal use.

About

DSL + OPA policy control plane: HTTP API for roles/policies, Rego bundle publish, allow/deny at /v1/authorize. Postgres, MySQL, SQLite, or file store.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors