From 787e2827f78cefef4a6de6e5f9302308bad8e1d1 Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Wed, 9 Sep 2026 11:13:43 +0200 Subject: [PATCH] chore(backend): update casbin documentation --- .../internal/middleware/casbin_model.conf | 52 +++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/components/backend/internal/middleware/casbin_model.conf b/components/backend/internal/middleware/casbin_model.conf index 45fdc95d..3d85ee72 100644 --- a/components/backend/internal/middleware/casbin_model.conf +++ b/components/backend/internal/middleware/casbin_model.conf @@ -23,13 +23,47 @@ g2 = _, _ e = some(where (p.eft == allow)) [matchers] -# this is the matcher logic for matching a request to one or more policies -# (g(r.sub, p.sub, r.domain) || p.sub=="*") -# this matches that the user has a specific role in a domain, or that the policy applies to all roles (e.g. hackathon 1 is read for all roles) -# keyMatch(r.domain, p.domain) -# this is a wildcard match on the domain, to allow rules to apply to all domains (e.g. role `member` is read on all hackathons). Mainly to cut down on number of rules, also to allow e.g. /hackathon/*/team/* style paths -# r.obj == p.obj && r.act == p.act -# this just check that the object and action in the request and policy match -# || g2(r.sub, "admin") -# this overrides all of the other stuff and allows if a user has the admin role +# Decides whether one request (r) is allowed by one policy row (p). Casbin runs +# this against every row; the effect rule above allows if any row says yes. +# +# It reads as four AND-ed tests, with a single OR-ed escape hatch at the end: +# +# ( subject matches ) && ( domain matches ) && ( object matches ) && ( action matches ) +# || user is a global admin +# +# Precedence matters: && binds tighter than ||, so the trailing +# `|| g2(r.sub, "admin")` sits OUTSIDE all four tests. A global admin is allowed +# regardless of domain, object and action. +# +# 1. subject — (g(r.sub, p.sub, r.domain) || g2(r.sub, p.sub) || p.sub == "*") +# Three ways a user can satisfy the role named by the policy row: +# g(r.sub, p.sub, r.domain) the user holds that role in THIS domain, e.g. +# "g, alice, owner, /hackathon/H1". +# g2(r.sub, p.sub) the user holds that role globally, in every +# domain, e.g. "g2, alice, hackathon_organizer". +# p.sub == "*" the row grants the role to everyone, e.g. read +# access on a public hackathon. +# +# 2. domain — globMatch(r.domain, p.domain) +# Domains are hierarchical paths: /hackathon/, /hackathon//team/, +# /hackathon//project/. The request always carries a concrete path; +# the policy row carries a pattern, so one row can cover every hackathon +# (/hackathon/*) or every team (/hackathon/*/team/*). +# +# globMatch is shell-style (doublestar.Match, pattern second): "*" matches +# within one path segment and does NOT cross "/". So /hackathon/* matches +# /hackathon/H1 but NOT /hackathon/H1/team/T7 — which is exactly what keeps +# team- and project-scoped rows from leaking up to the whole hackathon. +# Do not switch this to keyMatch: keyMatch truncates the pattern at its first +# "*" and prefix-matches, so /hackathon/*/team/* would collapse to the prefix +# /hackathon/ and grant every member every team's permissions. +# +# Note the asymmetry with clause 1: role grants (g) are matched on the domain +# EXACTLY, only policies (p) are pattern-matched. A user holding +# "g, bob, member, /hackathon/H1/team/T7" is invisible to a request made +# against /hackathon/H1 — the caller has to ask about the team path itself +# (see mw.WithTeam / mw.WithProject in rbac.go). +# +# 3./4. object and action — r.obj == p.obj && r.act == p.act +# Plain string equality, no wildcards. m = (g(r.sub, p.sub, r.domain) || g2(r.sub, p.sub) || p.sub=="*") && globMatch(r.domain, p.domain) && r.obj == p.obj && r.act == p.act || g2(r.sub, "admin")