215 lines
15 KiB
Markdown
215 lines
15 KiB
Markdown
# 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 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.
|
||
|
||
<!--
|
||
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 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)*
|
||
|
||
<!--
|
||
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 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.
|