Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 43 additions & 9 deletions components/backend/internal/middleware/casbin_model.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>, /hackathon/<id>/team/<id>,
# /hackathon/<id>/project/<id>. 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")
Loading