45
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Graph;
|
||||
|
||||
use App\Models\DirectoryGroup;
|
||||
use App\Models\DirectoryUser;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Réplique utilisateurs et groupes Entra dans la base locale.
|
||||
* - Utilisateurs : requête delta (incrémentale), complète si --full ou si le deltaLink a expiré.
|
||||
* - Groupes : ceux dont le nom commence par le préfixe configuré, avec appartenance transitive.
|
||||
*/
|
||||
class DirectorySync
|
||||
{
|
||||
private const USER_SELECT = 'id,displayName,givenName,surname,jobTitle,department,companyName,officeLocation,businessPhones,mobilePhone,mail,userPrincipalName,streetAddress,postalCode,city,accountEnabled';
|
||||
|
||||
public function run(Tenant $tenant, bool $full = false): array
|
||||
{
|
||||
$graph = new GraphClient($tenant->entra_tenant_id);
|
||||
$stats = ['users' => 0, 'removed' => 0, 'groups' => 0, 'memberships' => 0];
|
||||
|
||||
try {
|
||||
[$stats['users'], $stats['removed']] = $this->syncUsers($tenant, $graph, $full);
|
||||
[$stats['groups'], $stats['memberships']] = $this->syncGroups($tenant, $graph);
|
||||
$tenant->update(['last_sync_at' => now(), 'last_sync_status' => 'ok']);
|
||||
} catch (Throwable $e) {
|
||||
$tenant->update(['last_sync_at' => now(), 'last_sync_status' => 'erreur : '.mb_substr($e->getMessage(), 0, 200)]);
|
||||
Log::error('Synchro annuaire en échec', ['tenant' => $tenant->id, 'error' => $e->getMessage()]);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private function syncUsers(Tenant $tenant, GraphClient $graph, bool $full): array
|
||||
{
|
||||
$start = (! $full && $tenant->users_delta_link)
|
||||
? $tenant->users_delta_link
|
||||
: '/users/delta?$select='.self::USER_SELECT;
|
||||
|
||||
try {
|
||||
[$items, $deltaLink] = $graph->all($start);
|
||||
} catch (Throwable $e) {
|
||||
if ($full || ! $tenant->users_delta_link) {
|
||||
throw $e;
|
||||
}
|
||||
// deltaLink expiré (410) ou invalide : on repart d'une synchro complète.
|
||||
[$items, $deltaLink] = $graph->all('/users/delta?$select='.self::USER_SELECT);
|
||||
$full = true;
|
||||
}
|
||||
|
||||
$now = now();
|
||||
$upserted = 0;
|
||||
$removed = 0;
|
||||
$seen = [];
|
||||
|
||||
foreach ($items as $u) {
|
||||
if (isset($u['@removed'])) {
|
||||
$removed += DirectoryUser::where('tenant_id', $tenant->id)->where('entra_oid', $u['id'])->delete();
|
||||
continue;
|
||||
}
|
||||
// En delta, seules les propriétés modifiées sont renvoyées : on ne met à jour que celles-là.
|
||||
$attrs = array_filter([
|
||||
'display_name' => $u['displayName'] ?? null,
|
||||
'given_name' => $u['givenName'] ?? null,
|
||||
'surname' => $u['surname'] ?? null,
|
||||
'job_title' => $u['jobTitle'] ?? null,
|
||||
'department' => $u['department'] ?? null,
|
||||
'company_name' => $u['companyName'] ?? null,
|
||||
'office_location' => $u['officeLocation'] ?? null,
|
||||
'business_phone' => array_key_exists('businessPhones', $u) ? ($u['businessPhones'][0] ?? null) : null,
|
||||
'mobile_phone' => $u['mobilePhone'] ?? null,
|
||||
'mail' => isset($u['mail']) ? strtolower($u['mail']) : null,
|
||||
'user_principal_name' => isset($u['userPrincipalName']) ? strtolower($u['userPrincipalName']) : null,
|
||||
'street_address' => $u['streetAddress'] ?? null,
|
||||
'postal_code' => $u['postalCode'] ?? null,
|
||||
'city' => $u['city'] ?? null,
|
||||
], fn ($v, $k) => array_key_exists($this->graphKey($k), $u), ARRAY_FILTER_USE_BOTH);
|
||||
if (array_key_exists('accountEnabled', $u)) {
|
||||
$attrs['account_enabled'] = (bool) $u['accountEnabled'];
|
||||
}
|
||||
$attrs['synced_at'] = $now;
|
||||
|
||||
$user = DirectoryUser::withTrashed()->firstOrNew(['tenant_id' => $tenant->id, 'entra_oid' => $u['id']]);
|
||||
$user->fill($attrs);
|
||||
$user->deleted_at = null;
|
||||
$user->save();
|
||||
$seen[] = $u['id'];
|
||||
$upserted++;
|
||||
}
|
||||
|
||||
if ($full) {
|
||||
// Synchro complète : tout ce qui n'a pas été vu n'existe plus dans Entra.
|
||||
$removed += DirectoryUser::where('tenant_id', $tenant->id)->whereNotIn('entra_oid', $seen)->delete();
|
||||
}
|
||||
|
||||
$tenant->users_delta_link = $deltaLink;
|
||||
$tenant->save();
|
||||
|
||||
return [$upserted, $removed];
|
||||
}
|
||||
|
||||
private function syncGroups(Tenant $tenant, GraphClient $graph): array
|
||||
{
|
||||
$prefix = str_replace("'", "''", config('signature.graph.group_prefix'));
|
||||
[$groups] = $graph->all("/groups?\$filter=startswith(displayName,'{$prefix}')&\$select=id,displayName");
|
||||
|
||||
$memberships = 0;
|
||||
$keep = [];
|
||||
foreach ($groups as $g) {
|
||||
$group = DirectoryGroup::updateOrCreate(
|
||||
['tenant_id' => $tenant->id, 'entra_oid' => $g['id']],
|
||||
['display_name' => $g['displayName'], 'synced_at' => now()],
|
||||
);
|
||||
$keep[] = $group->id;
|
||||
|
||||
[$members] = $graph->all(
|
||||
"/groups/{$g['id']}/transitiveMembers/microsoft.graph.user?\$select=id&\$count=true&\$top=999",
|
||||
['ConsistencyLevel' => 'eventual'],
|
||||
);
|
||||
$userIds = DirectoryUser::where('tenant_id', $tenant->id)
|
||||
->whereIn('entra_oid', array_column($members, 'id'))
|
||||
->pluck('id')->all();
|
||||
|
||||
DB::transaction(fn () => $group->members()->sync($userIds));
|
||||
$memberships += count($userIds);
|
||||
}
|
||||
|
||||
DirectoryGroup::where('tenant_id', $tenant->id)->whereNotIn('id', $keep)->delete();
|
||||
|
||||
return [count($groups), $memberships];
|
||||
}
|
||||
|
||||
private function graphKey(string $column): string
|
||||
{
|
||||
return [
|
||||
'display_name' => 'displayName', 'given_name' => 'givenName', 'surname' => 'surname',
|
||||
'job_title' => 'jobTitle', 'department' => 'department', 'company_name' => 'companyName',
|
||||
'office_location' => 'officeLocation', 'business_phone' => 'businessPhones',
|
||||
'mobile_phone' => 'mobilePhone', 'mail' => 'mail', 'user_principal_name' => 'userPrincipalName',
|
||||
'street_address' => 'streetAddress', 'postal_code' => 'postalCode', 'city' => 'city',
|
||||
][$column];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user