TenantAtlas/app/Providers/AppServiceProvider.php
ahmido bc846d7c5c 051-entra-group-directory-cache (#57)
Summary

Adds a tenant-scoped Entra Groups “Directory Cache” to enable DB-only group name resolution across the app (no render-time Graph calls), plus sync runs + observability.

What’s included
	•	Entra Groups cache
	•	New entra_groups storage (tenant-scoped) for group metadata (no memberships).
	•	Retention semantics: groups become stale / retained per spec (no hard delete on first miss).
	•	Group Sync Runs
	•	New “Group Sync Runs” UI (list + detail) with tenant isolation (403 on cross-tenant access).
	•	Manual “Sync Groups” action: creates/reuses a run, dispatches job, DB notification with “View run” link.
	•	Scheduled dispatcher command wired in console.php.
	•	DB-only label resolution (US3)
	•	Shared EntraGroupLabelResolver with safe fallback Unresolved (…last8) and UUID guarding.
	•	Refactors to prefer cached names (no typeahead / no live Graph) in:
	•	Tenant RBAC group selects
	•	Policy version assignments widget
	•	Restore results + restore wizard group mapping labels

Safety / Guardrails
	•	No render-time Graph calls: fail-hard guard test verifies UI paths don’t call GraphClientInterface during page render.
	•	Tenant isolation & authorization: policies + scoped queries enforced (cross-tenant access returns 403, not 404).
	•	Data minimization: only group metadata is cached (no membership/owners).

Tests / Verification
	•	Added/updated tests under tests/Feature/DirectoryGroups and tests/Unit/DirectoryGroups:
	•	Start sync → run record + job dispatch + upserts
	•	Retention purge semantics
	•	Scheduled dispatch wiring
	•	Render-time Graph guard
	•	UI/resource access isolation
	•	Ran:
	•	./vendor/bin/pint --dirty
	•	./vendor/bin/sail artisan test tests/Feature/DirectoryGroups
	•	./vendor/bin/sail artisan test tests/Unit/DirectoryGroups

Notes / Follow-ups
	•	UI polish remains (picker/lookup UX, consistent progress widget/toasts across modules, navigation grouping).
	•	pr-gate checklist still has non-blocking open items (mostly UX/ops polish); requirements gate is green.

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #57
2026-01-11 23:24:12 +00:00

115 lines
4.0 KiB
PHP

<?php
namespace App\Providers;
use App\Models\BackupSchedule;
use App\Models\Tenant;
use App\Models\User;
use App\Models\UserTenantPreference;
use App\Policies\BackupSchedulePolicy;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\MicrosoftGraphClient;
use App\Services\Graph\NullGraphClient;
use App\Services\Intune\AppProtectionPolicyNormalizer;
use App\Services\Intune\CompliancePolicyNormalizer;
use App\Services\Intune\DeviceConfigurationPolicyNormalizer;
use App\Services\Intune\EnrollmentAutopilotPolicyNormalizer;
use App\Services\Intune\GroupPolicyConfigurationNormalizer;
use App\Services\Intune\ManagedDeviceAppConfigurationNormalizer;
use App\Services\Intune\ScriptsPolicyNormalizer;
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
use App\Services\Intune\TermsAndConditionsNormalizer;
use App\Services\Intune\WindowsDriverUpdateProfileNormalizer;
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
use App\Services\Intune\WindowsUpdateRingNormalizer;
use Filament\Events\TenantSet;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->singleton(GraphClientInterface::class, function ($app) {
$config = $app['config']->get('graph');
$hasCredentials = ! empty($config['client_id'])
&& ! empty($config['client_secret'])
&& ! empty($config['tenant_id']);
if (! empty($config['enabled']) && $hasCredentials) {
return $app->make(MicrosoftGraphClient::class);
}
return $app->make(NullGraphClient::class);
});
$this->app->tag(
[
AppProtectionPolicyNormalizer::class,
CompliancePolicyNormalizer::class,
DeviceConfigurationPolicyNormalizer::class,
EnrollmentAutopilotPolicyNormalizer::class,
GroupPolicyConfigurationNormalizer::class,
ManagedDeviceAppConfigurationNormalizer::class,
ScriptsPolicyNormalizer::class,
SettingsCatalogPolicyNormalizer::class,
TermsAndConditionsNormalizer::class,
WindowsDriverUpdateProfileNormalizer::class,
WindowsFeatureUpdateProfileNormalizer::class,
WindowsQualityUpdateProfileNormalizer::class,
WindowsUpdateRingNormalizer::class,
],
'policy-type-normalizers'
);
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::listen(TenantSet::class, function (TenantSet $event): void {
static $hasPreferencesTable;
$hasPreferencesTable ??= Schema::hasTable('user_tenant_preferences');
if (! $hasPreferencesTable) {
return;
}
$tenant = $event->getTenant();
$user = $event->getUser();
if (! $tenant instanceof Tenant) {
return;
}
if (! $user instanceof User) {
return;
}
UserTenantPreference::query()->updateOrCreate(
[
'user_id' => $user->getKey(),
'tenant_id' => $tenant->getKey(),
],
[
'last_used_at' => now(),
],
);
});
Gate::policy(BackupSchedule::class, BackupSchedulePolicy::class);
Gate::policy(BulkOperationRun::class, BulkOperationRunPolicy::class);
Gate::policy(\App\Models\EntraGroupSyncRun::class, \App\Policies\EntraGroupSyncRunPolicy::class);
Gate::policy(\App\Models\EntraGroup::class, \App\Policies\EntraGroupPolicy::class);
}
}