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.
┌─────────────┐ 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.
- Go 1.25+
- Docker (Postgres + OPA)
- Optional:
opaCLI (foropa checkon publish)
make docker-up # Postgres + OPA (deploy/docker-compose.dev.yml)
make run # role-server :8080make smoke
make smoke-file # ROLE_STORE=file, no PostgresConfigure roles, DSL policies, publish bundles, and test authorize via the HTTP API.
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-sqliteFile 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-publishSee 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.
| 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 |
Policy type: kind: "dsl". body.version is 1 (flat conditions) or 2 (condition groups + refs).
JSON Schema:
- v1: spec/dsl/v1.schema.json
- v2: spec/dsl/v2.schema.json —
all/anygroups;refsmap names toinput.*paths for{"ref":"name"}in conditions - HTTP:
GET /v1/dsl/schemaor?version=2
{
"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.
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.
| 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.
{
"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/authorizedoes not query the store by default; callers passuser.roles. SetRESOLVE_ROLES_FROM_STORE=trueto merge bindings fromprincipal_roles/principals.json.
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
make tidy
make test
make build
make k6 # requires k6Production Docker Compose stack: deploy/README.md
docker compose -f deploy/docker-compose.yml up -d --build- AGENTS.md — Agent guidance
- .cursor/rules/ — Project Cursor rules
Private / internal use.