From 5db75e007c41412aac1ecbf8870d5902298ef7e8 Mon Sep 17 00:00:00 2001 From: Uladzimir Tsykun Date: Sun, 15 Sep 2024 14:55:54 +0200 Subject: [PATCH] Allow to generate multiple credentials for API-only users --- src/Controller/UserController.php | 38 +++++++++++++++------ src/Form/Type/ApiTokenType.php | 13 ++++--- src/Resolver/ControllerArgumentResolver.php | 3 ++ templates/user/profile.html.twig | 7 ++++ templates/user/token_list.html.twig | 8 +++-- 5 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index dc4dcc43..c135cfdd 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -218,15 +218,16 @@ protected function handleUpdate(Request $request, User $user, $flashMessage) } #[Route('/profile/tokens', name: 'profile_list_tokens')] - public function tokenList(PatTokenManager $manager) + #[Route('/users/{name}/tokens', name: 'users_list_tokens')] + public function tokenList(PatTokenManager $manager, #[Vars(['name' => 'username'])] ?User $user = null) { - $user = $this->getUser(); + $user = $this->getPatTokenUser($user); $tokens = $this->registry->getRepository(ApiToken::class)->findAllTokens($user); foreach ($tokens as $token) { $token->setAttributes($manager->getStats($token->getId())); } - return $this->render('user/token_list.html.twig', ['tokens' => $tokens]); + return $this->render('user/token_list.html.twig', ['tokens' => $tokens, 'user' => $user]); } #[Route('/users/sessions/list', name: 'users_login_attempts_all')] @@ -261,9 +262,11 @@ public function loginAttemptsAction(#[Vars(['name' => 'username'])] ?User $user } #[Route('/profile/tokens/{id}/delete', name: 'profile_remove_tokens', methods: ['POST'])] - public function tokenDelete(#[Vars] ApiToken $token) + #[Route('/users/{name}/tokens/{id}/delete', name: 'users_remove_tokens', methods: ['POST'])] + public function tokenDelete(#[Vars] ApiToken $token, #[Vars(['name' => 'username'])] ?User $user = null) { - $user = $this->getUser(); + $user = $this->getPatTokenUser($user); + $identifier = $token->getOwner() ? $token->getOwner()->getUserIdentifier() : $token->getUserIdentifier(); if ($identifier === $user->getUserIdentifier()) { $this->getEM()->remove($token); @@ -276,18 +279,27 @@ public function tokenDelete(#[Vars] ApiToken $token) throw $this->createNotFoundException('Token not found'); } + protected function getPatTokenUser(?User $user = null): UserInterface + { + if (null !== $user && ($user->isAdmin() || !$this->isGranted('ROLE_ADMIN'))) { + throw $this->createAccessDeniedException(); + } + + return $user ?: $this->getUser(); + } + #[Route('/profile/tokens/new', name: 'profile_add_tokens')] - public function tokenAdd(Request $request) + #[Route('/users/{name}/tokens/new', name: 'users_add_tokens')] + public function tokenAdd(Request $request, #[Vars(['name' => 'username'])] ?User $user = null) { $token = new ApiToken(); - $form = $this->createForm(ApiTokenType::class, $token); + $form = $this->createForm(ApiTokenType::class, $token, ['user' => $user]); + $user = $this->getPatTokenUser($user); if ($request->getMethod() === 'POST') { $em = $this->registry->getManager(); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $user = $this->getUser(); - if ($user instanceof User) { $token->setOwner($user); } else { @@ -298,13 +310,17 @@ public function tokenAdd(Request $request) $em->persist($token); $em->flush(); $this->addFlash('success', 'Token was generated'); - return new RedirectResponse($this->generateUrl('profile_list_tokens')); + + return $user === $this->getUser() ? + new RedirectResponse($this->generateUrl('profile_list_tokens')) : + new RedirectResponse($this->generateUrl('users_list_tokens', ['name' => $user->getUserIdentifier()])); } } return $this->render('user/token_add.html.twig', [ 'form' => $form->createView(), - 'entity' => $token + 'entity' => $token, + 'user' => $user, ]); } diff --git a/src/Form/Type/ApiTokenType.php b/src/Form/Type/ApiTokenType.php index e79d11b7..bf2b1a0a 100644 --- a/src/Form/Type/ApiTokenType.php +++ b/src/Form/Type/ApiTokenType.php @@ -5,6 +5,7 @@ namespace Packeton\Form\Type; use Packeton\Entity\ApiToken; +use Packeton\Entity\User; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -45,7 +46,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'multiple' => true, 'expanded' => true, 'attr' => ['with_value' => true], - 'choices' => array_flip($this->getScores()) + 'choices' => array_flip($this->getScores($options['user'])) ]); $builder->addEventListener(FormEvents::POST_SUBMIT, $this->postSubmit(...)); @@ -69,17 +70,21 @@ public function postSubmit(FormEvent $event): void public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('data_class', ApiToken::class); + $resolver->setDefault('user', null); } - protected function getScores(): array + protected function getScores(?User $user = null): array { + $asMaintainer = $user?->isMaintainer() || (null === $user && $this->checker->isGranted('ROLE_MAINTAINER')); + $asAdmin = $user?->isAdmin() || (null === $user && $this->checker->isGranted('ROLE_ADMIN')); + $base = [ 'metadata' => 'Read composer packages.json metadata and ZIP archive access', 'mirror:all' => 'Full access to mirrored packages', 'mirror:read' => 'Read-only CI token to mirrored packages', ]; - if ($this->checker->isGranted('ROLE_MAINTAINER')) { + if ($asMaintainer) { $base += [ 'webhooks' => 'Update packages webhook', 'feeds' => 'Atom/RSS feed releases', @@ -87,7 +92,7 @@ protected function getScores(): array 'packages:all' => 'Submit and read packages API', ]; } - if ($this->checker->isGranted('ROLE_ADMIN')) { + if ($asAdmin) { $base += [ 'users' => 'Access to user API', 'groups' => 'Access to groups API', diff --git a/src/Resolver/ControllerArgumentResolver.php b/src/Resolver/ControllerArgumentResolver.php index 6bf97f6a..7a8820fe 100644 --- a/src/Resolver/ControllerArgumentResolver.php +++ b/src/Resolver/ControllerArgumentResolver.php @@ -71,6 +71,9 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable foreach ($mapping as $varName => $value) { if (empty($value)) { + if ($argument->hasDefaultValue()) { + return [$argument->getDefaultValue()]; + } throw new \UnexpectedValueException('Missing "'.$varName.'" in request attributes, cannot resolve $'.$argument->getName()); } } diff --git a/templates/user/profile.html.twig b/templates/user/profile.html.twig index 33949fce..86cebf43 100644 --- a/templates/user/profile.html.twig +++ b/templates/user/profile.html.twig @@ -24,6 +24,13 @@
Edit
+ + {% if is_granted('ROLE_ADMIN') and user.admin == false %} +
+ Pat Tokens +
+ {% endif %} +
{{ csrf_token_input('delete') }} diff --git a/templates/user/token_list.html.twig b/templates/user/token_list.html.twig index a1929c26..98dddd79 100644 --- a/templates/user/token_list.html.twig +++ b/templates/user/token_list.html.twig @@ -4,13 +4,15 @@ {% block title %}Authentication Tokens{% endblock %} {% block content %} + {% set is_profile = app.user.userIdentifier == user.userIdentifier %} +
-

Your Authentication Tokens

+

{% if is_profile %}Your {% else %} {{ user.userIdentifier|capitalize }} {% endif %} Authentication Tokens

All Tokens {% set csrfToken = csrf_token('actions') %}
- Add Token + Add Token
@@ -43,7 +45,7 @@ Show
- +