This is an automated email from the ASF dual-hosted git repository.
hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-website.git
The following commit(s) were added to refs/heads/master by this push:
new 59e863284 docs: add ABAC, NestJS, Rust, gRPC and AI agent
authorization guides
59e863284 is described below
commit 59e863284a7ccc776f049ee64b407835aba5fc65
Author: Yang Luo <[email protected]>
AuthorDate: Mon Oct 5 00:24:20 2026 +0800
docs: add ABAC, NestJS, Rust, gRPC and AI agent authorization guides
---
docs/ABAC.mdx | 2 +
docs/AIAgentAuthorization.mdx | 211 +++++++++++++++++++++++++++++++
docs/AccessControlModels.mdx | 224 +++++++++++++++++++++++++++++++++
docs/RBACOverview.mdx | 2 +
docs/SupportedModels.mdx | 2 +-
docs/TutorialFlaskABAC.mdx | 194 +++++++++++++++++++++++++++++
docs/TutorialGoABAC.mdx | 251 +++++++++++++++++++++++++++++++++++++
docs/TutorialGoRBAC.mdx | 4 +-
docs/TutorialGrpcRBAC.mdx | 284 ++++++++++++++++++++++++++++++++++++++++++
docs/TutorialJavaRBAC.mdx | 4 +-
docs/TutorialNestJSRBAC.mdx | 231 ++++++++++++++++++++++++++++++++++
docs/TutorialNodejsABAC.mdx | 178 ++++++++++++++++++++++++++
docs/TutorialNodejsRBAC.mdx | 4 +-
docs/TutorialPythonRBAC.mdx | 4 +-
docs/TutorialRustRBAC.mdx | 187 +++++++++++++++++++++++++++
sidebars.js | 8 ++
16 files changed, 1781 insertions(+), 9 deletions(-)
diff --git a/docs/ABAC.mdx b/docs/ABAC.mdx
index 602279081..3f9f92c64 100644
--- a/docs/ABAC.mdx
+++ b/docs/ABAC.mdx
@@ -10,6 +10,8 @@ authors: [hsluoyz]
**Attribute-Based Access Control (ABAC)** decides access by evaluating
**attributes** of the subject, object, and action (e.g. `resource.Owner`,
`user.Role`), not just by identity strings. Casbin keeps this simple: you pass
structs or objects into `Enforce()` and reference their fields in the matcher.
+For complete applications, see the ABAC tutorials for
[Go](/docs/go-abac-tutorial), [Node.js with
Express](/docs/nodejs-abac-tutorial), and [Python with
Flask](/docs/flask-abac-tutorial). To decide between ABAC and RBAC, see
[Choosing an access control model](/docs/access-control-models).
+
Minimal example:
```ini
diff --git a/docs/AIAgentAuthorization.mdx b/docs/AIAgentAuthorization.mdx
new file mode 100644
index 000000000..ef8ab32a8
--- /dev/null
+++ b/docs/AIAgentAuthorization.mdx
@@ -0,0 +1,211 @@
+---
+id: ai-agent-authorization
+title: Authorization for AI Agents and MCP Tools
+description: "Control which tools an AI agent may call with Casbin: per-agent
permissions, argument limits, deny rules, and no more access than its user.
With an MCP example."
+keywords: [ai agent authorization, ai agent permissions, rbac for ai agents,
mcp authorization, mcp tool permissions, llm tool calling security, agent
access control, casbin ai agent]
+authors: [hsluoyz]
+---
+
+An AI agent decides at run time which tools to call and with which arguments.
The model can be wrong, and it can be talked into things by text it reads in a
ticket, a web page, or a document. Authorization has to sit outside the model,
at the point where a tool call is executed, and answer two questions:
+
+1. **May this agent call this tool with these arguments?** A support assistant
may read tickets and issue small refunds, but should never run shell commands.
+2. **May the user it acts for do this?** An agent acting for someone must not
get more access than that person has.
+
+Casbin handles both with one model and one policy, in the same way it handles
users. This page shows the rules first, then wires them into a plain
tool-calling loop and into an [MCP](https://modelcontextprotocol.io) server.
The code is Python. The model and policy also work in the other Casbin
implementations; in Go, pass the arguments as a struct and use its exported
field names in conditions, such as `r.args.Amount`.
+
+## 1. Install
+
+```bash
+pip install pycasbin
+```
+
+## 2. Write the model
+
+Save this as `model.conf`:
+
+```ini
+[request_definition]
+r = sub, tool, args
+
+[policy_definition]
+p = sub, tool, cond, eft
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
+
+[matchers]
+m = g(r.sub, p.sub) && globMatch(r.tool, p.tool) && (p.cond == '' ||
eval(p.cond))
+```
+
+- `r = sub, tool, args`: the caller (an agent or a user), the tool name, and
the tool's arguments as an object.
+- `p = sub, tool, cond, eft`: each rule names a role, a tool pattern, an
optional condition on the arguments, and whether it allows or denies.
+- `g = _, _`: agents and users have roles, and roles can inherit from other
roles.
+- `globMatch` matches tool names with wildcards such as `tickets.*` or
`*.delete`.
+- The policy effect allows a call if some rule allows it **and no rule denies
it**, so a deny rule always wins.
+
+## 3. Write the policy
+
+Save this as `policy.csv`:
+
+```csv
+p, support-agent, tickets.*, , allow
+p, support-agent, orders.read, , allow
+p, support-agent, orders.refund, r.args.amount <= 100, allow
+p, any-agent, shell.*, , deny
+p, any-agent, *.delete, , deny
+
+p, support-staff, tickets.*, , allow
+p, support-staff, orders.*, , allow
+
+g, support-agent, any-agent
+g, triage-agent, any-agent
+p, triage-agent, tickets.read, , allow
+p, triage-agent, tickets.label, , allow
+
+g, alice, support-staff
+```
+
+Reading it from the top:
+
+- `support-agent` may use every ticket tool, read orders, and refund at most
100 per call. The condition `r.args.amount <= 100` is evaluated against the
arguments the model produced.
+- Every role that inherits from `any-agent` is denied shell tools and any tool
ending in `.delete`, whatever else it is granted. Put guardrails that apply to
all agents here.
+- `support-staff` is a human role; `alice` has it.
+- `triage-agent` is a second, narrower agent that may only read and label
tickets.
+
+:::tip
+Write string literals in conditions with single quotes, as in `r.args.region
== 'eu'`. If a condition needs a comma, wrap the whole field in double quotes.
+:::
+
+## 4. Check every tool call
+
+`authorize_tool_call` runs two checks. The agent's own permissions limit what
the agent can do at all, and the user's permissions limit what it can do for
this user. Only calls that pass both are executed.
+
+```python title="tools.py"
+from types import SimpleNamespace
+
+import casbin
+
+enforcer = casbin.Enforcer("model.conf", "policy.csv")
+
+
+class ToolDenied(Exception):
+ pass
+
+
+def authorize_tool_call(agent: str, user: str, tool: str, args: dict) -> None:
+ """Allow the call only if both the agent and the user it acts for may make
it."""
+ request_args = SimpleNamespace(**args)
+ if not enforcer.enforce(agent, tool, request_args):
+ raise ToolDenied(f"{agent} may not call {tool} with {args}")
+ if not enforcer.enforce(user, tool, request_args):
+ raise ToolDenied(f"{user} may not call {tool}, so an agent acting for
them may not either")
+
+
+# The tools your agent can see. In a real application they call your services.
+TOOLS = {
+ "tickets.read": lambda ticket_id: {"id": ticket_id, "subject": "Refund
request", "order_id": "A-1001"},
+ "orders.read": lambda order_id: {"id": order_id, "total": 240},
+ "orders.refund": lambda order_id, amount: {"order_id": order_id,
"refunded": amount},
+ "shell.exec": lambda command: {"output": "..."},
+}
+
+
+def call_tool(agent: str, user: str, tool: str, args: dict) -> dict:
+ """Run a tool call proposed by the model, or return the denial as the tool
result."""
+ try:
+ authorize_tool_call(agent, user, tool, args)
+ except ToolDenied as e:
+ # Returning the reason lets the model explain it or try something else.
+ return {"error": "permission_denied", "reason": str(e)}
+ return TOOLS[tool](**args)
+
+
+if __name__ == "__main__":
+ # Tool calls as a model might propose them while helping alice with a
ticket.
+ proposed = [
+ ("tickets.read", {"ticket_id": "T-42"}),
+ ("orders.read", {"order_id": "A-1001"}),
+ ("orders.refund", {"order_id": "A-1001", "amount": 240}),
+ ("orders.refund", {"order_id": "A-1001", "amount": 80}),
+ ("shell.exec", {"command": "rm -rf /"}),
+ ]
+ for tool, args in proposed:
+ print(tool, args, "->", call_tool("support-agent", "alice", tool,
args))
+
+ print()
+ print("bob:", call_tool("support-agent", "bob", "tickets.read",
{"ticket_id": "T-42"}))
+```
+
+Running it prints:
+
+```text
+tickets.read {'ticket_id': 'T-42'} -> {'id': 'T-42', 'subject': 'Refund
request', 'order_id': 'A-1001'}
+orders.read {'order_id': 'A-1001'} -> {'id': 'A-1001', 'total': 240}
+orders.refund {'order_id': 'A-1001', 'amount': 240} -> {'error':
'permission_denied', 'reason': "support-agent may not call orders.refund with
{'order_id': 'A-1001', 'amount': 240}"}
+orders.refund {'order_id': 'A-1001', 'amount': 80} -> {'order_id': 'A-1001',
'refunded': 80}
+shell.exec {'command': 'rm -rf /'} -> {'error': 'permission_denied', 'reason':
"support-agent may not call shell.exec with {'command': 'rm -rf /'}"}
+
+bob: {'error': 'permission_denied', 'reason': 'bob may not call tickets.read,
so an agent acting for them may not either'}
+```
+
+The refund of 240 is refused even though alice may refund any amount, because
the agent may not. Bob has no role, so the agent can do nothing for him.
Returning the refusal as the tool result, instead of raising it to the user,
lets the model explain the limit or hand the case to a person.
+
+## 5. Use it in an MCP server
+
+In an MCP server that uses OAuth, the access token already tells you both
identities: the OAuth client is the agent, and the token's subject is the user
who authorized it. With the official Python SDK (version 2), read them with
`get_access_token()` and check each tool call:
+
+```python title="server.py"
+from mcp.server.auth.middleware.auth_context import get_access_token
+from mcp.server.mcpserver import MCPServer
+from mcp.server.mcpserver.exceptions import ToolError
+
+from tools import ToolDenied, authorize_tool_call
+
+mcp = MCPServer("support-tools")
+
+
+def authorize(tool: str, args: dict) -> None:
+ """Check the tool call against Casbin for the OAuth client (the agent) and
the user it acts for."""
+ token = get_access_token()
+ if token is None or token.subject is None:
+ raise ToolError("not authenticated")
+ try:
+ authorize_tool_call(token.client_id, token.subject, tool, args)
+ except ToolDenied as e:
+ raise ToolError(str(e)) from e
+
+
[email protected]()
+def refund_order(order_id: str, amount: float) -> dict:
+ """Refund part or all of an order."""
+ authorize("orders.refund", {"order_id": order_id, "amount": amount})
+ return {"order_id": order_id, "refunded": amount}
+
+
[email protected]()
+def read_ticket(ticket_id: str) -> dict:
+ """Read a support ticket."""
+ authorize("tickets.read", {"ticket_id": ticket_id})
+ return {"id": ticket_id, "subject": "Refund request"}
+```
+
+Raising `ToolError` turns the refusal into a tool result with `isError` set,
which MCP clients pass back to the model. The token's `client_id` must match
the agent names in your policy; register each agent as its own OAuth client so
that you can give it its own permissions and revoke them separately.
+
+## Design notes
+
+- **Default deny.** A tool that no rule mentions cannot be called, so a new
tool is unavailable to agents until you grant it.
+- **Least privilege per agent.** Give each agent its own role rather than
sharing one powerful role. The `triage-agent` above cannot touch orders even if
a prompt asks it to.
+- **Check arguments, not just names.** Limits on amounts, recipients, paths,
or regions belong in conditions. Validate the argument types before the check:
the condition compares whatever the model sent, and comparing the string
`"500"` with a number raises an error instead of returning a decision.
+- **Change permissions without redeploying.** Rules are data:
`enforcer.add_policy(...)`, `enforcer.add_role_for_user(...)`, or edits through
an [adapter](/docs/adapters) take effect on the next call. Use a
[watcher](/docs/watchers) when several processes serve agents.
+- **Audit.** `enforcer.enforce_ex(...)` returns the rule that allowed a call;
log it next to the tool call.
+- **Filter the tool list.** Call the same check when listing tools, so the
model is not even offered tools it cannot use. This also saves tokens.
+
+## Next steps
+
+- [ABAC](/docs/abac) explains conditions and `eval()` in detail.
+- [RBAC with domains](/docs/rbac-with-domains) gives agents different
permissions per workspace or tenant.
+- [Effector](/docs/effector) describes allow, deny, and priority effects.
+- [Choosing an access control model](/docs/access-control-models) compares
RBAC, ABAC, and ReBAC.
diff --git a/docs/AccessControlModels.mdx b/docs/AccessControlModels.mdx
new file mode 100644
index 000000000..9b5614528
--- /dev/null
+++ b/docs/AccessControlModels.mdx
@@ -0,0 +1,224 @@
+---
+id: access-control-models
+title: "RBAC vs ABAC vs ACL vs ReBAC: Choosing an Access Control Model"
+sidebar_label: Choosing a Model
+description: "ACL, RBAC, ABAC, ReBAC and PBAC compared on one example: what
each decides on, strengths, weaknesses, when to choose it, and how to combine
RBAC with ABAC."
+keywords: [rbac vs abac, rbac vs acl, abac vs rbac, rbac vs abac vs acl, rebac
vs rbac, access control models, rbac vs abac vs pbac vs acl, role based access
control, attribute based access control]
+authors: [hsluoyz]
+---
+
+Access control models differ in one thing: **what an authorization decision is
based on**.
+
+| Model | Decides on | Example rule |
+|---|---|---|
+| ACL (access control list) | The user | alice may edit `design.md` |
+| RBAC (role-based) | The user's roles | editors may edit documents |
+| ABAC (attribute-based) | Attributes of the user, the resource, and the
context | owners may edit their documents; people in the same department may
read them |
+| ReBAC (relationship-based) | Relationships between the user and the resource
| anyone who is a viewer of the folder may read the documents in it |
+| PBAC (policy-based) | Rule expressions stored as policy | whatever condition
the policy row contains |
+
+These are not competing products: they are ways of writing rules, and real
systems mix them. In Casbin each one is a few lines of model configuration, and
you can switch or combine them without changing your application code. This
page expresses the same small example in each model so that the differences are
concrete.
+
+**The example:** a document app with one document, `design.md`, owned by
alice. alice should be able to edit it, bob should be able to read it, and
nobody else should have access.
+
+## ACL
+
+An access control list states, for each user, what they may do with each
object.
+
+```ini
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
+```
+
+```csv
+p, alice, design.md, read
+p, alice, design.md, edit
+p, bob, design.md, read
+```
+
+- **Good at:** small systems and per-object sharing ("share this file with
bob"). Every decision is easy to explain.
+- **Weak at:** scale. With *u* users and *o* objects the list grows toward *u
× o* rows, and when someone changes jobs you must find and edit all of their
rows.
+
+## RBAC
+
+Role-based access control puts roles between users and permissions.
Permissions are granted to roles, and users are assigned roles.
+
+```ini
+[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) && r.obj == p.obj && r.act == p.act
+```
+
+```csv
+p, editor, design.md, read
+p, editor, design.md, edit
+p, viewer, design.md, read
+
+g, alice, editor
+g, bob, viewer
+g, carol, viewer
+```
+
+Giving carol read access was one `g` line; she inherits everything `viewer`
can do. Roles can also inherit from other roles (`g, editor, viewer`), and
[RBAC with domains](/docs/rbac-with-domains) gives a user different roles in
different tenants.
+
+- **Good at:** permissions that follow job functions. It is easy to audit
("what can an editor do?", "who is an admin?"), and onboarding or offboarding
is a role change.
+- **Weak at:** rules about specific records. "Authors may edit *their own*
articles" cannot be written with roles alone; trying leads to one role per
record, known as role explosion.
+
+**RBAC vs ACL:** an ACL is RBAC without the roles. Start with RBAC unless you
really have only a handful of users, because adding roles later means rewriting
every rule.
+
+Tutorials: [Go](/docs/go-rbac-tutorial),
[Node.js](/docs/nodejs-rbac-tutorial), [NestJS](/docs/nestjs-rbac-tutorial),
[Python](/docs/python-rbac-tutorial), [Java](/docs/java-rbac-tutorial),
[Rust](/docs/rust-rbac-tutorial), [gRPC](/docs/grpc-rbac-tutorial).
+
+## ABAC
+
+Attribute-based access control evaluates conditions on attributes of the user
(department, clearance), the resource (owner, status, sensitivity), and the
environment (time, IP address). In Casbin you pass objects to `Enforce()` and
read their fields in the matcher.
+
+```ini
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.sub.Name == r.obj.Owner || (r.act == 'read' && r.sub.Department ==
r.obj.Department)
+```
+
+There is no policy file: alice may edit `design.md` because she is its owner,
and bob may read it because he works in the same department. A new document in
the design department is readable by bob without any change to the rules.
+
+- **Good at:** rules that depend on the record or on context: ownership,
department, document status, business hours, region. One rule replaces many
role grants.
+- **Weak at:** auditing. "Who can read this document?" has no direct answer;
you have to evaluate the conditions against every user. Conditions also need
the attributes at hand, which means loading the record before the check.
+
+With the [eval() pattern](/docs/abac#complex-rules-with-eval), conditions move
from the model into policy rows, so they can be added and removed at runtime
like any other rule.
+
+Tutorials: [Go](/docs/go-abac-tutorial), [Node.js with
Express](/docs/nodejs-abac-tutorial), [Python with
Flask](/docs/flask-abac-tutorial).
+
+## ReBAC
+
+Relationship-based access control derives permissions from relationships:
alice is the *owner* of `design.md`, bob is a *viewer* of it, and owners may
edit while viewers may read. It is the model behind Google Zanzibar and its
open-source descendants.
+
+```ini
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = relation, obj_type, act
+
+[role_definition]
+g = _, _, _
+g2 = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, r.obj, p.relation) && g2(r.obj, p.obj_type) && r.act == p.act
+```
+
+```csv
+p, owner, doc, read
+p, owner, doc, edit
+p, viewer, doc, read
+
+g, alice, design.md, owner
+g, bob, design.md, viewer
+g, bob, roadmap.md, owner
+
+g2, design.md, doc
+g2, roadmap.md, doc
+```
+
+bob can edit `roadmap.md`, which he owns, but only read `design.md`. The `p`
rules describe what each relation allows once, for a whole type of object, and
the `g` rows record who has which relation to which object.
+
+- **Good at:** sharing and collaboration (documents, folders, projects,
organizations), especially when permissions are inherited through a hierarchy
such as organization → folder → document.
+- **Weak at:** small or flat systems, where it is more machinery than needed.
Relationships must be written whenever objects are created or shared.
+
+See [ReBAC](/docs/rebac) for the full model, and [Casbin vs.
OpenFGA](/docs/casbin-vs-openfga) if you are considering a dedicated
Zanzibar-style service.
+
+## PBAC
+
+Policy-based access control is less a separate model than a way of writing the
others: the rules themselves are expressions stored as policy and evaluated at
run time. In Casbin that is the `eval()` pattern used in the ABAC tutorials and
on the [PBAC](/docs/pbac) page. Its value is operational: security or
compliance teams can change rules without a code release.
+
+## Side by side
+
+| | ACL | RBAC | ABAC | ReBAC |
+|---|---|---|---|---|
+| Rules written as | user–object–action | role–object–action, plus user–role |
conditions on attributes | relation–type–action, plus user–relation–object |
+| Number of rules grows with | users × objects | roles × permissions | number
of distinct conditions | number of shared objects |
+| "What can alice do?" | Easy | Easy | Hard | Medium |
+| "Who can read this document?" | Easy | Easy | Hard | Easy |
+| Per-record rules (ownership) | One row per record | Not expressible without
role explosion | Natural | Natural |
+| Context (time, IP, status) | No | No | Yes | Only with extra conditions |
+| Typical use | Small tools, file sharing | Admin panels, internal tools, most
business apps | Data platforms, regulated data, ownership rules | Collaboration
apps, multi-level hierarchies |
+
+## How to choose
+
+1. **Start with RBAC.** If permissions can be described by job function
("admins", "editors", "billing"), RBAC is the simplest model that scales and
the easiest to audit.
+2. **Add ABAC conditions for ownership and context.** When you find yourself
wanting "only their own" or "only during business hours", add conditions
instead of more roles.
+3. **Choose ReBAC when sharing is the product.** If users share objects with
each other and access inherits through folders, projects, or organizations,
model the relationships directly.
+4. **Use domains for multi-tenancy.** If the same user has different roles in
different organizations, use [RBAC with domains](/docs/rbac-with-domains)
whatever else you choose.
+5. **Consider mandatory models only when required.** [BLP, Biba, and
LBAC](/docs/mac-overview) suit environments with classification levels, such as
government or defense.
+
+## Combining RBAC and ABAC
+
+Most applications end up here: roles decide which features a user can reach,
and attributes narrow that down to the records they may touch. In this model
admins may edit any document, while authors may edit only documents they own:
+
+```ini
+[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.Name, p.sub) && r.obj.Type == p.obj && r.act == p.act && (p.sub ==
'admin' || r.sub.Name == r.obj.Owner)
+```
+
+```csv
+p, admin, doc, edit
+p, author, doc, edit
+
+g, erin, admin
+g, alice, author
+g, bob, author
+```
+
+For `design.md` (type `doc`, owner alice), alice may edit because she is an
author and the owner, erin may edit because she is an admin, and bob may not:
he is an author, but not the owner.
+
+## FAQ
+
+**Is ABAC more secure than RBAC?** Neither is inherently more secure. ABAC can
express finer rules, but its decisions are harder to audit; RBAC is coarser but
easy to review. Security comes from rules that match what you intend and from
being able to check that they do.
+
+**Can I start with RBAC and move to ABAC later?** Yes. In Casbin, the model is
a configuration file and the application calls the same `Enforce()` method, so
you can add conditions to the matcher, or combine both as shown above, without
rewriting your authorization calls.
+
+**Where does PBAC fit?** PBAC describes *how* rules are managed (as policy
data evaluated at run time) more than *what* they are based on. Casbin policies
are data in every model, so any of the models above can be managed this way.
+
+**Does Casbin support all of these?** Yes, along with others such as
[RBAC96](/docs/rbac-96), [OrBAC](/docs/orbac), [UCON](/docs/ucon), and priority
and deny-override policies. See [Supported models](/docs/supported-models), and
try any of them in the [online editor](/docs/online-editor).
diff --git a/docs/RBACOverview.mdx b/docs/RBACOverview.mdx
index a1450381d..ab61f9614 100644
--- a/docs/RBACOverview.mdx
+++ b/docs/RBACOverview.mdx
@@ -10,6 +10,8 @@ authors: [hsluoyz]
**Role-Based Access Control (RBAC)** assigns permissions to users according to
their roles. Casbin implements RBAC with support for role hierarchies, domains,
and conditions.
+To see how RBAC compares with ACL, ABAC, and ReBAC, read [Choosing an access
control model](/docs/access-control-models). Step-by-step tutorials are
available for [Go](/docs/go-rbac-tutorial),
[Node.js](/docs/nodejs-rbac-tutorial), [NestJS](/docs/nestjs-rbac-tutorial),
[Python](/docs/python-rbac-tutorial), [Java](/docs/java-rbac-tutorial),
[Rust](/docs/rust-rbac-tutorial), and [gRPC](/docs/grpc-rbac-tutorial).
+
## RBAC in Casbin
- **Basic RBAC** — Role hierarchies and user–role assignments
diff --git a/docs/SupportedModels.mdx b/docs/SupportedModels.mdx
index e7299bc74..344644f0b 100644
--- a/docs/SupportedModels.mdx
+++ b/docs/SupportedModels.mdx
@@ -6,7 +6,7 @@ keywords: [supported models]
authors: [nodece]
---
-Casbin supports the following access control models and patterns:
+Casbin supports the following access control models and patterns. If you are
not sure which one you need, start with [Choosing an access control
model](/docs/access-control-models).
1. [**ACL (Access Control
List)**](https://en.wikipedia.org/wiki/Access_control_list)
2. **ACL with [superuser](https://en.wikipedia.org/wiki/Superuser)**
diff --git a/docs/TutorialFlaskABAC.mdx b/docs/TutorialFlaskABAC.mdx
new file mode 100644
index 000000000..dd7ac8f2e
--- /dev/null
+++ b/docs/TutorialFlaskABAC.mdx
@@ -0,0 +1,194 @@
+---
+id: flask-abac-tutorial
+title: ABAC in Python with Flask
+description: "Python ABAC tutorial: attribute-based access control for a Flask
API with PyCasbin, using owner, department and status to decide who can read
and edit."
+keywords: [python abac, abac flask, flask abac, authz flask, flask
authorization, python access control, abac example, pycasbin]
+authors: [hsluoyz]
+---
+
+Role-based access control answers "what may an editor do?". Many real rules
need more than a role: *authors may edit their own drafts*, *editors may edit
articles from their own department*, *anyone may read a published article*.
These depend on attributes of the user and of the resource, which is what
attribute-based access control (ABAC) is for.
+
+This tutorial builds a small article API with
[Flask](https://flask.palletsprojects.com) and puts all of those rules in a
Casbin policy file. The same model and policy work unchanged in the
[Go](/docs/go-abac-tutorial) and [Node.js](/docs/nodejs-abac-tutorial) versions
of this tutorial. For role-based rules in Python, see the [FastAPI RBAC
tutorial](/docs/python-rbac-tutorial). If you are still choosing between RBAC
and ABAC, read [Choosing an access control model](/docs/access-control-m [...]
+
+## 1. Install
+
+```bash
+pip install pycasbin flask
+```
+
+## 2. Write the model
+
+Save this as `model.conf`:
+
+```ini
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub_rule, obj_type, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.obj.Type == p.obj_type && r.act == p.act && eval(p.sub_rule)
+```
+
+- `r.sub` and `r.obj` are Python objects. The matcher reads their attributes,
such as `r.obj.Type`.
+- Each policy rule carries a condition in `sub_rule`. `eval(p.sub_rule)`
evaluates that condition against the request, so the rules live in the policy
instead of in the model.
+
+## 3. Write the policy
+
+Save this as `policy.csv`:
+
+```csv
+p, r.obj.Status == 'published', article, read
+p, r.sub.Name == r.obj.Owner, article, read
+p, r.sub.Name == r.obj.Owner && r.obj.Status == 'draft', article, update
+p, r.sub.Role == 'editor' && r.sub.Department == r.obj.Department, article,
update
+p, r.sub.Role == 'editor' && r.sub.Department == r.obj.Department, article,
publish
+```
+
+Read each line as "allow this action on articles when the condition is true":
+
+1. Anyone may read a published article.
+2. Authors may read their own articles, including drafts.
+3. Authors may edit their own articles while they are drafts.
+4. Editors may edit any article from their own department.
+5. Editors may publish articles from their own department.
+
+Conditions use the same syntax in every Casbin implementation; PyCasbin
translates `&&` and `||` into Python's `and` and `or`.
+
+:::tip
+Write string literals in conditions with single quotes, as in `'published'`. A
double quote in the middle of a field breaks CSV parsing. If a condition needs
a comma, wrap the whole field in double quotes.
+:::
+
+## 4. Write the application
+
+ABAC needs the resource itself, not just its URL, so the check runs after the
article has been loaded. The `authorize` helper loads the article, asks Casbin,
and aborts with the right status code when the answer is no. Each view calls it
first.
+
+```python title="app.py"
+from dataclasses import asdict, dataclass
+
+import casbin
+from flask import Flask, abort, request
+
+enforcer = casbin.Enforcer("model.conf", "policy.csv")
+app = Flask(__name__)
+
+
+@dataclass
+class User:
+ Name: str
+ Role: str
+ Department: str
+
+
+@dataclass
+class Article:
+ Id: str
+ Title: str
+ Owner: str
+ Department: str
+ Status: str
+ Type: str = "article"
+
+
+# In a real application these come from your database.
+users = {
+ "alice": User("alice", "author", "engineering"),
+ "bob": User("bob", "editor", "engineering"),
+ "carol": User("carol", "editor", "sales"),
+ "dave": User("dave", "author", "engineering"),
+}
+
+articles = {
+ "1": Article("1", "Draft by alice", "alice", "engineering", "draft"),
+ "2": Article("2", "Published by alice", "alice", "engineering",
"published"),
+}
+
+
+def authorize(article_id: str, action: str) -> Article:
+ """Load the article and abort unless the current user may perform the
action on it."""
+ # Replace this with the user from your session or JWT.
+ user = users.get(request.headers.get("X-User", ""))
+ if user is None:
+ abort(401)
+ article = articles.get(article_id)
+ if article is None:
+ abort(404)
+ if not enforcer.enforce(user, article, action):
+ abort(403)
+ return article
+
+
[email protected]("/articles/<article_id>")
+def read_article(article_id):
+ return asdict(authorize(article_id, "read"))
+
+
[email protected]("/articles/<article_id>")
+def update_article(article_id):
+ article = authorize(article_id, "update")
+ article.Title = (request.get_json(silent=True) or {}).get("title",
article.Title)
+ return asdict(article)
+
+
[email protected]("/articles/<article_id>/publish")
+def publish_article(article_id):
+ article = authorize(article_id, "publish")
+ article.Status = "published"
+ return asdict(article)
+```
+
+Dataclasses keep the example short; any object works, including SQLAlchemy or
Django models, as long as the attributes named in the policy exist.
+
+:::note
+The `X-User` header keeps the example short. Casbin handles authorization
only; in a real application the user comes from your authentication layer, such
as Flask-Login or a verified JWT.
+:::
+
+## 5. Try it
+
+```bash
+flask --app app run --port 5000
+```
+
+Article 1 is alice's draft and article 2 is her published article, both in the
engineering department.
+
+```bash
+curl -i -H "X-User: dave" http://localhost:5000/articles/1 #
403, someone else's draft
+curl -i -H "X-User: dave" http://localhost:5000/articles/2 #
200, published
+curl -i -X PUT -H "X-User: alice" -H "Content-Type: application/json" -d
'{"title":"New title"}' http://localhost:5000/articles/1 # 200, her
own draft
+curl -i -X PUT -H "X-User: alice" http://localhost:5000/articles/2 #
403, no longer a draft
+curl -i -X PUT -H "X-User: bob" http://localhost:5000/articles/2 #
200, editor in engineering
+curl -i -X PUT -H "X-User: carol" http://localhost:5000/articles/2 #
403, editor in sales
+curl -i -X POST -H "X-User: alice" http://localhost:5000/articles/1/publish #
403, authors cannot publish
+curl -i -X POST -H "X-User: bob" http://localhost:5000/articles/1/publish #
200
+curl -i -H "X-User: dave" http://localhost:5000/articles/1 #
200, now published
+```
+
+## 6. Change rules at runtime
+
+Conditions are policy rows, so they can be added and removed while the server
is running, and saved to a database through an [adapter](/docs/adapters) such
as the SQLAlchemy adapter:
+
+```python
+# Admins may edit any article.
+enforcer.add_policy("r.sub.Role == 'admin'", "article", "update")
+
+# Which rule allowed this request? Useful for audit logs.
+enforcer.enforce_ex(users["bob"], articles["2"], "update")
+# (True, ["r.sub.Role == 'editor' && r.sub.Department == r.obj.Department",
'article', 'update'])
+```
+
+## ABAC or RBAC?
+
+- Use **RBAC** when permissions follow job functions and do not depend on
which record is being accessed. It is easier to audit: "list everything an
editor can do" is a simple query. See the [RBAC in Python
tutorial](/docs/python-rbac-tutorial).
+- Use **ABAC** when the answer depends on the record: its owner, department,
status, or sensitivity.
+- Most applications combine both: roles decide which features a user can
reach, and attributes narrow that down to the records they may touch. The
[comparison of access control
models](/docs/access-control-models#combining-rbac-and-abac) shows a combined
model.
+
+## Next steps
+
+- **Full ABAC reference.** [ABAC](/docs/abac) covers JSON request parameters,
quoting rules, and `eval()` in detail.
+- **Ready-made Flask integration.** The [middleware list](/docs/middlewares)
includes flask-authz, which checks route-level rules for every request; keep
record-level checks like the ones above in your views.
+- **Several workers.** Use a [watcher](/docs/watchers) so every Gunicorn or
uWSGI worker reloads the policy when one of them changes it.
+- **Experiment in the browser.** The [online editor](/docs/online-editor) can
evaluate ABAC requests with JSON attributes.
diff --git a/docs/TutorialGoABAC.mdx b/docs/TutorialGoABAC.mdx
new file mode 100644
index 000000000..e7d5e6317
--- /dev/null
+++ b/docs/TutorialGoABAC.mdx
@@ -0,0 +1,251 @@
+---
+id: go-abac-tutorial
+title: ABAC in Go
+description: "Go ABAC tutorial: attribute-based access control for a net/http
API with Casbin, using owner, department and status to decide who can read,
edit and publish."
+keywords: [golang abac, go abac, abac example, attribute-based access control
go, abac library, golang authorization library, casbin abac]
+authors: [hsluoyz]
+---
+
+Role-based access control answers "what may an editor do?". Many real rules
need more than a role: *authors may edit their own drafts*, *editors may edit
articles from their own department*, *anyone may read a published article*.
These depend on attributes of the user and of the resource, which is what
attribute-based access control (ABAC) is for.
+
+This tutorial builds a small article API in Go with the standard `net/http`
package and puts all of those rules in a Casbin policy file. The same model and
policy work unchanged in the [Node.js](/docs/nodejs-abac-tutorial) and
[Python](/docs/flask-abac-tutorial) versions of this tutorial. If you are still
choosing between RBAC and ABAC, read [Choosing an access control
model](/docs/access-control-models) first.
+
+## 1. Install
+
+Go 1.22 or later is needed for the method and wildcard patterns in
`http.ServeMux`.
+
+```bash
+go mod init example.com/abac
+go get github.com/casbin/casbin/v3
+```
+
+## 2. Write the model
+
+Save this as `model.conf`:
+
+```ini
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub_rule, obj_type, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.obj.Type == p.obj_type && r.act == p.act && eval(p.sub_rule)
+```
+
+- `r.sub` and `r.obj` are not strings here but Go structs. The matcher reads
their fields, such as `r.obj.Type`.
+- Each policy rule carries a condition in `sub_rule`. `eval(p.sub_rule)`
evaluates that condition against the request, so the rules live in the policy
instead of in the model.
+
+## 3. Write the policy
+
+Save this as `policy.csv`:
+
+```csv
+p, r.obj.Status == 'published', article, read
+p, r.sub.Name == r.obj.Owner, article, read
+p, r.sub.Name == r.obj.Owner && r.obj.Status == 'draft', article, update
+p, r.sub.Role == 'editor' && r.sub.Department == r.obj.Department, article,
update
+p, r.sub.Role == 'editor' && r.sub.Department == r.obj.Department, article,
publish
+```
+
+Read each line as "allow this action on articles when the condition is true":
+
+1. Anyone may read a published article.
+2. Authors may read their own articles, including drafts.
+3. Authors may edit their own articles while they are drafts.
+4. Editors may edit any article from their own department.
+5. Editors may publish articles from their own department.
+
+:::tip
+Write string literals in conditions with single quotes, as in `'published'`. A
double quote in the middle of a field breaks CSV parsing. If a condition needs
a comma, wrap the whole field in double quotes.
+:::
+
+## 4. Write the server
+
+ABAC needs the resource itself, not just its URL, so the check runs after the
article has been loaded. The `authorize` helper does that and writes the error
response when the answer is no.
+
+```go title="main.go"
+package main
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+ "sync"
+
+ "github.com/casbin/casbin/v3"
+)
+
+type User struct {
+ Name string
+ Role string
+ Department string
+}
+
+type Article struct {
+ ID string `json:"id"`
+ Type string `json:"-"`
+ Title string `json:"title"`
+ Owner string `json:"owner"`
+ Department string `json:"department"`
+ Status string `json:"status"`
+}
+
+// In a real application these come from your database.
+var users = map[string]User{
+ "alice": {Name: "alice", Role: "author", Department: "engineering"},
+ "bob": {Name: "bob", Role: "editor", Department: "engineering"},
+ "carol": {Name: "carol", Role: "editor", Department: "sales"},
+ "dave": {Name: "dave", Role: "author", Department: "engineering"},
+}
+
+var (
+ mu sync.Mutex
+ articles = map[string]*Article{
+ "1": {ID: "1", Type: "article", Title: "Draft by alice", Owner:
"alice", Department: "engineering", Status: "draft"},
+ "2": {ID: "2", Type: "article", Title: "Published by alice", Owner:
"alice", Department: "engineering", Status: "published"},
+ }
+)
+
+type server struct {
+ e *casbin.Enforcer
+}
+
+// authorize loads the current user and asks Casbin whether they may perform
act on the article.
+func (s *server) authorize(w http.ResponseWriter, r *http.Request, a *Article,
act string) bool {
+ // Replace this with the user from your session or JWT.
+ user, ok := users[r.Header.Get("X-User")]
+ if !ok {
+ http.Error(w, "unauthenticated", http.StatusUnauthorized)
+ return false
+ }
+ allowed, err := s.e.Enforce(user, *a, act)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return false
+ }
+ if !allowed {
+ http.Error(w, "forbidden", http.StatusForbidden)
+ return false
+ }
+ return true
+}
+
+func (s *server) article(w http.ResponseWriter, r *http.Request) *Article {
+ mu.Lock()
+ defer mu.Unlock()
+ a, ok := articles[r.PathValue("id")]
+ if !ok {
+ http.NotFound(w, r)
+ return nil
+ }
+ return a
+}
+
+func main() {
+ e, err := casbin.NewEnforcer("model.conf", "policy.csv")
+ if err != nil {
+ log.Fatal(err)
+ }
+ s := &server{e: e}
+
+ mux := http.NewServeMux()
+ mux.HandleFunc("GET /articles/{id}", func(w http.ResponseWriter, r
*http.Request) {
+ a := s.article(w, r)
+ if a == nil || !s.authorize(w, r, a, "read") {
+ return
+ }
+ json.NewEncoder(w).Encode(a)
+ })
+ mux.HandleFunc("PUT /articles/{id}", func(w http.ResponseWriter, r
*http.Request) {
+ a := s.article(w, r)
+ if a == nil || !s.authorize(w, r, a, "update") {
+ return
+ }
+ mu.Lock()
+ json.NewDecoder(r.Body).Decode(&struct {
+ Title *string `json:"title"`
+ }{&a.Title})
+ mu.Unlock()
+ json.NewEncoder(w).Encode(a)
+ })
+ mux.HandleFunc("POST /articles/{id}/publish", func(w http.ResponseWriter,
r *http.Request) {
+ a := s.article(w, r)
+ if a == nil || !s.authorize(w, r, a, "publish") {
+ return
+ }
+ mu.Lock()
+ a.Status = "published"
+ mu.Unlock()
+ json.NewEncoder(w).Encode(a)
+ })
+
+ log.Fatal(http.ListenAndServe(":8080", mux))
+}
+```
+
+Casbin reads struct fields through reflection, so the fields used in the
policy (`Name`, `Owner`, `Status`, and so on) must be exported.
+
+:::note
+The `X-User` header keeps the example short. Casbin handles authorization
only; in a real application the user comes from your authentication layer, such
as a verified JWT or a session.
+:::
+
+## 5. Try it
+
+```bash
+go run .
+```
+
+Article 1 is alice's draft and article 2 is her published article, both in the
engineering department.
+
+```bash
+curl -i -H "X-User: dave" http://localhost:8080/articles/1 #
403, someone else's draft
+curl -i -H "X-User: dave" http://localhost:8080/articles/2 #
200, published
+curl -i -X PUT -H "X-User: alice" -d '{"title":"New title"}' \
+ http://localhost:8080/articles/1 #
200, her own draft
+curl -i -X PUT -H "X-User: alice" -d '{"title":"x"}' \
+ http://localhost:8080/articles/2 #
403, no longer a draft
+curl -i -X PUT -H "X-User: bob" -d '{"title":"x"}' \
+ http://localhost:8080/articles/2 #
200, editor in engineering
+curl -i -X PUT -H "X-User: carol" -d '{"title":"x"}' \
+ http://localhost:8080/articles/2 #
403, editor in sales
+curl -i -X POST -H "X-User: alice" http://localhost:8080/articles/1/publish #
403, authors cannot publish
+curl -i -X POST -H "X-User: bob" http://localhost:8080/articles/1/publish #
200
+curl -i -H "X-User: dave" http://localhost:8080/articles/1 #
200, now published
+```
+
+## 6. Change rules at runtime
+
+Conditions are policy rows, so they can be added and removed while the server
is running, and saved to a database through an [adapter](/docs/adapters):
+
+```go
+// Admins may edit any article.
+e.AddPolicy("r.sub.Role == 'admin'", "article", "update")
+
+// Nobody may publish any more.
+e.RemoveFilteredPolicy(1, "article", "publish")
+```
+
+To find out which rule allowed a request, use `EnforceEx`. It returns the
matching rule, which is useful for audit logs:
+
+```go
+ok, rule, _ := e.EnforceEx(users["bob"], *articles["2"], "update")
+// true [r.sub.Role == 'editor' && r.sub.Department == r.obj.Department
article update]
+```
+
+## ABAC or RBAC?
+
+- Use **RBAC** when permissions follow job functions and do not depend on
which record is being accessed. It is easier to audit: "list everything an
editor can do" is a simple query. See the [RBAC in Go
tutorial](/docs/go-rbac-tutorial).
+- Use **ABAC** when the answer depends on the record: its owner, department,
status, or sensitivity.
+- Most applications combine both: roles decide which features a user can
reach, and attributes narrow that down to the records they may touch. The
[comparison of access control
models](/docs/access-control-models#combining-rbac-and-abac) shows a combined
model.
+
+## Next steps
+
+- **Full ABAC reference.** [ABAC](/docs/abac) covers JSON request parameters,
quoting rules, and `eval()` in detail.
+- **Rules about time and place.** Add more request fields, for example an IP
address checked with `ipMatch`, or the current time passed in as an attribute.
See [Functions](/docs/function).
+- **Deny rules.** Add an `eft` column and use a deny-override effect so that
one rule can block what others allow. See [Effector](/docs/effector).
+- **Experiment in the browser.** The [online editor](/docs/online-editor) can
evaluate ABAC requests with JSON attributes.
diff --git a/docs/TutorialGoRBAC.mdx b/docs/TutorialGoRBAC.mdx
index 2c65c13ae..2702fcba0 100644
--- a/docs/TutorialGoRBAC.mdx
+++ b/docs/TutorialGoRBAC.mdx
@@ -8,7 +8,7 @@ authors: [hsluoyz]
This tutorial adds role-based access control (RBAC) to a REST API written in
Go with [Gin](https://github.com/gin-gonic/gin). At the end, three roles
(`admin`, `editor`, `viewer`) are allowed different HTTP methods on different
routes, and the rules live in a policy file that you can change without
touching the handlers.
-The same model and policy work in every Casbin implementation, so you can
reuse them in the [Node.js](/docs/nodejs-rbac-tutorial),
[Python](/docs/python-rbac-tutorial), and [Java](/docs/java-rbac-tutorial)
versions of this tutorial.
+The same model and policy work in every Casbin implementation, so you can
reuse them in the [Node.js](/docs/nodejs-rbac-tutorial),
[Python](/docs/python-rbac-tutorial), [Java](/docs/java-rbac-tutorial), and
[Rust](/docs/rust-rbac-tutorial) versions of this tutorial. For gRPC services,
see [gRPC authorization in Go](/docs/grpc-rbac-tutorial).
## 1. Install
@@ -148,6 +148,6 @@ The full list is in the [RBAC API](/docs/rbac-api) and the
[Management API](/doc
- **Store the policy in a database.** Replace `policy.csv` with an
[adapter](/docs/adapters) for MySQL, PostgreSQL, MongoDB, Redis, and others.
Policy changes made through the API are then saved automatically.
- **Run more than one instance.** Use a [watcher](/docs/watchers) so every
instance reloads the policy when one of them changes it.
- **Multi-tenant applications.** [RBAC with domains](/docs/rbac-with-domains)
gives a user different roles in different tenants.
-- **Rules based on attributes.** Combine roles with [ABAC](/docs/abac), for
example "editors may update only their own articles".
+- **Rules based on attributes.** Combine roles with [ABAC](/docs/abac), for
example "editors may update only their own articles". The [ABAC
tutorial](/docs/go-abac-tutorial) builds this step by step.
- **Ready-made middleware.** The [middleware list](/docs/middlewares) has
plugins for Gin, Echo, Fiber, Chi, and other frameworks.
- **Experiment in the browser.** Paste the model and policy into the [online
editor](/docs/online-editor) to test requests without running any code.
diff --git a/docs/TutorialGrpcRBAC.mdx b/docs/TutorialGrpcRBAC.mdx
new file mode 100644
index 000000000..050aa20ee
--- /dev/null
+++ b/docs/TutorialGrpcRBAC.mdx
@@ -0,0 +1,284 @@
+---
+id: grpc-rbac-tutorial
+title: gRPC Authorization in Go
+description: "Role-based access control for a Go gRPC service with Casbin:
unary and stream interceptors that check the caller's role against the full
method name."
+keywords: [rbac grpc, grpc authorization, grpc interceptor authorization, go
grpc rbac, grpc access control, casbin grpc, golang authorization library]
+authors: [hsluoyz]
+---
+
+In a gRPC service, every call names a full method such as
`/library.v1.BookService/DeleteBook`. That makes method-level authorization
simple: decide which roles may call which methods, and check it in a server
interceptor before the handler runs. This tutorial does that in Go with Casbin,
for both unary and streaming calls.
+
+## 1. Define the service
+
+The example is a small library service. Save this as
`proto/library/v1/library.proto`:
+
+```protobuf title="proto/library/v1/library.proto"
+syntax = "proto3";
+
+package library.v1;
+
+option go_package = "example.com/library/gen/library/v1;libraryv1";
+
+service BookService {
+ rpc GetBook(GetBookRequest) returns (Book);
+ rpc CreateBook(CreateBookRequest) returns (Book);
+ rpc DeleteBook(DeleteBookRequest) returns (DeleteBookResponse);
+}
+
+message Book {
+ string id = 1;
+ string title = 2;
+}
+
+message GetBookRequest {
+ string id = 1;
+}
+
+message CreateBookRequest {
+ string title = 1;
+}
+
+message DeleteBookRequest {
+ string id = 1;
+}
+
+message DeleteBookResponse {}
+```
+
+Generate the Go code with `protoc` (or `buf generate`):
+
+```bash
+go mod init example.com/library
+go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
+go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
+
+protoc -Iproto --go_out=. --go_opt=module=example.com/library
--go-grpc_out=. --go-grpc_opt=module=example.com/library
proto/library/v1/library.proto
+
+go get github.com/casbin/casbin/v3 google.golang.org/grpc
+```
+
+## 2. Write the model
+
+Save this as `model.conf`:
+
+```ini
+[request_definition]
+r = sub, method
+
+[policy_definition]
+p = sub, method
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && keyMatch(r.method, p.method)
+```
+
+- `r = sub, method`: each request is a caller and the full gRPC method name.
+- `g = _, _`: callers can be assigned to roles, and roles to other roles.
+- `keyMatch` lets a rule end in `*`, so one line can cover every method of a
service.
+
+## 3. Write the policy
+
+Save this as `policy.csv`:
+
+```csv
+p, reader, /library.v1.BookService/GetBook
+p, librarian, /library.v1.BookService/CreateBook
+p, admin, /library.v1.BookService/*
+
+g, librarian, reader
+g, admin, librarian
+
+g, alice, admin
+g, bob, librarian
+g, carol, reader
+```
+
+A `reader` may call `GetBook`, a `librarian` may also call `CreateBook`, and
an `admin` may call every method of `BookService`, including methods added
later. The `g` lines between roles build the hierarchy, so each role lists only
what is new at its level.
+
+## 4. Write the interceptors
+
+```go title="server/authz.go"
+package main
+
+import (
+ "context"
+
+ "github.com/casbin/casbin/v3"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/metadata"
+ "google.golang.org/grpc/status"
+)
+
+// authorize asks Casbin whether the caller may invoke fullMethod, such as
/library.v1.BookService/DeleteBook.
+func authorize(ctx context.Context, e *casbin.Enforcer, fullMethod string)
error {
+ // Replace this with the identity from your authentication layer,
+ // such as a verified JWT in the "authorization" metadata or the client's
mTLS certificate.
+ md, _ := metadata.FromIncomingContext(ctx)
+ users := md.Get("x-user")
+ if len(users) == 0 {
+ return status.Error(codes.Unauthenticated, "missing user")
+ }
+
+ allowed, err := e.Enforce(users[0], fullMethod)
+ if err != nil {
+ return status.Error(codes.Internal, err.Error())
+ }
+ if !allowed {
+ return status.Errorf(codes.PermissionDenied, "%s may not call %s",
users[0], fullMethod)
+ }
+ return nil
+}
+
+func UnaryAuthz(e *casbin.Enforcer) grpc.UnaryServerInterceptor {
+ return func(ctx context.Context, req any, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (any, error) {
+ if err := authorize(ctx, e, info.FullMethod); err != nil {
+ return nil, err
+ }
+ return handler(ctx, req)
+ }
+}
+
+func StreamAuthz(e *casbin.Enforcer) grpc.StreamServerInterceptor {
+ return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo,
handler grpc.StreamHandler) error {
+ if err := authorize(ss.Context(), e, info.FullMethod); err != nil {
+ return err
+ }
+ return handler(srv, ss)
+ }
+}
+```
+
+Unauthenticated callers get `Unauthenticated`, and authenticated callers
without the permission get `PermissionDenied`, which is what gRPC clients and
gateways expect.
+
+:::note
+The `x-user` metadata keeps the example short. Casbin handles authorization
only. In a real service, verify a token from the `authorization` metadata, or
take the identity from the client certificate with `peer.FromContext` when you
use mTLS. If an authentication interceptor already does that, chain it before
the authorization interceptor.
+:::
+
+## 5. Register them on the server
+
+```go title="server/main.go"
+package main
+
+import (
+ "context"
+ "log"
+ "net"
+
+ "github.com/casbin/casbin/v3"
+ "google.golang.org/grpc"
+
+ libraryv1 "example.com/library/gen/library/v1"
+)
+
+type bookService struct {
+ libraryv1.UnimplementedBookServiceServer
+}
+
+func (bookService) GetBook(ctx context.Context, req *libraryv1.GetBookRequest)
(*libraryv1.Book, error) {
+ return &libraryv1.Book{Id: req.GetId(), Title: "The Go Programming
Language"}, nil
+}
+
+func (bookService) CreateBook(ctx context.Context, req
*libraryv1.CreateBookRequest) (*libraryv1.Book, error) {
+ return &libraryv1.Book{Id: "2", Title: req.GetTitle()}, nil
+}
+
+func (bookService) DeleteBook(ctx context.Context, req
*libraryv1.DeleteBookRequest) (*libraryv1.DeleteBookResponse, error) {
+ return &libraryv1.DeleteBookResponse{}, nil
+}
+
+func main() {
+ e, err := casbin.NewEnforcer("model.conf", "policy.csv")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ s := grpc.NewServer(
+ grpc.ChainUnaryInterceptor(UnaryAuthz(e)),
+ grpc.ChainStreamInterceptor(StreamAuthz(e)),
+ )
+ libraryv1.RegisterBookServiceServer(s, bookService{})
+
+ lis, err := net.Listen("tcp", ":50051")
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Fatal(s.Serve(lis))
+}
+```
+
+`grpc.ChainUnaryInterceptor` and `grpc.ChainStreamInterceptor` accept several
interceptors, so logging, metrics, and authentication can be added next to the
authorization check.
+
+## 6. Try it
+
+Start the server:
+
+```bash
+go run ./server
+```
+
+This client calls every method as four different users:
+
+```go title="client/main.go"
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+ "google.golang.org/grpc/metadata"
+ "google.golang.org/grpc/status"
+
+ libraryv1 "example.com/library/gen/library/v1"
+)
+
+func main() {
+ conn, err := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()))
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer conn.Close()
+ client := libraryv1.NewBookServiceClient(conn)
+
+ for _, user := range []string{"alice", "bob", "carol", "dave"} {
+ ctx := metadata.AppendToOutgoingContext(context.Background(),
"x-user", user)
+
+ _, getErr := client.GetBook(ctx, &libraryv1.GetBookRequest{Id: "1"})
+ _, createErr := client.CreateBook(ctx,
&libraryv1.CreateBookRequest{Title: "New book"})
+ _, deleteErr := client.DeleteBook(ctx,
&libraryv1.DeleteBookRequest{Id: "1"})
+
+ fmt.Printf("%-6s GetBook=%-16v CreateBook=%-16v DeleteBook=%v\n", user,
+ status.Code(getErr), status.Code(createErr),
status.Code(deleteErr))
+ }
+}
+```
+
+```bash
+go run ./client
+```
+
+```text
+alice GetBook=OK CreateBook=OK DeleteBook=OK
+bob GetBook=OK CreateBook=OK
DeleteBook=PermissionDenied
+carol GetBook=OK CreateBook=PermissionDenied
DeleteBook=PermissionDenied
+dave GetBook=PermissionDenied CreateBook=PermissionDenied
DeleteBook=PermissionDenied
+```
+
+With [grpcurl](https://github.com/fullstorydev/grpcurl), pass the user as a
header: `grpcurl -plaintext -H "x-user: bob" ...`.
+
+## Going further
+
+- **Public methods.** Health checks and server reflection usually need no
user. Return early at the top of `authorize` for them, for example `if
strings.HasPrefix(fullMethod, "/grpc.health.v1.Health/") { return nil }`.
+- **Per-record rules.** The interceptor sees the method name before the
handler runs. Rules that depend on the record, such as "librarians may delete
only books from their own branch", belong in the handler after loading the
record; see the [ABAC tutorial](/docs/go-abac-tutorial).
+- **Multi-tenant services.** Pass the tenant from metadata as a third argument
and use [RBAC with domains](/docs/rbac-with-domains).
+- **Store the policy in a database.** Replace `policy.csv` with an
[adapter](/docs/adapters), and add a [watcher](/docs/watchers) when several
replicas serve the same service.
+- **HTTP APIs.** For REST endpoints in Go, see [RBAC in Go with
Gin](/docs/go-rbac-tutorial).
diff --git a/docs/TutorialJavaRBAC.mdx b/docs/TutorialJavaRBAC.mdx
index cb00fa35e..bf8aee828 100644
--- a/docs/TutorialJavaRBAC.mdx
+++ b/docs/TutorialJavaRBAC.mdx
@@ -8,7 +8,7 @@ authors: [hsluoyz]
This tutorial adds role-based access control (RBAC) to a REST API built with
[Spring Boot](https://spring.io/projects/spring-boot) and jCasbin. At the end,
three roles (`admin`, `editor`, `viewer`) are allowed different HTTP methods on
different routes, and the rules live in a policy file that you can change
without touching the controllers.
-The same model and policy work in every Casbin implementation, so you can
reuse them in the [Go](/docs/go-rbac-tutorial),
[Node.js](/docs/nodejs-rbac-tutorial), and [Python](/docs/python-rbac-tutorial)
versions of this tutorial.
+The same model and policy work in every Casbin implementation, so you can
reuse them in the [Go](/docs/go-rbac-tutorial),
[Node.js](/docs/nodejs-rbac-tutorial), [Python](/docs/python-rbac-tutorial),
and [Rust](/docs/rust-rbac-tutorial) versions of this tutorial.
## 1. Add the dependency
@@ -205,5 +205,5 @@ The full list is in the [RBAC API](/docs/rbac-api) and the
[Management API](/doc
- **Use the Spring Boot starter.**
[casbin-spring-boot-starter](https://github.com/jcasbin/casbin-spring-boot-starter)
creates the `Enforcer` and a JDBC adapter from `application.yml`.
- **Run more than one instance.** Use a [watcher](/docs/watchers) so every
instance reloads the policy when one of them changes it.
- **Multi-tenant applications.** [RBAC with domains](/docs/rbac-with-domains)
gives a user different roles in different tenants.
-- **Rules based on attributes.** Combine roles with [ABAC](/docs/abac), for
example "editors may update only their own articles".
+- **Rules based on attributes.** Combine roles with [ABAC](/docs/abac), for
example "editors may update only their own articles". [Choosing an access
control model](/docs/access-control-models#combining-rbac-and-abac) shows a
combined model.
- **Other frameworks.** The [middleware list](/docs/middlewares) has plugins
for Spring Boot, Apache Shiro, JFinal, Nutz, and other frameworks.
diff --git a/docs/TutorialNestJSRBAC.mdx b/docs/TutorialNestJSRBAC.mdx
new file mode 100644
index 000000000..4120accc6
--- /dev/null
+++ b/docs/TutorialNestJSRBAC.mdx
@@ -0,0 +1,231 @@
+---
+id: nestjs-rbac-tutorial
+title: RBAC in NestJS
+description: "NestJS RBAC tutorial: role-based access control with role
inheritance for a NestJS API, using node-casbin, a global guard and a
permission decorator."
+keywords: [nestjs rbac, rbac nestjs, authz nestjs, nestjs authorization,
nestjs casbin, nestjs guard roles, role based access control nestjs, rbac
hierarchy]
+authors: [hsluoyz]
+---
+
+This tutorial adds role-based access control (RBAC) to a
[NestJS](https://nestjs.com) API with node-casbin. Each route declares the
permission it needs with a decorator, a global guard asks Casbin whether the
current user has it, and roles inherit from each other, so an `admin`
automatically gets everything an `editor` can do.
+
+The permissions are named after resources and actions (`articles`, `delete`)
instead of URL paths, which fits NestJS controllers well. For path-based rules,
see the [Express version](/docs/nodejs-rbac-tutorial).
+
+## 1. Install
+
+Start from a NestJS project (`nest new my-app`) and add Casbin:
+
+```bash
+npm install casbin
+```
+
+## 2. Write the model
+
+Save this as `model.conf` in the project root:
+
+```ini
+[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) && r.obj == p.obj && r.act == p.act
+```
+
+- `r = sub, obj, act`: each request is a user, a resource, and an action.
+- `g = _, _`: users can be assigned to roles, and roles to other roles.
+- `g(r.sub, p.sub)` is true when the user has the role in the policy rule,
either directly or through inherited roles.
+
+## 3. Write the policy
+
+Save this as `policy.csv`:
+
+```csv
+p, viewer, articles, read
+p, editor, articles, create
+p, editor, articles, update
+p, admin, articles, delete
+
+g, editor, viewer
+g, admin, editor
+
+g, alice, admin
+g, bob, editor
+g, carol, viewer
+```
+
+The first block grants each role only what is new at that level. The second
block builds the hierarchy: an `editor` is also a `viewer`, and an `admin` is
also an `editor`. The last block assigns users to roles. As a result:
+
+| User | Role | read | create | update | delete |
+|---|---|---|---|---|---|
+| alice | admin | yes | yes | yes | yes |
+| bob | editor | yes | yes | yes | no |
+| carol | viewer | yes | no | no | no |
+| dave | (none) | no | no | no | no |
+
+## 4. Add a permission decorator
+
+```ts title="src/permission.decorator.ts"
+import { SetMetadata } from '@nestjs/common';
+
+export const PERMISSION_KEY = 'permission';
+
+export interface Permission {
+ resource: string;
+ action: string;
+}
+
+// Declares which permission a route needs, for example
@RequirePermission('articles', 'delete').
+export const RequirePermission = (resource: string, action: string) =>
+ SetMetadata(PERMISSION_KEY, { resource, action } as Permission);
+```
+
+## 5. Add the guard
+
+The guard reads the permission from the route's metadata and asks Casbin.
Routes without the decorator stay public.
+
+```ts title="src/casbin.guard.ts"
+import { CanActivate, ExecutionContext, ForbiddenException, Inject, Injectable
} from '@nestjs/common';
+import { Reflector } from '@nestjs/core';
+import { Enforcer } from 'casbin';
+import { Request } from 'express';
+import { Permission, PERMISSION_KEY } from './permission.decorator';
+
+export const ENFORCER = 'CASBIN_ENFORCER';
+
+@Injectable()
+export class CasbinGuard implements CanActivate {
+ constructor(
+ private readonly reflector: Reflector,
+ @Inject(ENFORCER) private readonly enforcer: Enforcer,
+ ) {}
+
+ async canActivate(context: ExecutionContext): Promise<boolean> {
+ const permission =
this.reflector.getAllAndOverride<Permission>(PERMISSION_KEY, [
+ context.getHandler(),
+ context.getClass(),
+ ]);
+ if (!permission) {
+ return true; // Routes without @RequirePermission are public.
+ }
+
+ const request = context.switchToHttp().getRequest<Request>();
+ // Replace this with the user your authentication guard puts on the
request, such as request.user.
+ const user = request.header('X-User') ?? '';
+
+ const allowed = await this.enforcer.enforce(user, permission.resource,
permission.action);
+ if (!allowed) {
+ throw new ForbiddenException();
+ }
+ return true;
+ }
+}
+```
+
+:::note
+The `X-User` header keeps the example short. Casbin handles authorization
only. In a real application, an authentication guard, such as one based on
`@nestjs/passport`, runs first and sets `request.user`; read the user name or
ID from there.
+:::
+
+## 6. Use it in a controller
+
+```ts title="src/articles.controller.ts"
+import { Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
+import { RequirePermission } from './permission.decorator';
+
+@Controller('articles')
+export class ArticlesController {
+ @Get()
+ @RequirePermission('articles', 'read')
+ findAll() {
+ return { articles: [] };
+ }
+
+ @Post()
+ @RequirePermission('articles', 'create')
+ create() {
+ return { created: true };
+ }
+
+ @Put(':id')
+ @RequirePermission('articles', 'update')
+ update(@Param('id') id: string) {
+ return { updated: id };
+ }
+
+ @Delete(':id')
+ @RequirePermission('articles', 'delete')
+ remove(@Param('id') id: string) {
+ return { deleted: id };
+ }
+}
+```
+
+## 7. Register the enforcer and the guard
+
+The enforcer is created once by a factory provider and injected into the
guard. Registering the guard with `APP_GUARD` applies it to every route in the
application.
+
+```ts title="src/app.module.ts"
+import { Module } from '@nestjs/common';
+import { APP_GUARD } from '@nestjs/core';
+import { newEnforcer } from 'casbin';
+import { ArticlesController } from './articles.controller';
+import { CasbinGuard, ENFORCER } from './casbin.guard';
+
+@Module({
+ controllers: [ArticlesController],
+ providers: [
+ {
+ provide: ENFORCER,
+ useFactory: () => newEnforcer('model.conf', 'policy.csv'),
+ },
+ {
+ provide: APP_GUARD,
+ useClass: CasbinGuard,
+ },
+ ],
+})
+export class AppModule {}
+```
+
+## 8. Try it
+
+```bash
+npm run start
+```
+
+```bash
+curl -i -X DELETE -H "X-User: alice" http://localhost:3000/articles/1 # 200,
admins can delete
+curl -i -X DELETE -H "X-User: bob" http://localhost:3000/articles/1 # 403,
editors cannot
+curl -i -X PUT -H "X-User: bob" http://localhost:3000/articles/1 # 200,
editors can update
+curl -i -X POST -H "X-User: carol" http://localhost:3000/articles # 403,
viewers are read-only
+curl -i -H "X-User: carol" http://localhost:3000/articles # 200
+curl -i -H "X-User: dave" http://localhost:3000/articles # 403,
no role
+```
+
+## 9. Manage roles at runtime
+
+Inject the enforcer anywhere you need it, for example in an admin service,
with `@Inject(ENFORCER)`:
+
+```ts
+await enforcer.addRoleForUser('dave', 'editor'); // dave becomes an
editor
+await enforcer.getImplicitRolesForUser('alice'); // [ 'admin',
'editor', 'viewer' ]
+await enforcer.getImplicitPermissionsForUser('bob');
+// [ [ 'editor', 'articles', 'create' ], [ 'editor', 'articles', 'update' ], [
'viewer', 'articles', 'read' ] ]
+```
+
+`getImplicitPermissionsForUser` is handy for a "what can I do" endpoint that
the frontend uses to show or hide buttons. The full list is in the [RBAC
API](/docs/rbac-api).
+
+## Next steps
+
+- **Store the policy in a database.** Replace `policy.csv` with an
[adapter](/docs/adapters), for example TypeORM, Prisma, Sequelize, or MongoDB.
Policy changes made through the API are then saved automatically.
+- **Several instances.** Use a [watcher](/docs/watchers) so every instance
reloads the policy when one of them changes it.
+- **Ownership rules.** "Authors may edit only their own articles" depends on
the article, not just the role. Load the article in the handler and check it
with an [ABAC](/docs/nodejs-abac-tutorial) model.
+- **Multi-tenant applications.** [RBAC with domains](/docs/rbac-with-domains)
gives a user different roles in different tenants; add the tenant as a fourth
argument to `enforce`.
+- **Ready-made modules.** The [middleware list](/docs/middlewares) links
community NestJS modules such as nest-authz.
diff --git a/docs/TutorialNodejsABAC.mdx b/docs/TutorialNodejsABAC.mdx
new file mode 100644
index 000000000..28235a1cd
--- /dev/null
+++ b/docs/TutorialNodejsABAC.mdx
@@ -0,0 +1,178 @@
+---
+id: nodejs-abac-tutorial
+title: ABAC in Node.js with Express
+description: "Node.js ABAC tutorial: attribute-based access control for an
Express API with node-casbin, using owner, department and status to decide who
can edit."
+keywords: [abac nodejs, abac expressjs, express abac, node.js abac, abac
example, attribute-based access control javascript, node-casbin, nodejs
authorization library]
+authors: [hsluoyz]
+---
+
+Role-based access control answers "what may an editor do?". Many real rules
need more than a role: *authors may edit their own drafts*, *editors may edit
articles from their own department*, *anyone may read a published article*.
These depend on attributes of the user and of the resource, which is what
attribute-based access control (ABAC) is for.
+
+This tutorial builds a small article API with [Express](https://expressjs.com)
and puts all of those rules in a Casbin policy file. The same model and policy
work unchanged in the [Go](/docs/go-abac-tutorial) and
[Python](/docs/flask-abac-tutorial) versions of this tutorial. If you are still
choosing between RBAC and ABAC, read [Choosing an access control
model](/docs/access-control-models) first.
+
+## 1. Install
+
+```bash
+npm install casbin express
+```
+
+## 2. Write the model
+
+Save this as `model.conf`:
+
+```ini
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub_rule, obj_type, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = r.obj.Type == p.obj_type && r.act == p.act && eval(p.sub_rule)
+```
+
+- `r.sub` and `r.obj` are plain JavaScript objects. The matcher reads their
properties, such as `r.obj.Type`.
+- Each policy rule carries a condition in `sub_rule`. `eval(p.sub_rule)`
evaluates that condition against the request, so the rules live in the policy
instead of in the model.
+
+## 3. Write the policy
+
+Save this as `policy.csv`:
+
+```csv
+p, r.obj.Status == 'published', article, read
+p, r.sub.Name == r.obj.Owner, article, read
+p, r.sub.Name == r.obj.Owner && r.obj.Status == 'draft', article, update
+p, r.sub.Role == 'editor' && r.sub.Department == r.obj.Department, article,
update
+p, r.sub.Role == 'editor' && r.sub.Department == r.obj.Department, article,
publish
+```
+
+Read each line as "allow this action on articles when the condition is true":
+
+1. Anyone may read a published article.
+2. Authors may read their own articles, including drafts.
+3. Authors may edit their own articles while they are drafts.
+4. Editors may edit any article from their own department.
+5. Editors may publish articles from their own department.
+
+:::tip
+Write string literals in conditions with single quotes, as in `'published'`. A
double quote in the middle of a field breaks CSV parsing. If a condition needs
a comma, wrap the whole field in double quotes.
+:::
+
+## 4. Add the middleware
+
+ABAC needs the resource itself, not just its URL. The `authorize` middleware
loads the article, asks Casbin, and puts the article on `req` so the route
handler does not load it again.
+
+```js title="app.js"
+const express = require('express');
+const { newEnforcer } = require('casbin');
+
+// In a real application these come from your database.
+const users = {
+ alice: { Name: 'alice', Role: 'author', Department: 'engineering' },
+ bob: { Name: 'bob', Role: 'editor', Department: 'engineering' },
+ carol: { Name: 'carol', Role: 'editor', Department: 'sales' },
+ dave: { Name: 'dave', Role: 'author', Department: 'engineering' },
+};
+
+const articles = {
+ 1: { Type: 'article', Id: '1', Title: 'Draft by alice', Owner: 'alice',
Department: 'engineering', Status: 'draft' },
+ 2: { Type: 'article', Id: '2', Title: 'Published by alice', Owner: 'alice',
Department: 'engineering', Status: 'published' },
+};
+
+// Loads the article, then asks Casbin whether the current user may perform
`action` on it.
+function authorize(enforcer, action) {
+ return async (req, res, next) => {
+ // Replace this with the user from your session or JWT.
+ const user = users[req.get('X-User')];
+ if (!user) {
+ return res.status(401).json({ error: 'unauthenticated' });
+ }
+ const article = articles[req.params.id];
+ if (!article) {
+ return res.status(404).json({ error: 'not found' });
+ }
+ if (!(await enforcer.enforce(user, article, action))) {
+ return res.status(403).json({ error: 'forbidden' });
+ }
+ req.article = article;
+ next();
+ };
+}
+
+async function main() {
+ const enforcer = await newEnforcer('model.conf', 'policy.csv');
+
+ const app = express();
+ app.use(express.json());
+
+ app.get('/articles/:id', authorize(enforcer, 'read'), (req, res) => {
+ res.json(req.article);
+ });
+ app.put('/articles/:id', authorize(enforcer, 'update'), (req, res) => {
+ req.article.Title = req.body?.title ?? req.article.Title;
+ res.json(req.article);
+ });
+ app.post('/articles/:id/publish', authorize(enforcer, 'publish'), (req, res)
=> {
+ req.article.Status = 'published';
+ res.json(req.article);
+ });
+
+ app.listen(3000, () => console.log('listening on http://localhost:3000'));
+}
+
+main();
+```
+
+:::note
+The `X-User` header keeps the example short. Casbin handles authorization
only; in a real application the user comes from your authentication layer, such
as a verified JWT or a session.
+:::
+
+## 5. Try it
+
+```bash
+node app.js
+```
+
+Article 1 is alice's draft and article 2 is her published article, both in the
engineering department.
+
+```bash
+curl -i -H "X-User: dave" http://localhost:3000/articles/1 #
403, someone else's draft
+curl -i -H "X-User: dave" http://localhost:3000/articles/2 #
200, published
+curl -i -X PUT -H "X-User: alice" -H "Content-Type: application/json" \
+ -d '{"title":"New title"}' http://localhost:3000/articles/1 #
200, her own draft
+curl -i -X PUT -H "X-User: alice" http://localhost:3000/articles/2 #
403, no longer a draft
+curl -i -X PUT -H "X-User: bob" http://localhost:3000/articles/2 #
200, editor in engineering
+curl -i -X PUT -H "X-User: carol" http://localhost:3000/articles/2 #
403, editor in sales
+curl -i -X POST -H "X-User: alice" http://localhost:3000/articles/1/publish #
403, authors cannot publish
+curl -i -X POST -H "X-User: bob" http://localhost:3000/articles/1/publish #
200
+curl -i -H "X-User: dave" http://localhost:3000/articles/1 #
200, now published
+```
+
+## 6. Change rules at runtime
+
+Conditions are policy rows, so they can be added and removed while the server
is running, and saved to a database through an [adapter](/docs/adapters):
+
+```js
+// Admins may edit any article.
+await enforcer.addPolicy("r.sub.Role == 'admin'", 'article', 'update');
+
+// Which rule allowed this request? Useful for audit logs.
+const [ok, rule] = await enforcer.enforceEx(users.bob, articles[2], 'update');
+// true [ "r.sub.Role == 'editor' && r.sub.Department == r.obj.Department",
'article', 'update' ]
+```
+
+## ABAC or RBAC?
+
+- Use **RBAC** when permissions follow job functions and do not depend on
which record is being accessed. It is easier to audit: "list everything an
editor can do" is a simple query. See the [RBAC in Node.js
tutorial](/docs/nodejs-rbac-tutorial).
+- Use **ABAC** when the answer depends on the record: its owner, department,
status, or sensitivity.
+- Most applications combine both: roles decide which features a user can
reach, and attributes narrow that down to the records they may touch. The
[comparison of access control
models](/docs/access-control-models#combining-rbac-and-abac) shows a combined
model.
+
+## Next steps
+
+- **Full ABAC reference.** [ABAC](/docs/abac) covers JSON request parameters,
quoting rules, and `eval()` in detail.
+- **NestJS.** The [NestJS tutorial](/docs/nestjs-rbac-tutorial) shows a guard
and a decorator, which you can extend with the same ABAC model.
+- **Check permissions in the browser.** [Casbin.js](/docs/frontend) uses the
same rules to show or hide buttons in the frontend.
+- **Experiment in the browser.** The [online editor](/docs/online-editor) can
evaluate ABAC requests with JSON attributes.
diff --git a/docs/TutorialNodejsRBAC.mdx b/docs/TutorialNodejsRBAC.mdx
index 1cd3f2577..6b2f3d7e8 100644
--- a/docs/TutorialNodejsRBAC.mdx
+++ b/docs/TutorialNodejsRBAC.mdx
@@ -8,7 +8,7 @@ authors: [hsluoyz]
This tutorial adds role-based access control (RBAC) to a REST API built with
[Express](https://expressjs.com). At the end, three roles (`admin`, `editor`,
`viewer`) are allowed different HTTP methods on different routes, and the rules
live in a policy file that you can change without touching the route handlers.
-The same model and policy work in every Casbin implementation, so you can
reuse them in the [Go](/docs/go-rbac-tutorial),
[Python](/docs/python-rbac-tutorial), and [Java](/docs/java-rbac-tutorial)
versions of this tutorial.
+The same model and policy work in every Casbin implementation, so you can
reuse them in the [Go](/docs/go-rbac-tutorial),
[Python](/docs/python-rbac-tutorial), [Java](/docs/java-rbac-tutorial), and
[Rust](/docs/rust-rbac-tutorial) versions of this tutorial. If you use NestJS,
see [RBAC in NestJS](/docs/nestjs-rbac-tutorial).
## 1. Install
@@ -143,5 +143,5 @@ The full list is in the [RBAC API](/docs/rbac-api) and the
[Management API](/doc
- **Run more than one instance.** Use a [watcher](/docs/watchers) so every
instance reloads the policy when one of them changes it.
- **Show or hide UI elements.** [Frontend usage](/docs/frontend) explains how
to send a user's permissions to the browser with `casbin.js`.
- **Multi-tenant applications.** [RBAC with domains](/docs/rbac-with-domains)
gives a user different roles in different tenants.
-- **Rules based on attributes.** Combine roles with [ABAC](/docs/abac), for
example "editors may update only their own articles".
+- **Rules based on attributes.** Combine roles with [ABAC](/docs/abac), for
example "editors may update only their own articles". The [ABAC
tutorial](/docs/nodejs-abac-tutorial) builds this step by step.
- **Ready-made middleware.** The [middleware list](/docs/middlewares) has
plugins for Express, Koa, NestJS, Fastify, and other frameworks.
diff --git a/docs/TutorialPythonRBAC.mdx b/docs/TutorialPythonRBAC.mdx
index 1c82f94bf..55295524c 100644
--- a/docs/TutorialPythonRBAC.mdx
+++ b/docs/TutorialPythonRBAC.mdx
@@ -8,7 +8,7 @@ authors: [hsluoyz]
This tutorial adds role-based access control (RBAC) to a REST API built with
[FastAPI](https://fastapi.tiangolo.com) and PyCasbin. At the end, three roles
(`admin`, `editor`, `viewer`) are allowed different HTTP methods on different
routes, and the rules live in a policy file that you can change without
touching the route functions.
-The same model and policy work in every Casbin implementation, so you can
reuse them in the [Go](/docs/go-rbac-tutorial),
[Node.js](/docs/nodejs-rbac-tutorial), and [Java](/docs/java-rbac-tutorial)
versions of this tutorial.
+The same model and policy work in every Casbin implementation, so you can
reuse them in the [Go](/docs/go-rbac-tutorial),
[Node.js](/docs/nodejs-rbac-tutorial), [Java](/docs/java-rbac-tutorial), and
[Rust](/docs/rust-rbac-tutorial) versions of this tutorial.
## 1. Install
@@ -139,6 +139,6 @@ The full list is in the [RBAC API](/docs/rbac-api) and the
[Management API](/doc
- **Store the policy in a database.** Replace `policy.csv` with an
[adapter](/docs/adapters) for SQLAlchemy, Django ORM, MongoDB, Redis, and
others. Policy changes made through the API are then saved automatically.
- **Run more than one worker.** Use a [watcher](/docs/watchers) so every
process reloads the policy when one of them changes it.
- **Multi-tenant applications.** [RBAC with domains](/docs/rbac-with-domains)
gives a user different roles in different tenants.
-- **Rules based on attributes.** Combine roles with [ABAC](/docs/abac), for
example "editors may update only their own articles".
+- **Rules based on attributes.** Combine roles with [ABAC](/docs/abac), for
example "editors may update only their own articles". The [ABAC
tutorial](/docs/flask-abac-tutorial) builds this step by step.
- **Ready-made middleware.** The [middleware list](/docs/middlewares) has
plugins for FastAPI, Django, Flask, and other frameworks.
- **Experiment in the browser.** Paste the model and policy into the [online
editor](/docs/online-editor) to test requests without running any code.
diff --git a/docs/TutorialRustRBAC.mdx b/docs/TutorialRustRBAC.mdx
new file mode 100644
index 000000000..5b5d2aac4
--- /dev/null
+++ b/docs/TutorialRustRBAC.mdx
@@ -0,0 +1,187 @@
+---
+id: rust-rbac-tutorial
+title: RBAC in Rust with Axum
+description: "Rust RBAC tutorial: role-based access control for an Axum REST
API with casbin-rs, including the model, policy, middleware and runtime role
changes."
+keywords: [rust rbac, axum rbac, axum authorization, casbin-rs, rust
authorization library, role-based access control rust, axum middleware casbin]
+authors: [hsluoyz]
+---
+
+This tutorial adds role-based access control (RBAC) to a REST API written in
Rust with [Axum](https://github.com/tokio-rs/axum) and
[casbin-rs](https://github.com/apache/casbin-rs). At the end, three roles
(`admin`, `editor`, `viewer`) are allowed different HTTP methods on different
routes, and the rules live in a policy file that you can change without
touching the handlers.
+
+The model and policy are the same as in the [Go](/docs/go-rbac-tutorial),
[Node.js](/docs/nodejs-rbac-tutorial), [Python](/docs/python-rbac-tutorial),
and [Java](/docs/java-rbac-tutorial) versions of this tutorial.
+
+## 1. Install
+
+```bash
+cargo new rbac && cd rbac
+cargo add casbin axum serde_json
+cargo add tokio --features full
+```
+
+This tutorial was tested with casbin 2.20 and axum 0.8.
+
+## 2. Write the model
+
+Save this as `model.conf` in the project root:
+
+```ini
+[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) && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)
+```
+
+- `r = sub, obj, act`: each request is a user, a URL path, and an HTTP method.
+- `g = _, _`: users can be assigned to roles.
+- `keyMatch2` matches paths such as `/api/articles/:id`, and `regexMatch` lets
one rule list several methods. See [Functions](/docs/function) for the other
built-in matchers.
+
+## 3. Write the policy
+
+Save this as `policy.csv`:
+
+```csv
+p, admin, /api/*, (GET)|(POST)|(PUT)|(DELETE)
+p, editor, /api/articles, (GET)|(POST)
+p, editor, /api/articles/:id, (GET)|(PUT)
+p, viewer, /api/articles, GET
+p, viewer, /api/articles/:id, GET
+
+g, alice, admin
+g, bob, editor
+g, carol, viewer
+```
+
+Lines starting with `p` grant a role access to a path and a set of methods.
Lines starting with `g` assign users to roles.
+
+## 4. Add the middleware
+
+The enforcer is shared between requests as `Arc<RwLock<Enforcer>>`: checks
take the read lock, so they run in parallel, and policy changes take the write
lock. The middleware is attached with `route_layer`, so it runs only for routes
that exist and unknown paths still return `404`.
+
+```rust title="src/main.rs"
+use std::sync::Arc;
+
+use axum::{
+ extract::{OriginalUri, Path, Request, State},
+ http::StatusCode,
+ middleware::{self, Next},
+ response::{IntoResponse, Response},
+ routing::get,
+ Json, Router,
+};
+use casbin::{CoreApi, Enforcer};
+use serde_json::{json, Value};
+use tokio::sync::RwLock;
+
+type SharedEnforcer = Arc<RwLock<Enforcer>>;
+
+/// Asks Casbin whether the current user may call this route.
+async fn authorize(State(enforcer): State<SharedEnforcer>, req: Request, next:
Next) -> Response {
+ // Replace this with the user from your session or JWT.
+ let user = req
+ .headers()
+ .get("x-user")
+ .and_then(|v| v.to_str().ok())
+ .unwrap_or("")
+ .to_string();
+ // Inside a nested router req.uri() has the /api prefix stripped, so use
the original URI.
+ let path = match req.extensions().get::<OriginalUri>() {
+ Some(OriginalUri(uri)) => uri.path().to_string(),
+ None => req.uri().path().to_string(),
+ };
+ let method = req.method().as_str().to_string();
+
+ match enforcer.read().await.enforce((user, path, method)) {
+ Ok(true) => next.run(req).await,
+ Ok(false) => (StatusCode::FORBIDDEN, Json(json!({ "error": "forbidden"
}))).into_response(),
+ Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error":
e.to_string() }))).into_response(),
+ }
+}
+
+async fn list_articles() -> Json<Value> {
+ Json(json!({ "articles": [] }))
+}
+
+async fn create_article() -> (StatusCode, Json<Value>) {
+ (StatusCode::CREATED, Json(json!({ "created": true })))
+}
+
+async fn update_article(Path(id): Path<u32>) -> Json<Value> {
+ Json(json!({ "updated": id }))
+}
+
+async fn delete_article(Path(id): Path<u32>) -> Json<Value> {
+ Json(json!({ "deleted": id }))
+}
+
+#[tokio::main]
+async fn main() {
+ let enforcer = Enforcer::new("model.conf", "policy.csv").await.unwrap();
+ let enforcer: SharedEnforcer = Arc::new(RwLock::new(enforcer));
+
+ let api = Router::new()
+ .route("/articles", get(list_articles).post(create_article))
+ .route("/articles/{id}",
axum::routing::put(update_article).delete(delete_article))
+ .route_layer(middleware::from_fn_with_state(enforcer.clone(),
authorize));
+
+ let app = Router::new().nest("/api", api).with_state(enforcer);
+
+ let listener =
tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
+ axum::serve(listener, app).await.unwrap();
+}
+```
+
+:::caution
+Inside a router added with `nest`, `req.uri()` has the prefix removed: a
request to `/api/articles` arrives as `/articles`, which matches no policy
rule. That is why the middleware reads `OriginalUri`. If your middleware sees
every request denied, check which path it passes to Casbin.
+:::
+
+:::note
+The `X-User` header keeps the example short. Casbin handles authorization
only; in a real application the user name comes from your authentication layer,
such as a verified JWT.
+:::
+
+## 5. Try it
+
+```bash
+cargo run
+```
+
+```bash
+curl -i -X POST -H "X-User: bob" http://localhost:8080/api/articles #
201, editors can create
+curl -i -X DELETE -H "X-User: bob" http://localhost:8080/api/articles/1 #
403, editors cannot delete
+curl -i -X DELETE -H "X-User: alice" http://localhost:8080/api/articles/1 #
200, admins can
+curl -i -X POST -H "X-User: carol" http://localhost:8080/api/articles #
403, viewers are read-only
+```
+
+## 6. Change roles at runtime
+
+Roles and permissions are data. Take the write lock to change them while the
server is running:
+
+```rust
+use casbin::{MgmtApi, RbacApi};
+
+let mut e = enforcer.write().await;
+e.add_role_for_user("dave", "editor", None).await?; // dave becomes an
editor
+e.delete_role_for_user("bob", "editor", None).await?; // bob is no longer
an editor
+e.add_policy(vec!["editor".into(), "/api/comments".into(),
"POST".into()]).await?;
+
+e.get_roles_for_user("alice", None); // ["admin"]
+```
+
+The `None` argument is the domain; pass `Some("tenant1")` when you use [RBAC
with domains](/docs/rbac-with-domains).
+
+## Next steps
+
+- **Store the policy in a database.** Replace `policy.csv` with an adapter
such as [sqlx-adapter](https://github.com/apache/casbin-sqlx-adapter) or
[diesel-adapter](https://github.com/apache/casbin-rust-diesel-adapter) (see
[Adapters](/docs/adapters)). Policy changes made through the API are then saved
automatically.
+- **Ready-made middleware.**
[axum-casbin](https://github.com/apache/casbin-axum-casbin) and the Actix
plugins are listed under [Middleware](/docs/middlewares).
+- **Rules based on attributes.** Combine roles with [ABAC](/docs/abac), for
example "editors may update only their own articles". [Choosing an access
control model](/docs/access-control-models#combining-rbac-and-abac) shows a
combined model.
+- **gRPC services.** The same idea applies to gRPC: see [gRPC
authorization](/docs/grpc-rbac-tutorial), which uses Go but translates directly
to a tonic interceptor.
+- **Experiment in the browser.** Paste the model and policy into the [online
editor](/docs/online-editor) to test requests without running any code.
diff --git a/sidebars.js b/sidebars.js
index a6d0a066e..c2af775f6 100644
--- a/sidebars.js
+++ b/sidebars.js
@@ -37,6 +37,13 @@ module.exports = {
"nodejs-rbac-tutorial",
"python-rbac-tutorial",
"java-rbac-tutorial",
+ "nestjs-rbac-tutorial",
+ "rust-rbac-tutorial",
+ "grpc-rbac-tutorial",
+ "go-abac-tutorial",
+ "nodejs-abac-tutorial",
+ "flask-abac-tutorial",
+ "ai-agent-authorization",
],
},
{
@@ -44,6 +51,7 @@ module.exports = {
label: "Access Control Models",
link: {type: "generated-index"},
items: [
+ "access-control-models",
"supported-models",
"syntax-for-models",
"effector",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]