Constitution v1.8.2: scope ownership + spec scope fields #111
@ -1,14 +1,15 @@
|
||||
<!--
|
||||
Sync Impact Report
|
||||
|
||||
- Version change: 1.8.0 → 1.8.1
|
||||
- Version change: 1.8.1 → 1.8.2
|
||||
- Modified principles:
|
||||
- Workspace Isolation is Non-negotiable (new core principle)
|
||||
- Tenant Isolation is Non-negotiable (clarified tenant-plane scope + canonical tenantless views)
|
||||
- RBAC-UX-002 / RBAC-UX-003 (clarified workspace + tenant membership semantics)
|
||||
- Filament UI — Action Surface Contract (NON-NEGOTIABLE) (added micro-rules + clarified CI enforcement phrasing)
|
||||
- RBAC Context — Planes, Roles, and Auditability (clarified admin vs tenant-context vs workspace-context)
|
||||
- Tenant Isolation is Non-negotiable (added scope + ownership rules)
|
||||
- RBAC-UX-007 — Global search must be tenant-safe (added workspace-context rules)
|
||||
- Filament UI — Action Surface Contract (NON-NEGOTIABLE) (added required spec scope fields)
|
||||
- Added sections:
|
||||
- Workspace Isolation is Non-negotiable
|
||||
- Scope & Ownership Clarification (SCOPE-001)
|
||||
- Spec Scope Fields (SCOPE-002)
|
||||
- Removed sections: None
|
||||
- Templates requiring updates:
|
||||
- ✅ .specify/templates/plan-template.md
|
||||
@ -56,11 +57,27 @@ ### Tenant Isolation is Non-negotiable
|
||||
- Tenant membership is an isolation boundary. If the actor is not entitled to the tenant scope, the system MUST respond as
|
||||
deny-as-not-found (404).
|
||||
|
||||
Scope & Ownership Clarification (SCOPE-001)
|
||||
|
||||
- The system MUST enforce a strict ownership model:
|
||||
- Workspace-owned objects define standards, templates, and global configuration (e.g., Baseline Profiles, Notification Targets, Alert Routing Rules, Framework/Control catalogs).
|
||||
- Tenant-owned objects represent observed state, evidence, and operational artifacts for a specific tenant (e.g., Inventory, Backups/Snapshots, OperationRuns for tenant operations, Drift/Findings, Exceptions/Risk Acceptance, EvidenceItems, StoredReports/Exports).
|
||||
- Workspace-owned objects MUST NOT directly embed or persist tenant-owned records (no “copying tenant data into templates”).
|
||||
- Tenant-owned objects MUST always be bound to an established workspace + tenant scope at authorization time.
|
||||
|
||||
Database convention:
|
||||
|
||||
- Tenant-owned tables MUST include workspace_id and tenant_id as NOT NULL.
|
||||
- Workspace-owned tables MUST include workspace_id and MUST NOT include tenant_id.
|
||||
- Exception: OperationRun MAY have tenant_id nullable to support canonical workspace-context monitoring views; however, revealing any tenant-bound runs still MUST enforce entitlement checks to the referenced tenant scope.
|
||||
|
||||
### RBAC & UI Enforcement Standards (RBAC-UX)
|
||||
|
||||
RBAC Context — Planes, Roles, and Auditability
|
||||
- The platform MUST maintain two strictly separated authorization planes:
|
||||
- Tenant plane (`/admin/t/{tenant}`): authenticated Entra users (`users`), authorization is tenant-scoped.
|
||||
- Tenant/Admin plane (`/admin`): authenticated Entra users (`users`).
|
||||
- Tenant-context routes (`/admin/t/{tenant}/...`) are tenant-scoped.
|
||||
- Workspace-context canonical routes (`/admin/...`, e.g. Monitoring/Operations) are tenantless by URL but MUST still enforce workspace + tenant entitlement before revealing tenant-owned records.
|
||||
- Platform plane (`/system`): authenticated platform users (`platform_users`), authorization is platform-scoped.
|
||||
- Cross-plane access MUST be deny-as-not-found (404) (not 403) to avoid route enumeration.
|
||||
- Tenant role semantics MUST remain least-privilege:
|
||||
@ -108,9 +125,12 @@ ### RBAC & UI Enforcement Standards (RBAC-UX)
|
||||
- CI MUST fail if unknown/unregistered capabilities are used.
|
||||
|
||||
RBAC-UX-007 — Global search must be tenant-safe
|
||||
- Global search results MUST be scoped to the current tenant.
|
||||
- Global search MUST be context-safe (workspace-context vs tenant-context).
|
||||
- Non-members MUST never learn about resources in other tenants (no results, no hints).
|
||||
- If a result exists but is not accessible, it MUST be treated as not found (404 semantics).
|
||||
- In workspace-context (no active tenant selected), Global Search MUST NOT return tenant-owned results.
|
||||
- It MAY search workspace-owned objects only (e.g., Tenants list entries, Baseline Profiles, Alert Rules/Targets, workspace settings).
|
||||
- If tenant-context is active, Global Search MUST be scoped to the current tenant only (existing rule remains).
|
||||
|
||||
RBAC-UX-008 — Regression guards are mandatory
|
||||
- The repo MUST include RBAC regression tests asserting at least:
|
||||
@ -178,6 +198,17 @@ ### Filament UI — Action Surface Contract (NON-NEGOTIABLE)
|
||||
- A change is not “Done” unless the Action Surface Contract is met OR an explicit exemption exists with documented reason.
|
||||
- CI MUST run an automated Action Surface Contract check (test suite and/or command) that fails when required surfaces are missing.
|
||||
|
||||
Spec Scope Fields (SCOPE-002)
|
||||
|
||||
- Every feature spec MUST declare:
|
||||
- Scope: workspace | tenant | canonical-view
|
||||
- Primary Routes
|
||||
- Data Ownership: workspace-owned vs tenant-owned tables/records impacted
|
||||
- RBAC: membership requirements + capability requirements
|
||||
- For canonical-view specs, the spec MUST define:
|
||||
- Default filter behavior when tenant-context is active (e.g., prefilter to current tenant)
|
||||
- Explicit entitlement checks that prevent cross-tenant leakage
|
||||
|
||||
### Data Minimization & Safe Logging
|
||||
- Inventory MUST store only metadata + whitelisted `meta_jsonb`.
|
||||
- Payload-heavy content belongs in immutable snapshots/backup storage, not Inventory.
|
||||
@ -213,4 +244,4 @@ ### Versioning Policy (SemVer)
|
||||
- **MINOR**: new principle/section or materially expanded guidance.
|
||||
- **MAJOR**: removing/redefining principles in a backward-incompatible way.
|
||||
|
||||
**Version**: 1.8.1 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-02-09
|
||||
**Version**: 1.8.2 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-02-14
|
||||
|
||||
@ -35,7 +35,7 @@ ## Constitution Check
|
||||
- Read/write separation: any writes require preview + confirmation + audit + tests
|
||||
- Graph contract path: Graph calls only via `GraphClientInterface` + `config/graph_contracts.php`
|
||||
- Deterministic capabilities: capability derivation is testable (snapshot/golden tests)
|
||||
- RBAC-UX: two planes (/admin vs /system) remain separated; cross-plane is 404; non-member tenant access is 404; member-but-missing-capability is 403; authorization checks use Gates/Policies + capability registries (no raw strings, no role-string checks)
|
||||
- RBAC-UX: two planes (/admin vs /system) remain separated; cross-plane is 404; tenant-context routes (/admin/t/{tenant}/...) are tenant-scoped; canonical workspace-context routes under /admin remain tenant-safe; non-member tenant/workspace access is 404; member-but-missing-capability is 403; authorization checks use Gates/Policies + capability registries (no raw strings, no role-string checks)
|
||||
- Workspace isolation: non-member workspace access is 404; tenant-plane routes require an established workspace context; workspace context switching is separate from Filament Tenancy
|
||||
- RBAC-UX: destructive-like actions require `->requiresConfirmation()` and clear warning text
|
||||
- RBAC-UX: global search is tenant-scoped; non-members get no hints; inaccessible results are treated as not found (404 semantics)
|
||||
|
||||
@ -5,6 +5,18 @@ # Feature Specification: [FEATURE NAME]
|
||||
**Status**: Draft
|
||||
**Input**: User description: "$ARGUMENTS"
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: [workspace | tenant | canonical-view]
|
||||
- **Primary Routes**: [List the primary routes/pages affected]
|
||||
- **Data Ownership**: [workspace-owned vs tenant-owned tables/records impacted]
|
||||
- **RBAC**: [membership requirements + capability requirements]
|
||||
|
||||
For canonical-view specs, the spec MUST define:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: [e.g., prefilter to current tenant]
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: [Describe checks]
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
<!--
|
||||
@ -83,7 +95,7 @@ ## Requirements *(mandatory)*
|
||||
If security-relevant DB-only actions intentionally skip `OperationRun`, the spec MUST describe `AuditLog` entries.
|
||||
|
||||
**Constitution alignment (RBAC-UX):** If this feature introduces or changes authorization behavior, the spec MUST:
|
||||
- state which authorization plane(s) are involved (tenant `/admin/t/{tenant}` vs platform `/system`),
|
||||
- state which authorization plane(s) are involved (tenant/admin `/admin` + tenant-context `/admin/t/{tenant}/...` vs platform `/system`),
|
||||
- ensure any cross-plane access is deny-as-not-found (404),
|
||||
- explicitly define 404 vs 403 semantics:
|
||||
- non-member / not entitled to workspace scope OR tenant scope → 404 (deny-as-not-found)
|
||||
|
||||
@ -20,6 +20,7 @@ # Tasks: [FEATURE NAME]
|
||||
- non-member / not entitled to workspace scope OR tenant scope → 404 (deny-as-not-found)
|
||||
- member but missing capability → 403,
|
||||
- capability registry usage (no raw capability strings; no role-string checks in feature code),
|
||||
- stating which authorization plane(s) are involved (tenant/admin `/admin` + tenant-context `/admin/t/{tenant}/...` vs platform `/system`),
|
||||
- tenant-safe global search scoping (no hints; inaccessible results treated as 404 semantics),
|
||||
- destructive-like actions use `->requiresConfirmation()` (authorization still server-side),
|
||||
- cross-plane deny-as-not-found (404) checks where applicable,
|
||||
|
||||
@ -10,6 +10,17 @@
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Livewire;
|
||||
|
||||
function getTableEmptyStateAction($component, string $name): ?\Filament\Actions\Action
|
||||
{
|
||||
foreach ($component->instance()->getTable()->getEmptyStateActions() as $action) {
|
||||
if ($action instanceof \Filament\Actions\Action && $action->getName() === $name) {
|
||||
return $action;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
@ -74,11 +85,14 @@
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
$component = Livewire::actingAs($user)
|
||||
->test(ListBackupSets::class)
|
||||
->assertTableEmptyStateActionsExistInOrder(['create'])
|
||||
->assertActionVisible('create')
|
||||
->assertActionEnabled('create');
|
||||
->assertTableEmptyStateActionsExistInOrder(['create']);
|
||||
|
||||
$action = getTableEmptyStateAction($component, 'create');
|
||||
expect($action)->not->toBeNull();
|
||||
expect($action->isVisible())->toBeTrue();
|
||||
expect($action->isDisabled())->toBeFalse();
|
||||
});
|
||||
|
||||
test('backup sets list shows empty state create action disabled for members without sync capability', function () {
|
||||
@ -87,9 +101,12 @@
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
$component = Livewire::actingAs($user)
|
||||
->test(ListBackupSets::class)
|
||||
->assertTableEmptyStateActionsExistInOrder(['create'])
|
||||
->assertActionVisible('create')
|
||||
->assertActionDisabled('create');
|
||||
->assertTableEmptyStateActionsExistInOrder(['create']);
|
||||
|
||||
$action = getTableEmptyStateAction($component, 'create');
|
||||
expect($action)->not->toBeNull();
|
||||
expect($action->isVisible())->toBeTrue();
|
||||
expect($action->isDisabled())->toBeTrue();
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user