PR Body Implements Spec 065 “Tenant RBAC v1” with capabilities-first RBAC, tenant membership scoping (Option 3), and consistent Filament action semantics. Key decisions / rules Tenancy Option 3: tenant switching is tenantless (ChooseTenant), tenant-scoped routes stay scoped, non-members get 404 (not 403). RBAC model: canonical capability registry + role→capability map + Gates for each capability (no role-string checks in UI logic). UX policy: for tenant members lacking permission → actions are visible but disabled + tooltip (avoid click→403). Security still enforced server-side. What’s included Capabilities foundation: Central capability registry (Capabilities::*) Role→capability mapping (RoleCapabilityMap) Gate registration + resolver/manager updates to support tenant-scoped authorization Filament enforcement hardening across the app: Tenant registration & tenant CRUD properly gated Backup/restore/policy flows aligned to “visible-but-disabled” where applicable Provider operations (health check / inventory sync / compliance snapshot) guarded and normalized Directory groups + inventory sync start surfaces normalized Policy version maintenance actions (archive/restore/prune/force delete) gated SpecKit artifacts for 065: spec.md, plan/tasks updates, checklists, enforcement hitlist Security guarantees Non-member → 404 via tenant scoping/membership guards. Member without capability → 403 on execution, even if UI is disabled. No destructive actions execute without proper authorization checks. Tests Adds/updates Pest coverage for: Tenant scoping & membership denial behavior Role matrix expectations (owner/manager/operator/readonly) Filament surface checks (visible/disabled actions, no side effects) Provider/Inventory/Groups run-start authorization Verified locally with targeted vendor/bin/sail artisan test --compact … Deployment / ops notes No new services required. Safe change: behavior is authorization + UI semantics; no breaking route changes intended. Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box> Reviewed-on: #79
20 KiB
#+#+#+#+--------------------------------------------------------------------------
Spec 065 Enforcement Hitlist — role-ish helpers sweep
#+#+#+#+--------------------------------------------------------------------------
Generated: 2026-01-28 (updated)
Step-2 (T024) — Filament mutation and operation entry points
Goal: Enumerate every Filament action/page hook that (a) mutates tenant-scoped state or (b) dispatches jobs / operation runs. This is the authoritative checklist for the enforcement sweep in T025–T033.
Legend:
- kind: mutate | dispatch | destructive | secret
- capability (target):
- Use existing App\Support\Auth\Capabilities constants where available.
- Mark missing ones as NEW for addition/mapping in T025/T026.
Tenant (tenant-plane)
| ID | Location | Action | Kind | Current guard | Capability (target) | Notes |
|---|---|---|---|---|---|---|
| M001 | app/Filament/Resources/TenantResource.php | syncTenant | dispatch | visible() checks Gate::allows(Capabilities::TENANT_SYNC, record) | Capabilities::TENANT_SYNC | Uses OperationRunService to dispatch SyncPoliciesJob. |
| M002 | app/Filament/Resources/TenantResource.php | syncSelected (bulk) | dispatch | visible()+authorize() checks rolesWithCapability(Capabilities::TENANT_SYNC) | Capabilities::TENANT_SYNC | Dispatches BulkTenantSyncJob. |
| M003 | app/Filament/Resources/TenantResource.php | makeCurrent | mutate | none obvious | NEW | Sets current tenant context; should be capability-gated. |
| M004 | app/Filament/Resources/TenantResource.php | archive / deactivate | destructive | none obvious | Capabilities::TENANT_DELETE (or NEW) | Soft-deletes tenant; confirmation already present. |
| M005 | app/Filament/Resources/TenantResource.php | forceDelete | destructive | none obvious | Capabilities::TENANT_DELETE | Permanent delete; confirmation already present. |
| M006 | app/Filament/Resources/TenantResource.php | verify | mutate/dispatch | none obvious | Capabilities::TENANT_MANAGE | May update status fields; should be capability-gated. |
| M007 | app/Filament/Resources/TenantResource.php | setup_rbac | mutate/dispatch | none obvious | Capabilities::TENANT_MANAGE | Intune RBAC setup; should be capability-gated + confirmed (confirmation present). |
Tenant membership
| ID | Location | Action | Kind | Current guard | Capability (target) | Notes |
|---|---|---|---|---|---|---|
| M010 | app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php | add member | mutate | relation manager auth + server-side manager guards | Capabilities::TENANT_MEMBERSHIP_MANAGE | Uses TenantMembershipManager (audited). |
| M011 | app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php | change role | mutate | relation manager auth + last-owner protection | Capabilities::TENANT_MEMBERSHIP_MANAGE | Privilege change; requires confirmation. |
| M012 | app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php | remove member | destructive | relation manager auth + last-owner protection | Capabilities::TENANT_MEMBERSHIP_MANAGE | Requires confirmation; blocked attempts are audited. |
Providers (provider-plane)
| ID | Location | Action | Kind | Current guard | Capability (target) | Notes |
|---|---|---|---|---|---|---|
| M020 | app/Filament/Resources/ProviderConnectionResource.php | check_connection | dispatch | Gate::allows(Capabilities::PROVIDER_RUN_OPERATIONS, record) | Capabilities::PROVIDER_RUN_OPERATIONS | Dispatches ProviderConnectionHealthCheckJob via OperationRunService. |
| M021 | app/Filament/Resources/ProviderConnectionResource.php | inventory_sync | dispatch | Gate::allows(Capabilities::PROVIDER_RUN_OPERATIONS, record) | Capabilities::PROVIDER_RUN_OPERATIONS | Dispatches ProviderInventorySyncJob. |
| M022 | app/Filament/Resources/ProviderConnectionResource.php | compliance_snapshot | dispatch | Gate::allows(Capabilities::PROVIDER_RUN_OPERATIONS, record) | Capabilities::PROVIDER_RUN_OPERATIONS | Dispatches ProviderComplianceSnapshotJob. |
| M023 | app/Filament/Resources/ProviderConnectionResource.php | set_default | mutate | Gate::allows(Capabilities::PROVIDER_MANAGE, record) | Capabilities::PROVIDER_MANAGE | Changes default provider; audited. |
| M024 | app/Filament/Resources/ProviderConnectionResource.php | update_credentials | secret | Gate::allows(Capabilities::PROVIDER_MANAGE, record) | Capabilities::PROVIDER_MANAGE | Credential/secret handling; audited. |
| M025 | app/Filament/Resources/ProviderConnectionResource.php | enable / disable | mutate | Gate::allows(Capabilities::PROVIDER_MANAGE, record) | Capabilities::PROVIDER_MANAGE | Connection state change; audited. |
Backup schedules
| ID | Location | Action | Kind | Current guard | Capability (target) | Notes |
|---|---|---|---|---|---|---|
| M030 | app/Filament/Resources/BackupScheduleResource.php | create/edit/delete | mutate/destructive | Resource can* + policy guard | Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE | Already mapped in Step-1. |
| M031 | app/Filament/Resources/BackupScheduleResource.php | run now / retry (row + bulk) | dispatch | visible()+abort_unless guards | Capabilities::TENANT_BACKUP_SCHEDULES_RUN | Already mapped in Step-1. |
Backup sets
| ID | Location | Action | Kind | Current guard | Capability (target) | Notes |
|---|---|---|---|---|---|---|
| M040 | app/Filament/Resources/BackupSetResource.php | restore | dispatch | none obvious | NEW | Starts restore workflow from a backup set; uses OperationRunService. |
| M041 | app/Filament/Resources/BackupSetResource.php | archive / delete (bulk) | destructive | none obvious | NEW (or Capabilities::TENANT_DELETE) | Bulk job: BulkBackupSetDeleteJob. |
| M042 | app/Filament/Resources/BackupSetResource.php | restore (bulk) | dispatch | none obvious | NEW | BulkBackupSetRestoreJob. |
| M043 | app/Filament/Resources/BackupSetResource.php | force delete (row + bulk) | destructive | none obvious | NEW | Bulk job: BulkBackupSetForceDeleteJob. |
Restore runs
| ID | Location | Action | Kind | Current guard | Capability (target) | Notes |
|---|---|---|---|---|---|---|
| M050 | app/Filament/Resources/RestoreRunResource/Pages/CreateRestoreRun.php | create/queue restore run | dispatch | tenant match + non-dry-run confirmation | NEW | Dispatches ExecuteRestoreRunJob; emits restore.queued audit. |
| M051 | app/Filament/Resources/RestoreRunResource.php | rerun | dispatch | none obvious | NEW | Starts restore rerun. |
| M052 | app/Filament/Resources/RestoreRunResource.php | archive / restore / forceDelete | destructive | none obvious | NEW (or Capabilities::TENANT_DELETE) | Row-level destructive actions; confirmations exist. |
| M053 | app/Filament/Resources/RestoreRunResource.php | bulk delete / restore / force delete | destructive | none obvious | NEW | Bulk jobs: BulkRestoreRunDeleteJob, BulkRestoreRunRestoreJob, BulkRestoreRunForceDeleteJob. |
Drift
| ID | Location | Action | Kind | Current guard | Capability (target) | Notes |
|---|---|---|---|---|---|---|
| M060 | app/Filament/Pages/DriftLanding.php | auto enqueue findings generation (mount) | dispatch | Gate::allows(Capabilities::TENANT_SYNC, tenant) | Capabilities::TENANT_SYNC | Dispatches GenerateDriftFindingsJob when no findings exist. |
Findings
| ID | Location | Action | Kind | Current guard | Capability (target) | Notes |
|---|---|---|---|---|---|---|
| M070 | app/Filament/Resources/FindingResource.php | acknowledge (row + bulk) | mutate | policy + tenant scoping | NEW (or policy-only) | Local mutation; decide in T025 whether to require a dedicated capability. |
Policies
| ID | Location | Action | Kind | Current guard | Capability (target) | Notes |
|---|---|---|---|---|---|---|
| M080 | app/Filament/Resources/PolicyResource.php | ignore / unignore (row + bulk) | mutate | mixed (some Gate checks) | NEW | Local policy lifecycle; bulk jobs include BulkPolicyDeleteJob / BulkPolicyUnignoreJob (verify naming). |
| M081 | app/Filament/Resources/PolicyResource.php | sync (row + bulk) | dispatch | requires Capabilities::TENANT_SYNC in places | Capabilities::TENANT_SYNC (or NEW) | Dispatches SyncPoliciesJob; ensure all entry points have server-side authorization. |
| M082 | app/Filament/Resources/PolicyResource.php | export (row + bulk) | dispatch | none obvious | NEW | BulkPolicyExportJob; capability needed to prevent data exfil. |
| M083 | app/Filament/Resources/PolicyResource/RelationManagers/VersionsRelationManager.php | restore_to_intune | dispatch | none obvious | NEW | Calls RestoreService::executeFromPolicyVersion. |
Entra groups
| ID | Location | Action | Kind | Current guard | Capability (target) | Notes |
|---|---|---|---|---|---|---|
| M090 | app/Filament/Resources/EntraGroupSyncRunResource/Pages/ListEntraGroupSyncRuns.php | sync_groups (header) | dispatch | abort(403) when role cannot sync | Capabilities::TENANT_SYNC | Execution guard already present. |
| M091 | app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php | sync_groups (header) | dispatch | abort(403) when role cannot sync | Capabilities::TENANT_SYNC | Execution guard already present. |
Inventory
| ID | Location | Action | Kind | Current guard | Capability (target) | Notes |
|---|---|---|---|---|---|---|
| M100 | app/Filament/Resources/InventorySyncRunResource.php | view runs | read | relies on tenant scoping | Capabilities::TENANT_VIEW (or NEW) | Decide whether listing historical runs needs explicit capability in T025. |
Scope: discovery only (phase 1). This file enumerates every remaining occurrence matched by the stop-regex:
TenantRole::|->tenantRole\(|currentTenantRole\(|canManage|canRun|canSync
Notes:
rg(ripgrep) is not available in this environment, so discovery uses GNU/BSDgrep.- The “allowed” exclusions for sweep progress reporting are:
app/Services/Auth/RoleCapabilityMap.phpapp/Services/Auth/CapabilityResolver.phpapp/Support/Auth/Capabilities.phpapp/Support/TenantRole.php
Discovery commands + counts
Total matches (all of app/)
grep -RInE --include='*.php' 'TenantRole::|->tenantRole\(|currentTenantRole\(|canManage|canRun|canSync' app | wc -l
Result: 31
Remaining matches (excluding mapping/registry/TenantRole enum definition)
grep -RInE --include='*.php' 'TenantRole::|->tenantRole\(|currentTenantRole\(|canManage|canRun|canSync' app \
| grep -vE '^app/Services/Auth/RoleCapabilityMap\.php:|^app/Services/Auth/CapabilityResolver\.php:|^app/Support/Auth/Capabilities\.php:|^app/Support/TenantRole\.php:' \
| wc -l
Result: 21
Top files by remaining match count
grep -RInE --include='*.php' 'TenantRole::|->tenantRole\(|currentTenantRole\(|canManage|canRun|canSync' app \
| grep -vE '^app/Services/Auth/RoleCapabilityMap\.php:|^app/Services/Auth/CapabilityResolver\.php:|^app/Support/Auth/Capabilities\.php:|^app/Support/TenantRole\.php:' \
| cut -d: -f1 | sort | uniq -c | sort -nr | head -n 20
Result:
10 app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php
6 app/Services/Auth/TenantMembershipManager.php
2 app/Models/User.php
2 app/Filament/Pages/Tenancy/RegisterTenant.php
1 app/Filament/Resources/TenantResource/Pages/CreateTenant.php
Full remaining match list (excluding mapping/registry/TenantRole enum definition)
app/Models/User.php:116: return TenantRole::tryFrom($role);
app/Models/User.php:119: public function canSyncTenant(Tenant $tenant): bool
app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php:66: TenantRole::Owner->value => 'Owner',
app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php:67: TenantRole::Manager->value => 'Manager',
app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php:68: TenantRole::Operator->value => 'Operator',
app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php:69: TenantRole::Readonly->value => 'Readonly',
app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php:100: role: TenantRole::from((string) $data['role']),
app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php:135: TenantRole::Owner->value => 'Owner',
app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php:136: TenantRole::Manager->value => 'Manager',
app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php:137: TenantRole::Operator->value => 'Operator',
app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php:138: TenantRole::Readonly->value => 'Readonly',
app/Filament/Resources/TenantResource/RelationManagers/TenantMembershipsRelationManager.php:162: newRole: TenantRole::from((string) $data['role']),
app/Filament/Resources/TenantResource/Pages/CreateTenant.php:23: $this->record->getKey() => ['role' => TenantRole::Owner->value],
app/Filament/Pages/Tenancy/RegisterTenant.php:79: 'role' => TenantRole::Owner->value,
app/Filament/Pages/Tenancy/RegisterTenant.php:91: 'role' => TenantRole::Owner->value,
app/Services/Auth/TenantMembershipManager.php:178: role: TenantRole::Owner,
app/Services/Auth/TenantMembershipManager.php:203: if ($membership->role !== TenantRole::Owner->value) {
app/Services/Auth/TenantMembershipManager.php:209: ->where('role', TenantRole::Owner->value)
app/Services/Auth/TenantMembershipManager.php:219: if ($membership->role !== TenantRole::Owner->value) {
app/Services/Auth/TenantMembershipManager.php:223: if ($newRole === TenantRole::Owner) {
app/Services/Auth/TenantMembershipManager.php:229: ->where('role', TenantRole::Owner->value)
| H003 | app/Filament/Resources/EntraGroupSyncRunResource/Pages/ListEntraGroupSyncRuns.php:62 | if (! ($role?->canSync() ?? false)) abort(403); | Header action execution: sync_groups | Capabilities::TENANT_SYNC | Execution guard. |
| H004 | app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php:50 | $role?->canSync() ?? false | Header action visibility: sync_groups | Capabilities::TENANT_SYNC | Visible guard only. |
| H005 | app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php:71 | if (! ($role?->canSync() ?? false)) abort(403); | Header action execution: sync_groups | Capabilities::TENANT_SYNC | Execution guard. |
| H006 | app/Filament/Resources/BackupScheduleResource.php:86 | static::currentTenantRole()?->canManageBackupSchedules() ?? false | Resource ability: canCreate() | Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE | New capability added in Option-1 patch. |
| H007 | app/Filament/Resources/BackupScheduleResource.php:91 | static::currentTenantRole()?->canManageBackupSchedules() ?? false | Resource ability: canEdit() | Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE | New capability added in Option-1 patch. |
| H008 | app/Filament/Resources/BackupScheduleResource.php:96 | static::currentTenantRole()?->canManageBackupSchedules() ?? false | Resource ability: canDelete() | Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE | New capability added in Option-1 patch. |
| H009 | app/Filament/Resources/BackupScheduleResource.php:101 | static::currentTenantRole()?->canManageBackupSchedules() ?? false | Resource ability: canDeleteAny() | Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE | New capability added in Option-1 patch. |
| H010 | app/Filament/Resources/BackupScheduleResource.php:303 | static::currentTenantRole()?->canRunBackupSchedules() ?? false | Table row action visibility: runNow | Capabilities::TENANT_BACKUP_SCHEDULES_RUN | New capability added in Option-1 patch. |
| H011 | app/Filament/Resources/BackupScheduleResource.php:305 | abort_unless(static::currentTenantRole()?->canRunBackupSchedules() ?? false, 403); | Table row action execution: runNow | Capabilities::TENANT_BACKUP_SCHEDULES_RUN | New capability added in Option-1 patch. |
| H012 | app/Filament/Resources/BackupScheduleResource.php:427 | static::currentTenantRole()?->canRunBackupSchedules() ?? false | Table row action visibility: retry | Capabilities::TENANT_BACKUP_SCHEDULES_RUN | New capability added in Option-1 patch. |
| H013 | app/Filament/Resources/BackupScheduleResource.php:429 | abort_unless(static::currentTenantRole()?->canRunBackupSchedules() ?? false, 403); | Table row action execution: retry | Capabilities::TENANT_BACKUP_SCHEDULES_RUN | New capability added in Option-1 patch. |
| H014 | app/Filament/Resources/BackupScheduleResource.php:548 | static::currentTenantRole()?->canManageBackupSchedules() ?? false | Table row action visibility: EditAction | Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE | New capability added in Option-1 patch. |
| H015 | app/Filament/Resources/BackupScheduleResource.php:550 | static::currentTenantRole()?->canManageBackupSchedules() ?? false | Table row action visibility: DeleteAction | Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE | New capability added in Option-1 patch. |
| H016 | app/Filament/Resources/BackupScheduleResource.php:559 | static::currentTenantRole()?->canRunBackupSchedules() ?? false | Bulk action visibility: bulk_run_now | Capabilities::TENANT_BACKUP_SCHEDULES_RUN | New capability added in Option-1 patch. |
| H017 | app/Filament/Resources/BackupScheduleResource.php:561 | abort_unless(static::currentTenantRole()?->canRunBackupSchedules() ?? false, 403); | Bulk action execution: bulk_run_now | Capabilities::TENANT_BACKUP_SCHEDULES_RUN | New capability added in Option-1 patch. |
| H018 | app/Filament/Resources/BackupScheduleResource.php:688 | static::currentTenantRole()?->canRunBackupSchedules() ?? false | Bulk action visibility: bulk_retry | Capabilities::TENANT_BACKUP_SCHEDULES_RUN | New capability added in Option-1 patch. |
| H019 | app/Filament/Resources/BackupScheduleResource.php:690 | abort_unless(static::currentTenantRole()?->canRunBackupSchedules() ?? false, 403); | Bulk action execution: bulk_retry | Capabilities::TENANT_BACKUP_SCHEDULES_RUN | New capability added in Option-1 patch. |
| H020 | app/Filament/Resources/BackupScheduleResource.php:814 | static::currentTenantRole()?->canManageBackupSchedules() ?? false | Bulk action visibility: DeleteBulkAction | Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE | New capability added in Option-1 patch. |
| H021 | app/Policies/BackupSchedulePolicy.php:34 | $this->resolveRole($user)?->canManageBackupSchedules() ?? false | Policy: create() | Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE | New capability added in Option-1 patch. |
| H022 | app/Policies/BackupSchedulePolicy.php:39 | $this->resolveRole($user)?->canManageBackupSchedules() ?? false | Policy: update() | Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE | New capability added in Option-1 patch. |
| H023 | app/Policies/BackupSchedulePolicy.php:44 | $this->resolveRole($user)?->canManageBackupSchedules() ?? false | Policy: delete() | Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE | New capability added in Option-1 patch. |
Step-1 conclusion (guardrails)
canSync()has a clear mapping:Capabilities::TENANT_SYNC.canManageBackupSchedules()now maps toCapabilities::TENANT_BACKUP_SCHEDULES_MANAGE.canRunBackupSchedules()now maps toCapabilities::TENANT_BACKUP_SCHEDULES_RUN.- No unmapped
TenantRole::can*()usages remain in this hitlist.