A partner integration request landed for programmatic merchant-hierarchy management (Company/Project/Store/PaymentPoint). Built the answer generically: partner-specific behaviour is a PartnerProfile config row, and no partner name appears in any entity, field, endpoint or status value. New: - docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md - hierarchy, idempotency, node-scoped public-key credentials, TEST/LIVE partition, routing context - docs/context/adrs/ADR-0003-generic-partner-provisioning-api.md Amended, because the schema impact must land before Phase 1 is implemented: - Phase 1 gains RoutingContext on CheckoutSession/PaymentIntent/Payment, frozen at checkout-session creation and immutable after - Phase 7 gains routing on Refund/ReconciliationRecord, plus the rule that seller settlement splits happen after routing, never as a hierarchy level - Phase 9 gains Company/Project above Marketplace and PaymentPoint below it, with a backfill sequence for existing marketplaces - Track S gains partner credentials: public key only, node-scoped authority, rotation with overlap, immediate revoke, audit coverage Also: Track P (P1-P10) in the delivery plan, and backend ownership closed as answered across the contract set. Card payment was checked, not added - qr and card both already ship in cart.component.ts with separate create paths and status pollers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.3 KiB
Track S Backend Contract — RBAC, Audit, Secrets, Rate Limiting
Companion to 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)
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 §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
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.
4.1 Partner credentials (inbound)
Added 2026-08-18. Partners calling our API authenticate with signed requests, not bearer tokens. Full contract: PARTNER-PROVISIONING-API-CONTRACT.md §6.
These are the opposite direction from the rest of §4 and follow a different rule:
- We hold only the partner's public key. The private key is generated by the partner and never transmitted to us, never accepted by any endpoint, never logged. There is nothing to store in secret storage on our side.
- Authority is node-scoped: a credential may act on its
scopeNodeIdand that node's descendants, nothing above or beside it. This is a separate axis from the 17 roles in §1 — partner credentials never map onto a human role, and a partner credential can never be granted an admin role. TESTandLIVEcredentials are disjoint. ATESTkey addressing aLIVEnode is403.- Rotation runs with a bounded overlap window (default 7 days) during which both keys verify. Revocation is immediate and irreversible.
- A credential can never widen its own scope or register another credential at a wider scope.
Audit coverage (§3) extends to: partner_credential.registered, partner_credential.rotated, partner_credential.revoked, and every partner-initiated node write, with actor set to the keyId that signed the request.
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 for the full error-envelope contract this should follow.
Partner API limits are per partnerId, by tier, with the tier set on PartnerProfile. Published in the partner OpenAPI spec — a partner must be able to read its own limit rather than discover it by getting 429.
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$, flaggedmustChangePassword: true- Login succeeds but every non-auth request 403s with
PASSWORD_CHANGE_REQUIREDuntil 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 (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:
rolemust be one of theMarketplaceRoleset (§1) — neverPlatformRole. Backend rejects any attempt to grant a platform-scope role through this endpoint (403 SCOPE_ESCALATION_DENIED).marketplaceIdis 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_ADMINlevel 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.