TenantAtlas/specs/068-workspaces-v2/spec.md
2026-02-01 12:19:57 +01:00

215 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)*
<!--
IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
you should still have a viable MVP (Minimum Viable Product) that delivers value.
Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
Think of each story as a standalone slice of functionality that can be:
- Developed independently
- Tested independently
- Deployed independently
- Demonstrated to users independently
-->
### 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 members 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 users 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.
<!--
ACTION REQUIRED: The content in this section represents placeholders.
Fill them out with the right functional requirements.
-->
### 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 users 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 users 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)*
<!--
ACTION REQUIRED: Define measurable success criteria.
These must be technology-agnostic and measurable.
-->
### 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 users 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.