From 8546f4dadc680628d74ab0ade6ad2cca0e0d0ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 22 Jan 2026 14:39:46 +0000 Subject: [PATCH] [Zimbra] Use admin delegated account for authenticating users against Zimbra --- .../unreleased/Added-20260122-153223.yaml | 3 + .../ZimbraConnector/CreateZimbraComponent.php | 9 +- .../ZimbraConnector/SoapClientBuilder.php | 88 ++++++++++++++++--- 3 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 packages/ChillZimbraBundle/.changes/unreleased/Added-20260122-153223.yaml diff --git a/packages/ChillZimbraBundle/.changes/unreleased/Added-20260122-153223.yaml b/packages/ChillZimbraBundle/.changes/unreleased/Added-20260122-153223.yaml new file mode 100644 index 000000000..da145c2e3 --- /dev/null +++ b/packages/ChillZimbraBundle/.changes/unreleased/Added-20260122-153223.yaml @@ -0,0 +1,3 @@ +kind: Added +body: Use admin delegated account for handling authentication +time: 2026-01-22T15:32:23.932994899+01:00 diff --git a/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/CreateZimbraComponent.php b/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/CreateZimbraComponent.php index 9fa14d75c..8acfa0337 100644 --- a/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/CreateZimbraComponent.php +++ b/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/CreateZimbraComponent.php @@ -80,12 +80,19 @@ final readonly class CreateZimbraComponent $location = $calendar->getCalendar()->getLocation(); $hasLocation = $calendar->getCalendar()->hasLocation(); $isPrivate = $calendar->getCalendar()->getAccompanyingPeriod()?->isConfidential() ?? false; - } else { + } elseif ($calendar instanceof Calendar) { $startDate = $calendar->getStartDate(); $endDate = $calendar->getEndDate(); $location = $calendar->getLocation(); $hasLocation = $calendar->hasLocation(); $isPrivate = $calendar->getAccompanyingPeriod()?->isConfidential() ?? false; + } else { + // Calendar range case + $startDate = $calendar->getStartDate(); + $endDate = $calendar->getEndDate(); + $location = $calendar->getLocation(); + $hasLocation = $calendar->hasLocation(); + $isPrivate = false; } $comp = new InviteComponent(); diff --git a/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/SoapClientBuilder.php b/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/SoapClientBuilder.php index e66c45e85..bf8a2311d 100644 --- a/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/SoapClientBuilder.php +++ b/packages/ChillZimbraBundle/src/Calendar/Connector/ZimbraConnector/SoapClientBuilder.php @@ -11,48 +11,84 @@ declare(strict_types=1); namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector; +use Symfony\Component\Clock\ClockInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\HttpClient\Psr18Client; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Zimbra\Admin\AdminApi; use Zimbra\Common\Enum\AccountBy; use Zimbra\Common\Soap\ClientFactory; +use Zimbra\Common\Struct\AccountSelector; use Zimbra\Common\Struct\Header\AccountInfo; use Zimbra\Mail\MailApi; -final readonly class SoapClientBuilder +final class SoapClientBuilder { - private string $username; + private readonly string $username; - private string $password; + private readonly string $password; - private string $url; + private readonly string $url; - public function __construct(private ParameterBagInterface $parameterBag, private HttpClientInterface $client) - { + private readonly string $adminUrl; + + private readonly bool $verifyHost; + + private readonly bool $verifyPeer; + + private readonly bool $adminVerifyHost; + + private readonly bool $adminVerifyPeer; + + /** + * Keep the cache of the tokens. + * + * @var array + */ + private array $tokenCache = []; + + public function __construct( + private readonly ParameterBagInterface $parameterBag, + private readonly HttpClientInterface $client, + private readonly ClockInterface $clock, + ) { $dsn = $this->parameterBag->get('chill_calendar.remote_calendar_dsn'); $url = parse_url($dsn); $this->username = urldecode($url['user']); $this->password = urldecode($url['pass']); if ('zimbra+http' === $url['scheme']) { - $scheme = 'http://'; + $scheme = 'http'; $port = $url['port'] ?? 80; } elseif ('zimbra+https' === $url['scheme']) { - $scheme = 'https://'; + $scheme = 'https'; $port = $url['port'] ?? 443; } else { throw new \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('Unsupported remote calendar scheme: '.$url['scheme']); } - $this->url = $scheme.$url['host'].':'.$port; + // get attributes for adminUrl + $query = []; + parse_str($url['query'] ?? '', $query); + $adminPort = $query['adminPort'] ?? '7071'; + $adminHost = $query['adminHost'] ?? $url['host']; + $adminScheme = $query['adminScheme'] ?? $scheme; + + $this->verifyPeer = (bool) ($query['verifyPeer'] ?? true); + $this->verifyHost = (bool) ($query['verifyHost'] ?? true); + $this->adminVerifyHost = (bool) ($query['adminVerifyHost'] ?? $this->verifyPeer); + $this->adminVerifyPeer = (bool) ($query['adminVerifyPeer'] ?? $this->verifyHost); + + $this->url = $scheme.'://'.$url['host'].':'.$port; + $this->adminUrl = $adminScheme.'://'.$adminHost.':'.$adminPort; } private function buildApi(): MailApi { $baseClient = $this->client->withOptions([ 'base_uri' => $location = $this->url.'/service/soap', - 'verify_host' => false, - 'verify_peer' => false, + 'verify_host' => $this->verifyHost, + 'verify_peer' => $this->verifyPeer, ]); $psr18Client = new Psr18Client($baseClient); $api = new MailApi(); @@ -62,12 +98,36 @@ final readonly class SoapClientBuilder return $api; } + private function buildAdminApi(): AdminApi + { + $baseClient = $this->client->withOptions([ + 'base_uri' => $location = $this->adminUrl.'/service/admin/soap', + 'verify_host' => $this->adminVerifyHost, + 'verify_peer' => $this->adminVerifyPeer, + ]); + $psr18Client = new Psr18Client($baseClient); + $api = new AdminApi(); + $client = ClientFactory::create($location, $psr18Client); + $api->setClient($client); + + return $api; + } + public function getApiForAccount(string $accountName): MailApi { - $api = $this->buildApi(); - $response = $api->authByAccountName($this->username, $this->password); + ['token' => $token, 'expirationTime' => $expirationTime] = $this->tokenCache[$accountName] + ?? ['token' => null, 'expirationTime' => null]; - $token = $response->getAuthToken(); + if (null === $token || null === $expirationTime || $expirationTime <= $this->clock->now()) { + $adminApi = $this->buildAdminApi(); + $adminApi->auth($this->username, $this->password); + + $delegateResponse = $adminApi->delegateAuth(new AccountSelector(AccountBy::NAME, $accountName)); + $token = $delegateResponse->getAuthToken(); + $expiration = $delegateResponse->getLifetime(); + $expirationTime = $this->clock->now()->add(new \DateInterval('PT'.$expiration.'S')); + $this->tokenCache[$accountName] = ['token' => $token, 'expirationTime' => $expirationTime]; + } $apiBy = $this->buildApi(); $apiBy->setAuthToken($token);