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()
{
$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

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 MachineTokenStorage $machineTokenStorage;
/**
* @param mixed $calendar
*
@ -36,4 +38,6 @@ class MSGraphClient
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;
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

View File

@ -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'];
}
}

View File

@ -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');
}
}
}

View File

@ -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;
}

View File

@ -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;