Der Berechtigungskatalog¶
Alles, was einem Mitglied erlaubt werden kann, wie weit jede Berechtigung reicht, und wie sich die eingebauten Rollen daraus zusammensetzen.
Siehe Berechtigungen für die Erklärung und dafür, wie die vier Schichten zusammenwirken; diese Seite ist die generierte Referenz und bleibt deshalb englisch — sie wird zur Build-Zeit aus den Docstrings der Quelle gelesen.
app.core.permissions
¶
Permission catalog - the single source of truth for what a member may do.
The rule the rest of the codebase follows: permissions are defined in code, roles are composed from them. Call sites check permissions, never role names, so adding or re-shaping a role never means editing an endpoint.
Two kinds of permission, and they behave differently:
Global permissions are binary and org-wide - you may manage members, or you
may not. Resource permissions carry a :class:Scope answering the second
question a role cannot: not "may this role touch agents?" but "which agents?"
A Member creates their own agents and sees only those plus what was shared with
them; a Builder sees every agent in the org but may only edit the shared ones.
Effective access to one resource is max(role scope, grant on that resource)
- see :func:app.services.access.resolve_access. A grant can widen what a role
allows for a single row; it never narrows it.
Perm
¶
Bases: StrEnum
Everything a member can be allowed to do.
Clients cannot invent permissions - a custom role (Phase 2) may only recombine the values listed here.
Scope
¶
Bases: StrEnum
How much of a resource type a permission reaches.
Ordered: NONE < OWN < SHARED < TEAM < ALL. Comparison is what makes
"effective access = max(role, grant)" expressible, so use the operators
rather than comparing the string values.
OrgRoleName
¶
Bases: StrEnum
The role names a membership row may hold.
Nothing seeds these and there is no roles table: a role is a string on
organization_members, and what it means is ROLE_PERMS below. Adding a
role is an edit here, not a migration - which is the point of composing
roles from permissions rather than storing them.
The column carries no CHECK constraint, unlike resource_grants.level.
What keeps an invented role out is a field_validator on the member and
invitation schemas; if one ever got through, ROLE_PERMS.get would answer
with no permissions rather than with somebody else's.
Not user-editable. Custom roles are Phase 2, and may only ever recombine
the permissions in :class:Perm.
AuthContext
dataclass
¶
Who is asking, in which organization, and what that lets them do.
Built once per request by :func:app.api.deps.get_auth_context and passed
to whatever needs to make a decision, so a route never re-derives it.
user_id is optional, and that is a statement rather than a convenience.
Every run on this platform has a subject - budgets, resource grants, the
audit trail and the approval gate all key on one, and
:mod:app.services.channels.mentions refuses an unlinked chat identity for
exactly that reason. A surface open to anonymous visitors breaks the
invariant, and the honest answer is to make its absence visible in the type
so every consumer has to handle it, rather than to invent a fallback user
whose runs nobody could be held to. What such a run is accountable to is
the exposure that admitted it.
channel_identity_id
class-attribute
instance-attribute
¶
The chat account that asked, when the asker is not a person.
Beside user_id rather than instead of it, because in a group chat the two
are different answers: the turn runs as the binding's creator, and this is
who typed it. It decides nothing - no permission reads it - and travels here
only because every consumer that opens a run already holds this object
(#639).
subject_is_publisher_fallback
class-attribute
instance-attribute
¶
Whether user_id is the publisher standing in for an unidentified asker.
On a hosted or embedded surface there is no signed-in person, so
publisher_context runs the turn as the agent's publisher: user_id is the
owner, not whoever is actually chatting. This is how a consumer tells that
apart from a real subject (web chat, API, a linked member), where user_id
is the asker. Like channel_identity_id it decides no permission - it
exists so a person store can refuse to attribute a stranger's note to the
owner rather than leak it into the owner's store (#788).
Invariant: set True in exactly one place - publisher_context, the sole
constructor that runs a turn as someone other than the asker. Any future
stand-in constructor must set it too, or a person-store write silently lands
in the owner's store as if it were the visitor's. test_publisher_standing.py pins
the one assignment site by grep, the way AuthContext.anonymous is the one
subject-less constructor.
subject_id
property
¶
The person this context is, for work that cannot be done by nobody.
Most of what a service does keys on a person: an audit entry names an
actor, an approval names who decided it, and a listing of "mine plus
what was shared with me" is meaningless without a me. Those call sites
read this instead of :attr:user_id, which keeps their typing honest
and - more usefully - makes "this needs a person" something the code
says rather than assumes.
Raises:
| Type | Description |
|---|---|
AuthorizationError
|
If there is no subject. Loudly, and here: an
authenticated path that reached this far has a person, and
letting the absence travel writes an entry naming nobody -
indistinguishable from the two writers that legitimately name
nobody, and by then the request has half happened. A caller
that genuinely has no session reads :attr: |
permissions
property
¶
Effective permissions from the role alone, before resource grants.
A context with no subject holds nothing, whatever its role says. The
check is on the subject rather than on the role string because a role is
just a string: a subject-less context built with "owner" would
otherwise reach every row in the organization, and nothing structural
would have stopped it.
A platform superadmin gets everything: they administer the deployment and already have database access - pretending otherwise would be security theatre, and the audit log is what actually holds them to it.
anonymous
classmethod
¶
A context for a visitor nobody can name.
The single constructor for one, so "where can a subject-less context come from" is a grep rather than an audit. It holds no permission and reaches no row; whatever such a run is allowed to do comes from the exposure that admitted it, which was created by somebody who did have a role.
has
¶
Whether the permission is held at all, at any scope.
For a resource permission this only says "may touch this kind of thing";
whether they may touch a given row is
:func:app.services.access.resolve_access.
role_has
¶
Whether a role holds a permission, ignoring resource scope.
For code that has a role string but no request - a service checking what the other party may do, a background job reasoning about a stored membership. Call sites check permissions rather than role names for the same reason endpoints do: adding a role should never mean editing an authorization check.
assignable_roles
¶
The roles a member holding role may hand out.
A role may only assign one whose authority it strictly exceeds: every
permission the offered role holds, held at least as widely by the assigner,
and something the assigner holds that the offered role does not. So nobody
assigns their own level - promoting a peer to it is an ownership decision,
not a management one - and nobody at all assigns owner, since no role
outranks it. Ownership moves through
:meth:app.services.member.MemberService.transfer_ownership, which demotes
the outgoing owner in the same breath.
Derived from the catalog rather than from a role name, which is the whole
point: the ceiling this replaced compared against the literal "admin", so
a custom role (Phase 2) holding roles:manage passed it unseen and could
mint a second owner (#672). An unknown role holds nothing and so may assign
nothing.
Zugriff auf eine einzelne Zeile auflösen¶
Nicht generiert. app/services/ ist ein implizites Namespace-Package - es hat
kein __init__.py - deshalb kann der statische Collector nicht hineinlaufen,
und eine Referenzseite, die stillschweigend die Hälfte ihrer Symbole weglässt,
wäre schlechter als eine, die sagt, wo man nachschauen muss.
Die Formel und jede Ablehnung, die sie ausspricht, sind in
Berechtigungen dokumentiert. Die
Quelle ist
app/services/access.py,
die die Begründung in ihren Docstrings trägt.