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.
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.
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:
[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.actThe 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.
# 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, viewerPick a subject, object, and action. This is exactly what enforce() evaluates.
- 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.
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.
// 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.
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.
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.