msgraph: add metadata to users to connect with default calendar

This commit is contained in:
Julien Fastré 2022-05-06 10:13:32 +02:00
parent 5331f1becc
commit 9935af0497
9 changed files with 181 additions and 12 deletions

View File

@ -40,13 +40,10 @@ class AzureGetMachineAccessTokenCommand extends Command
protected function configure() protected function configure()
{ {
$this
->addOption('tenant', 't', InputOption::VALUE_OPTIONAL, 'the tenant, usually the application name', 'common');
} }
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output)
{ {
$this->azure->tenant = $input->getOption('tenant');
$this->azure->scope = ['https://graph.microsoft.com/.default']; $this->azure->scope = ['https://graph.microsoft.com/.default'];
$authorizationUrl = explode('?', $this->azure->getAuthorizationUrl(['prompt' => 'consent'])); $authorizationUrl = explode('?', $this->azure->getAuthorizationUrl(['prompt' => 'consent']));
// replace the first part by the admin consent authorization url // replace the first part by the admin consent authorization url

View File

@ -0,0 +1,50 @@
<?php
namespace Chill\CalendarBundle\Command;
use Chill\CalendarBundle\Synchro\Connector\MSGraph\MachineTokenStorage;
use Chill\CalendarBundle\Synchro\Connector\MSGraphRemoteCalendarConnector;
use Chill\MainBundle\Repository\UserRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MapUserCalendarCommand extends Command
{
private MSGraphRemoteCalendarConnector $remoteCalendarConnector;
private UserRepository $userRepository;
public function __construct(MSGraphRemoteCalendarConnector $remoteCalendarConnector)
{
parent::__construct('chill:calendar:map-user');
$this->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;
}
}

View File

@ -20,6 +20,8 @@ class MSGraphClient
private MSGraphTokenStorage $tokenStorage; private MSGraphTokenStorage $tokenStorage;
private MachineTokenStorage $machineTokenStorage;
/** /**
* @param mixed $calendar * @param mixed $calendar
* *
@ -36,4 +38,6 @@ class MSGraphClient
return $response; return $response;
} }
} }

View File

@ -0,0 +1,46 @@
<?php
namespace Chill\CalendarBundle\Synchro\Connector\MSGraph;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
class MachineHttpClient implements HttpClientInterface
{
private HttpClientInterface $decoratedClient;
private MachineTokenStorage $machineTokenStorage;
use BearerAuthorizationTrait;
/**
* @param HttpClientInterface $decoratedClient
*/
public function __construct(MachineTokenStorage $machineTokenStorage, ?HttpClientInterface $decoratedClient = null)
{
$this->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);
}
}

View File

@ -12,6 +12,8 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Synchro\Connector\MSGraph; namespace Chill\CalendarBundle\Synchro\Connector\MSGraph;
use Chill\MainBundle\Redis\ChillRedis; use Chill\MainBundle\Redis\ChillRedis;
use League\OAuth2\Client\Token\AccessTokenInterface;
use TheNetworg\OAuth2\Client\Provider\Azure;
use TheNetworg\OAuth2\Client\Token\AccessToken; use TheNetworg\OAuth2\Client\Token\AccessToken;
class MachineTokenStorage class MachineTokenStorage
@ -20,14 +22,29 @@ class MachineTokenStorage
private ChillRedis $chillRedis; 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; $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 public function storeToken(AccessToken $token): void

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Synchro\Connector; 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\MSGraphClient;
use Chill\CalendarBundle\Synchro\Connector\MSGraph\MSGraphTokenStorage; use Chill\CalendarBundle\Synchro\Connector\MSGraph\MSGraphTokenStorage;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
@ -18,21 +19,26 @@ use DateTimeImmutable;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use function Amp\Iterator\toArray;
class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
{ {
private MSGraphClient $client; private MSGraphClient $client;
private MachineHttpClient $machineHttpClient;
private MSGraphTokenStorage $tokenStorage; private MSGraphTokenStorage $tokenStorage;
private UrlGeneratorInterface $urlGenerator; private UrlGeneratorInterface $urlGenerator;
public function __construct( public function __construct(
MachineHttpClient $machineHttpClient,
MSGraphClient $client, MSGraphClient $client,
MSGraphTokenStorage $tokenStorage, MSGraphTokenStorage $tokenStorage,
UrlGeneratorInterface $urlGenerator UrlGeneratorInterface $urlGenerator
) { ) {
$this->client = $client; $this->client = $client;
$this->machineHttpClient = $machineHttpClient;
$this->tokenStorage = $tokenStorage; $this->tokenStorage = $tokenStorage;
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
} }
@ -52,4 +58,18 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
{ {
return $this->client->listEventsForUserCalendar($user->getEmail(), $startDate, $endDate); 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'];
}
} }

View File

@ -15,8 +15,10 @@ use Chill\CalendarBundle\Synchro\Connector\MSGraphRemoteCalendarConnector;
use Chill\CalendarBundle\Synchro\Connector\NullRemoteCalendarConnector; use Chill\CalendarBundle\Synchro\Connector\NullRemoteCalendarConnector;
use Chill\CalendarBundle\Synchro\Connector\RemoteCalendarConnectorInterface; use Chill\CalendarBundle\Synchro\Connector\RemoteCalendarConnectorInterface;
use RuntimeException; use RuntimeException;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use TheNetworg\OAuth2\Client\Provider\Azure;
class RemoteCalendarCompilerPass implements CompilerPassInterface class RemoteCalendarCompilerPass implements CompilerPassInterface
{ {
@ -30,6 +32,10 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface
} else { } else {
if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) { if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) {
$connector = MSGraphRemoteCalendarConnector::class; $connector = MSGraphRemoteCalendarConnector::class;
if (!$container->hasAlias(Azure::class)) {
$container->setAlias(Azure::class, 'knpu.oauth2.provider.azure');
}
} }
} }

View File

@ -350,15 +350,20 @@ class User implements AdvancedUserInterface
} }
/** /**
* Set attributes. * Merge the attributes with existing attributes.
* *
* @param array $attributes * Only the key provided will be created or updated.
*
* @return Report
*/ */
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; return $this;
} }

View File

@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
@ -157,6 +158,29 @@ final class UserRepository implements ObjectRepository
return $qb->getQuery()->getResult(); 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() public function getClassName()
{ {
return User::class; return User::class;