The permission catalog¶
Everything a member can be allowed to do, how far each permission reaches, and how the built-in roles are composed from them.
See Permissions for the explanation and for how the four layers combine; this page is the generated reference.
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.
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: the
audit actor column is |
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.
Resolving access to one row¶
Not generated. app/services/ is an implicit namespace package - it has no
__init__.py - so the static collector cannot traverse into it, and a reference
page that silently omitted half its symbols would be worse than one that says
where to look.
The formula and every refusal it makes are documented in
Permissions. The source is
app/services/access.py,
which carries the reasoning in its docstrings.