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]

Reply via email to