From da0ea8416e0211fc9d5d7d138b8160d5d7a4d4de Mon Sep 17 00:00:00 2001 From: pfleu <fleutotp@gmail.com> Date: Wed, 8 Feb 2023 11:52:39 +0100 Subject: [PATCH] Ajout d'un module de gestion des applis clientes --- config/packages/security.yaml | 12 +- src/Controller/OauthClientController.php | 134 ++++++++++++++++++ src/Controller/RegistrationController.php | 2 +- src/Form/ClientProfileType.php | 95 +++++++++++++ src/Form/ClientType.php | 102 +++++++++++++ templates/lexicon/index.html.twig | 2 +- templates/nav.html.twig | 7 +- templates/oauth_client/_delete_form.html.twig | 4 + templates/oauth_client/index.html.twig | 61 ++++++++ 9 files changed, 409 insertions(+), 10 deletions(-) create mode 100644 src/Controller/OauthClientController.php create mode 100644 src/Form/ClientProfileType.php create mode 100644 src/Form/ClientType.php create mode 100644 templates/oauth_client/_delete_form.html.twig create mode 100644 templates/oauth_client/index.html.twig diff --git a/config/packages/security.yaml b/config/packages/security.yaml index c1989d4..26f884b 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -18,12 +18,12 @@ security: api_token: pattern: ^/api/token$ security: false -# api: -# pattern: ^/api(?!/doc$) # Accepts routes under /api except /api/doc (pour api/doc, on utilisera donc le firewall "main" ce qui permettra d'accéder au swagger quand on est authentifié via Session PHP avec le role Admin -# security: true -# stateless: true # Pas d'authentification, pas de session utilisateur (mais le compte user est vérifié via le token) -# oauth2: true -# user_checker: App\Security\EasyUserChecker + api: + pattern: ^/api(?!/doc$) # Accepts routes under /api except /api/doc (pour api/doc, on utilisera donc le firewall "main" ce qui permettra d'accéder au swagger quand on est authentifié via Session PHP avec le role Admin + security: true + stateless: true # Pas d'authentification, pas de session utilisateur (mais le compte user est vérifié via le token) + oauth2: true + user_checker: App\Security\EasyUserChecker dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ diff --git a/src/Controller/OauthClientController.php b/src/Controller/OauthClientController.php new file mode 100644 index 0000000..8b391f8 --- /dev/null +++ b/src/Controller/OauthClientController.php @@ -0,0 +1,134 @@ +<?php + +namespace App\Controller; + +use App\Entity\Entry; +use App\Entity\Log; +use App\Entity\OAuth2ClientProfile; +use App\Form\ClientProfileType; +use App\Repository\OAuth2ClientProfileRepository; +use League\Bundle\OAuth2ServerBundle\Model\Client; +use Doctrine\Persistence\ManagerRegistry; +use League\Bundle\OAuth2ServerBundle\Repository\ClientRepository; +use League\Bundle\OAuth2ServerBundle\ValueObject\Grant; +use League\Bundle\OAuth2ServerBundle\ValueObject\RedirectUri; +use League\Bundle\OAuth2ServerBundle\ValueObject\Scope; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +/** + * Paramétrage des applications clientes autorisées + * + * @Route("/oauth-clients") + * @IsGranted("ROLE_ADMIN") + */ +class OauthClientController extends AbstractController +{ + /** + * @Route("/", name="app_client_index", methods={"GET"}) + */ + public function index(OAuth2ClientProfileRepository $oAuth2ClientProfileRepository): Response + { + return $this->render('oauth_client/index.html.twig', [ + 'clientProfiles' => $oAuth2ClientProfileRepository->findAll(), + ]); + } + + /** + * @Route("/créer", name="app_client_new", methods={"GET", "POST"}) + */ + public function new(Request $request, ManagerRegistry $managerRegistry): Response + { + $clientProfile = new OAuth2ClientProfile(); + $form = $this->createForm(ClientProfileType::class, $clientProfile); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $client = new Client($form->get('client_name')->getData(), $form->get('identifier')->getData(), $form->get('secret')->getData()); + + $client = $this->updateClient($client, $form); + + $clientProfile->setClient($client); + + $managerRegistry->getManager()->persist($clientProfile); + $managerRegistry->getManager()->flush(); + + return $this->redirectToRoute('app_client_index'); + } + + return $this->render('genericForm.html.twig', [ + 'title' => "Créer un client", + 'form' => $form->createView(), + 'back_url' => $this->generateUrl('app_client_index'), + ]); + } + + /** + * @Route("/{id}/modifier", name="app_client_edit", methods={"GET", "POST"}) + */ + public function edit(Request $request, OAuth2ClientProfile $clientProfile, ManagerRegistry $managerRegistry): Response + { + $client = $clientProfile->getClient(); + $form = $this->createForm(ClientProfileType::class, $clientProfile); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $client->setName($form->get('client_name')->getData()); + $client = $this->updateClient($client, $form); + $managerRegistry->getManager()->flush(); + + return $this->redirectToRoute('app_client_index'); + } + + return $this->render('genericForm.html.twig', [ + 'title' => "Modifier un client", + 'form' => $form->createView(), + 'back_url' => $this->generateUrl('app_client_index'), + ]); + } + + private function updateClient(Client $client, FormInterface $form) + { + $uris = []; + foreach (array_map('trim', explode("\n", $form->get('redirectUris')->getData())) as $string) { + $redirectUri = new RedirectUri($string); + $uris[] = $redirectUri; + } + $client->setRedirectUris(...$uris); + + $scopes = []; + foreach (array_map('trim', explode("\n", $form->get('scopes')->getData())) as $string) { + $scope = new Scope($string); + $scopes[] = $scope; + } + $client->setScopes(...$scopes); + + $grants = []; + foreach (array_map('trim', explode("\n", $form->get('grants')->getData())) as $string) { + $grant = new Grant($string); + $grants[] = $grant; + } + $client->setGrants(...$grants); + + $client->setActive($form->get('active')->getData()); + + return $client; + } + + /** + * @Route("/{id}/supprimer", name="app_client_delete", methods={"POST"}) + */ + public function delete(Request $request, OAuth2ClientProfile $clientProfile, OAuth2ClientProfileRepository $oAuth2ClientProfileRepository): Response + { + if ($this->isCsrfTokenValid('delete'.$clientProfile->getId(), $request->request->get('_token'))) { + $oAuth2ClientProfileRepository->remove($clientProfile, true); + } + + return $this->redirectToRoute('app_client_index'); + } + +} diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php index d0a2a3e..6d2b4ad 100644 --- a/src/Controller/RegistrationController.php +++ b/src/Controller/RegistrationController.php @@ -64,7 +64,7 @@ class RegistrationController extends AbstractController ); $this->sendRegistrationEmail($mailer, $user, $signatureComponents->getSignedUrl()); - $this->addFlash('success', sprintf("Un email de vérification vous a été envoyé. Veuillez cliquer sur le lien inclus pour finaliser al création de votre compte avant de vous connecter.")); + $this->addFlash('success', sprintf("Un email de vérification vous a été envoyé. Veuillez cliquer sur le lien inclus pour finaliser la création de votre compte avant de vous connecter.")); return $this->redirectToRoute('app_login'); } diff --git a/src/Form/ClientProfileType.php b/src/Form/ClientProfileType.php new file mode 100644 index 0000000..68cd1ca --- /dev/null +++ b/src/Form/ClientProfileType.php @@ -0,0 +1,95 @@ +<?php + +namespace App\Form; + +use App\Entity\GraphyList; +use App\Entity\Group; +use App\Entity\OAuth2ClientProfile; +use App\Entity\User; +use App\Languages\LanguagesIso; +use League\Bundle\OAuth2ServerBundle\ValueObject\RedirectUri; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class ClientProfileType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $client = $builder->getData()->getClient(); + + $builder + ->add('name', TextType::class, [ + 'label' => 'Profile Name', + ]) + ->add('description', TextareaType::class, [ + 'label' => 'Profile Description', + ]) + ->add('client_name', TextType::class, [ + 'mapped' => false, + 'data' => $client ? $client->getName() : '', + ]); + + if (!$builder->getData()->getId()) { + $builder + ->add('identifier', TextType::class, [ + 'mapped' => false, + ]) + ->add('secret', TextType::class, [ + 'mapped' => false, + ]); + } + + $builder + ->add('redirectUris', TextareaType::class, [ + 'mapped' => false, + 'label' => 'redirect uris (separate items with new lines)', + 'data' => $client ? implode("\n", $client->getRedirectUris()) : '', + ]) + ->add('grants', TextareaType::class, [ + 'mapped' => false, + 'label' => 'grants (separate items with new lines)', + 'data' => $client ? implode("\n", $client->getGrants()) : '', + ]) + ->add('scopes', TextareaType::class, [ + 'mapped' => false, + 'label' => 'scopes (separate items with new lines)', + 'data' => $client ? implode("\n", $client->getScopes()) : '', + ]) + ->add('active', CheckboxType::class, [ + 'mapped' => false, + 'data' => ($client && $client->isActive()) ? true : false, + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'Enregistrer', + ]) + ; + + // Ajout de contraintes + $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) { + $form = $event->getForm(); + foreach (array_map('trim', explode("\n", $form->get('redirectUris')->getData())) as $string) { + if (!filter_var($string, \FILTER_VALIDATE_URL)) { + $form->get('redirectUris')->addError(new FormError(sprintf('The \'%s\' string is not a valid URI.', $string))); + } + } + }); + + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => OAuth2ClientProfile::class, + ]); + } +} diff --git a/src/Form/ClientType.php b/src/Form/ClientType.php new file mode 100644 index 0000000..aa3a07d --- /dev/null +++ b/src/Form/ClientType.php @@ -0,0 +1,102 @@ +<?php + +namespace App\Form; + +use App\Entity\GraphyList; +use App\Entity\Group; +use App\Entity\OAuth2ClientProfile; +use App\Entity\User; +use App\Languages\LanguagesIso; +use League\Bundle\OAuth2ServerBundle\Model\Client; +use League\Bundle\OAuth2ServerBundle\ValueObject\RedirectUri; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class ClientType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('identifier', null, [ + 'mapped' => false, + ]) + ->add('name', null, [ + 'mapped' => false, + ]) + ->add('secret', null, [ + 'mapped' => false, + ]) + ->add('redirectUris', TextareaType::class, [ + 'mapped' => false, + 'label' => 'redirect uris (separate items with new lines)', + ]) + ->add('grants', TextareaType::class, [ + 'label' => 'grants (separate items with new lines)', + ]) + ->add('scopes', TextareaType::class, [ + 'label' => 'scopes (separate items with new lines)', + ]) + ->add('active') + ; + + $stringToArrayTransformer = new CallbackTransformer( + function ($listAsArray) { + // transform the array to a string + if ($listAsArray) { + return implode("\n", $listAsArray); + } else { + return ''; + } + }, + function ($listAsString) { + // transform the string back to an array + if (empty($listAsString)) { + return array(); + } else { + return array_map('trim', explode("\n", $listAsString)); + } + } + ); + +// $stringToRedirectUriArrayTransformer = new CallbackTransformer( +// function ($listAsArray) { +// // transform the array of redirectUris to a string +// if ($listAsArray) { +// return implode("\n", $listAsArray); +// } else { +// return ''; +// } +// }, +// function ($listAsString) { +// // transform the string back to an array of redirectUris +// if (empty($listAsString)) { +// return array(); +// } else { +// $result = []; +// foreach (array_map('trim', explode("\n", $listAsString)) as $string) { +// $redirectUri = new RedirectUri($string); +// $result[] = $redirectUri; +// } +// return ...$result; +// } +// } +// ); + + $builder->get('scopes')->addModelTransformer($stringToArrayTransformer); + $builder->get('grants')->addModelTransformer($stringToArrayTransformer); + + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Client::class, + ]); + } +} diff --git a/templates/lexicon/index.html.twig b/templates/lexicon/index.html.twig index 14ec2ae..8cb1c25 100644 --- a/templates/lexicon/index.html.twig +++ b/templates/lexicon/index.html.twig @@ -54,4 +54,4 @@ </div> </div> -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/nav.html.twig b/templates/nav.html.twig index c01583e..33dfb46 100644 --- a/templates/nav.html.twig +++ b/templates/nav.html.twig @@ -18,10 +18,13 @@ {% if is_granted('ROLE_ADMIN') %} <li class="nav-item"> - <a class="nav-link {{ 'app_user_index' in app.request.attributes.get('_route') ? 'active' }}" href="{{ path('app_user_index') }}">Utilisateurs</a> + <a class="nav-link {{ 'app_user' in app.request.attributes.get('_route') ? 'active' }}" href="{{ path('app_user_index') }}">Utilisateurs</a> </li> <li class="nav-item"> - <a class="nav-link {{ 'app_lexicon_index' in app.request.attributes.get('_route') ? 'active' }}" href="{{ path('app_lexicon_index') }}">Lexiques</a> + <a class="nav-link {{ 'app_lexicon' in app.request.attributes.get('_route') ? 'active' }}" href="{{ path('app_lexicon_index') }}">Lexiques</a> + </li> + <li class="nav-item"> + <a class="nav-link {{ 'app_client' in app.request.attributes.get('_route') ? 'active' }}" href="{{ path('app_client_index') }}">Applis clientes</a> </li> <li class="nav-item"> <a class="nav-link" href="{{ path('app.swagger_ui') }}">Swagger</a> diff --git a/templates/oauth_client/_delete_form.html.twig b/templates/oauth_client/_delete_form.html.twig new file mode 100644 index 0000000..5286fc9 --- /dev/null +++ b/templates/oauth_client/_delete_form.html.twig @@ -0,0 +1,4 @@ +<form method="post" action="{{ path('app_client_delete', {'id': clientProfile.id}) }}" onsubmit="return confirm('Confirmer la suppression ?');"> + <input type="hidden" name="_token" value="{{ csrf_token('delete' ~ clientProfile.id) }}"> + <button title="Supprimer" class="btn btn-xs btn-danger"><i class="bi-x-circle"></i></button> +</form> diff --git a/templates/oauth_client/index.html.twig b/templates/oauth_client/index.html.twig new file mode 100644 index 0000000..e4d706d --- /dev/null +++ b/templates/oauth_client/index.html.twig @@ -0,0 +1,61 @@ +{% extends 'base.html.twig' %} +{% block container %}container-fluid{% endblock %} + +{% block title %}Clients{% endblock %} + +{% block body %} + + <div class="row justify-content-center m-5"> + <div class="col-md-12"> + <h3 class="card-title d-flex justify-content-between"> + Clients + <a href="{{ path('app_client_new') }}" class="btn btn-dark"><i class="bi-plus"></i> Créer</a> + </h3> + <div class="card mt-3"> + <div class="card-body"> + <table class="table"> + <thead> + <tr> + <th>Client profile name</th> + <th>Client profile description</th> + <th>Client Identifier</th> + <th>Client Name</th> + <th>Client Secret</th> + <th>Client Redirect Uris</th> + <th>Client Grants</th> + <th>Client Scopes</th> + <th>Client Active</th> + <th></th> + </tr> + </thead> + <tbody> + {% for clientProfile in clientProfiles %} + {% set client = clientProfile.client %} + <tr> + <td>{{ clientProfile.name }}</td> + <td>{{ clientProfile.description }}</td> + <td>{{ client.identifier }}</td> + <td>{{ client.name }}</td> + <td>{{ client.secret }}</td> + <td>{{ client.redirectUris|join('<br>')|raw }}</td> + <td>{{ client.grants|join('<br>')|raw }}</td> + <td>{{ client.scopes|join('<br>')|raw }}</td> + <td>{{ client.active ? 'Yes' : 'No' }}</td> + <td class="text-end"> + <a href="{{ path('app_client_edit', {'id': clientProfile.id}) }}" class="btn btn-dark btn-xs">Modifier</a> + <div class="d-inline-block">{{ include('oauth_client/_delete_form.html.twig') }}</div> + </td> + </tr> + {% else %} + <tr> + <td colspan="9">Aucun client</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> + </div> + +{% endblock %} \ No newline at end of file -- GitLab