time()) return $cache['token']; $ch = curl_init(ALPID_BASE . '/realms/master/protocol/openid-connect/token'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query([ 'grant_type' => 'client_credentials', 'client_id' => KC_SERVICE_CLIENT_ID, 'client_secret' => KC_SERVICE_CLIENT_SECRET, ]), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { throw new RuntimeException("Authentification Keycloak Admin échouée ($status)"); } $data = json_decode($body, true); $cache = ['token' => $data['access_token'], 'expires' => time() + $data['expires_in'] - 10]; return $cache['token']; } function _kc_request(string $method, string $path, mixed $body = null): array { $url = KC_ADMIN_BASE . $path; $token = _kc_admin_token(); $headers = ["Authorization: Bearer $token", "Content-Type: application/json"]; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_HEADER => true, ]); if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); } $response = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); curl_close($ch); $resp_headers = substr($response, 0, $header_size); $resp_body = substr($response, $header_size); return ['status' => $status, 'headers' => $resp_headers, 'body' => $resp_body]; } function _kc_encode_username(string $lastname): string { $name = strtolower(iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $lastname)); $name = preg_replace('/[^a-z]/', '', $name); $len = strlen($name); if ($len <= 2) return $name; return $name[0] . ($len - 2) . $name[$len - 1]; } function kc_username_exists(string $username): bool { $resp = _kc_request('GET', '/users?username=' . urlencode($username) . '&exact=true'); if ($resp['status'] !== 200) return false; return count(json_decode($resp['body'], true) ?? []) > 0; } function kc_generate_username(string $lastname, string $firstname): string { $fn = strtolower(preg_replace('/[^a-z]/i', '', iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $firstname))); $code = _kc_encode_username($lastname); $base = $fn . '.' . $code; if (!kc_username_exists($base)) return $base; for ($i = 2; $i <= 99; $i++) { $try = $base . $i; if (!kc_username_exists($try)) return $try; } return $base . uniqid('', false); } function kc_create_user(string $username, string $email, string $first_name, string $last_name, string $password): string { $resp = _kc_request('POST', '/users', [ 'username' => $username, 'email' => $email, 'firstName' => $first_name, 'lastName' => $last_name, 'enabled' => true, 'emailVerified' => false, 'credentials' => [['type' => 'password', 'value' => $password, 'temporary' => false]], ]); if ($resp['status'] === 409) { $detail = json_decode($resp['body'], true)['errorMessage'] ?? ''; $msg = str_contains(strtolower($detail), 'email') ? 'Cet email est déjà associé à un compte existant.' : 'Ce nom d\'utilisateur est déjà utilisé.'; throw new KcUserExistsException($msg); } if ($resp['status'] !== 201) { throw new RuntimeException("Erreur création compte ({$resp['status']}) : {$resp['body']}"); } // L'ID est dans le header Location preg_match('#/users/([a-f0-9-]+)#', $resp['headers'], $m); return $m[1] ?? ''; } function kc_get_otp_credential(string $user_id): ?array { $resp = _kc_request('GET', "/users/$user_id/credentials"); if ($resp['status'] !== 200) return null; foreach (json_decode($resp['body'], true) ?? [] as $c) { if ($c['type'] === 'otp') return $c; } return null; } function kc_delete_credential(string $user_id, string $credential_id): void { $resp = _kc_request('DELETE', "/users/$user_id/credentials/$credential_id"); if ($resp['status'] >= 300) { throw new RuntimeException("Erreur suppression credential ({$resp['status']})"); } } function kc_update_name(string $user_id, string $first_name, string $last_name): void { $resp = _kc_request('PUT', "/users/$user_id", [ 'firstName' => $first_name, 'lastName' => $last_name, ]); if ($resp['status'] >= 300) { throw new RuntimeException("Erreur mise à jour nom ({$resp['status']})"); } } function kc_update_email(string $user_id, string $email): void { $resp = _kc_request('PUT', "/users/$user_id", [ 'email' => $email, 'emailVerified' => false, ]); if ($resp['status'] >= 300) { throw new RuntimeException("Erreur mise à jour email ({$resp['status']})"); } } function kc_set_password(string $user_id, string $password): void { $resp = _kc_request('PUT', "/users/$user_id/reset-password", [ 'type' => 'password', 'value' => $password, 'temporary' => false, ]); if ($resp['status'] >= 300) { throw new RuntimeException("Erreur changement mot de passe ({$resp['status']})"); } } function kc_list_users(int $max = 200): array { $resp = _kc_request('GET', '/users?max=' . $max); if ($resp['status'] !== 200) return []; $users = json_decode($resp['body'], true) ?? []; foreach ($users as &$user) { $gr = _kc_request('GET', "/users/{$user['id']}/groups"); $user['groupNames'] = $gr['status'] === 200 ? array_column(json_decode($gr['body'], true) ?? [], 'name') : []; } return $users; } function kc_list_groups(): array { $resp = _kc_request('GET', '/groups?max=200&briefRepresentation=false'); if ($resp['status'] !== 200) return []; return json_decode($resp['body'], true) ?? []; } function kc_create_group(string $name): void { $resp = _kc_request('POST', '/groups', ['name' => $name]); if ($resp['status'] === 409) throw new RuntimeException("Le groupe « $name » existe déjà."); if ($resp['status'] !== 201) throw new RuntimeException("Erreur création groupe ({$resp['status']})"); } function kc_delete_group(string $group_id): void { $resp = _kc_request('DELETE', "/groups/$group_id"); if ($resp['status'] >= 300) throw new RuntimeException("Erreur suppression groupe ({$resp['status']})"); } function kc_group_members(string $group_id, int $max = 200): array { $resp = _kc_request('GET', "/groups/$group_id/members?max=$max"); if ($resp['status'] !== 200) return []; return json_decode($resp['body'], true) ?? []; } function _kc_group_id(string $group_name): string { $resp = _kc_request('GET', '/groups?search=' . urlencode($group_name)); $groups = json_decode($resp['body'], true) ?? []; foreach ($groups as $g) { if ($g['name'] === $group_name) return $g['id']; } throw new RuntimeException("Groupe « $group_name » introuvable dans Keycloak."); } function kc_add_to_group(string $user_id, string $group_name): void { $group_id = _kc_group_id($group_name); $resp = _kc_request('PUT', "/users/$user_id/groups/$group_id"); if ($resp['status'] >= 300) { throw new RuntimeException("Erreur ajout groupe ({$resp['status']})"); } } function kc_remove_from_group(string $user_id, string $group_name): void { $group_id = _kc_group_id($group_name); $resp = _kc_request('DELETE', "/users/$user_id/groups/$group_id"); if ($resp['status'] >= 300) { throw new RuntimeException("Erreur suppression groupe ({$resp['status']})"); } }