RFD-AGT-013 CC-50 / TICKET-MAP T2k: the firm admin opens the channel activity register and sees each live binding plus a metadata-only rollup of fetches and commands. Admin visibility is the compensating control that justifies skipping step-up auth, so this endpoint is a security surface with a hard payload discipline: counts and timestamps only — never message content, never provider internals, subjects always masked. This gates the W3 launch (with T2f-b).
| Surface | Change |
|---|---|
GET /app/channels/register (new, src/domains/channel/router.py) |
The firm-admin register. Returns ChannelRegisterResponse:
every binding in the firm (masked subject, status, bound/revoked
stamps) with a per-binding activity rollup
(totalDeliveries, failedDeliveries,
fetchLinksSent, lastDeliveryAt,
lastDeliveredAt, lastReadAt,
commandsReceived, lastCommandAt) and a
firm-level refusal summary (total, byKind,
lastAttemptedAt). Read-only.
|
require_firm_admin (src/shared/auth/permissions.py) |
New auth gate: the caller's active membership role must be
firm_admin. Permissions are too coarse for this surface —
settings:write is also held by firm_editor and support,
and billing:manage is money semantics. Fail-closed: an
org-less token (role None) or any other role is a 403
with an authz_denied log line.
|
src/domains/channel/register.py (new) |
The read service. Four constant queries regardless of row counts
(ADR-130 DR-1): the binding list plus one grouped rollup each over
channel_delivery_records,
channel_inbound_messages, and
channel_refusal_ledger, zipped in memory. No N+1. Every
statement runs on the firm-scoped RLS session, so cross-firm rows are
invisible at the database layer; zero-firm refusal rows
(firm_id NULL) are staff-visible only and never appear.
|
GET /app/channels/firm-bindings (deleted) |
The Story-2848 placeholder seam ("T2k owns the full admin register
with activity"). This PR is T2k: per R1 the replaced mechanism is
deleted in the same PR — route, service.list_firm_bindings,
and its tests. No consumer exists in pramaan-app. The
POST /firm-bindings/{id}/revoke admin kill is untouched.
|
openapi/app-api.json, openapi/permission-map.json |
Snapshots regenerated (make openapi, make gen-permissions). |
The register payload carries bindings (subjects masked,
•••1234), activity counts and timestamps, and refusal
counts. It never carries: message content (none exists in the channel
tables), provider message ids, provider thread ids, template names,
payload hashes, traceparents, provider error text, or unmasked subjects.
The contract test seeds rows that contain all of those in the database
and asserts none appear anywhere in the response — by key
(case-insensitive walk) and by value.
| Proof | Where |
|---|---|
| Admin sees own firm's register: two live bindings with exact activity rollups and the refusal summary | tests/integration/test_app_channel_register.py::test_admin_sees_own_firm_register_with_metadata_activity |
firm_member AND firm_editor (with settings:write) are 403; firm_admin with no claim permissions is 200 — the gate is the role |
…::test_register_is_firm_admin_only |
| Cross-firm binding/delivery/refusal rows are invisible in every rollup | …::test_register_hides_other_firms |
| Metadata-only payload contract (forbidden keys + seeded secret values absent) | …::test_register_payload_is_metadata_only |
O(1) query count: 1 binding vs 5 bindings (+deliveries, +refusals) → identical db_query_count (DR-1/HR-19) |
…::test_register_query_count_is_o1 |
require_firm_admin unit proofs: admin allowed; editor/member/support/None refused; authz_denied log shape |
tests/unit/test_authz_permissions.py |
tests/integration/test_app_channel_register.py (route,
authz, cross-firm, payload contract, O(1)) and
tests/unit/test_authz_permissions.py
(require_firm_admin).test_admin_sees_own_firm_register_with_metadata_activity
failed before the register route/service existed (404); the
require_firm_admin unit tests fail against the base
(import error). Green runs are in this branch's history.GET /app/channels/firm-bindings and
service.list_firm_bindings (the Story-2848 placeholder
this ticket replaces) plus their tests. Nothing stays temporary.
20 unit tests fail identically on the base commit
(test_user_updated_webhook,
test_staff_invite_user_created,
test_firm_support_staff,
test_clerk_dispatcher_user_profile_fields — Clerk
profile-field cluster, unrelated to channels). Verified by stashing this
branch's changes and re-running on the clean base.