# Research — RBAC Troubleshooting & Tenant UI Bugfix Pack v1 **Date**: 2026-01-31 **Spec**: [spec.md](spec.md) This document captures design decisions and supporting rationale. Where applicable, decisions are grounded in the current codebase and the TenantPilot Constitution. ## Decision 1 — Archived tenants must remain resolvable in tenant-scoped routes **Decision**: Make the tenant binding used by Filament tenancy include archived (soft-deleted) tenants so entitled members can view `/admin/t/{tenant}/...` even when archived. **Rationale**: - The current tenant lifecycle uses soft deletes + `status='archived'` on delete and restores back to `active`. - If tenancy resolution excludes trashed records, the app will 404 *before* membership middleware can apply “deny-as-not-found for non-members”, causing false 404s for legitimate members. - Constitution RBAC-UX requires: - non-members → 404 - members but missing capability → 403 - archived is a lifecycle state, not an isolation boundary. **Evidence**: - Tenant tenancy config (slug is `external_id`, route prefix `t`): [Admin panel tenancy](../../app/Providers/Filament/AdminPanelProvider.php#L32-L45) - Archived behavior is implemented via soft deletes + status mutations: [Tenant booted hooks](../../app/Models/Tenant.php#L63-L92) **Alternatives considered**: - Stop using soft deletes for tenants and represent archived purely via a `status` field. - Rejected: larger behavioral change, wider surface area (queries, “activeQuery”, etc.), higher regression risk. - Keep current 404 and treat “archived” as inaccessible. - Rejected: contradicts spec FR-004 and the operational need to restore. ## Decision 2 — Standardize Tenant view header actions via `UiEnforcement` **Decision**: Wrap Tenant view page header actions (Edit/Deactivate) in the existing UI enforcement helper so tenant members without the capability see disabled actions with tooltips, avoiding “normal click leads to 403 page”. **Rationale**: - Constitution RBAC-UX-004 requires visible-but-disabled for members lacking capability. - Existing code already follows this pattern in multiple resources (e.g., tenant list actions, membership manager relation managers). - The current `ViewTenant` header actions include an unconditional edit action and an archive action that executes without capability checks. **Evidence**: - Unconditional edit header action: [ViewTenant header action group](../../app/Filament/Resources/TenantResource/Pages/ViewTenant.php#L19-L24) - Deactivate executes without capability gating: [ViewTenant archive action](../../app/Filament/Resources/TenantResource/Pages/ViewTenant.php#L51-L74) **Alternatives considered**: - Hide actions entirely for users without capability. - Rejected by default: RBAC-UX prefers visible-but-disabled. - Leave the action visible and rely only on server-side 403. - Rejected: fails the UX requirement (normal click to 403). ## Decision 3 — Restore icon consistency is a UI-only patch **Decision**: Add an icon to the tenant row `Restore` action. **Rationale**: - Improves consistency and reduces “is this safe?” uncertainty. - No behavioral/authorization changes; minimal regression risk. **Evidence**: - Restore action currently has no icon: [TenantResource restore action](../../app/Filament/Resources/TenantResource.php#L406-L454) **Alternatives considered**: - Leave it as-is. - Rejected: explicit FR-001. ## Decision 4 — Diagnostics is DB-only and uses existing audit logging patterns **Decision**: Implement tenant-scoped diagnostics as DB-only rendering and DB-only repairs with explicit confirmation + server-side authorization, recording audit logs. **Rationale**: - Constitution requires read/write separation and that monitoring/diagnostics pages do not trigger external calls during render. - Repairs are security-relevant mutations and must be audited. **Alternatives considered**: - Attach diagnostics to Monitoring/Operations. - Rejected: these repairs are DB-only and do not need `OperationRun` by default; keep scope small. ## Decision 5 — Membership invariants **Decision**: - Continue using server-side guards preventing last-owner removal/demotion. - Add diagnostics checks for “missing owner” and provide a safe repair (promote chosen member to owner) gated by an existing management capability. **Rationale**: - Last-owner guard exists and is correct; the missing-owner case needs a UI recovery path. **Evidence**: - Last-owner guard behavior: [TenantMembershipManager last-owner guards](../../app/Services/Auth/TenantMembershipManager.php#L253-L287) **Alternatives considered**: - DB constraint enforcing at least one owner. - Rejected: non-trivial, would complicate bulk edits and migrations; better handled at app layer with explicit repair tools. ## Decision 6 — Duplicate memberships **Decision**: Keep the DB-level uniqueness constraint as the primary protection, but still implement diagnostics that can detect historical duplicates and merge them safely (no-op when none exist). **Rationale**: - The uniqueness constraint exists today, but the product requirement includes a recovery/repair flow. **Evidence**: - Unique constraint in migration: [tenant_memberships unique index](../../database/migrations/2026_01_25_022729_create_tenant_memberships_table.php#L12-L26) **Alternatives considered**: - Remove diagnostics for duplicates since the DB constraint exists. - Rejected: conflicts with FR-006 and the “UI-only recovery” goal. ## Decision 7 — GUID vs bigint guardrails **Decision**: Explicitly distinguish: - internal tenant primary key (`tenants.id`, bigint) - external tenant identifiers (`tenant_id` GUID / `external_id` string) and ensure code paths that need an internal FK use `$tenant->getKey()` (cast to int), not `$tenant->tenant_id`. **Rationale**: - Prevent PostgreSQL `invalid input syntax for type bigint` errors caused by passing the GUID into bigint `tenant_id` columns. **Alternatives considered**: - Rename columns. - Rejected (out of scope / migration-heavy).