diff --git a/src/Bundle/ChillCalendarBundle/Command/AzureGetMachineAccessTokenCommand.php b/src/Bundle/ChillCalendarBundle/Command/AzureGetMachineAccessTokenCommand.php index f59f188f8..e854fe17d 100644 --- a/src/Bundle/ChillCalendarBundle/Command/AzureGetMachineAccessTokenCommand.php +++ b/src/Bundle/ChillCalendarBundle/Command/AzureGetMachineAccessTokenCommand.php @@ -40,13 +40,10 @@ class AzureGetMachineAccessTokenCommand extends Command protected function configure() { - $this - ->addOption('tenant', 't', InputOption::VALUE_OPTIONAL, 'the tenant, usually the application name', 'common'); } protected function execute(InputInterface $input, OutputInterface $output) { - $this->azure->tenant = $input->getOption('tenant'); $this->azure->scope = ['https://graph.microsoft.com/.default']; $authorizationUrl = explode('?', $this->azure->getAuthorizationUrl(['prompt' => 'consent'])); // replace the first part by the admin consent authorization url diff --git a/src/Bundle/ChillCalendarBundle/Command/MapUserCalendarCommand.php b/src/Bundle/ChillCalendarBundle/Command/MapUserCalendarCommand.php new file mode 100644 index 000000000..9a150727f --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Command/MapUserCalendarCommand.php @@ -0,0 +1,50 @@ +remoteCalendarConnector = $remoteCalendarConnector; + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $limit = 2; + + do { + $users = $this->userRepository->findByNotHavingAttribute('ms:graph', $limit); + + foreach ($users as $user) { + $usersData = $this->remoteCalendarConnector->getUserByEmail($user->getEmailCanonical()); + + $defaultCalendar + + $user->setAttributes(['ms:graph' => [ + + ]]); + } + + } while (count($users) === $limit); + + + + return 0; + } + + +} diff --git a/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraph/MSGraphClient.php b/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraph/MSGraphClient.php index 4391435ce..d43156828 100644 --- a/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraph/MSGraphClient.php +++ b/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraph/MSGraphClient.php @@ -20,6 +20,8 @@ class MSGraphClient private MSGraphTokenStorage $tokenStorage; + private MachineTokenStorage $machineTokenStorage; + /** * @param mixed $calendar * @@ -36,4 +38,6 @@ class MSGraphClient return $response; } + + } diff --git a/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraph/MachineHttpClient.php b/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraph/MachineHttpClient.php new file mode 100644 index 000000000..a410f1c59 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraph/MachineHttpClient.php @@ -0,0 +1,46 @@ +decoratedClient = $decoratedClient ?? \Symfony\Component\HttpClient\HttpClient::create(); + $this->machineTokenStorage = $machineTokenStorage; + } + + public function request(string $method, string $url, array $options = []): ResponseInterface + { + $options['headers'] = array_merge( + $options['headers'] ?? [], + //['Content-Type' => 'application/json'], + $this->getAuthorizationHeaders($this->machineTokenStorage->getToken()) + ); + $options['base_uri'] = 'https://graph.microsoft.com/v1.0/'; + + dump($options); + + return $this->decoratedClient->request($method, $url, $options); + } + + public function stream($responses, float $timeout = null): ResponseStreamInterface + { + return $this->decoratedClient->stream($responses, $timeout); + } +} diff --git a/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraph/MachineTokenStorage.php b/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraph/MachineTokenStorage.php index 89166874d..1b007932e 100644 --- a/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraph/MachineTokenStorage.php +++ b/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraph/MachineTokenStorage.php @@ -12,6 +12,8 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Synchro\Connector\MSGraph; use Chill\MainBundle\Redis\ChillRedis; +use League\OAuth2\Client\Token\AccessTokenInterface; +use TheNetworg\OAuth2\Client\Provider\Azure; use TheNetworg\OAuth2\Client\Token\AccessToken; class MachineTokenStorage @@ -20,14 +22,29 @@ class MachineTokenStorage private ChillRedis $chillRedis; - public function __construct(ChillRedis $chillRedis) + private Azure $azure; + + private ?AccessTokenInterface $accessToken = null; + + public function __construct(Azure $azure, ChillRedis $chillRedis) { + $this->azure = $azure; $this->chillRedis = $chillRedis; } - public function getToken(): AccessToken + public function getToken(): AccessTokenInterface { - return unserialize($this->chillRedis->get(self::KEY)); + if (null === $this->accessToken || $this->accessToken->hasExpired()) { + $this->accessToken = $this->azure->getAccessToken('client_credentials', [ + 'scope' => 'https://graph.microsoft.com/.default', + ]); + } + + dump($this->accessToken); + + return $this->accessToken; + + //return unserialize($this->chillRedis->get(self::KEY)); } public function storeToken(AccessToken $token): void diff --git a/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraphRemoteCalendarConnector.php b/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraphRemoteCalendarConnector.php index 7cdf37205..69edb017e 100644 --- a/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraphRemoteCalendarConnector.php +++ b/src/Bundle/ChillCalendarBundle/Synchro/Connector/MSGraphRemoteCalendarConnector.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Synchro\Connector; +use Chill\CalendarBundle\Synchro\Connector\MSGraph\MachineHttpClient; use Chill\CalendarBundle\Synchro\Connector\MSGraph\MSGraphClient; use Chill\CalendarBundle\Synchro\Connector\MSGraph\MSGraphTokenStorage; use Chill\MainBundle\Entity\User; @@ -18,21 +19,26 @@ use DateTimeImmutable; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use function Amp\Iterator\toArray; class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface { private MSGraphClient $client; + private MachineHttpClient $machineHttpClient; + private MSGraphTokenStorage $tokenStorage; private UrlGeneratorInterface $urlGenerator; public function __construct( + MachineHttpClient $machineHttpClient, MSGraphClient $client, MSGraphTokenStorage $tokenStorage, UrlGeneratorInterface $urlGenerator ) { $this->client = $client; + $this->machineHttpClient = $machineHttpClient; $this->tokenStorage = $tokenStorage; $this->urlGenerator = $urlGenerator; } @@ -52,4 +58,18 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface { return $this->client->listEventsForUserCalendar($user->getEmail(), $startDate, $endDate); } + + public function getUserByEmail(string $email): array + { + return $this->machineHttpClient->request('GET', 'users', [ + 'query' => ['$filter' => "mail eq '${email}'"], + ])->toArray()['value']; + } + + public function getDefaultUserCalendar(string $idOrUserPrincipalName): array + { + return $this->machineHttpClient->request('GET', "users/$idOrUserPrincipalName/calendars", [ + 'query' => ['$filter' => 'isDefaultCalendar eq true'], + ])->toArray()['value']; + } } diff --git a/src/Bundle/ChillCalendarBundle/Synchro/DependencyInjection/RemoteCalendarCompilerPass.php b/src/Bundle/ChillCalendarBundle/Synchro/DependencyInjection/RemoteCalendarCompilerPass.php index 9c83bdebe..dfabf0dbb 100644 --- a/src/Bundle/ChillCalendarBundle/Synchro/DependencyInjection/RemoteCalendarCompilerPass.php +++ b/src/Bundle/ChillCalendarBundle/Synchro/DependencyInjection/RemoteCalendarCompilerPass.php @@ -15,8 +15,10 @@ use Chill\CalendarBundle\Synchro\Connector\MSGraphRemoteCalendarConnector; use Chill\CalendarBundle\Synchro\Connector\NullRemoteCalendarConnector; use Chill\CalendarBundle\Synchro\Connector\RemoteCalendarConnectorInterface; use RuntimeException; +use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use TheNetworg\OAuth2\Client\Provider\Azure; class RemoteCalendarCompilerPass implements CompilerPassInterface { @@ -30,6 +32,10 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface } else { if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) { $connector = MSGraphRemoteCalendarConnector::class; + + if (!$container->hasAlias(Azure::class)) { + $container->setAlias(Azure::class, 'knpu.oauth2.provider.azure'); + } } } diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index 76fd7196d..25d8c29e9 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -350,15 +350,20 @@ class User implements AdvancedUserInterface } /** - * Set attributes. + * Merge the attributes with existing attributes. * - * @param array $attributes - * - * @return Report + * Only the key provided will be created or updated. */ - public function setAttributes($attributes) + public function setAttributes(array $attributes): self { - $this->attributes = $attributes; + $this->attributes = array_merge($this->attributes, $attributes); + + return $this; + } + + public function unsetAttribute($key): self + { + unset($this->attributes[$key]); return $this; } diff --git a/src/Bundle/ChillMainBundle/Repository/UserRepository.php b/src/Bundle/ChillMainBundle/Repository/UserRepository.php index fc3a6e187..43428fa9c 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserRepository.php @@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\GroupCenter; use Chill\MainBundle\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; @@ -157,6 +158,29 @@ final class UserRepository implements ObjectRepository return $qb->getQuery()->getResult(); } + /** + * Find users which does not have a key on attribute column + * + * @return array|User[] + */ + public function findByNotHavingAttribute(string $key, ?int $limit = null, ?int $offset = null): array + { + $rsm = new ResultSetMappingBuilder($this->entityManager); + $rsm->addRootEntityFromClassMetadata(User::class, 'u'); + + $sql = "SELECT ".$rsm->generateSelectClause()." FROM users u WHERE NOT attributes ? :key OR attributes IS NULL AND enabled IS TRUE"; + + if (null !== $limit) { + $sql .= " LIMIT $limit"; + } + + if (null !== $offset) { + $sql .= " OFFET $offset"; + } + + return $this->entityManager->createNativeQuery($sql, $rsm)->setParameter(':key', $key)->getResult(); + } + public function getClassName() { return User::class;