Skip to content

security(transcriber): any signed-in user can change the ASR provider for everyone #16254

Description

@mrveiss

Problem

PATCH /api/transcriber/providers changes the active cloud ASR provider for the whole backend process, but the UI presents it as a personal preference, and any signed-in user can call it.

# autobot-backend/transcriber/routes/providers.py
@router.patch("/providers")
async def set_provider(body: SetProviderRequest) -> Dict[str, Any]:
    """Set the active cloud ASR provider.

    Persistence limitation: this is in-process only and is lost on restart.
    """
    set_active_provider(body.provider)

set_active_provider mutates module-level state, so one user's choice becomes every user's provider until the next change or restart. The caller is src/components/transcriber/AsrProviderSelector.vue:60 (await api.setAsrProvider(next)), which carries no admin guard and sits in the ordinary transcriber UI.

So the same action is a per-user control in the frontend and a global setting in the backend. A user choosing a provider for themselves silently changes it for everyone, including which third party receives everyone's audio.

What changed recently

Until #15758's fix this route was reachable with no credentials at all — the transcriber sub-routers carried no dependency. That fix gates every transcriber route behind authentication, so anonymous callers can no longer change the provider. What remains is the authorization question below, which is a decision rather than a bug fix.

Why this is not simply "make it admin-only"

Admin-gating the route is the obvious fix and it would break AsrProviderSelector for every non-admin user who uses it today. The two coherent designs are:

  1. Global, admin-only — the provider is a system setting. Gate PATCH with check_admin_permission, and move or hide the selector for non-admins.
  2. Per-user — the provider is a user preference. Store it against the caller and resolve it per request, so one user's choice cannot affect another's.

Which is right depends on whether provider choice is meant to be a deployment decision (cost, data-processing agreements, where audio is sent) or an individual one.

Acceptance criteria

  • Decide global-admin or per-user, and record the decision on this issue.
  • The backend and AsrProviderSelector.vue agree on that model — a per-user control must not mutate global state, and a global setting must not be offered to users who cannot change it.
  • A test asserts that one non-admin user's PATCH does not change the provider another user's request resolves to (per-user), or returns 403 (global-admin).
  • The "lost on restart" limitation is either fixed or stated in the UI, since a setting that silently reverts looks like a bug to whoever set it.

Found while gating the transcriber routes for #15758.

Activity

  1. added this to the Backlog milestone on Sep 12, 2026
  2. mrveiss commented on Sep 13, 2026

    @mrveiss
    OwnerAuthor

    Closure check against origin/main (batch-2a audit flagged this). 3 of 4 ACs verified; leaving open for the fourth.

    AC1 — Decide global-admin or per-user, record the decision. Global-admin. Owner ruling recorded 2026-09-11 on #15758 (via #16259's review-follow-up): "PATCH /providers is admin-only (owner ruling, 2026-09-11) through a new require_admin dependency... GET stays login-only." Recording it here too per this AC.

    AC2 — Backend and AsrProviderSelector.vue agree on the model. ✅

    • Backend: autobot-backend/transcriber/routes/providers.py:60 — @router.patch("/providers", dependencies=[Depends(require_admin)]).
    • Frontend: autobot-frontend/src/views/transcriber/TranscriberLayout.vue:13-22 — the settings surface (which hosts AsrProviderSelector) is gated v-if="isAdmin", so non-admins are never offered the control at all.

    AC3 — A test asserts 403 for a non-admin PATCH. ✅
    autobot-backend/transcriber/tests/test_route_authz_15758.py:192-196, test_a_non_admin_cannot_switch_the_provider: asserts r.status_code == 403 and set_provider.assert_not_called() for a non-admin caller.

    AC4 — "lost on restart" limitation fixed or stated in the UI. ❌ Not met.

    • Not fixed: set_active_provider is still in-process-only (autobot-backend/transcriber/routes/providers.py:65, autobot-backend/voice_processing/providers/selection.py:52).
    • Not stated in the UI: the restart-loses-selection wording exists only in the backend docstring (and the OpenAPI-generated api.ts comment it produces) — AsrProviderSelector.vue and TranscriberLayout.vue carry no restart/persistence text for the admin who sets it.

    Leaving this open for AC4 — either persist the selection (e.g. write-through to TRANSCRIBER_ASR_PROVIDER or a durable store) or surface the "resets on restart" caveat in the admin-facing selector UI.

  3. modified the milestones: Backlog, v0.11.0 on Sep 14, 2026
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

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions