mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-10-28 21:15:42 +00:00
93 lines
3.6 KiB
PHP
93 lines
3.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Mailcow\Identity\Http;
|
|
|
|
use Mailcow\Identity\Directory\DirectoryException;
|
|
|
|
/**
|
|
* Admin REST API of a Keycloak realm with the client_credentials token of the mailcow client
|
|
* (service account roles: view-users, query-groups). The HTTP call sits in send() so tests can replace it.
|
|
*/
|
|
class KeycloakClient
|
|
{
|
|
private ?string $token = null;
|
|
|
|
public function __construct(private string $serverUrl, private string $realm, private string $clientId,
|
|
private string $clientSecret, private bool $ignoreSslError = false)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* GET below /admin/realms/<realm>, decoded JSON. A rejected token is renewed once.
|
|
*
|
|
* @throws DirectoryException
|
|
*/
|
|
public function get(string $path, array $query = []): array
|
|
{
|
|
$url = $this->serverUrl . '/admin/realms/' . rawurlencode($this->realm) . $path . (empty($query) ? '' : '?' . http_build_query($query));
|
|
for ($attempt = 0; $attempt < 2; $attempt++) {
|
|
$this->token ??= $this->fetchToken();
|
|
[$code, $body] = $this->send('GET', $url, ['Authorization: Bearer ' . $this->token, 'Accept: application/json'], null);
|
|
if ($code === 401 && $attempt === 0) {
|
|
$this->token = null;
|
|
continue;
|
|
}
|
|
if ($code !== 200) {
|
|
throw new DirectoryException('Keycloak returned HTTP ' . $code . ' for ' . $path);
|
|
}
|
|
$data = json_decode($body, true);
|
|
if (!is_array($data)) {
|
|
throw new DirectoryException('Keycloak returned invalid JSON for ' . $path);
|
|
}
|
|
return $data;
|
|
}
|
|
throw new DirectoryException('Keycloak rejected the access token of the mailcow client');
|
|
}
|
|
|
|
/** @throws DirectoryException */
|
|
private function fetchToken(): string
|
|
{
|
|
[$code, $body] = $this->send('POST', $this->serverUrl . '/realms/' . rawurlencode($this->realm) . '/protocol/openid-connect/token',
|
|
['Content-Type: application/x-www-form-urlencoded'],
|
|
http_build_query(['grant_type' => 'client_credentials', 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret]));
|
|
$data = json_decode($body, true);
|
|
if ($code !== 200 || !is_array($data) || empty($data['access_token'])) {
|
|
throw new DirectoryException('Keycloak token request failed (HTTP ' . $code . ')');
|
|
}
|
|
return (string)$data['access_token'];
|
|
}
|
|
|
|
/**
|
|
* @return array{0: int, 1: string} HTTP status and body
|
|
* @throws DirectoryException on transport errors
|
|
*/
|
|
protected function send(string $method, string $url, array $headers, ?string $body): array
|
|
{
|
|
$curl = curl_init($url);
|
|
curl_setopt_array($curl, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_CONNECTTIMEOUT => 5,
|
|
CURLOPT_TIMEOUT => 30,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
]);
|
|
if ($method === 'POST') {
|
|
curl_setopt($curl, CURLOPT_POST, true);
|
|
curl_setopt($curl, CURLOPT_POSTFIELDS, (string)$body);
|
|
}
|
|
if ($this->ignoreSslError) {
|
|
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
|
|
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
|
|
}
|
|
$response = curl_exec($curl);
|
|
$code = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|
$error = curl_error($curl);
|
|
curl_close($curl);
|
|
if ($response === false) {
|
|
throw new DirectoryException('Keycloak request failed: ' . $error);
|
|
}
|
|
return [$code, (string)$response];
|
|
}
|
|
}
|