# Track S Backend Contract — RBAC, Audit, Secrets, Rate Limiting Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Track S. Covers plan §4.4, §10. **Status: ready to build. Gates the launch — this is the single most serious security gap identified in this session's audit.** Today the admin role model is decorative: `AdminRole` and permissions exist as types, but nothing gates any button, page, or action anywhere in the app. Any authenticated admin has full access. --- ## 1. Roles (17 total, 3 scopes, per plan §4.4) ```ts type PlatformRole = 'PLATFORM_OWNER' | 'TECH_ADMIN' | 'SECURITY_ADMIN' | 'DOMAIN_MANAGER' | 'VIEWER'; type MarketplaceRole = | 'MARKETPLACE_ADMIN' | 'CONTENT_MANAGER' | 'CATALOG_MANAGER' | 'ORDER_MANAGER' | 'FINANCE_MANAGER' | 'SUPPORT_MANAGER' | 'VIEWER'; type SellerRole = | 'SELLER_OWNER' | 'SELLER_CATALOG_MANAGER' | 'SELLER_ORDER_MANAGER' | 'SELLER_FINANCE_VIEWER' | 'SELLER_VIEWER'; ``` `SellerRole` is already specified in [Phase 5's contract](PHASE-5-SELLER-PORTAL-CONTRACT.md) §4 — this doc adds the platform and marketplace scopes around it. ## 2. Enforcement (backend-side, non-negotiable) Every `/api/admin/v2/*` and `/api/platform/v1/*` endpoint must check `(role, tenantScope)` against the acting user's session — **before** touching data, not as a post-hoc filter. `tenant scope` here means: a `MARKETPLACE_ADMIN` for marketplace A must get a `403` (not an empty result) querying marketplace B's data, never a silently-scoped response that looks like "there's just nothing here." ``` GET /api/identity/v1/session/permissions -> { role, scopes: string[], marketplaceIds: string[] } ``` Frontend route/action guards derive from this endpoint's response — never hardcode role logic client-side beyond hiding UI affordances (which is convenience, not security). ## 3. Audit log ```ts interface AuditEvent { id: string; actor: string; action: string; // e.g. 'role.changed', 'offer.price_updated', 'refund.approved' entityType: string; entityId: string; before?: unknown; after?: unknown; reason?: string; occurredAt: string; ip?: string; } ``` Mandatory coverage (plan §10.1): permission changes, seller status changes, catalog moderation actions, price changes, payment/refund actions, manual order overrides, integration credential changes, production launch actions. ``` GET /api/admin/v2/audit?marketplaceId=&entityType=&actor=&from=&to= ``` ## 4. Secrets All provider/connector credentials (payment providers, external marketplace connectors, VK/MAX/Telegram bot tokens, FX source keys) live in dedicated secret storage, referenced by opaque `credentialRef` strings in every other contract in this series — never returned in any API response body, never logged in plaintext. ## 5. Rate limiting ``` 429 response: { error: { code: 'RATE_LIMITED', retryAfterSeconds: number } } ``` Applies to storefront/auth/provider endpoints. Frontend currently has **zero** 429 handling anywhere — see [BACKEND-API-REFERENCE.md §5](../../BACKEND-API-REFERENCE.md) for the full error-envelope contract this should follow. ## 6. Step-up authentication Required before: bank/payment detail changes (Phase 5 §5), production launch (Phase 9 §3 step 8), role grants at `PLATFORM_OWNER`/`MARKETPLACE_ADMIN` level, and any manual financial override (refund approval outside normal flow, price override on a live order). ## 7. PII minimization Customer/seller PII is exposed only to roles that need it for their scope (e.g. `FINANCE_VIEWER` sees payout totals, not raw bank account numbers unless `FINANCE_MANAGER`+). Export endpoints (`GET .../export`) are themselves audit-logged actions per §3. ## 8. Initial admin provisioning & self-service admin management Each marketplace ships with one bootstrap `MARKETPLACE_ADMIN` account, seeded at provisioning time (Phase 9 launch step): - `login` = marketplace slug (`projectName`) - `password` = `{projectName}2026$`, flagged `mustChangePassword: true` - Login succeeds but every non-auth request 403s with `PASSWORD_CHANGE_REQUIRED` until password is changed. ``` POST /api/identity/v1/session/change-password { currentPassword, newPassword } ``` A `MARKETPLACE_ADMIN` can then provision sub-admins scoped to their own marketplace only — mirrors the seller-team invite pattern in [Phase 5](PHASE-5-SELLER-PORTAL-CONTRACT.md) (`POST /api/seller/v1/team/invite`): ``` POST /api/admin/v2/team/invite { email, role: MarketplaceRole, marketplaceId } GET /api/admin/v2/team?marketplaceId= PATCH /api/admin/v2/team/{userId} { role } DELETE /api/admin/v2/team/{userId} ``` Invariants: - `role` must be one of the `MarketplaceRole` set (§1) — never `PlatformRole`. Backend rejects any attempt to grant a platform-scope role through this endpoint (`403 SCOPE_ESCALATION_DENIED`). - `marketplaceId` is forced server-side to the caller's own tenant scope — request body value is ignored/validated, never trusted. - Every invite/role-change/removal is an audit-logged action (§3, `action: 'admin_team.invited' | 'admin_team.role_changed' | 'admin_team.removed'`). - Role grants at `MARKETPLACE_ADMIN` level require step-up auth (§6). - Invited admins get their own credentials (email + set-password flow), not the shared bootstrap login — the bootstrap account is for first login only and should be rotated/retired once real admins exist. ## 9. What the frontend will start doing once this ships - Route guards and action-level permission checks across the entire backoffice — currently none exist. - Backoffice **Audit & Security** section (missing from admin nav today): role changes, sensitive actions, login/security events, exports. - Reconcile `AdminRole` (already de-duplicated to one canonical type this session) against the real 17-role table from §1. - 429 interceptor + retry-after UI.