## Summary <!-- Kurz: Was ändert sich und warum? --> ## Spec-Driven Development (SDD) - [ ] Es gibt eine Spec unter `specs/<NNN>-<feature>/` - [ ] Enthaltene Dateien: `plan.md`, `tasks.md`, `spec.md` - [ ] Spec beschreibt Verhalten/Acceptance Criteria (nicht nur Implementation) - [ ] Wenn sich Anforderungen während der Umsetzung geändert haben: Spec/Plan/Tasks wurden aktualisiert ## Implementation - [ ] Implementierung entspricht der Spec - [ ] Edge cases / Fehlerfälle berücksichtigt - [ ] Keine unbeabsichtigten Änderungen außerhalb des Scopes ## Tests - [ ] Tests ergänzt/aktualisiert (Pest/PHPUnit) - [ ] Relevante Tests lokal ausgeführt (`./vendor/bin/sail artisan test` oder `php artisan test`) ## Migration / Config / Ops (falls relevant) - [ ] Migration(en) enthalten und getestet - [ ] Rollback bedacht (rückwärts kompatibel, sichere Migration) - [ ] Neue Env Vars dokumentiert (`.env.example` / Doku) - [ ] Queue/cron/storage Auswirkungen geprüft ## UI (Filament/Livewire) (falls relevant) - [ ] UI-Flows geprüft - [ ] Screenshots/Notizen hinzugefügt ## Notes <!-- Links, Screenshots, Follow-ups, offene Punkte --> Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local> Reviewed-on: #5
177 lines
6.1 KiB
PHP
177 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Intune;
|
|
|
|
use App\Models\Policy;
|
|
use App\Models\Tenant;
|
|
use App\Services\Graph\GraphClientInterface;
|
|
use App\Services\Graph\GraphErrorMapper;
|
|
use App\Services\Graph\GraphLogger;
|
|
use Illuminate\Support\Arr;
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
class PolicySyncService
|
|
{
|
|
public function __construct(
|
|
private readonly GraphClientInterface $graphClient,
|
|
private readonly GraphLogger $graphLogger,
|
|
) {}
|
|
|
|
/**
|
|
* Sync supported policies for a tenant from Microsoft Graph.
|
|
*
|
|
* @return array<int> IDs of policies synced or created
|
|
*/
|
|
public function syncPolicies(Tenant $tenant, ?array $supportedTypes = null): array
|
|
{
|
|
if (! $tenant->isActive()) {
|
|
throw new \RuntimeException('Tenant is archived or inactive.');
|
|
}
|
|
|
|
$types = $supportedTypes ?? config('tenantpilot.supported_policy_types', []);
|
|
$synced = [];
|
|
$tenantIdentifier = $tenant->tenant_id ?? $tenant->external_id;
|
|
|
|
foreach ($types as $typeConfig) {
|
|
$policyType = $typeConfig['type'];
|
|
$platform = $typeConfig['platform'] ?? null;
|
|
|
|
$this->graphLogger->logRequest('list_policies', [
|
|
'tenant' => $tenantIdentifier,
|
|
'policy_type' => $policyType,
|
|
'platform' => $platform,
|
|
]);
|
|
|
|
try {
|
|
$response = $this->graphClient->listPolicies($policyType, [
|
|
'tenant' => $tenantIdentifier,
|
|
'client_id' => $tenant->app_client_id,
|
|
'client_secret' => $tenant->app_client_secret,
|
|
'platform' => $platform,
|
|
]);
|
|
} catch (Throwable $throwable) {
|
|
throw GraphErrorMapper::fromThrowable($throwable, [
|
|
'policy_type' => $policyType,
|
|
'tenant_id' => $tenant->id,
|
|
'tenant_identifier' => $tenantIdentifier,
|
|
]);
|
|
}
|
|
|
|
$this->graphLogger->logResponse('list_policies', $response, [
|
|
'policy_type' => $policyType,
|
|
'tenant_id' => $tenant->id,
|
|
'tenant' => $tenantIdentifier,
|
|
]);
|
|
|
|
if ($response->failed()) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($response->data as $policyData) {
|
|
$externalId = $policyData['id'] ?? $policyData['external_id'] ?? null;
|
|
|
|
if ($externalId === null) {
|
|
continue;
|
|
}
|
|
|
|
$displayName = $policyData['displayName'] ?? $policyData['name'] ?? 'Unnamed policy';
|
|
$policyPlatform = $platform ?? ($policyData['platform'] ?? null);
|
|
|
|
$existingWithDifferentType = Policy::query()
|
|
->where('tenant_id', $tenant->id)
|
|
->where('external_id', $externalId)
|
|
->where('policy_type', '!=', $policyType)
|
|
->exists();
|
|
|
|
if ($existingWithDifferentType) {
|
|
continue;
|
|
}
|
|
|
|
$policy = Policy::updateOrCreate(
|
|
[
|
|
'tenant_id' => $tenant->id,
|
|
'external_id' => $externalId,
|
|
'policy_type' => $policyType,
|
|
],
|
|
[
|
|
'display_name' => $displayName,
|
|
'platform' => $policyPlatform,
|
|
'last_synced_at' => now(),
|
|
'ignored_at' => null,
|
|
'metadata' => Arr::except($policyData, ['id', 'external_id', 'displayName', 'name', 'platform']),
|
|
]
|
|
);
|
|
|
|
$synced[] = $policy->id;
|
|
}
|
|
}
|
|
|
|
return $synced;
|
|
}
|
|
|
|
/**
|
|
* Re-fetch a single policy from Graph and update local metadata.
|
|
*/
|
|
public function syncPolicy(Tenant $tenant, Policy $policy): void
|
|
{
|
|
if (! $tenant->isActive()) {
|
|
throw new RuntimeException('Tenant is archived or inactive.');
|
|
}
|
|
|
|
$tenantIdentifier = $tenant->tenant_id ?? $tenant->external_id;
|
|
|
|
$this->graphLogger->logRequest('get_policy', [
|
|
'tenant' => $tenantIdentifier,
|
|
'policy_type' => $policy->policy_type,
|
|
'policy_id' => $policy->external_id,
|
|
'platform' => $policy->platform,
|
|
]);
|
|
|
|
try {
|
|
$response = $this->graphClient->getPolicy($policy->policy_type, $policy->external_id, [
|
|
'tenant' => $tenantIdentifier,
|
|
'client_id' => $tenant->app_client_id,
|
|
'client_secret' => $tenant->app_client_secret,
|
|
'platform' => $policy->platform,
|
|
]);
|
|
} catch (Throwable $throwable) {
|
|
throw GraphErrorMapper::fromThrowable($throwable, [
|
|
'policy_type' => $policy->policy_type,
|
|
'policy_id' => $policy->external_id,
|
|
'tenant_id' => $tenant->id,
|
|
'tenant_identifier' => $tenantIdentifier,
|
|
]);
|
|
}
|
|
|
|
$this->graphLogger->logResponse('get_policy', $response, [
|
|
'tenant_id' => $tenant->id,
|
|
'tenant' => $tenantIdentifier,
|
|
'policy_type' => $policy->policy_type,
|
|
'policy_id' => $policy->external_id,
|
|
]);
|
|
|
|
if ($response->failed()) {
|
|
$message = $response->errors[0]['message'] ?? $response->data['error']['message'] ?? 'Graph request failed.';
|
|
|
|
throw new RuntimeException($message);
|
|
}
|
|
|
|
$payload = $response->data['payload'] ?? $response->data;
|
|
|
|
if (! is_array($payload)) {
|
|
throw new RuntimeException('Invalid Graph response payload.');
|
|
}
|
|
|
|
$displayName = $payload['displayName'] ?? $payload['name'] ?? $policy->display_name;
|
|
$platform = $payload['platform'] ?? $policy->platform;
|
|
|
|
$policy->forceFill([
|
|
'display_name' => $displayName,
|
|
'platform' => $platform,
|
|
'last_synced_at' => now(),
|
|
'metadata' => Arr::except($payload, ['id', 'external_id', 'displayName', 'name', 'platform']),
|
|
])->save();
|
|
}
|
|
}
|