feat: cut over admin directory groups
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m39s
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m39s
This commit is contained in:
parent
d072b0107b
commit
a6718e8148
@ -5,12 +5,14 @@
|
||||
use App\Filament\Concerns\InteractsWithTenantOwnedRecords;
|
||||
use App\Filament\Concerns\ResolvesPanelTenantContext;
|
||||
use App\Filament\Concerns\ScopesGlobalSearchToTenant;
|
||||
use App\Filament\Concerns\WorkspaceScopedTenantRoutes;
|
||||
use App\Filament\Resources\EntraGroupResource\Pages;
|
||||
use App\Models\EntraGroup;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Support\Badges\BadgeDomain;
|
||||
use App\Support\Badges\BadgeRenderer;
|
||||
use App\Support\Filament\TablePaginationProfiles;
|
||||
use App\Support\Navigation\NavigationScope;
|
||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
||||
@ -37,6 +39,7 @@ class EntraGroupResource extends Resource
|
||||
use InteractsWithTenantOwnedRecords;
|
||||
use ResolvesPanelTenantContext;
|
||||
use ScopesGlobalSearchToTenant;
|
||||
use WorkspaceScopedTenantRoutes;
|
||||
|
||||
protected static bool $isScopedToTenant = false;
|
||||
|
||||
@ -54,11 +57,8 @@ class EntraGroupResource extends Resource
|
||||
|
||||
public static function shouldRegisterNavigation(): bool
|
||||
{
|
||||
if (Filament::getCurrentPanel()?->getId() === 'admin') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::shouldRegisterNavigation();
|
||||
return NavigationScope::shouldRegisterEnvironmentNavigation()
|
||||
&& parent::shouldRegisterNavigation();
|
||||
}
|
||||
|
||||
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
||||
@ -187,7 +187,7 @@ public static function table(Table $table): Table
|
||||
->actions([])
|
||||
->bulkActions([])
|
||||
->emptyStateHeading('No groups cached yet')
|
||||
->emptyStateDescription('Sync groups for the current tenant to browse directory data here.')
|
||||
->emptyStateDescription('No groups are available for this managed environment yet. Run or refresh the relevant directory inventory operation to make groups visible here.')
|
||||
->emptyStateIcon('heroicon-o-user-group');
|
||||
}
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
use App\Filament\Resources\AlertRuleResource;
|
||||
use App\Filament\Resources\BaselineProfileResource;
|
||||
use App\Filament\Resources\BaselineSnapshotResource;
|
||||
use App\Filament\Resources\EntraGroupResource;
|
||||
use App\Filament\Resources\InventoryItemResource;
|
||||
use App\Filament\Resources\PolicyResource;
|
||||
use App\Filament\Resources\ProviderConnectionResource;
|
||||
@ -98,6 +99,12 @@ public function panel(Panel $panel): Panel
|
||||
->group('Inventory')
|
||||
->sort(3)
|
||||
->visible(fn (): bool => NavigationScope::shouldRegisterEnvironmentNavigation() && InventoryCoverage::canAccess()),
|
||||
NavigationItem::make('Groups')
|
||||
->url(fn (): string => EntraGroupResource::getUrl(panel: 'admin'))
|
||||
->icon('heroicon-o-user-group')
|
||||
->group('Directory')
|
||||
->sort(10)
|
||||
->visible(fn (): bool => NavigationScope::shouldRegisterEnvironmentNavigation() && EntraGroupResource::canViewAny()),
|
||||
NavigationItem::make(fn (): string => __('localization.navigation.integrations'))
|
||||
->url(fn (): string => route('filament.admin.resources.provider-connections.index'))
|
||||
->icon('heroicon-o-link')
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Resources\EntraGroupResource;
|
||||
use App\Models\EntraGroup;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Support\ManagedEnvironmentLinks;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
pest()->browser()->timeout(20_000);
|
||||
|
||||
it('smokes environment-bound directory groups navigation and view drilldown', function (): void {
|
||||
[$user, $environment] = createUserWithTenant(
|
||||
role: 'owner',
|
||||
workspaceRole: 'manager',
|
||||
ensureDefaultMicrosoftProviderConnection: false,
|
||||
);
|
||||
|
||||
$environment->forceFill([
|
||||
'name' => 'Spec 303 Production',
|
||||
'slug' => 'spec-303-production',
|
||||
'kind' => 'production',
|
||||
'lifecycle_status' => ManagedEnvironment::STATUS_ACTIVE,
|
||||
])->save();
|
||||
|
||||
$group = EntraGroup::factory()->for($environment)->create([
|
||||
'display_name' => 'Spec 303 Browser Directory Group',
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
$groupsUrl = EntraGroupResource::getUrl(
|
||||
panel: 'admin',
|
||||
tenant: $environment,
|
||||
);
|
||||
$groupsPath = (string) parse_url($groupsUrl, PHP_URL_PATH);
|
||||
$groupViewPath = (string) parse_url(
|
||||
EntraGroupResource::getUrl(
|
||||
'view',
|
||||
['record' => $group],
|
||||
panel: 'admin',
|
||||
tenant: $environment,
|
||||
),
|
||||
PHP_URL_PATH,
|
||||
);
|
||||
|
||||
$this->actingAs($user)->withSession([
|
||||
WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id,
|
||||
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
|
||||
(string) $environment->workspace_id => (int) $environment->getKey(),
|
||||
],
|
||||
]);
|
||||
|
||||
visit(route('admin.workspace.home', ['workspace' => $environment->workspace]))
|
||||
->waitForText('Overview')
|
||||
->assertScript("Array.from(document.querySelectorAll('a')).every((link) => link.pathname !== '{$groupsPath}')", true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
$environmentPage = visit(ManagedEnvironmentLinks::viewUrl($environment))
|
||||
->waitForText('Spec 303 Production')
|
||||
->assertNoJavaScriptErrors();
|
||||
|
||||
$environmentPage
|
||||
->assertScript("Array.from(document.querySelectorAll('a')).some((link) => link.pathname === '{$groupsPath}' && link.textContent?.includes('Groups'))", true)
|
||||
->assertScript("Array.from(document.querySelectorAll('a')).filter((link) => link.pathname === '{$groupsPath}' && link.textContent?.includes('Groups')).length === 1", true)
|
||||
->click("a[href$=\"{$groupsPath}\"]")
|
||||
->waitForText('Spec 303 Browser Directory Group')
|
||||
->assertScript("window.location.pathname === '{$groupsPath}'", true)
|
||||
->assertScript("! window.location.pathname.includes('/admin/t/')", true)
|
||||
->click('tbody tr.fi-ta-row:first-of-type')
|
||||
->waitForText('Spec 303 Browser Directory Group')
|
||||
->assertScript("window.location.pathname === '{$groupViewPath}'", true)
|
||||
->assertScript("! window.location.pathname.includes('/admin/t/')", true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
});
|
||||
@ -5,13 +5,26 @@
|
||||
use App\Models\EntraGroup;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\User;
|
||||
use App\Support\ManagedEnvironmentLinks;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function bindDirectoryGroupsNavigationRequestPath(string $path): void
|
||||
{
|
||||
$request = Request::create($path);
|
||||
$route = app('router')->getRoutes()->match($request);
|
||||
|
||||
$request->setRouteResolver(static fn () => $route);
|
||||
|
||||
app()->instance('request', $request);
|
||||
}
|
||||
|
||||
test('cached groups can be listed, searched, and filtered (DB-only)', function () {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner', fixtureProfile: 'credential-enabled');
|
||||
$this->actingAs($user);
|
||||
@ -124,14 +137,18 @@
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
test('keeps Entra groups out of admin sidebar navigation after tenant-panel retirement', function () {
|
||||
test('registers Entra groups navigation only inside the environment admin context', function () {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$this->actingAs($user);
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||
Filament::setCurrentPanel(Filament::getPanel('admin'));
|
||||
|
||||
bindDirectoryGroupsNavigationRequestPath(route('admin.workspace.home', ['workspace' => $tenant->workspace_id]));
|
||||
expect(EntraGroupResource::shouldRegisterNavigation())->toBeFalse();
|
||||
|
||||
Filament::setCurrentPanel(Filament::getPanel('admin'));
|
||||
|
||||
expect(EntraGroupResource::shouldRegisterNavigation())->toBeFalse();
|
||||
bindDirectoryGroupsNavigationRequestPath(ManagedEnvironmentLinks::viewUrl($tenant));
|
||||
expect(EntraGroupResource::shouldRegisterNavigation())->toBeTrue();
|
||||
|
||||
Filament::setCurrentPanel(null);
|
||||
});
|
||||
|
||||
@ -13,19 +13,22 @@
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('returns not found for the admin group list when no canonical tenant context exists', function (): void {
|
||||
it('returns not found for the retired direct admin group list when no canonical tenant context exists', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
expect(EntraGroupResource::getUrl(panel: 'admin'))
|
||||
->not->toContain('/entra-groups');
|
||||
|
||||
$this->withSession([
|
||||
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
||||
])->get(EntraGroupResource::getUrl(panel: 'admin'))
|
||||
])->get('/admin/entra-groups')
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
it('scopes the admin group list to the remembered tenant context', function (): void {
|
||||
it('scopes the admin group list to the canonical tenant context', function (): void {
|
||||
$tenantA = ManagedEnvironment::factory()->create();
|
||||
[$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner');
|
||||
|
||||
@ -46,12 +49,21 @@
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
$url = EntraGroupResource::getUrl(panel: 'admin', tenant: $tenantA);
|
||||
|
||||
expect($url)
|
||||
->toContain('/admin/workspaces/')
|
||||
->toContain('/environments/')
|
||||
->toContain('/entra-groups')
|
||||
->not->toContain('/admin/t/')
|
||||
->not->toBe('/admin/entra-groups');
|
||||
|
||||
$this->withSession([
|
||||
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
||||
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
|
||||
(string) $tenantA->workspace_id => (int) $tenantA->getKey(),
|
||||
],
|
||||
])->get(EntraGroupResource::getUrl(panel: 'admin'))
|
||||
])->get($url)
|
||||
->assertOk()
|
||||
->assertSee((string) $groupA->display_name)
|
||||
->assertDontSee('Other tenant group');
|
||||
@ -80,12 +92,53 @@
|
||||
],
|
||||
];
|
||||
|
||||
$groupAUrl = EntraGroupResource::getUrl('view', ['record' => $groupA], panel: 'admin', tenant: $tenantA);
|
||||
$groupBUrl = EntraGroupResource::getUrl('view', ['record' => $groupB], panel: 'admin', tenant: $tenantA);
|
||||
|
||||
expect($groupAUrl)
|
||||
->toContain('/admin/workspaces/')
|
||||
->toContain('/environments/')
|
||||
->not->toContain('/admin/t/');
|
||||
|
||||
$this->withSession($session)
|
||||
->get(EntraGroupResource::getUrl('view', ['record' => $groupA], panel: 'admin'))
|
||||
->get($groupAUrl)
|
||||
->assertOk();
|
||||
|
||||
$this->withSession($session)
|
||||
->get(EntraGroupResource::getUrl('view', ['record' => $groupB], panel: 'admin'))
|
||||
->get($groupBUrl)
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
it('returns not found when the remembered admin tenant belongs to another workspace', function (): void {
|
||||
$tenantA = ManagedEnvironment::factory()->create();
|
||||
[$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner');
|
||||
|
||||
$tenantB = ManagedEnvironment::factory()->create();
|
||||
createUserWithTenant(tenant: $tenantB, user: $user, role: 'owner');
|
||||
|
||||
EntraGroup::factory()->for($tenantB)->create([
|
||||
'display_name' => 'Cross workspace remembered group',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
$mismatchedWorkspaceUrl = EntraGroupResource::getUrl(
|
||||
parameters: ['workspace' => $tenantA->workspace],
|
||||
panel: 'admin',
|
||||
tenant: $tenantB,
|
||||
);
|
||||
|
||||
expect($mismatchedWorkspaceUrl)
|
||||
->toContain('/entra-groups')
|
||||
->not->toContain('/admin/t/');
|
||||
|
||||
$this->withSession([
|
||||
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
||||
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
|
||||
(string) $tenantA->workspace_id => (int) $tenantB->getKey(),
|
||||
],
|
||||
])->get($mismatchedWorkspaceUrl)
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
|
||||
@ -44,6 +44,9 @@ function entraGroupSearchTitles($results): array
|
||||
|
||||
createUserWithTenant(tenant: $tenantB, user: $user, role: 'owner');
|
||||
|
||||
$tenantC = ManagedEnvironment::factory()->create();
|
||||
createUserWithTenant(tenant: $tenantC, user: $user, role: 'owner');
|
||||
|
||||
$groupA = EntraGroup::factory()->for($tenantA)->create([
|
||||
'display_name' => 'Remembered search group',
|
||||
]);
|
||||
@ -52,6 +55,10 @@ function entraGroupSearchTitles($results): array
|
||||
'display_name' => 'Other search group',
|
||||
]);
|
||||
|
||||
EntraGroup::factory()->for($tenantC)->create([
|
||||
'display_name' => 'Other workspace search group',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
Filament::setCurrentPanel('admin');
|
||||
Filament::setTenant(null, true);
|
||||
@ -68,8 +75,13 @@ function entraGroupSearchTitles($results): array
|
||||
'Remembered search group',
|
||||
]);
|
||||
|
||||
expect($results->first()?->url)
|
||||
->toBe(EntraGroupResource::getUrl('view', ['record' => $groupA], panel: 'admin', tenant: $tenantA));
|
||||
$url = $results->first()?->url;
|
||||
|
||||
expect($url)
|
||||
->toBe(EntraGroupResource::getUrl('view', ['record' => $groupA], panel: 'admin', tenant: $tenantA))
|
||||
->not->toContain('/admin/t/');
|
||||
|
||||
$this->get($url)->assertOk();
|
||||
});
|
||||
|
||||
it('keeps tenant-panel global-search results panel-native', function (): void {
|
||||
@ -103,6 +115,11 @@ function entraGroupSearchTitles($results): array
|
||||
'ManagedEnvironment panel group',
|
||||
]);
|
||||
|
||||
expect($results->first()?->url)
|
||||
->toBe(EntraGroupResource::getUrl('view', ['record' => $groupA], panel: 'admin', tenant: $tenantA));
|
||||
$url = $results->first()?->url;
|
||||
|
||||
expect($url)
|
||||
->toBe(EntraGroupResource::getUrl('view', ['record' => $groupA], panel: 'admin', tenant: $tenantA))
|
||||
->not->toContain('/admin/t/');
|
||||
|
||||
$this->get($url)->assertOk();
|
||||
});
|
||||
|
||||
@ -26,16 +26,13 @@
|
||||
Filament::setCurrentPanel(null);
|
||||
});
|
||||
|
||||
dataset('admin hidden navigation classes', [
|
||||
EntraGroupResource::class,
|
||||
]);
|
||||
|
||||
dataset('environment visible navigation classes', [
|
||||
InventoryCluster::class,
|
||||
InventoryCoverage::class,
|
||||
InventoryItemResource::class,
|
||||
PolicyResource::class,
|
||||
PolicyVersionResource::class,
|
||||
EntraGroupResource::class,
|
||||
BackupScheduleResource::class,
|
||||
BackupSetResource::class,
|
||||
RestoreRunResource::class,
|
||||
@ -52,12 +49,6 @@ function bindNavigationRequestPath(string $path): void
|
||||
app()->instance('request', $request);
|
||||
}
|
||||
|
||||
it('keeps admin-hidden tenant surfaces out of navigation registration', function (string $class): void {
|
||||
Filament::setCurrentPanel('admin');
|
||||
|
||||
expect($class::shouldRegisterNavigation())->toBeFalse();
|
||||
})->with('admin hidden navigation classes');
|
||||
|
||||
it('hides environment-owned navigation classes on workspace surfaces', function (string $class): void {
|
||||
Filament::setCurrentPanel('admin');
|
||||
bindNavigationRequestPath('/admin/workspaces/workspace-alpha');
|
||||
@ -152,6 +143,8 @@ function bindNavigationRequestPath(string $path): void
|
||||
$response->assertDontSee('>Items</span>', false);
|
||||
$response->assertDontSee('>Policies</span>', false);
|
||||
$response->assertDontSee('>Policy Versions</span>', false);
|
||||
$response->assertDontSee('>Groups</span>', false);
|
||||
$response->assertDontSee('/entra-groups', false);
|
||||
$response->assertDontSee('>Backup Schedules</span>', false);
|
||||
$response->assertDontSee('>Backup Sets</span>', false);
|
||||
$response->assertDontSee('>Restore Runs</span>', false);
|
||||
@ -175,6 +168,9 @@ function bindNavigationRequestPath(string $path): void
|
||||
|
||||
$response->assertSeeText('Policies');
|
||||
$response->assertSeeText('Policy Versions');
|
||||
$response->assertSeeText('Groups');
|
||||
$response->assertSee((string) parse_url(EntraGroupResource::getUrl(panel: 'admin', tenant: $tenant), PHP_URL_PATH), false);
|
||||
$response->assertDontSee('/admin/entra-groups', false);
|
||||
$response->assertSeeText('Items');
|
||||
$response->assertSeeText('Backup Schedules');
|
||||
$response->assertSeeText('Backup Sets');
|
||||
|
||||
@ -0,0 +1,91 @@
|
||||
# Requirements Checklist: Admin Directory Groups Cutover
|
||||
|
||||
**Purpose**: Validate that the Spec 303 preparation package is complete, bounded, and ready for implementation.
|
||||
**Created**: 2026-05-14
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Applicability And Low-Impact Gate
|
||||
|
||||
- [x] The package explicitly changes an operator-facing navigation/resource/search surface and does not use a false low-impact `N/A`.
|
||||
- [x] `spec.md`, `plan.md`, and `tasks.md` carry the same native Filament navigation/resource/search classification, shared-family relevance, and no-new-action decision.
|
||||
|
||||
## Candidate Selection
|
||||
|
||||
- [x] The selected candidate exists in `docs/product/spec-candidates.md` as `admin-directory-groups-cutover`.
|
||||
- [x] The candidate was explicitly manually promoted by the user and assigned number `303`.
|
||||
- [x] Spec 301 is treated as completed Inventory context and is not modified.
|
||||
- [x] Spec 302 is treated as completed audit evidence and is not modified.
|
||||
- [x] Close alternatives are deferred: `navigation-contract-split`, `tenant-panel-dead-code-retirement`, and any broader Directory/Admin Roles IA.
|
||||
|
||||
## Scope And Requirements
|
||||
|
||||
- [x] Entra Groups has an explicit admin role as a secondary environment-bound Directory/Identity surface.
|
||||
- [x] Workspace-home sidebar cleanliness remains an explicit negative-control requirement.
|
||||
- [x] Environment-bound Groups visibility is explicit and testable.
|
||||
- [x] List, View, and global-search scoping requirements cover no-context, cross-environment, and cross-workspace cases.
|
||||
- [x] Global-search View destination requirements explicitly reject legacy `/admin/t` routes.
|
||||
- [x] No generic M365 Admin mirror or broad Identity Center is in scope.
|
||||
- [x] No new group mutation/admin action is in scope.
|
||||
- [x] No new persistence, migration, model, service, job, provider contract, route family, asset, or provider registration change is introduced.
|
||||
|
||||
## Native, Shared-Family, And State Ownership
|
||||
|
||||
- [x] The surface remains native Filament resource navigation, table, View page, and global search.
|
||||
- [x] The shared paths to reuse are named as `NavigationScope`, `OperateHubShell`, `ScopesGlobalSearchToTenant`, `ResolvesPanelTenantContext`, and tenant-owned record helpers.
|
||||
- [x] Shell, page, route context, remembered environment context, and record resolution are named without introducing a second state owner.
|
||||
- [x] The likely next operator action is coherent: open or inspect Groups inside the selected environment.
|
||||
|
||||
## Shared Pattern Reuse
|
||||
|
||||
- [x] Navigation and search interaction classes are explicitly marked.
|
||||
- [x] The package extends existing navigation/context/search helpers instead of adding a Directory navigation framework.
|
||||
- [x] No direct-route posture is retained; Entra Groups uses workspace/environment resource routes.
|
||||
|
||||
## OperationRun Start UX Contract
|
||||
|
||||
- [x] The package explicitly says it does not create, queue, deduplicate, resume, block, complete, or deep-link to a new `OperationRun`.
|
||||
- [x] Existing directory group sync behavior remains outside new behavior and must continue using existing shared operation-start helpers if touched.
|
||||
- [x] No queued or terminal notification policy changes are introduced.
|
||||
|
||||
## Provider Boundary And Vocabulary
|
||||
|
||||
- [x] The package explicitly classifies the provider/platform boundary as mixed and bounded.
|
||||
- [x] Microsoft Entra terminology remains provider-owned and does not become platform-core identity truth.
|
||||
- [x] Operator-visible terms prefer Workspace, Managed Environment, Directory Groups, and Directory inventory.
|
||||
|
||||
## Signals, Exceptions, And Test Depth
|
||||
|
||||
- [x] The triggered repository signal is classified as a review-mandatory Groups navigation/search repair.
|
||||
- [x] No broad exception is needed; `WorkspaceScopedTenantRoutes` was adopted and verified.
|
||||
- [x] The required surface profile is `standard-native-filament`.
|
||||
- [x] Planned proof stays focused on Pest feature tests plus one explicit Browser smoke for the rendered sidebar navigation path.
|
||||
- [x] Fixture/helper cost remains low and reuses existing workspace/environment test helpers.
|
||||
|
||||
## Audience-Aware Disclosure And Decision Hierarchy
|
||||
|
||||
- [x] Directory Groups is secondary context, not a primary decision surface.
|
||||
- [x] Workspace home remains workspace-level and avoids environment-owned default-visible content.
|
||||
- [x] Raw/support diagnostics are not promoted by navigation.
|
||||
- [x] Exactly one dominant navigation intent is preserved: open Groups within the active environment.
|
||||
|
||||
## Filament v5 Checklist
|
||||
|
||||
- [x] Filament v5 targets Livewire v4.0+; this repo currently uses Livewire 4.1.4.
|
||||
- [x] No provider registration changes are planned; existing providers remain registered in `apps/platform/bootstrap/providers.php`.
|
||||
- [x] `EntraGroupResource` has a View page, so global search destination eligibility is satisfied.
|
||||
- [x] Global-search result URL customization is explicitly covered by requirements and tasks.
|
||||
- [x] No destructive actions are introduced or changed.
|
||||
- [x] No assets are registered; deploy `filament:assets` posture remains unchanged.
|
||||
- [x] Planned tests target Filament navigation/resource/search behavior using Feature tests.
|
||||
|
||||
## Review Outcome
|
||||
|
||||
- [x] Review outcome class: `acceptable-special-case`.
|
||||
- [x] Workflow outcome: `document-in-feature`.
|
||||
- [x] Final note location: active feature PR close-out entry `Guardrail / Exception / Smoke Coverage`.
|
||||
|
||||
## Preparation Result
|
||||
|
||||
- No application implementation was performed while preparing this package.
|
||||
- Preparation analysis found no critical or high-severity cross-artifact issues.
|
||||
- Spec Readiness Gate passes for preparation: `spec.md`, `plan.md`, `tasks.md`, and this checklist exist, contain no placeholders, and keep implementation scope bounded to Spec 303.
|
||||
267
specs/303-admin-directory-groups-cutover/plan.md
Normal file
267
specs/303-admin-directory-groups-cutover/plan.md
Normal file
@ -0,0 +1,267 @@
|
||||
# Implementation Plan: Admin Directory Groups Cutover
|
||||
|
||||
**Branch**: `303-admin-directory-groups-cutover` | **Date**: 2026-05-14 | **Spec**: [spec.md](spec.md)
|
||||
**Input**: Feature specification from `/specs/303-admin-directory-groups-cutover/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Cut over `EntraGroupResource` from a stale admin-hidden posture to an explicit secondary environment-bound Directory Groups surface. The implementation keeps workspace-home navigation clean, preserves server-side context/RBAC boundaries, keeps global search scoped to the active Managed Environment, and avoids any broad Identity Center or M365 admin mirror.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4.15
|
||||
**Primary Dependencies**: Laravel 12.52.0, Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1
|
||||
**Storage**: PostgreSQL via existing `entra_groups` persistence; no schema changes
|
||||
**Testing**: Pest feature tests through Laravel Sail
|
||||
**Validation Lanes**: confidence, formatting/diff-check
|
||||
**Target Platform**: Laravel Sail locally; Dokploy container deployment for staging/production
|
||||
**Project Type**: Web application under `apps/platform`
|
||||
**Performance Goals**: No new query families or polling. Existing scoped list/search queries must remain bounded to one Managed Environment.
|
||||
**Constraints**: No migrations, no assets, no provider registration changes, no Graph adapter changes, no new actions, no legacy `/admin/t` route revival.
|
||||
**Scale/Scope**: One existing Filament resource plus focused tests.
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: changed operator-facing navigation/resource/search surfaces.
|
||||
- **Native vs custom classification summary**: native Filament resource navigation, table, View page, and global search.
|
||||
- **Shared-family relevance**: navigation and global search.
|
||||
- **State layers in scope**: shell, route context, remembered environment context, resource page, record resolution.
|
||||
- **Audience modes in scope**: operator-MSP and support-platform where existing capability paths already permit.
|
||||
- **Decision/diagnostic/raw hierarchy plan**: Directory Groups is secondary context. List remains scan-oriented; View remains diagnostics/evidence-oriented. Raw/provider detail is not promoted.
|
||||
- **Raw/support gating plan**: unchanged from existing detail patterns.
|
||||
- **One-primary-action / duplicate-truth control**: primary interaction is inspect/open. This spec adds no new mutation action.
|
||||
- **Handling modes by drift class or surface**: review-mandatory for stale hidden navigation and route-context hardening.
|
||||
- **Repository-signal treatment**: Spec 302's audit matrix is the active signal; Entra Groups product IA is resolved here.
|
||||
- **Special surface test profiles**: standard-native-filament.
|
||||
- **Required tests or manual smoke**: focused feature tests for navigation, list/detail scope, search scope, search URL canonicality, no legacy route URLs, and regression surfaces. A focused browser smoke is required for the rendered Filament sidebar link because this implementation changes an operator-facing navigation flow.
|
||||
- **Exception path and spread control**: none. `WorkspaceScopedTenantRoutes` is adopted for Entra Groups and verified with focused route/search/reference/browser tests.
|
||||
- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage.
|
||||
|
||||
## Shared Pattern & System Fit
|
||||
|
||||
- **Cross-cutting feature marker**: yes.
|
||||
- **Systems touched**: `EntraGroupResource`, `ListEntraGroups`, `ViewEntraGroup`, `AdminPanelProvider`, `NavigationScope`, `OperateHubShell`, `ScopesGlobalSearchToTenant`, `ResolvesPanelTenantContext`, `WorkspaceScopedTenantRoutes`, and focused Filament tests.
|
||||
- **Shared abstractions reused**: `NavigationScope::shouldRegisterEnvironmentNavigation()`, `OperateHubShell::tenantOwnedPanelContext()`, `ScopesGlobalSearchToTenant`, `InteractsWithTenantOwnedRecords`, existing Filament resource authorization and URL helpers.
|
||||
- **New abstraction introduced? why?**: none.
|
||||
- **Why the existing abstraction was sufficient or insufficient**: Existing helpers already encode environment surface detection and scoped tenant-owned access. The only confirmed gap is `EntraGroupResource::shouldRegisterNavigation()` returning false for every admin context.
|
||||
- **Bounded deviation / spread control**: No direct admin route exception is retained; the explicit `Groups` navigation item is a local environment-bound admin entry and not a new Directory suite.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: no new behavior.
|
||||
- **Shared OperationRun UX contract/layer reused**: existing directory group sync, if still visible, remains on the current provider-operation start path.
|
||||
- **Delegated start/completion UX behaviors**: N/A for new behavior.
|
||||
- **Local surface-owned behavior that remains**: existing Groups list header actions remain unchanged unless a focused test proves they violate this spec's no-new-action contract.
|
||||
- **Queued DB-notification policy**: N/A.
|
||||
- **Terminal notification path**: N/A.
|
||||
- **Exception required?**: none.
|
||||
|
||||
## Provider Boundary / Platform Core
|
||||
|
||||
- **Shared provider/platform boundary touched?**: yes, bounded to labels/navigation/search around one existing provider-owned resource.
|
||||
- **Classification**: mixed.
|
||||
- **Provider-owned seams**: Entra Group source object, provider IDs, group type labels, Microsoft Entra source wording.
|
||||
- **Platform-core seams**: workspace/environment context, navigation visibility, RBAC, search destination safety.
|
||||
- **Neutral terms preserved**: Workspace, Managed Environment, Directory Groups, Directory inventory.
|
||||
- **Why no accidental provider deepening**: No new identity model, Graph contract, provider framework, capability registry, persisted taxonomy, or route family is introduced.
|
||||
|
||||
## Technical Approach
|
||||
|
||||
1. Confirm current repo posture from Spec 302 and the existing `EntraGroupResource` implementation.
|
||||
2. Update tests first so they encode the new product decision:
|
||||
- workspace-home sidebar stays clean
|
||||
- environment context shows Groups
|
||||
- no-context list/search denies or returns no results
|
||||
- cross-environment and cross-workspace access denies as not found
|
||||
- global search is environment-scoped
|
||||
- search URLs point to canonical admin View destinations and never `/admin/t`
|
||||
3. Replace the blanket admin-hidden `shouldRegisterNavigation()` rule with the shared environment navigation rule.
|
||||
4. Verify whether `EntraGroupResource` can safely adopt `WorkspaceScopedTenantRoutes`.
|
||||
- Prefer adopting it if focused route/search/reference tests stay green.
|
||||
- If the rendered sidebar does not receive the resource auto-navigation item, add a bounded explicit `Groups` navigation item in the admin panel contract.
|
||||
5. Preserve existing list/detail/search server-side scope helpers.
|
||||
6. Update customer/operator copy only where needed to remove stale "tenant" wording and avoid M365 Admin mirror implications.
|
||||
7. Run focused validation commands and formatting checks.
|
||||
|
||||
## Existing Repository Surfaces Likely Affected
|
||||
|
||||
- `apps/platform/app/Filament/Resources/EntraGroupResource.php`
|
||||
- `apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php`
|
||||
- `apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ViewEntraGroup.php`
|
||||
- `apps/platform/app/Support/Navigation/NavigationScope.php` only if a minimal helper adjustment is required; no contract split.
|
||||
- `apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/EntraGroupAdminScopeTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/EntraGroupGlobalSearchScopeTest.php`
|
||||
- `apps/platform/tests/Feature/DirectoryGroups/BrowseGroupsTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/EntraGroupEnterpriseDetailPageTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/EntraGroupResolvedReferencePresentationTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/PolicyVersionResolvedReferenceLinksTest.php`
|
||||
|
||||
## Domain / Model Implications
|
||||
|
||||
- No new domain model.
|
||||
- No new persisted entity or table.
|
||||
- No new status, enum, reason family, or taxonomy.
|
||||
- `EntraGroup` remains provider-owned directory cache data scoped to one Managed Environment.
|
||||
- Workspace ownership remains derived through the Managed Environment relationship and existing entitlement checks.
|
||||
|
||||
## UI / Filament Implications
|
||||
|
||||
- Filament v5 targets Livewire v4.0+; this repo currently uses Livewire 4.1.4.
|
||||
- `shouldRegisterNavigation()` is navigation only, not authorization. Resource/page authorization remains server-side.
|
||||
- `EntraGroupResource` already has a View page, satisfying Filament global-search destination eligibility.
|
||||
- Global-search URL generation must remain explicit because this resource has environment-sensitive destinations.
|
||||
- Destructive actions are not added. Existing URL-only actions must not be treated as confirmation-protected mutations.
|
||||
- No assets are added; deploy-time `cd apps/platform && php artisan filament:assets` posture is unchanged.
|
||||
|
||||
## RBAC / Policy Implications
|
||||
|
||||
- Navigation visibility requires active environment context, but that is not the authorization boundary.
|
||||
- Existing list/detail access must continue through workspace membership, environment access, and resource policy/capability checks.
|
||||
- Non-members must receive deny-as-not-found for environment-owned data.
|
||||
- Members without capability must receive the existing capability denial outcome after membership is established.
|
||||
- Global search must not bypass `canAccessTenant()` or resource policies.
|
||||
|
||||
## Global Search Plan
|
||||
|
||||
- Keep `ScopesGlobalSearchToTenant` as the primary query scope.
|
||||
- Keep or adjust `getGlobalSearchResultUrl()` so result URLs use the active/canonical admin View destination for the record's Managed Environment.
|
||||
- Add explicit assertions that:
|
||||
- no active environment produces no results
|
||||
- active environment returns only matching local groups
|
||||
- cross-workspace groups are absent
|
||||
- result URLs do not contain `/admin/t`
|
||||
- result URLs resolve to View destinations, not legacy tenant-panel routes
|
||||
|
||||
## Data / Migration Implications
|
||||
|
||||
- No migrations.
|
||||
- No seed changes.
|
||||
- No new indexes.
|
||||
- No data backfill.
|
||||
- No Graph contract or adapter changes.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- Use Pest feature tests plus one focused Pest Browser smoke for the rendered sidebar navigation and View drilldown.
|
||||
- Update `PanelNavigationSegregationTest` so Entra Groups is no longer protected by the blanket admin-hidden dataset and is instead tested as environment-visible and workspace-home-hidden.
|
||||
- Update `BrowseGroupsTest` stale hidden navigation assertion.
|
||||
- Extend `EntraGroupAdminScopeTest` for no-context, cross-environment, cross-workspace, and canonical destination behavior.
|
||||
- Extend `EntraGroupGlobalSearchScopeTest` for search scoping and URL canonicality.
|
||||
- Run the user-specified focused suite plus Directory Groups, inventory, governance artifact, and operation legacy-route regression tests.
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before implementation. Re-check after design and implementation.*
|
||||
|
||||
- Inventory-first: no Inventory or snapshot truth changes.
|
||||
- Read/write separation: read-oriented Groups surface; no new write/change function.
|
||||
- Single Graph contract path: no Graph calls or contract changes.
|
||||
- Deterministic capabilities: no capability resolver changes.
|
||||
- Proportionality: no new structure or abstraction; bounded repair.
|
||||
- No premature abstraction: no Directory framework or Identity Center.
|
||||
- First provider is not platform core: Microsoft Entra semantics remain provider-owned and bounded.
|
||||
- Persisted truth: no new persistence.
|
||||
- State: no new state/status family.
|
||||
- Shared pattern first: reuse existing navigation/context/search helpers.
|
||||
- Workspace isolation: workspace membership remains an isolation boundary.
|
||||
- Tenant/environment isolation: Managed Environment access remains required for list/detail/search.
|
||||
- RBAC-UX: UI visibility is not authorization; server-side checks remain mandatory.
|
||||
- Test governance: focused feature tests, one explicit browser smoke for rendered navigation, low helper cost, no hidden heavy family.
|
||||
- Filament native first: native resource navigation/table/View/search; no custom UI.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
- **Test purpose / classification by changed surface**: Feature plus focused Browser smoke for rendered navigation.
|
||||
- **Affected validation lanes**: confidence and formatting/diff-check.
|
||||
- **Why this lane mix is the narrowest sufficient proof**: The behavior is Filament resource navigation, scoped resource access, and global search URL generation; focused feature tests prove backend/context/search behavior, while one browser smoke proves the actual rendered sidebar link and row drilldown.
|
||||
- **Narrowest proving command(s)**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/PanelNavigationSegregationTest.php tests/Feature/Filament/AdminTenantSurfaceParityTest.php tests/Feature/Filament/AdminSharedSurfacePanelParityTest.php tests/Feature/Filament/TenantOwnedResourceScopeParityTest.php tests/Feature/Filament/EntraGroupAdminScopeTest.php tests/Feature/Filament/EntraGroupGlobalSearchScopeTest.php tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/DirectoryGroups/BrowseGroupsTest.php tests/Feature/Filament/EntraGroupEnterpriseDetailPageTest.php tests/Feature/Filament/EntraGroupResolvedReferencePresentationTest.php tests/Feature/Filament/PolicyVersionResolvedReferenceLinksTest.php tests/Browser/Spec303AdminDirectoryGroupsCutoverSmokeTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php tests/Feature/Filament/InventoryHubDbOnlyTest.php tests/Feature/Filament/InventoryPagesTest.php tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactAdminPanelRegistrationTest.php tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactEnvironmentContextTest.php tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactLegacyTenantPanelGuardTest.php tests/Feature/Operations/LegacyRunRoutesNotFoundTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||
- `git diff --check`
|
||||
- **Fixture / helper / factory / seed / context cost risks**: low; reuse existing helpers.
|
||||
- **Expensive defaults or shared helper growth introduced?**: no.
|
||||
- **Heavy-family additions, promotions, or visibility changes**: one focused browser smoke was added after implementation revealed that resource-level feature assertions did not prove the rendered sidebar link.
|
||||
- **Surface-class relief / special coverage rule**: standard-native-filament relief.
|
||||
- **Closing validation and reviewer handoff**: verify the browser lane remains explicit and focused, no workspace-home leakage occurs, and no new mutation action is introduced.
|
||||
- **Budget / baseline / trend follow-up**: none expected.
|
||||
- **Review-stop questions**: Did Groups leak onto workspace home? Did any search result cross environment/workspace? Did any URL contain `/admin/t`? Did implementation add a new action or route alias?
|
||||
- **Escalation path**: follow-up-spec for broader navigation-contract split.
|
||||
- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage.
|
||||
- **Why no dedicated follow-up spec is needed now**: This spec resolves the only confirmed blocker; broader contract split remains conditional.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/303-admin-directory-groups-cutover/
|
||||
+-- checklists/
|
||||
| +-- requirements.md
|
||||
+-- plan.md
|
||||
+-- spec.md
|
||||
+-- tasks.md
|
||||
```
|
||||
|
||||
### Source Code (likely implementation surfaces)
|
||||
|
||||
```text
|
||||
apps/platform/app/Filament/Resources/EntraGroupResource.php
|
||||
apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php
|
||||
apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ViewEntraGroup.php
|
||||
apps/platform/app/Support/Navigation/NavigationScope.php
|
||||
apps/platform/tests/Feature/Filament/
|
||||
apps/platform/tests/Feature/DirectoryGroups/
|
||||
```
|
||||
|
||||
**Structure Decision**: Implement in the existing Filament resource and focused tests. Do not create new base folders.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|---|---|---|
|
||||
| None | N/A | N/A |
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1 - Preparation and Test Contract
|
||||
|
||||
Read Spec 301, Spec 302, this package, and the current Entra Groups resource/tests. Update tests to encode the new environment-bound contract before changing runtime code.
|
||||
|
||||
### Phase 2 - Navigation Contract
|
||||
|
||||
Change `EntraGroupResource::shouldRegisterNavigation()` to use the shared environment-navigation decision, preserving workspace-home absence.
|
||||
|
||||
### Phase 3 - Route and Search Destination Safety
|
||||
|
||||
Adopt `WorkspaceScopedTenantRoutes` for Entra Groups and prove scoped canonical View URLs with no `/admin/t` links.
|
||||
|
||||
### Phase 4 - Scoped Access and Copy
|
||||
|
||||
Preserve list/detail/search scoping. Adjust only minimal stale copy such as empty-state wording if needed to say "managed environment" rather than "tenant".
|
||||
|
||||
### Phase 5 - Regression Validation
|
||||
|
||||
Run focused suites for Entra Groups, tenant-owned surface parity, Inventory, Policy, governance artifacts, and operation routing. Run Pint and `git diff --check`.
|
||||
|
||||
## Rollout Considerations
|
||||
|
||||
- No environment variables.
|
||||
- No database migrations.
|
||||
- No queues or scheduled workers are changed.
|
||||
- No storage or volume changes.
|
||||
- No Dokploy runtime configuration change.
|
||||
- No asset registration; `filament:assets` deployment requirement is unchanged.
|
||||
- Staging validation should focus on sidebar visibility and scoped Groups search in a seeded/test workspace and environment.
|
||||
|
||||
## Risk Controls
|
||||
|
||||
- Do not revive `/admin/t/{tenant}` or `/admin/tenants/{tenant}`.
|
||||
- Do not add compatibility aliases.
|
||||
- Do not add group create/edit/delete/membership actions.
|
||||
- Do not introduce an Identity Center or Directory suite.
|
||||
- Do not split navigation contracts beyond minimal test reshaping needed for Groups.
|
||||
- Do not touch tenant-panel dead code in this spec.
|
||||
- If broader route side effects appear, stop and move them to a separate route/navigation-contract spec.
|
||||
289
specs/303-admin-directory-groups-cutover/spec.md
Normal file
289
specs/303-admin-directory-groups-cutover/spec.md
Normal file
@ -0,0 +1,289 @@
|
||||
# Feature Specification: Admin Directory Groups Cutover
|
||||
|
||||
**Feature Branch**: `303-admin-directory-groups-cutover`
|
||||
**Created**: 2026-05-14
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Erstelle die naechste Spec als 303-admin-directory-groups-cutover. Entscheide und implementiere den admin role contract fuer EntraGroupResource als environment-bound Directory/Identity Surface im workspace-first Admin Runtime Modell."
|
||||
|
||||
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: `EntraGroupResource` is already a scoped admin runtime surface with list, detail, and global-search behavior, but its navigation contract still preserves an older blanket admin-hidden assumption. Spec 302 identified this as the only real blocker for the next runtime repair.
|
||||
- **Today's failure**: Operators can reach directory groups through direct/scoped admin paths and global search, while admin navigation still says Groups are never an admin surface. Current tests also protect that stale hidden assumption.
|
||||
- **User-visible improvement**: Operators who are inside an active Workspace plus Managed Environment context can discover read-oriented Directory Groups consistently, while the workspace-home sidebar stays clean.
|
||||
- **Smallest enterprise-capable version**: Make Entra Groups a secondary environment-bound Directory/Identity surface. Keep list/detail/global search scoped to the active Managed Environment, ensure search destinations use canonical admin View URLs, and update stale navigation/search tests.
|
||||
- **Explicit non-goals**: No generic M365 Admin mirror, no broad Identity Center, no group lifecycle management, no group membership mutation workflow, no new admin actions, no tenant-panel dead-code cleanup, no navigation-contract-split except minimal test adjustment, no migrations, no assets.
|
||||
- **Permanent complexity imported**: No new models, tables, statuses, enums, services, registries, provider adapters, or UI frameworks. The permanent cost is a documented admin role contract plus focused navigation/search/scope tests.
|
||||
- **Why now**: `docs/product/spec-candidates.md` sequences `admin-directory-groups-cutover` directly after Spec 301 and Spec 302. Spec 302's audit matrix names Entra Groups product IA as the only blocker before the next runtime repair.
|
||||
- **Why not local**: A local one-line navigation flip would risk workspace-home leakage or unscoped search/detail destinations. The contract must distinguish workspace-home cleanliness from environment-bound Directory visibility and keep server-side RBAC/context checks authoritative.
|
||||
- **Approval class**: Workflow Compression
|
||||
- **Red flags triggered**: One provider-facing surface is touched, but it is bounded as provider-owned Directory inventory and does not introduce a platform-core identity framework.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 1 | **Gesamt: 11/12**
|
||||
- **Decision**: approve
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: canonical-view
|
||||
- **Primary Routes**:
|
||||
- Entra Groups collection route in the admin panel, resolved only with an active Managed Environment context.
|
||||
- Entra Groups View route in the admin panel, resolved only for records in the active Managed Environment context.
|
||||
- Workspace-home route `/admin/workspaces/{workspace}` as the negative-control surface.
|
||||
- Canonical environment route `/admin/workspaces/{workspace}/environments/{environment}` as the positive-control surface.
|
||||
- **Data Ownership**: Existing `EntraGroup` rows remain managed-environment-owned directory cache records. This spec does not change tables, columns, Graph payload storage, or snapshot truth.
|
||||
- **RBAC**: Workspace membership and Managed Environment access are required for visibility and access. Existing server-side policy/capability checks remain the authority for list, detail, search, and any existing operation-start action. UI visibility is never authorization.
|
||||
|
||||
For canonical-view specs, the spec MUST define:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: The Groups list and global search are scoped to the active Managed Environment resolved by the workspace-first admin shell. If no Managed Environment context exists, list access must deny as not found and search must return no unsafe records.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: Direct URL manipulation, stale remembered context, cross-workspace records, and cross-environment records must not reveal data. Non-members remain deny-as-not-found; members without the relevant capability remain denied by existing policy/capability semantics.
|
||||
|
||||
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
|
||||
|
||||
- **Cross-cutting feature?**: yes
|
||||
- **Interaction class(es)**: Filament resource navigation, environment-bound sidebar visibility, global-search result destinations, read-only resource list/detail access.
|
||||
- **Systems touched**:
|
||||
- `apps/platform/app/Filament/Resources/EntraGroupResource.php`
|
||||
- `apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php`
|
||||
- `apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ViewEntraGroup.php`
|
||||
- `apps/platform/app/Filament/Concerns/ScopesGlobalSearchToTenant.php`
|
||||
- `apps/platform/app/Filament/Concerns/ResolvesPanelTenantContext.php`
|
||||
- `apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php`
|
||||
- `apps/platform/app/Providers/Filament/AdminPanelProvider.php`
|
||||
- `apps/platform/app/Support/Navigation/NavigationScope.php`
|
||||
- `apps/platform/app/Support/OperateHub/OperateHubShell.php`
|
||||
- focused tests under `apps/platform/tests/Feature/Filament/` and `apps/platform/tests/Feature/DirectoryGroups/`
|
||||
- **Existing pattern(s) to extend**: `NavigationScope::shouldRegisterEnvironmentNavigation()`, `OperateHubShell` environment context resolution, `ScopesGlobalSearchToTenant`, `ResolvesPanelTenantContext`, `InteractsWithTenantOwnedRecords`, and the existing canonical admin route/link helpers.
|
||||
- **Shared contract / presenter / builder / renderer to reuse**: Existing navigation/context/search traits and helpers. No new Directory navigation framework is introduced.
|
||||
- **Why the existing shared path is sufficient or insufficient**: The shared paths already solve environment detection, scoped query/search, and tenant-owned record resolution. They are insufficient only where `EntraGroupResource::shouldRegisterNavigation()` still returns false for every admin context.
|
||||
- **Allowed deviation and why**: No route-posture deviation is applied. `EntraGroupResource` adopts `WorkspaceScopedTenantRoutes`; the rendered sidebar link is registered explicitly in `AdminPanelProvider` because the runtime navigation contract uses explicit environment-bound entries for operator-facing surface links.
|
||||
- **Consistency impact**: Directory Groups must follow the same workspace-home clean-sidebar rule and environment-bound visibility rule as migrated tenant-owned surfaces, without becoming a broader Identity Center.
|
||||
- **Review focus**: Reviewers must verify the implementation removes only the stale Groups admin-hidden assumption, does not reopen workspace-home tenant-owned navigation, keeps search scoped, and does not introduce group mutation workflows.
|
||||
|
||||
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: no new OperationRun behavior.
|
||||
- **Shared OperationRun UX contract/layer reused**: Existing directory group sync behavior, if still present, remains outside this spec's new behavior and must continue using existing operation-start, notification, and link helpers.
|
||||
- **Delegated start/completion UX behaviors**: N/A for new behavior.
|
||||
- **Local surface-owned behavior that remains**: Existing Groups list operation/header actions may remain only as pre-existing behavior. This spec must not add, promote, or redesign mutation/admin actions.
|
||||
- **Queued DB-notification policy**: N/A.
|
||||
- **Terminal notification path**: N/A.
|
||||
- **Exception required?**: none.
|
||||
|
||||
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
|
||||
|
||||
- **Shared provider/platform boundary touched?**: yes, bounded to operator vocabulary and resource navigation.
|
||||
- **Boundary classification**: mixed, with provider-owned Directory data inside platform-owned workspace/environment context.
|
||||
- **Seams affected**: navigation label, resource label, global-search destination URL, empty-state copy if adjusted, tests that name the surface.
|
||||
- **Neutral platform terms preserved or introduced**: Workspace, Managed Environment, Directory Groups, Directory inventory, environment-bound surface.
|
||||
- **Provider-specific semantics retained and why**: "Microsoft Entra" and "Entra Group" remain where accuracy is required for the source object and provider cache. They must not become a broad platform-core identity framework.
|
||||
- **Why this does not deepen provider coupling accidentally**: The implementation is limited to one existing provider-owned resource and does not add shared identity abstractions, provider capability registries, tables, or Graph adapters.
|
||||
- **Follow-up path**: `navigation-contract-split` remains conditional after this spec; broader Directory/Admin Roles IA remains out of scope unless separately promoted.
|
||||
|
||||
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
|
||||
|
||||
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Workspace-home sidebar | yes, negative control | Native Filament navigation | navigation | shell, route context | no | Must remain clean; no Groups entry without active environment context |
|
||||
| Environment-bound admin sidebar Directory Groups entry | yes | Native Filament resource navigation | navigation | shell, route context | no | Secondary Directory surface visible only in active Managed Environment context |
|
||||
| Entra Groups list page | yes, contract clarification | Native Filament resource table | resource list, empty state | page, table, tenant-owned query | no | Read-oriented list remains scoped; no new actions |
|
||||
| Entra Group View page | yes, destination contract | Native Filament View page/infolist | resource detail, global-search destination | page, record resolution | no | Scoped View destination must be canonical and not legacy `/admin/t` |
|
||||
| Global search result | yes | Native Filament global search | search result link | shell, query, URL | no | Results remain environment-scoped and point to View page |
|
||||
|
||||
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Environment-bound Directory Groups navigation | Secondary Context | Operator needs to inspect directory group inventory for the selected environment | `Directory` group and `Groups` item | List and View pages | Secondary because it helps inspect context, not operate a full identity control plane | Workspace -> Managed Environment -> Directory Groups | Avoids direct URL knowledge without adding a broad Identity Center |
|
||||
| Groups list | Secondary Context | Operator checks known groups for diagnostics, governance, restore mapping, or supportability | Group name, type, source/freshness signals already in model | View page diagnostics and existing enterprise detail sections | Not primary because it does not decide remediation by itself | Read-oriented directory inventory | Keeps Directory visibility separate from governance decision surfaces |
|
||||
| Group View | Tertiary Evidence / Diagnostics | Operator inspects one group safely | Group identity and source details | Existing diagnostics/detail layout | Detail supports evidence and troubleshooting, not lifecycle management | Scoped inspection | Reduces cross-page reconstruction from search or references |
|
||||
| Workspace-home sidebar | Secondary Context | Operator is at workspace level without an environment selected | Workspace-owned entries only | N/A | Negative-control surface | Workspace overview IA | Prevents environment-owned noise |
|
||||
|
||||
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Directory Groups navigation | operator-MSP | Groups entry under Directory only in environment context | None in navigation | None in navigation | Open Groups | Hidden on workspace home and without environment context | Navigation only links to the list; it does not repeat status summaries |
|
||||
| Groups list | operator-MSP, support-platform by capability | Known groups for current environment, type, last seen/freshness where available | Provider IDs and less common columns stay toggleable or detail-only | Raw payloads stay out of default list | Open group | Mutations, raw provider data, and cross-environment data | List scope is the truth; no duplicated environment selector |
|
||||
| Group View | operator-MSP, support-platform by capability | Safe group identity summary | Existing enterprise detail diagnostics | Support/raw detail only where already gated by existing detail patterns | Inspect group | Mutation actions and cross-environment data | View adds evidence instead of restating list filters |
|
||||
|
||||
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Directory Groups sidebar entry | Navigation / Sidebar | Resource navigation | Open Groups | Sidebar item | N/A | Existing page actions only | N/A | Admin Groups collection in active environment context | N/A | Active workspace and environment shell | Groups | Entry exists only when context is active | none |
|
||||
| Groups list | List / Table | Read-oriented resource list | Open a group | Clickable row or identifier link | allowed if existing pattern remains | Existing safe secondary actions only | N/A | Admin Groups collection in active environment context | Admin Groups View route for record | Active environment route/session context | Groups | Group name, type, freshness/source signals | none |
|
||||
| Group View | Detail / Evidence | Read-only View page | Inspect group | View page | N/A | Existing safe links in header/detail only | N/A | N/A | Admin Groups View route for record | Active environment and record ownership | Group | Group identity and source context | none |
|
||||
| Global search result | Search / Navigation | Scoped result link | Open matching group | Search result URL | N/A | N/A | N/A | N/A | Admin Groups View route for record | Active environment context | Group | Matching group title | none |
|
||||
|
||||
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Directory Groups navigation | Tenant operator | Continue into directory group inventory for the selected environment | Navigation | Where can I inspect groups for this environment? | Directory / Groups navigation label | None | Environment context only | Navigation only | Open Groups | none |
|
||||
| Groups list | Tenant operator, support operator | Determine whether a group is known and inspect it | Read-oriented table | Which groups are known for this managed environment? | Name, type, last seen/source cues already available | Provider IDs, raw/source detail, diagnostics | freshness/source availability, provider type | Existing behavior only; no new actions | Open Group | none added |
|
||||
| Group View | Tenant operator, support operator | Inspect one group safely | View page | What is this group and what source context does it belong to? | Group identity and readable metadata | Raw/provider diagnostics if already present/gated | source, freshness, provider type | Read-only inspection | Return/list navigation, existing safe links | none added |
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: no
|
||||
- **New persisted entity/table/artifact?**: no
|
||||
- **New abstraction?**: no
|
||||
- **New enum/state/reason family?**: no
|
||||
- **New cross-domain UI framework/taxonomy?**: no
|
||||
- **Current operator problem**: Groups are scoped and reachable but undiscoverable in the correct environment-bound admin context.
|
||||
- **Existing structure is insufficient because**: The resource bypasses the shared environment-navigation decision with a blanket admin-panel hidden check.
|
||||
- **Narrowest correct implementation**: Update `EntraGroupResource` and focused tests to express the environment-bound Directory Groups contract while preserving scoped access/search.
|
||||
- **Ownership cost**: Focused navigation, access, and global-search tests. No new architecture ownership.
|
||||
- **Alternative intentionally rejected**: A broad Identity Center or M365 admin mirror is rejected because the current product need is only read-oriented Directory Groups visibility inside the selected environment.
|
||||
- **Release truth**: Current-release runtime repair.
|
||||
|
||||
### Compatibility posture
|
||||
|
||||
This feature assumes a pre-production environment.
|
||||
|
||||
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec.
|
||||
|
||||
Canonical replacement is preferred over preservation. Legacy `/admin/t` routes must not be revived.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Feature
|
||||
- **Validation lane(s)**: confidence
|
||||
- **Why this classification and these lanes are sufficient**: Feature tests can prove navigation registration, sidebar cleanliness/presence in response HTML, list/detail scope, global-search scoping, canonical result URLs, no-context denial, cross-environment denial, and no legacy route revival. One focused Browser smoke is required for the real rendered Filament sidebar link and Groups View drilldown.
|
||||
- **New or expanded test families**: Focused updates to existing Filament and Directory Groups feature tests plus one explicit Spec 303 Browser smoke. No new heavy-governance family.
|
||||
- **Fixture / helper cost impact**: Low. Reuse `createUserWithTenant`, `ManagedEnvironment` factories, `WorkspaceContext`, `ManagedEnvironmentLinks`, and current EntraGroup factories.
|
||||
- **Heavy-family visibility / justification**: none.
|
||||
- **Special surface test profile**: standard-native-filament
|
||||
- **Standard-native relief or required special coverage**: Native Filament resource/navigation/search behavior; use feature tests for backend/context/search truth and one Browser smoke for rendered navigation.
|
||||
- **Reviewer handoff**: Confirm lane fit, workspace-home negative controls, environment-bound positive controls, search URL canonicality, no `/admin/t`, and no new mutation actions.
|
||||
- **Budget / baseline / trend impact**: Low; focused feature tests plus one narrow Browser smoke.
|
||||
- **Escalation needed**: document-in-feature
|
||||
- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage
|
||||
- **Planned validation commands**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/PanelNavigationSegregationTest.php tests/Feature/Filament/AdminTenantSurfaceParityTest.php tests/Feature/Filament/AdminSharedSurfacePanelParityTest.php tests/Feature/Filament/TenantOwnedResourceScopeParityTest.php tests/Feature/Filament/EntraGroupAdminScopeTest.php tests/Feature/Filament/EntraGroupGlobalSearchScopeTest.php tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/DirectoryGroups/BrowseGroupsTest.php tests/Feature/Filament/EntraGroupEnterpriseDetailPageTest.php tests/Feature/Filament/EntraGroupResolvedReferencePresentationTest.php tests/Feature/Filament/PolicyVersionResolvedReferenceLinksTest.php tests/Browser/Spec303AdminDirectoryGroupsCutoverSmokeTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php tests/Feature/Filament/InventoryHubDbOnlyTest.php tests/Feature/Filament/InventoryPagesTest.php tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactAdminPanelRegistrationTest.php tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactEnvironmentContextTest.php tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactLegacyTenantPanelGuardTest.php tests/Feature/Operations/LegacyRunRoutesNotFoundTest.php`
|
||||
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||
- `git diff --check`
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Groups are visible inside an active environment context (Priority: P1)
|
||||
|
||||
As a tenant operator working inside a selected Managed Environment, I need a secondary Directory Groups entry so I can inspect known directory groups without knowing a direct URL.
|
||||
|
||||
**Why this priority**: This resolves the exact stale navigation contract identified by Spec 302.
|
||||
|
||||
**Independent Test**: Render or evaluate the canonical environment-bound admin context and assert `EntraGroupResource` registers navigation while existing authorization checks still protect access.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** an authenticated user with workspace and environment membership, **When** the canonical environment route is active, **Then** Groups appears as an environment-bound Directory surface.
|
||||
2. **Given** the same user on the Groups list, **When** records exist in the active environment and another environment, **Then** only active-environment groups are listed.
|
||||
3. **Given** the environment context is missing, **When** the admin Groups list is requested, **Then** the response is deny-as-not-found and no groups are leaked.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Workspace home remains clean (Priority: P2)
|
||||
|
||||
As an operator on the workspace home page, I need the sidebar to stay workspace-focused, even if the system remembers my last environment, so I do not confuse environment-owned Directory inventory with workspace-wide state.
|
||||
|
||||
**Why this priority**: The workspace-home clean-sidebar rule is intentional and must survive the Groups cutover.
|
||||
|
||||
**Independent Test**: Open the workspace home route with a remembered Managed Environment in session and assert Groups/Directory Groups are absent.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a workspace-home route and remembered environment context, **When** the sidebar renders, **Then** Groups is absent.
|
||||
2. **Given** no active Managed Environment context, **When** `EntraGroupResource::shouldRegisterNavigation()` is evaluated for the admin panel, **Then** it returns false.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Global search stays scoped and lands on valid View pages (Priority: P3)
|
||||
|
||||
As an operator using global search, I need group results to only include the active environment and open the correct View page, so search cannot bypass context or send me to retired routes.
|
||||
|
||||
**Why this priority**: Global search is already enabled for `EntraGroupResource`; its safety depends on scoped queries and truthful destinations.
|
||||
|
||||
**Independent Test**: Seed matching groups in multiple workspaces/environments, run admin global search with and without active context, and assert results plus URLs.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** no active Managed Environment context, **When** admin global search runs for a matching group, **Then** no group results are returned.
|
||||
2. **Given** matching groups in two environments, **When** admin global search runs with environment A active, **Then** only environment A's group appears.
|
||||
3. **Given** a matching result, **When** its URL is inspected, **Then** it points to the canonical admin View destination for that group and does not contain `/admin/t`.
|
||||
4. **Given** a user manipulates a View URL for a group in another workspace or environment, **When** the request is made, **Then** access is denied as not found.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- Remembered environment belongs to a different workspace than the current workspace session.
|
||||
- Current user is a workspace member but not an environment member.
|
||||
- Current user is an environment member but lacks the capability required by existing list/detail policy.
|
||||
- A global-search result is generated from a record whose relation is not eager-loaded.
|
||||
- A stale URL references a retired `/admin/t/{tenant}` path.
|
||||
- A stale direct `/admin/entra-groups` URL is requested after cutover.
|
||||
- Existing directory sync action remains present; the implementation must not create or promote new mutation/admin actions.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
- **FR-001**: `EntraGroupResource` MUST have an explicit admin contract as a secondary environment-bound Directory/Identity surface.
|
||||
- **FR-002**: Entra Groups MUST NOT appear in workspace-home sidebar navigation when no active Managed Environment context exists.
|
||||
- **FR-003**: Entra Groups MUST appear in the environment-bound admin context when the user has required workspace and environment access.
|
||||
- **FR-004**: The Groups list MUST only show groups for the active Managed Environment.
|
||||
- **FR-005**: The Group View page MUST deny access to records outside the active Managed Environment or workspace.
|
||||
- **FR-006**: Admin global search MUST return no Entra Group records without an active authorized Managed Environment context.
|
||||
- **FR-007**: Admin global search MUST remain scoped to the active Managed Environment and MUST NOT leak cross-workspace or cross-environment records.
|
||||
- **FR-008**: Admin global-search result URLs MUST point to valid View destinations and MUST NOT use retired `/admin/t` or tenant-panel routes.
|
||||
- **FR-009**: Existing RBAC, policy, and capability enforcement MUST remain server-side and MUST NOT rely on navigation visibility.
|
||||
- **FR-010**: This spec MUST NOT introduce create, edit, delete, membership mutation, sync mutation, destructive group actions, or a broad Directory/Identity Center.
|
||||
- **FR-011**: Existing Inventory, Policy, PolicyVersion, BackupSchedule, BackupSet, RestoreRun, Finding, EvidenceSnapshot, EnvironmentReview, StoredReport, and OperationRun routing/navigation behavior MUST NOT regress.
|
||||
- **FR-012**: Filament v5 and Livewire v4 patterns MUST be preserved. No Livewire v3 or legacy Filament APIs may be introduced.
|
||||
- **FR-013**: Provider registration MUST remain unchanged in `apps/platform/bootstrap/providers.php`.
|
||||
- **FR-014**: No assets may be registered or changed by this spec.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- **NFR-001**: The implementation MUST remain a narrow runtime repair and avoid new abstractions unless repo evidence proves the existing shared helpers cannot express the contract.
|
||||
- **NFR-002**: Tests MUST distinguish workspace-home sidebar cleanliness from environment-bound navigation visibility.
|
||||
- **NFR-003**: Test fixture and helper cost MUST stay low and reuse existing workspace/environment helpers.
|
||||
- **NFR-004**: UI copy MUST avoid implying TenantPilot is a full Microsoft 365 or Entra administration center.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Entra Groups has a written admin role contract as an environment-bound Directory/Identity surface.
|
||||
- Workspace-home sidebar remains clean with and without remembered environment context.
|
||||
- Environment-bound admin context can consistently reach Groups when RBAC/context allows.
|
||||
- List and View access remain scoped and deny no-context, cross-environment, and cross-workspace access.
|
||||
- Global search remains scoped and opens valid View destinations.
|
||||
- Search and related links do not point to legacy `/admin/t` routes.
|
||||
- Old blanket admin-hidden tests no longer protect Entra Groups incorrectly.
|
||||
- Existing focused Inventory, Policy, Evidence, Review, and Operation routing tests remain green.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Focused validation commands pass.
|
||||
- No application code outside the scoped Entra Groups/navigation/search/tests surface is modified.
|
||||
- No migrations, assets, provider registration, Graph adapters, jobs, or tenant-panel retirement changes are introduced.
|
||||
- Reviewers can tell from `spec.md`, `plan.md`, and `tasks.md` why Groups is visible only in environment context and why this is not an M365 Admin mirror.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Spec 301 is completed and Inventory is the reference for environment-bound navigation without workspace-home leakage.
|
||||
- Spec 302 is completed and its audit matrix is authoritative for Entra Groups as the next blocker.
|
||||
- Existing Entra Group list/detail/global-search scoping is mostly correct and should be preserved unless tests reveal a narrow bug.
|
||||
- Existing directory sync operation behavior is outside this spec unless a test must prove no new action was added.
|
||||
|
||||
## Risks
|
||||
|
||||
- Adding `WorkspaceScopedTenantRoutes` to `EntraGroupResource` affects direct admin URLs and related links, so focused route, search, reference-link, and browser-smoke tests must prove the canonical workspace/environment path.
|
||||
- Navigation labels such as `Directory` could be mistaken for a larger Identity Center if copy and scope are not restrained.
|
||||
- Existing tests may encode the blanket hidden assumption in more than one file.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None blocking. Route posture was verified during implementation; the narrow safe change is to use workspace/environment resource routes and keep scoped canonical View destinations away from `/admin/t`.
|
||||
|
||||
## Follow-up Spec Candidates
|
||||
|
||||
- `navigation-contract-split`, only if workspace-home and environment-bound assertions still collide after this cutover.
|
||||
- `tenant-panel-dead-code-retirement`, only after active route/navigation dependencies no longer rely on retired tenant-panel assumptions.
|
||||
- A broader Directory/Admin Roles IA spec only if future roadmap work explicitly promotes a Directory suite beyond read-oriented Groups.
|
||||
121
specs/303-admin-directory-groups-cutover/tasks.md
Normal file
121
specs/303-admin-directory-groups-cutover/tasks.md
Normal file
@ -0,0 +1,121 @@
|
||||
# Tasks: Admin Directory Groups Cutover
|
||||
|
||||
**Input**: Design documents from `/specs/303-admin-directory-groups-cutover/`
|
||||
**Prerequisites**: `spec.md`, `plan.md`, `checklists/requirements.md`
|
||||
|
||||
**Tests**: Tests are required. This is a runtime behavior change for Filament navigation, scoped access, and global-search destinations.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
|
||||
- [x] New or changed tests stay in the smallest honest family; one focused browser smoke was added because rendered Filament sidebar navigation was the changed behavior and feature tests did not prove the real sidebar link.
|
||||
- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default.
|
||||
- [x] Planned validation commands cover the change without pulling unrelated lane cost.
|
||||
- [x] The declared surface test profile is `standard-native-filament` plus focused browser-smoke verification for rendered navigation.
|
||||
- [x] Any material budget, baseline, trend, or escalation note is recorded in the active spec or PR.
|
||||
|
||||
## Phase 1: Preparation and Scope Lock
|
||||
|
||||
**Purpose**: Confirm repo truth and keep implementation limited to Entra Groups.
|
||||
|
||||
- [x] T001 Review `specs/303-admin-directory-groups-cutover/spec.md`, `specs/303-admin-directory-groups-cutover/plan.md`, `specs/303-admin-directory-groups-cutover/tasks.md`, and `specs/303-admin-directory-groups-cutover/checklists/requirements.md`.
|
||||
- [x] T002 Review completed context in `specs/301-admin-inventory-navigation-cutover/` and `specs/302-tenant-owned-surface-route-audit/` without modifying those completed specs.
|
||||
- [x] T003 Inspect `apps/platform/app/Filament/Resources/EntraGroupResource.php`, `apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php`, `apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ViewEntraGroup.php`, `apps/platform/app/Support/Navigation/NavigationScope.php`, `apps/platform/app/Support/OperateHub/OperateHubShell.php`, `apps/platform/app/Filament/Concerns/ScopesGlobalSearchToTenant.php`, and `apps/platform/app/Filament/Concerns/ResolvesPanelTenantContext.php`.
|
||||
- [x] T004 Confirm no implementation task introduces migrations, assets, provider registration changes, Graph adapter changes, tenant-panel dead-code deletion, or new group mutation/admin actions.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: User Story 1 - Groups are visible inside an active environment context (Priority: P1)
|
||||
|
||||
**Goal**: Entra Groups appears as a secondary Directory surface only inside the active Managed Environment context.
|
||||
|
||||
**Independent Test**: The environment-bound admin context registers and renders Groups navigation while workspace-home/no-context checks remain false.
|
||||
|
||||
- [x] T005 [P] [US1] Update `apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php` so `EntraGroupResource` is removed from the blanket admin-hidden dataset and covered by environment-visible navigation assertions.
|
||||
- [x] T006 [P] [US1] Update `apps/platform/tests/Feature/DirectoryGroups/BrowseGroupsTest.php` to replace the stale "keeps Entra groups out of admin sidebar navigation" assertion with the new workspace-home-hidden and environment-visible contract.
|
||||
- [x] T007 [US1] Update `apps/platform/app/Filament/Resources/EntraGroupResource.php` so `shouldRegisterNavigation()` uses the shared environment-navigation rule for the admin panel and still returns false on workspace-home/no-context surfaces.
|
||||
- [x] T008 [US1] Verify `Directory` / `Groups` navigation labels remain secondary and do not introduce a top-level Identity Center or M365 Admin mirror.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 2 - Workspace home remains clean (Priority: P2)
|
||||
|
||||
**Goal**: Workspace-home navigation stays free of environment-owned Directory Groups entries, even with remembered environment context.
|
||||
|
||||
**Independent Test**: Workspace-home response assertions prove Groups is absent, while environment route assertions prove it is present when allowed.
|
||||
|
||||
- [x] T009 [P] [US2] Extend `apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php` to assert the workspace-home sidebar does not render `Groups` or a Directory Groups entry when `WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY` is set.
|
||||
- [x] T010 [P] [US2] Extend `apps/platform/tests/Feature/Filament/EntraGroupAdminScopeTest.php` to cover no-context list access and cross-workspace or invalid remembered-environment denial as not found.
|
||||
- [x] T011 [US2] Preserve `apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php` no-context abort behavior and avoid adding workspace-home recovery shortcuts that reveal group data.
|
||||
- [x] T012 [US2] If list empty-state copy is touched in `apps/platform/app/Filament/Resources/EntraGroupResource.php`, change stale "tenant" wording to managed-environment-safe Directory inventory copy without inventing a refresh operation.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 3 - Global search stays scoped and lands on valid View pages (Priority: P3)
|
||||
|
||||
**Goal**: Global search returns only active-environment group records and opens valid canonical admin View destinations.
|
||||
|
||||
**Independent Test**: Seed groups across environments/workspaces, run global search, and assert result titles and URLs.
|
||||
|
||||
- [x] T013 [P] [US3] Extend `apps/platform/tests/Feature/Filament/EntraGroupGlobalSearchScopeTest.php` to assert no-context admin search returns no results.
|
||||
- [x] T014 [P] [US3] Extend `apps/platform/tests/Feature/Filament/EntraGroupGlobalSearchScopeTest.php` to assert cross-environment and cross-workspace matches are excluded.
|
||||
- [x] T015 [P] [US3] Extend `apps/platform/tests/Feature/Filament/EntraGroupGlobalSearchScopeTest.php` to assert each result URL resolves to `EntraGroupResource` View and does not contain `/admin/t`.
|
||||
- [x] T016 [P] [US3] Existing Entra group reference-link tests already cover canonical links; reran them with the Spec 303 suite and did not need additional assertions.
|
||||
- [x] T017 [US3] Verify `apps/platform/app/Filament/Resources/EntraGroupResource.php::getGlobalSearchResultUrl()` still passes the correct Managed Environment context and does not generate tenant-panel URLs.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Route Posture and Scoped Detail Safety
|
||||
|
||||
**Purpose**: Keep route/context changes minimal while making View destinations truthful.
|
||||
|
||||
- [x] T018 Inspect whether adding `WorkspaceScopedTenantRoutes` to `apps/platform/app/Filament/Resources/EntraGroupResource.php` is safe with focused URL, list, View, global-search, and resolved-reference tests.
|
||||
- [x] T019 Applied `WorkspaceScopedTenantRoutes` to `apps/platform/app/Filament/Resources/EntraGroupResource.php` so Groups list/detail URLs use the workspace/environment admin context.
|
||||
- [x] T020 Added the bounded explicit `Groups` navigation item in `apps/platform/app/Providers/Filament/AdminPanelProvider.php` after browser smoke showed the rendered sidebar did not receive the resource auto-navigation entry.
|
||||
- [x] T021 Preserve or extend `apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ViewEntraGroup.php` authorization so cross-environment and cross-workspace records deny as not found.
|
||||
- [x] T022 Confirm `apps/platform/app/Filament/Resources/EntraGroupResource.php` keeps `actions([])` and `bulkActions([])` and does not add create/edit/delete/membership/destructive actions.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Regression and Validation
|
||||
|
||||
**Purpose**: Prove the Groups cutover did not regress adjacent migrated surfaces.
|
||||
|
||||
- [x] T023 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/PanelNavigationSegregationTest.php tests/Feature/Filament/AdminTenantSurfaceParityTest.php tests/Feature/Filament/AdminSharedSurfacePanelParityTest.php tests/Feature/Filament/TenantOwnedResourceScopeParityTest.php tests/Feature/Filament/EntraGroupAdminScopeTest.php tests/Feature/Filament/EntraGroupGlobalSearchScopeTest.php tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php`.
|
||||
- [x] T024 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/DirectoryGroups/BrowseGroupsTest.php tests/Feature/Filament/EntraGroupEnterpriseDetailPageTest.php tests/Feature/Filament/EntraGroupResolvedReferencePresentationTest.php tests/Feature/Filament/PolicyVersionResolvedReferenceLinksTest.php tests/Browser/Spec303AdminDirectoryGroupsCutoverSmokeTest.php`.
|
||||
- [x] T025 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php tests/Feature/Filament/InventoryHubDbOnlyTest.php tests/Feature/Filament/InventoryPagesTest.php tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactAdminPanelRegistrationTest.php tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactEnvironmentContextTest.php tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactLegacyTenantPanelGuardTest.php tests/Feature/Operations/LegacyRunRoutesNotFoundTest.php`.
|
||||
- [x] T026 Run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`.
|
||||
- [x] T027 Run `git diff --check` from `/Users/ahmeddarrazi/Documents/projects/wt-plattform`.
|
||||
- [x] T028 Confirm `git status --short` includes only the intended Spec 303 package, Entra Groups implementation files, AdminPanelProvider navigation item, and focused tests.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Phase 1 must complete before test or implementation changes.
|
||||
- Phase 2 test updates should be written before the navigation implementation in T007.
|
||||
- Phase 3 and Phase 4 can proceed in parallel after T007 if different files are owned.
|
||||
- Phase 5 route posture must be settled before final global-search URL assertions are considered complete.
|
||||
- Phase 6 runs after all implementation tasks are complete.
|
||||
|
||||
## Parallel Execution Examples
|
||||
|
||||
- T005, T006, T010, T013, T014, T015, and T016 can run in parallel if each worker owns a distinct test file.
|
||||
- T011, T017, T021, and T022 can run in parallel after route posture is clear because they touch separate behavior checks.
|
||||
- T023, T024, and T025 should run after implementation; they may run independently if the Sail stack is available.
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
1. Make tests describe the new admin role contract first.
|
||||
2. Change navigation through the smallest existing shared helper path.
|
||||
3. Keep route changes minimal and evidence-backed.
|
||||
4. Preserve scoped query/search/detail behavior.
|
||||
5. Validate adjacent migrated surfaces before handoff.
|
||||
|
||||
## Explicit Non-Goals
|
||||
|
||||
- [x] Do not create a generic M365 Admin mirror.
|
||||
- [x] Do not create a broad Identity Center or Directory suite.
|
||||
- [x] Do not add group create/edit/delete, membership mutation, sync mutation, or destructive actions.
|
||||
- [x] Do not delete tenant-panel dead code.
|
||||
- [x] Do not revive `/admin/t` routes or add compatibility aliases.
|
||||
- [x] Do not introduce migrations, models, jobs, Graph adapters, provider registration changes, or assets.
|
||||
- [x] Do not split the navigation contract beyond the minimal test changes needed for Groups.
|
||||
Loading…
Reference in New Issue
Block a user