TenantAtlas/specs/065-tenant-rbac-v1/enforcement-hitlist.md
ahmido d90fb0f963 065-tenant-rbac-v1 (#79)
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
2026-01-28 21:09:47 +00:00

20 KiB
Raw Blame History

#+#+#+#+--------------------------------------------------------------------------

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 T025T033.

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/BSD grep.
  • The “allowed” exclusions for sweep progress reporting are:
    • app/Services/Auth/RoleCapabilityMap.php
    • app/Services/Auth/CapabilityResolver.php
    • app/Support/Auth/Capabilities.php
    • app/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 to Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE.
  • canRunBackupSchedules() now maps to Capabilities::TENANT_BACKUP_SCHEDULES_RUN.
  • No unmapped TenantRole::can*() usages remain in this hitlist.