# Feature Specification: Workspace Model, Memberships & Managed Tenants (v2) **Feature Branch**: `068-workspaces-v2` **Created**: 2026-01-31 **Status**: Draft **Input**: User description: "Spec 068 v2 — Workspace Model, Memberships & Managed Tenants (admin panel scope; replace tenant-scoped UI with workspace-scoped hierarchy; add workspace memberships/roles, switcher, 404/403 semantics, audit logs, and default workspace migration)" ## Clarifications ### Session 2026-01-31 - Q: What should identify the workspace in URLs (id vs slug)? → A: Prefer `slug` in URLs; if missing, fall back to numeric `id`. - Q: How should current workspace selection be persisted? → A: Persist in session and also store a nullable `last_workspace_id` on the user. - Q: Should an Entra tenant id be globally unique or unique per workspace? → A: Global unique `entra_tenant_id` (a Managed Tenant belongs to exactly one Workspace). - Q: What should users with 0 workspace memberships see? → A: A neutral `/admin/no-access` page (not 404). ## Routing, Scoping & Selection Semantics ### Routing “planes” This feature introduces **Workspace-scoped** admin routing and treats Workspace membership as a primary isolation boundary. - **Workspace plane (workspace-scoped)**: `/admin/w/{workspace}/...` - **Entry points (unscoped)**: `/admin/choose-workspace`, `/admin/no-access` If the repository also has existing tenant-scoped routes (e.g. `/admin/t/{tenant}/...`), they MUST be made workspace-safe by ensuring the tenant belongs to the currently selected workspace (or by denying-as-not-found). ### Workspace selection algorithm (deterministic) When a signed-in user enters the admin panel: 1) If the session has a selected workspace and it is still valid (user is a member, workspace not archived) → use it. 2) Else if `users.last_workspace_id` is set and still valid → select it, store into session, and use it. 3) Else if the user has exactly 1 active membership → auto-select that workspace, store into session + `last_workspace_id`, and use it. 4) Else if the user has 2+ active memberships → route to `/admin/choose-workspace`. 5) Else (0 memberships) → route to `/admin/no-access`. If a selected workspace becomes invalid (membership removed or workspace archived), selection MUST be cleared and the algorithm re-run. ## User Scenarios & Testing *(mandatory)* ### User Story 1 - Choose workspace and get correct scope (Priority: P1) As a signed-in user, I want the admin panel to require an active Workspace context so that everything I see and search is scoped to my current Workspace and cannot leak information about other Workspaces. **Why this priority**: This is the foundation for correct navigation, isolation, and predictable operations in enterprise / MSP usage. **Independent Test**: Can be tested by creating 2 workspaces, adding a user to only one, and verifying workspace-scoped navigation + 404 semantics for non-members. **Acceptance Scenarios**: 1. **Given** a user who is a member of exactly one Workspace, **When** they enter the admin panel entry point, **Then** they are automatically routed into that Workspace context. 2. **Given** a user who is a member of multiple Workspaces, **When** they enter the admin panel entry point, **Then** they are routed to a "choose workspace" page and can select one. 3. **Given** a user who is not a member of Workspace A, **When** they attempt to access any Workspace A scoped URL, **Then** they receive a not-found outcome (deny-as-not-found; no hints). 4. **Given** a user is a member of Workspace A, **When** they switch to Workspace B, **Then** navigation and global search results reflect only data in Workspace B. --- ### User Story 2 - Manage workspace members and roles (Priority: P2) As a Workspace Owner or Manager, I want to add and manage Workspace members and their roles so that the right people can access the Workspace with the right level of permissions. **Why this priority**: Multi-user operation is required for MSP/enterprise teams; role-based access is a core control. **Independent Test**: Can be tested by creating a workspace with 2+ users and validating add/change/remove behavior, including last-owner protections and audit entries. **Acceptance Scenarios**: 1. **Given** a Workspace with an Owner, **When** the Owner adds an existing user as a member, **Then** the user can access the Workspace within the admin panel. 2. **Given** a Workspace member, **When** an Owner changes that member’s role, **Then** their effective permissions change accordingly. 3. **Given** a Workspace with exactly one Owner, **When** that Owner is removed or demoted, **Then** the system prevents the action and records that it was blocked. 4. **Given** a user who is a member but lacks a required capability for an action, **When** they try to execute the action, **Then** the action is blocked with a forbidden outcome (member → 403 semantics) and does not mutate data. --- ### User Story 3 - Onboard a managed tenant inside a workspace (Priority: P3) As a Workspace Owner or Manager, I want to add a Managed Tenant into the current Workspace from a single canonical onboarding entry so that onboarding is consistent now and can later evolve into a wizard without changing the entry point. **Why this priority**: Managed Tenants are the core “things being managed” and must clearly live under a Workspace. **Independent Test**: Can be tested by adding a Managed Tenant under Workspace A and verifying it does not appear under Workspace B. **Acceptance Scenarios**: 1. **Given** an active Workspace context, **When** an authorized member starts "add managed tenant" onboarding, **Then** the resulting Managed Tenant belongs to the current Workspace. 2. **Given** a Managed Tenant in Workspace A, **When** viewing managed tenants in Workspace B, **Then** the tenant is not visible. 3. **Given** legacy onboarding entry points, **When** a user visits them, **Then** they are redirected into the canonical onboarding entry for the appropriate Workspace context. --- [Add more user stories as needed, each with an assigned priority] ### Edge Cases - User has zero Workspace memberships (newly provisioned account) → should see a neutral "no access" page without leaking other workspaces. - Previously-selected Workspace is archived or the user’s membership was removed → current selection must be invalidated and user returned to choose/no-access safely. - Two concurrent admins attempt to remove/demote the last owner at the same time → system still guarantees at least one owner. - Managed Tenant identifier is attempted to be added twice (duplicate Entra tenant id) → the second attempt is blocked without creating an ambiguous partial record. - Global search term matches records in a Workspace the user is not a member of → results must not hint those records exist. ## Requirements *(mandatory)* **Constitution alignment (required):** This feature changes access control and tenant-plane data ownership. The implementation MUST include: - tenant/workspace isolation guarantees, - safety gates for any write/change behavior (preview/confirmation/audit as applicable), - run visibility/traceability for any long-running work, - and automated tests that prove isolation and authorization. If any security-relevant change intentionally skips run tracking, the implementation MUST still emit audit entries that allow incident review. **Constitution alignment (RBAC-UX):** This feature introduces Workspace-scoped authorization for the admin panel. The implementation MUST: - explicitly enforce deny-as-not-found for non-members (no hints), - enforce forbidden outcomes for members who lack the required capability, - ensure global search is scoped to the active Workspace and non-member-safe, - use a centralized capability registry and deterministic role mapping (no ad-hoc string checks), - require explicit confirmation for destructive-like actions, - and include at least one positive and one negative authorization test. **Constitution alignment (OPS-EX-AUTH-001):** Authentication handshakes may perform synchronous outbound requests during login, but this exception MUST NOT be used for monitoring/operations functionality. **Constitution alignment (BADGE-001):** If this feature changes status-like badges (status/outcome/severity/risk/availability/boolean), the spec MUST describe how badge semantics stay centralized (no ad-hoc mappings) and which tests cover any new/changed values. ### Functional Requirements - **FR-001 (Workspace entity)**: System MUST support a Workspace concept as a top-level container representing an organization/customer context. - **FR-002 (Workspace lifecycle)**: Authorized users MUST be able to create a Workspace and view Workspace details. Workspaces MUST be markable as active or archived. - **FR-003 (Membership source of truth)**: System MUST support Workspace membership records that assign a role per (workspace, user). Membership MUST be the sole source of truth for Workspace access. - **FR-004 (Membership constraints)**: System MUST prevent duplicate memberships for the same (workspace, user) pair. - **FR-005 (Last owner protection)**: System MUST prevent removing or demoting the last remaining Owner of a Workspace. - **FR-006 (Membership management)**: Workspace Owners/Managers MUST be able to add existing users as members, change member roles, and remove members. - **FR-007 (Workspace switcher + persistence)**: System MUST provide a workspace switcher that lists only workspaces where the user is a member, and MUST persist the user’s current workspace selection for the session. - **FR-008 (Choose-workspace routing)**: If a user is a member of multiple workspaces, system MUST provide a dedicated choose-workspace page and MUST require an explicit selection before accessing workspace-scoped pages. - **FR-008a (No-access routing)**: If a user has zero workspace memberships, system MUST route them to a neutral "no access" page and MUST NOT return deny-as-not-found for this entry-point case. - **FR-009 (Workspace-scoped URLs)**: System MUST require Workspace context for all Workspace-plane admin resources under `/admin/w/{workspace}/...`, except authentication and the choose-workspace/no-access entry points. - If tenant-scoped routes exist elsewhere (e.g. `/admin/t/{tenant}/...`), they MUST be workspace-safe by ensuring the tenant belongs to the currently selected workspace (or by denying-as-not-found). - **FR-010 (Managed tenant belongs to workspace)**: Each Managed Tenant MUST belong to exactly one Workspace. - **FR-011 (Managed tenant uniqueness)**: A Managed Tenant MUST be uniquely identifiable by its Entra tenant identifier, and the system MUST prevent duplicates. - **FR-012 (Canonical onboarding entry)**: System MUST provide a single canonical "add managed tenant" entry inside Workspace context; legacy entry points MUST redirect to it without allowing creation in an incorrect scope. - **FR-013 (Global search safety)**: Global search MUST return results only from the user’s current Workspace, and MUST not leak existence of records in other Workspaces. - **FR-014 (404 vs 403 semantics)**: For any Workspace-scoped resource: - Non-member access MUST be deny-as-not-found. - Member access without the required capability MUST be forbidden. - **FR-015 (Capabilities and role mapping)**: System MUST derive effective capabilities from Workspace role deterministically using a centralized role → capability mapping. - **FR-016 (UI behavior for missing capability)**: For members without required capability, the UI MUST show relevant actions as disabled (where applicable) rather than hidden, and execution MUST still be blocked server-side. - **FR-017 (Audit logging for membership changes)**: System MUST record audit events for membership add, role change, removal, and last-owner-blocked outcomes. #### Clarified Authorization Rules - Workspace creation: any signed-in user MAY create a Workspace; the creator becomes an Owner. - Workspace membership management: only Workspace Owners/Managers. #### Assumptions - Creating a Workspace is allowed for signed-in users in v2; the creator becomes an Owner of the new Workspace. - Workspace URL identifier prefers human-friendly `slug`, falling back to numeric `id` if a workspace does not have a slug. - Current workspace selection is persisted in session and also saved as a nullable `last_workspace_id` on the user. #### Legacy Route Scope (explicit) For v2, the only guaranteed legacy onboarding entry point that MUST redirect is: - `/admin/new` Additional legacy routes may exist in the codebase; they should be enumerated and handled as part of implementation if discovered. #### Out of Scope (v2) - Onboarding wizard UI (stepper-based experience) - Automated mapping from external groups to Workspace membership - Billing and multi-region concerns ### Key Entities *(include if feature involves data)* - **Workspace**: Represents an organization/customer scope; includes display name, optional human-friendly key, and an active/archived status. - **Workspace Membership**: Associates a user to a workspace with one role (Owner/Manager/Operator/Readonly). - **Managed Tenant**: Represents a Microsoft/Entra/Intune tenant managed inside a Workspace; belongs to exactly one Workspace and is uniquely identified by Entra tenant id. - **Workspace Context**: The current active Workspace selection that scopes navigation and search. - **Capability**: A named permission (e.g., workspace management, member management, managed tenant management) derived from Workspace role via centralized mapping. - **Audit Event**: An immutable record of sensitive Workspace membership changes with minimal, non-secret details. ## Success Criteria *(mandatory)* ### Measurable Outcomes - **SC-001 (Isolation)**: 100% of attempts by non-members to access a Workspace-scoped page result in a not-found outcome. - **SC-002 (Correct scoping)**: 0 global search results are returned from outside the user’s current Workspace during acceptance testing. - **SC-003 (Membership management)**: An Owner can add a member, change role, and remove member in under 2 minutes end-to-end using the admin UI. - **SC-004 (Last-owner safety)**: 100% of attempts to remove/demote the last Owner are blocked and recorded. - **SC-005 (Migration safety)**: After migration, 100% of existing Managed Tenants are associated with exactly one Workspace.