← Projects
Platform Engineering

Casbin: RBAC Made Easy

The authorization framework behind LetsBloom: one RBAC policy engine, the model separated from the policy, enforced at the Envoy sidecar in the Consul service mesh.

sub, obj, act
every request an explicit authorization check
model ≠ policy
the PERM model in a .conf, rules as data
Postgres-backed
one shared policy store via the pg-adapter
Envoy ext_authz
enforced at the Consul mesh sidecar

LetsBloom needed authorization that was consistent across services and auditable, without every team hand-rolling its own permission checks. I built it on Casbin: an open-source authorization library where the access-control model lives in a config file and the rules live as data, so who-can-do-what becomes something you change without redeploying code.

What Casbin is, and isn't

Casbin is an open-source authorization library, now an Apache project, built around a small idea it calls the PERM metamodel: Policy, Effect, Request, and Matchers. You describe your access-control model in a short config file, and Casbin evaluates every request against it. The model can be ACL, RBAC, ABAC, and several others; we used RBAC, straight from Casbin's model gallery.

The important discipline is what it separates. The model, the shape of your authorization, lives in a .conf file. The policy, the actual rules, lives as data. So changing who can do what is a data change, not a code change and a redeploy. One thing it deliberately does not do is authentication: Casbin never checks a password or owns your user list. The application authenticates the caller and hands Casbin a subject. Authorization only.

Part of LetsBloom: Compliant-by-Default Landing Zones
The compliant landing-zone platform this authorization layer was built for.

How it works: the PERM model

A Casbin model is five short sections. A request is r = sub, obj, act: a subject, the object it wants, and the action. A policy rule has the same shape, p = sub, obj, act. For RBAC you add a role definition, g = _, _, which maps users to roles. The effect, e = some(where (p.eft == allow)), says a request is allowed if any rule allows it. And the matcher ties it all together:

rbac_model.conf
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && r.act == p.act

The RBAC model, hands-on

Here is a small version of the policy we ran. p rules grant a role an action on an object; g rules assign users to roles. Roles keep the rules tiny: you grant a role once, then move people in and out of it.

policy.csv
# p, role, object, action
p, admin,  /*,            read
p, admin,  /*,            write
p, admin,  /*,            delete
p, editor, /reports/*,    read
p, editor, /reports/*,    write
p, editor, /dashboards/*, read
p, viewer, /reports/*,    read
p, viewer, /dashboards/*, read

# g, user, role
g, alice, admin
g, bob,   editor
g, carol, viewer
Try the RBAC policy

Pick a subject, object, and action. This is exactly what enforce() evaluates.

e.Enforce("alice", "/reports/q3", "read")
sub
obj
act
ALLOW
  • g, alice, admin: the subject resolves to role admin.
  • p, admin, /*, read matches: keyMatch("/reports/q3", "/*") and act == read.
  • Effect e = some(where (p.eft == allow)) is satisfied, so the request is allowed.
The policy: admin can do anything (/*); editor reads and writes /reports/* and reads /dashboards/*; viewer reads both. Assignments: alice is admin, bob is editor, carol is viewer.
The RBAC model, live. Every decision is g (which role) plus a p rule (what that role can do).

Policy as data: the Postgres adapter

Rules in a CSV are fine for a demo. In production they belong in a database every instance of a service can share, so a permission change lands everywhere at once. Casbin does this through adapters, and I used the Apache casbin-pg-adapter to keep the policy in Postgres.

The adapter creates a casbin_rule table and loads it into the enforcer with LoadPolicy(); changes made through the enforcer are written back with SavePolicy(). Point every service at the same table and they all enforce the same rules, updated centrally.

Go · casbin + pg-adapter
// One shared policy store: every service instance reads the same rules.
a, _ := pgadapter.NewAdapter("postgres://.../letsbloom?sslmode=require")
e, _ := casbin.NewEnforcer("rbac_model.conf", a)
e.LoadPolicy()

// The whole authorization check, in one line:
ok, _ := e.Enforce(sub, obj, act) // true => allow

// Grant a permission at runtime; it persists to Postgres.
e.AddPolicy("editor", "/exports/*", "read")

From a Gatekeeper experiment to Envoy in the mesh

The last question is where the check actually runs. I first experimented with casbin-k8s-gatekeeper, a Kubernetes admission webhook that runs Casbin models and policies as CRDs. It is a neat fit for admission-time rules (block an image tag, restrict a namespace), but it governs the cluster's control plane, not the request path of a running service, which is what LetsBloom needed.

So the final design put Casbin on the data path. Casbin ships an envoy-authz service that implements Envoy's external authorization (ext_authz) gRPC API. In our Consul service mesh every service already had an Envoy sidecar, so I wired the ext_authz filter to the Casbin service. Now every request in the mesh is authorized before it reaches the app, and no service carries authorization code of its own.

Enforcement in the service mesh

Where the policy actually runs: an Envoy ext_authz filter in the Consul mesh, backed by a Casbin service. No authorization code in the app.

Client request
caller identity carried in a JWT / header
Envoy sidecar
ext_authz filter intercepts every call
Casbin authorization service
enforce(sub, obj, act) against the RBAC model
policy loaded from Postgres (casbin_rule) via the pg-adapter
Allow
request reaches the upstream service
Deny
Envoy returns 403
The Envoy sidecar calls the Casbin service for every request; the app never runs an authorization check itself.

Why Casbin: RBAC made easy

The title is only half a joke. RBAC is genuinely easy to get wrong, usually by smearing it across every service as bespoke permission logic that drifts and can never be audited as a whole. Casbin's contribution is not a clever algorithm; it is the separation. Model in a file, policy as data, enforcement at one well-understood point. Once those three are pulled apart, RBAC becomes a thing you can reason about, change safely, and prove.

For a compliance-driven platform like LetsBloom, that last word is the one that mattered. Being able to answer 'who can do what, and how do you know' with a table and a model file, rather than a code review across a dozen repos, is worth a great deal.

Stack
CasbinGoPostgreSQLcasbin-pg-adapterEnvoyConsul Service MeshgRPCKubernetes