15 KiB
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
slugin URLs; if missing, fall back to numericid. - Q: How should current workspace selection be persisted? → A: Persist in session and also store a nullable
last_workspace_idon 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-accesspage (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:
- If the session has a selected workspace and it is still valid (user is a member, workspace not archived) → use it.
- Else if
users.last_workspace_idis set and still valid → select it, store into session, and use it. - Else if the user has exactly 1 active membership → auto-select that workspace, store into session +
last_workspace_id, and use it. - Else if the user has 2+ active memberships → route to
/admin/choose-workspace. - 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:
- 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.
- 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.
- 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).
- 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:
- 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.
- Given a Workspace member, When an Owner changes that member’s role, Then their effective permissions change accordingly.
- 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.
- 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:
- Given an active Workspace context, When an authorized member starts "add managed tenant" onboarding, Then the resulting Managed Tenant belongs to the current Workspace.
- Given a Managed Tenant in Workspace A, When viewing managed tenants in Workspace B, Then the tenant is not visible.
- 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).
- If tenant-scoped routes exist elsewhere (e.g.
- 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 numericidif a workspace does not have a slug. - Current workspace selection is persisted in session and also saved as a nullable
last_workspace_idon 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.