Files
descrybe/docs/admin-roles-support/04-contract.json
T

277 lines
10 KiB
JSON
Raw Normal View History

{
"agent": "04/20",
"title": "Unified contract: plan features + staff roles + legacy",
"status": "design_only",
"version": "1.0.0",
"coordinates_with": [
"docs/admin-roles-support/01-ux-research.md",
"docs/admin-roles-support/02-current-inventory.md",
"docs/admin-roles-support/03-roles-matrix.md",
"docs/admin-roles-support/03-roles-matrix.json",
"docs/plan-permissions/03-permission-contract.md",
"docs/plan-permissions/03-permission-contract.json",
"docs/plan-permissions/01-feature-keys.json",
"docs/plan-permissions/06-defaults-matrix.json"
],
"assumptions": [
"Product cohort (legacy/standard/custom) is a plan feature_profile, not a stored end-user RBAC role; legacy_user/standard_user are derived labels",
"support_staff is least privilege: ticket queue assign/reply only — narrower than optional read-assist in 03",
"packages ≡ plans rows; no parallel permission service outside billing + auth staff flags",
"is_platform_admin retained; staff_role additive with null+admin-flag ⇒ staff_role admin back-compat",
"A1/Local Demo Co Enterprise → Legacy assign is explicit ops step, not silent migration"
],
"axes": {
"plan_features": {
"subject": "company.active_plan",
"storage": ["plans.features", "plans.feature_profile", "platform_feature_gates"],
"resolver": "ResolveEffectiveFeatures / CapabilitiesForCompany",
"failure": { "http": 402, "code": "plan_gate" }
},
"company_membership": {
"subject": "user in company",
"storage": "memberships.role",
"values": ["admin", "member"],
"notes": "Orthogonal; do not overload with platform admin"
},
"platform_staff": {
"subject": "user",
"storage": ["users.is_platform_admin", "users.staff_role"],
"resolver": "resolve_staff_role + staff_allows(capability)",
"failure": { "http": 403, "error": "staff capability required" }
}
},
"runtime_formulas": {
"effective_feature": "plan_allows(key) AND global_section_enabled(section(key)) AND global_feature_enabled(key)",
"plan_allows": [
"if key in plans.features -> plans.features[key]",
"else if resolve_plan_profile == legacy -> LegacyMatrix[key] (missing -> false)",
"else if resolve_plan_profile == custom -> true",
"else -> DefaultPlanFeatures(name, false)[key]"
],
"resolve_plan_profile_priority": [
"plans.feature_profile if set (legacy|ladder|custom)",
"legacy name patterns from 03-roles-matrix",
"IsCustomPackage -> custom",
"else ladder"
],
"staff_allows": "user.is_active AND resolve_staff_role(user) in allowed_roles(capability)",
"resolve_staff_role": [
"inactive -> none",
"staff_role if not null",
"else if is_platform_admin -> admin",
"else none"
]
},
"legacy": {
"profile": "legacy",
"shipping_modes": [
{
"id": "named_profile",
"preferred": true,
"mechanism": "plans.feature_profile='legacy' or name-pattern derive; DefaultPlanFeatures/LegacyMatrix"
},
{
"id": "sparse_or_dense_features",
"preferred": false,
"mechanism": "plans.features explicit map; requires profile precedence so missing keys are not custom-all-on"
}
],
"must_not": "Fall through IsCustomPackage all-ON for A1/legacy names",
"allowlist_authority": "docs/admin-roles-support/03-roles-matrix.md",
"excluded_sections": [
"processing.monitor",
"stores.*",
"marketing.*",
"integrations.*",
"support.*"
],
"name_patterns_ref": "docs/admin-roles-support/03-roles-matrix.json#plan_name_patterns",
"a1_example": {
"legacy_company_id": "97e1a309-3d23-4aa2-b518-8e8d7afdfec7",
"demo_note": "Local Demo Co may still be on Enterprise until explicit Legacy assign"
}
},
"staff_roles": {
"values": ["admin", "developer", "support_staff"],
"storage": {
"keep": "users.is_platform_admin",
"add": {
"column": "users.staff_role",
"type": "TEXT NULL CHECK IN (admin, developer, support_staff)",
"invariant": "staff_role set implies is_platform_admin=true"
}
},
"backfill": "UPDATE users SET staff_role='admin' WHERE is_platform_admin AND staff_role IS NULL",
"capabilities": {
"staff.admin_shell": ["admin", "developer", "support_staff"],
"staff.support.queue": ["admin", "developer", "support_staff"],
"staff.support.reply": ["admin", "developer", "support_staff"],
"staff.support.assign": ["admin", "developer", "support_staff"],
"staff.users.read": ["admin", "developer"],
"staff.users.write": ["admin", "developer"],
"staff.analytics": ["admin", "developer"],
"staff.billing": ["admin", "developer"],
"staff.plans_features": ["admin", "developer"],
"staff.feature_gates": ["admin", "developer"],
"staff.settings": ["admin", "developer"],
"staff.jobs_stuck": ["admin", "developer"],
"staff.impersonate": ["admin", "developer"],
"staff.dev_password": ["admin", "developer"]
},
"support_staff_least_privilege": {
"allowed_web": ["/admin/support", "/admin/support/[id]"],
"allowed_api": [
"GET /api/admin/support/tickets",
"GET /api/admin/support/tickets/{id}",
"POST /api/admin/support/tickets/{id}/messages",
"PATCH /api/admin/support/tickets/{id}"
],
"denied": [
"billing",
"plans",
"feature_gates",
"credits",
"settings",
"users_write",
"impersonate",
"stuck_cleanup",
"self_escalate_staff_role"
],
"deferred": "ticket-side read-only company context without tenant write APIs"
},
"middleware": {
"prefer": ["RequireStaff", "RequireStaffCapability"],
"extend": [
"apps/api/internal/httpapi/middleware.go#RequirePlatformAdmin",
"apps/api/internal/httpapi/middleware.go#checkPlatformAdmin",
"apps/api/internal/auth/invites.go#IsPlatformAdmin"
],
"router_today": "apps/api/internal/httpapi/server.go /api/admin RequireSession+RequirePlatformAdmin",
"ui": [
"apps/web/src/lib/admin-gate.ts",
"apps/web/src/lib/components/AdminNav.svelte"
]
}
},
"security": {
"session": "scs cookie + RequireSession; staff flags from DB not client claims",
"csrf": "double-submit X-CSRF-Token on dashboard /api including /api/admin; v1/public/webhooks exempt",
"company_isolation": [
"tenant APIs RequireCompany scoped",
"admin support cross-company ticket view does not open tenant write APIs",
"impersonate admin|developer non-prod only; never support_staff",
"allowCompanyAdminOrPlatform: only admin|developer count as platform bypass — not support_staff"
],
"no_privilege_escalation": [
"capability middleware on admin routes",
"no self-service staff_role",
"plan features never grant /admin",
"staff never granted by plan JSON",
"fail closed on missing role/capability"
]
},
"performance": {
"existing": {
"symbol": "featureETag",
"path": "apps/api/internal/billing/plan_features.go",
"semantics": "sha256 of sorted enabled feature keys"
},
"rules": [
"Do not fold staff_role into feature_etag",
"Keep company capabilities cache keyed by company_id + feature_etag",
"Invalidate on plan assign, plans.features write, global gates write",
"etag remains pure function of effective enabled-key set (profile already reflected in map)",
"Resolve staff role once per request in middleware"
],
"me_payload_additive": {
"user.staff_role": "string|null",
"staff_capabilities": "string[]",
"credits.feature_profile": "legacy|ladder|custom|null",
"credits.feature_etag": "unchanged semantics"
}
},
"migration": {
"file_suggestion": "apps/api/sql/schema/027_staff_roles_legacy_profile.sql",
"idempotent": true,
"destructive": false,
"steps": [
"ALTER users ADD staff_role NULL CHECK",
"ALTER plans ADD feature_profile NULL CHECK",
"Backfill staff_role=admin for existing platform admins",
"Ensure Legacy plan row with feature_profile=legacy",
"Optional sparse plans.features from LegacyMatrix",
"Explicit A1/demo assign — no silent Enterprise downgrade",
"EnsureDefaultPlans must not clobber features or feature_profile"
],
"optional_flag": "STAFF_RBAC=1 to enforce capability middleware",
"breaking_changes": [],
"preserve": [
"is_platform_admin",
"public ladder names",
"Stripe public-ladder checkout",
"402 plan_gate shapes",
"support assignee_admin_user_id schema",
"CSRF session behavior"
]
},
"reuse_symbols": {
"billing": [
"DefaultPlanFeatures",
"SparseDefaultOverrides",
"IsCustomPackage",
"PlanAllowsFeature",
"ResolveEffectiveFeatures",
"CapabilitiesForCompany",
"featureETag",
"EntitlementsForCompany"
],
"auth_http": [
"IsPlatformAdmin",
"RequirePlatformAdmin",
"checkPlatformAdmin",
"CSRF",
"RequireSession",
"allowCompanyAdminOrPlatform"
],
"support": [
"ListAdmin",
"GetAdmin",
"ReplyAsAgent",
"UpdateAdmin",
"handleAdminListSupportTickets"
],
"web": [
"requirePlatformAdmin",
"AdminNav",
"PlanPermissionsPanel",
"planCapabilities",
"Nav.svelte"
]
},
"do_not_add": [
"parallel permissions package outside billing/auth",
"packages table",
"tenant feature_keys for /admin/*",
"mixing LLM admin-ai-roles into staff RBAC",
"staff fields inside feature_etag"
],
"verification": [
"legacy excludes processing/stores/marketing/integrations/support",
"A1/legacy profile not custom-all-on",
"custom non-legacy still all-on",
"ladder Free/Starter denials unchanged",
"null staff_role + is_platform_admin => admin",
"support_staff 403 on billing/plans/settings",
"support_staff AdminNav Support-only",
"CSRF still on admin mutations",
"feature_etag stable across staff_role-only changes",
"tenant APIs remain company-scoped for support_staff"
],
"open_questions_defaults": {
"a1_demo_cutover": "explicit_ops_assign",
"support_staff_sets_is_platform_admin": true,
"company_admin_platform_bypass": "admin_and_developer_only",
"legacy_seed_density": "profile_first_sparse_optional"
}
}