Skip to content

auth: extend Permission to cover chat, teams and webhooks, define key scopes as permission bundles, and return effective permissions from /me #16270

Description

@mrveiss

Decision this implements

The repo owner ruled (recorded on #16040):

  1. Permission is the single vocabulary of authority. It is extended to model the concepts API-key scopes and the frontend already use, and each scope in API_KEY_SCOPES becomes a defined bundle of permissions.
  2. GET /me returns a user's effective permissions, and the frontend's own role→permission map is retired (tech-debt(frontend): usePermissions' permission strings share nothing with the backend's Permission vocabulary #16243).

This is the foundation both depend on. #16040's AC2 — a key's authority is its owner's permissions intersected with its scopes — cannot be computed while the two sides use different vocabularies.

The gap, measured

Permission (autobot_shared/auth/permissions.py:29) is dot strings. API_KEY_SCOPES (autobot_shared/user_management/models/api_key.py:230) is 14 colon strings.

scope relationship to Permission proposed bundle
knowledge:read / write / delete exact knowledge.read / write / delete
files:delete exact files.delete
files:read same namespace, different verbs files.view, files.download
files:write same namespace, different verbs files.upload, files.manage
admin:* namespace only every Permission member — mirrors Role.ADMIN
chat:use, chat:history absent new chat.use, chat.history
webhooks:trigger absent new webhooks.trigger
users:read / write exists under another name admin.users.read / write — flagged, see below
settings:read / write possibly exists admin.config.read / write — flagged, see below

The frontend's usePermissions.ts adds teams:* (absent → new teams.read / create / manage / delete), audit:* and admin:access / organization / users.

Extend, don't fork. Where a concept already exists, map onto it. Adding audit.read beside security.audit, or users.* beside admin.users.*, would create a second name for one authority.

Constraints the change must honour

These are recorded in permissions.py itself:

Grants must preserve today's behaviour

A new member is granted to no role until someone grants it. The moment #16040 AC4 enforces a permission on a route, any role without it loses that capability. So each new permission is granted to exactly the roles that effectively hold the capability today. Tightening is a separate, deliberate act — the same principle the superadmin entry states.

What is known so far:

  • chat — nothing gates chat today, so every authenticated role has it. Grant chat.use and chat.history to all interactive roles.
  • teams — no team-management write routes were found in autobot-backend/api/. Establish whether they exist in autobot-slm-backend or anywhere, before choosing grants.
  • webhooks — the webhook routes found (api/telegram_bot.py, api/whatsapp.py) are inbound, authenticated by provider secrets, not by user permission. No route was found that enforces webhooks:trigger. A permission that gates nothing today is fine to add, but its grants are then a policy choice, not behaviour preservation, and must be recorded as such.

GET /me

It returns username, role, email, auth_method, authenticated, deployment_mode today (api/auth.py:300). Add:

  • permissions — computed from ROLE_PERMISSIONS, the same source role_has_permission reads, never a second copy of the rule
  • is_admin — from is_admin_role. This is required, not optional: a superadmin's permissions is the empty list by design, so a frontend gating only on permissions would hide everything from superadmins. The frontend must mirror both kinds of server check.

Decisions this issue still needs

Flagged rather than guessed, because each changes who can do what:

  • settings:* → admin.config.* is right only if "application settings" means system configuration. If the frontend's settings are per-user preferences, mapping them onto admin config would stop users changing their own settings, and they need their own permission.
  • users:read → admin.users.read means no non-admin can read user information. Correct for user-management screens; wrong if anything shows other users' names to ordinary users.
  • audit:write has no obvious counterpart — security.audit is a single read-style permission.
  • admin:organization has no counterpart at all.

Acceptance criteria

  • New Permission members exist for concepts with no existing counterpart — at minimum chat.use, chat.history, webhooks.trigger, and teams.* if a team backend exists.
  • Every scope in API_KEY_SCOPES has a defined bundle of Permission members, held in one place, and a test asserts no scope maps to an empty bundle.
  • Every new member is in ROLE_PERMISSIONS[Role.ADMIN] and in no SUPERADMIN entry; roles_are_canonical_test passes unmodified.
  • Each new permission's grants are recorded as either behaviour-preserving (with the evidence of who holds the capability today) or a policy choice.
  • GET /me returns permissions and is_admin; a test asserts a superadmin gets an empty permissions and is_admin: true.
  • The four flagged decisions are settled and recorded on this issue before their mappings are merged.

Notes

Blocks #16040 and #16243.

Activity

  1. mrveiss commented on Sep 11, 2026

    @mrveiss
    OwnerAuthor

    Update: the frontend side of the four open decisions is dead code

    I checked where the SPA actually uses each flagged string. None of the six has a real call site:

    string where it appears
    settings:read, audit:read, audit:write, admin:organization only their definition in usePermissions.ts
    settings:write, users:read usePermissions.ts, plus src/directives/permission.ts:20,23 — inside a JSDoc comment, as usage examples for v-permission

    No template passes any of them to v-permission, hasPermission or can. So the concern that drove two of the flags — that mapping settings:* onto admin config would stop users changing their own settings — has nothing behind it. No view checks it.

    What that leaves, and the proposed resolution:

    • settings:* and users:* matter only as API-key scopes, where the key's authority is intersected with its owner's. Proposed: settings:read/write → admin.config.read/write, users:read/write → admin.users.read/write. Both concepts already exist, and the intersection means a key only carries them if its owner does. Mapping onto them is an extension, not a fork.
    • audit:* and admin:organization exist only in the frontend map and are checked nowhere. Proposed: no backend permission, and tech-debt(frontend): usePermissions' permission strings share nothing with the backend's Permission vocabulary #16243 drops them from the frontend rather than preserving dead strings.

    These are routine mappings inside the owner's ruling, now backed by evidence, so they're recorded here for review rather than escalated. Object on this issue if any is wrong.

    Correction: teams do have a backend

    The issue body says no team-management write routes were found in autobot-backend/api/. That search was too narrow — get_team_service exists at api/user_management/dependencies.py:450, so teams are a real backend concept. Before choosing grants for teams.*, establish which routes use that service and what gates them today. The behaviour-preserving rule applies to it like the others.

  2. mrveiss commented on Sep 11, 2026

    @mrveiss
    OwnerAuthor

    Decisions recorded (owner, 2026-09-11)

    These settle this issue's four flagged decisions (AC6) and one question the investigation raised. Migration 062 had already mapped every colon string that is flagged here.

    1. Chat and teams get dedicated members: chat.use, chat.history, and teams.read / create / manage / delete. This replaces migration 062's fold (20260623_062_rbac_colon_to_dot_reconcile.py, COLON_TO_DOT), which maps chat:* to api.read / api.write and teams:* to admin.users.*. The reason: a key scoped to chat:use must not gain api.write's reach. Migration 062 is applied history and stays as it is.

    2. The four flagged mappings adopt migration 062's choices:

    scope permission
    settings:* admin.config.*
    users:* admin.users.*
    audit:write security.manage
    admin:organization admin.system

    The evidence behind them:

    • "settings" is system config. GET and POST /api/settings/ read and write the system config through ConfigService. Per-user preferences have their own routes: GET / PATCH /api/users/me/preferences (api/users.py:142, :173).
    • audit:write and admin:organization are defined in usePermissions.ts:55, :59 and checked by no view.

    3. teams.* is granted to ADMIN only. This is a policy choice, not behaviour-preserving. Today any logged-in org member can manage teams in the main backend (#16276). The owner ruled that admin-only, to match the SLM backend's ADMIN_USERS_WRITE gate, and the grant follows that ruling.

    The team backend exists, which answers AC1's "if a team backend exists": autobot-backend/api/user_management/teams.py and autobot-slm-backend/api/autobot_teams.py.

    Also found during this investigation and filed: #16276 (team routes have no caller authorization), #16277 (GET /teams/my-teams is unreachable), a critical finding that /api/settings is readable and writable anonymously, and a finding that /users/search searches across organisations for anonymous callers. The last two are being filed now.

  3. mrveiss commented on Sep 11, 2026

    @mrveiss
    OwnerAuthor

    Correction to this issue's own Notes. They say the permissions.py comment near line 181 cites a test_permission_parity that does not exist under that name. That is wrong: autobot_shared/auth/test_permission_parity.py exists, and its test_all_roles_have_role_permissions_entry (line 58) asserts exactly what the comment says, that every Role has a ROLE_PERMISSIONS entry. The reference is correct and stays as it is. roles_are_canonical_test.py:158 asserts the same thing independently.

  4. added this to the v0.9.0 milestone on Sep 12, 2026
  5. mrveiss commented on Sep 13, 2026

    @mrveiss
    OwnerAuthor

    Closure evidence (auto-closed by #16298, merge fe7b58f80). Every acceptance criterion is verified against origin/main:

    # Criterion Evidence on main
    1 New Permission members for chat, webhooks and teams autobot_shared/auth/permissions.py:85-91: CHAT_USE = "chat.use", CHAT_HISTORY, TEAMS_READ/CREATE/MANAGE/DELETE, WEBHOOKS_TRIGGER = "webhooks.trigger". The team backend exists (api/user_management/teams.py), per the 2026-09-11 ruling.
    2 Every API_KEY_SCOPES scope has a non-empty bundle, held in one place SCOPE_PERMISSIONS lives in autobot_shared/auth/key_scopes.py. key_scopes_test.py:14 asserts set(SCOPE_PERMISSIONS) == set(API_KEY_SCOPES), and :21 asserts that no scope maps to an empty bundle ("scopes that would grant nothing").
    3 New members in ROLE_PERMISSIONS[Role.ADMIN], in no SUPERADMIN entry, and roles_are_canonical_test unmodified permissions.py:341-348 lists all seven under admin, and :389 is Role.SUPERADMIN: []. roles_are_canonical_test.py was last changed on 2026-08-24 (b17468aa9), and merge fe7b58f80 does not touch it.
    4 Each grant recorded as behaviour-preserving or a policy choice permissions.py:289-293: chat.* are behaviour-preserving, because every chat route is login-only. teams.* and webhooks.trigger are a policy choice, admin only, per the #16276 ruling. The same record is in #16298's "Grant record" table.
    5 GET /me returns permissions and is_admin, and superadmin gets [] and true autobot-backend/api/auth_me.py:81-82 returns effective_permissions(role) and is_admin_role(role). auth_me_test.py:32 test_a_superadmin_gets_no_granular_permissions_and_is_admin asserts body["permissions"] == [] and body["is_admin"] is True.
    6 The four flagged decisions settled here before merge 2026-09-11 06:26Z ruling, about 42 h before the merge: settings:* → admin.config.*, users:* → admin.users.*, audit:write → security.manage, admin:organization → admin.system.

    Carried to #16243 / #16493: #16298 mapped neither audit:write nor admin:organization, because neither is an API-key scope. Its body says "#16243 retires them", which follows the earlier proposal, not the 06:26Z ruling. No conflicting mapping was merged, so criterion 6 holds. #16493 will state how the frontend treats those two concepts. Stated gaps, tracked elsewhere: existing deployments' permissions tables are not seeded with the new members (#16295), and there is no HTTP-level require_key_permission test yet (#16294).

  6. mrveiss commented on Sep 13, 2026

    @mrveiss
    OwnerAuthor

    Post-merge audit: all 6 acceptance criteria verified on origin/main

    PR #16298, merge commit fe7b58f80. The issue stays closed.

    AC Evidence on origin/main
    1. New Permission members autobot_shared/auth/permissions.py:85-91: CHAT_USE = "chat.use", CHAT_HISTORY, TEAMS_READ / CREATE / MANAGE / DELETE, WEBHOOKS_TRIGGER = "webhooks.trigger"
    2. Every scope has a bundle, held in one place, and none is empty autobot_shared/auth/key_scopes.py:34 SCOPE_PERMISSIONS. key_scopes_test.py:14 test_every_published_scope_has_a_bundle_and_nothing_else_does checks that it equals API_KEY_SCOPES, and :21 test_no_scope_maps_to_an_empty_bundle.
    3. In ROLE_PERMISSIONS[Role.ADMIN], in no SUPERADMIN entry, canonical test passes unmodified The new members are at permissions.py:342-348, inside the Role.ADMIN list that opens at :295. :389 is Role.SUPERADMIN: []. roles_are_canonical_test.py is not in fe7b58f80's diff. PR CI: all 13 python-suite checks passed.
    4. Grants recorded as behaviour-preserving or policy The comment at permissions.py:288-293. chat.use and chat.history are behaviour-preserving, because every chat route is login-only today. teams.* and webhooks.trigger are a policy choice: admin only.
    5. GET /me returns permissions and is_admin; superadmin test autobot-backend/api/auth_me.py:36-37 (model fields) and :81-82. auth_me_test.py:32 test_a_superadmin_gets_no_granular_permissions_and_is_admin asserts permissions == [] and is_admin is True.
    6. Four flagged decisions settled and recorded here before merge The owner's comment "Decisions recorded (owner, 2026-09-11)" on this issue, at 2026-09-11 09:26 Riga time. The PR merged at 2026-09-13 03:45 Riga time.
  7. mrveiss commented on Sep 13, 2026

    @mrveiss
    OwnerAuthor

    Closure evidence (batch 2c)

    Re-verified the 6 already-ticked ACs against origin/main (referenced by PR #16493, 79c6344). All hold.

    AC Evidence
    New Permission members autobot_shared/auth/permissions.py:85-91 CHAT_USE = "chat.use" … TEAMS_* … WEBHOOKS_TRIGGER = "webhooks.trigger"
    Every API_KEY_SCOPES entry has a bundle autobot_shared/auth/key_scopes.py:34 SCOPE_PERMISSIONS (14 scopes); tests key_scopes_test.py::test_every_published_scope_has_a_bundle_and_nothing_else_does, test_no_scope_maps_to_an_empty_bundle
    New members in ROLE_PERMISSIONS[Role.ADMIN], none in SUPERADMIN permissions.py:341-348 (admin holds 62 of 62); :389 Role.SUPERADMIN: []
    Grants recorded as behaviour-preserving or policy choice permissions.py:289-293 "chat.use / chat.history are behaviour-preserving… teams.* and webhooks.trigger are a policy choice: admin only"
    GET /me returns permissions and is_admin; superadmin test autobot-backend/api/auth_me.py:36-37; test auth_me_test.py::test_a_superadmin_gets_no_granular_permissions_and_is_admin asserts permissions == [] and is_admin is True
    The four flagged decisions settled before the mappings The owner's ruling was posted here on 2026-09-11 (Riga time), before the mappings merged on 2026-09-13; key_scopes.py maps settings:read → ADMIN_CONFIG_READ and users:read → ADMIN_USERS_READ, as ruled
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions