Merge branch 'keycloak'

This commit is contained in:
Jan Böhmer 2023-03-04 17:15:50 +01:00
commit 6230ad971b
45 changed files with 1291 additions and 39 deletions

View file

@ -0,0 +1,115 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Command\User;
use App\Entity\UserSystem\User;
use App\Security\SamlUserFactory;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class ConvertToSAMLUserCommand extends Command
{
protected static $defaultName = 'partdb:user:convert-to-saml-user|partdb:users:convert-to-saml-user';
protected EntityManagerInterface $entityManager;
protected bool $saml_enabled;
public function __construct(EntityManagerInterface $entityManager, bool $saml_enabled)
{
parent::__construct();
$this->entityManager = $entityManager;
$this->saml_enabled = $saml_enabled;
}
protected function configure(): void
{
$this
->setDescription('Converts a local user to a SAML user (and vice versa)')
->setHelp('This converts a local user, which can login via the login form, to a SAML user, which can only login via SAML. This is useful if you want to migrate from a local user system to a SAML user system.')
->addArgument('user', InputArgument::REQUIRED, 'The username (or email) of the user')
->addOption('to-local', null, InputOption::VALUE_NONE, 'Converts a SAML user to a local user')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$user_name = $input->getArgument('user');
$to_local = $input->getOption('to-local');
if (!$this->saml_enabled && !$to_local) {
$io->confirm('SAML login is not configured. You will not be able to login with this user anymore, when SSO is not configured. Do you really want to continue?');
}
/** @var User $user */
$user = $this->entityManager->getRepository(User::class)->findByEmailOrName($user_name);
if (!$user) {
$io->error('User not found!');
return 1;
}
$io->info('User found: '.$user->getFullName(true) . ': '.$user->getEmail().' [ID: ' . $user->getID() . ']');
if ($to_local) {
return $this->toLocal($user, $io);
}
return $this->toSAML($user, $io);
}
public function toLocal(User $user, SymfonyStyle $io): int
{
$io->confirm('You are going to convert a SAML user to a local user. This means, that the user can only login via the login form. '
. 'The permissions and groups settings of the user will remain unchanged. Do you really want to continue?');
$user->setSAMLUser(false);
$user->setPassword(SamlUserFactory::SAML_PASSWORD_PLACEHOLDER);
$this->entityManager->flush();
$io->success('User converted to local user! You will need to set a password for this user, before you can login with it.');
return 0;
}
public function toSAML(User $user, SymfonyStyle $io): int
{
$io->confirm('You are going to convert a local user to a SAML user. This means, that the user can only login via SAML afterwards. The password in the DB will be removed. '
. 'The permissions and groups settings of the user will remain unchanged. Do you really want to continue?');
$user->setSAMLUser(true);
$user->setPassword(SamlUserFactory::SAML_PASSWORD_PLACEHOLDER);
$this->entityManager->flush();
$io->success('User converted to SAML user! You can now login with this user via SAML.');
return 0;
}
}

View file

@ -56,7 +56,7 @@ class SetPasswordCommand extends Command
$this
->setDescription('Sets the password of a user')
->setHelp('This password allows you to set the password of a user, without knowing the old password.')
->addArgument('user', InputArgument::REQUIRED, 'The name of the user')
->addArgument('user', InputArgument::REQUIRED, 'The username or email of the user')
;
}
@ -65,19 +65,21 @@ class SetPasswordCommand extends Command
$io = new SymfonyStyle($input, $output);
$user_name = $input->getArgument('user');
/** @var User[] $users */
$users = $this->entityManager->getRepository(User::class)->findBy(['name' => $user_name]);
$user = $this->entityManager->getRepository(User::class)->findByEmailOrName($user_name);
if (empty($users)) {
if (!$user) {
$io->error(sprintf('No user with the given username %s found in the database!', $user_name));
return 1;
}
$user = $users[0];
$io->note('User found!');
if ($user->isSamlUser()) {
$io->error('This user is a SAML user, so you can not change the password!');
return 1;
}
$proceed = $io->confirm(
sprintf('You are going to change the password of %s with ID %d. Proceed?',
$user->getFullName(true), $user->getID()));

View file

@ -46,22 +46,39 @@ class UserListCommand extends Command
$this
->setDescription('Lists all users')
->setHelp('This command lists all users in the database.')
->addOption('local', 'l', null, 'Only list local users')
->addOption('saml', 's', null, 'Only list SAML users')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$only_local = $input->getOption('local');
$only_saml = $input->getOption('saml');
//Get all users from database
$users = $this->entityManager->getRepository(User::class)->findAll();
if ($only_local && $only_saml) {
$io->error('You can not use --local and --saml at the same time!');
return Command::FAILURE;
}
$repo = $this->entityManager->getRepository(User::class);
if ($only_local) {
$users = $repo->onlyLocalUsers();
} elseif ($only_saml) {
$users = $repo->onlySAMLUsers();
} else {
$users = $repo->findAll();
}
$io->info(sprintf("Found %d users in database.", count($users)));
$io->title('Users:');
$table = new Table($output);
$table->setHeaders(['ID', 'Username', 'Name', 'Email', 'Group', 'Login Disabled']);
$table->setHeaders(['ID', 'Username', 'Name', 'Email', 'Group', 'Login Disabled', 'Type']);
foreach ($users as $user) {
$table->addRow([
@ -71,6 +88,7 @@ class UserListCommand extends Command
$user->getEmail(),
$user->getGroup() !== null ? $user->getGroup()->getName() . ' (ID: ' . $user->getGroup()->getID() . ')' : 'No group',
$user->isDisabled() ? 'Yes' : 'No',
$user->isSAMLUser() ? 'SAML' : 'Local',
]);
}

View file

@ -57,7 +57,7 @@ class UsersPermissionsCommand extends Command
protected function configure(): void
{
$this
->addArgument('user', InputArgument::REQUIRED, 'The username of the user to view')
->addArgument('user', InputArgument::REQUIRED, 'The username or email of the user to view')
->addOption('noInherit', null, InputOption::VALUE_NONE, 'Do not inherit permissions from groups')
->addOption('edit', '', InputOption::VALUE_NONE, 'Edit the permissions of the user')
;

View file

@ -83,6 +83,10 @@ class UserSettingsController extends AbstractController
return new RuntimeException('This controller only works only for Part-DB User objects!');
}
if ($user->isSamlUser()) {
throw new RuntimeException('You can not remove U2F keys from SAML users!');
}
if (empty($user->getBackupCodes())) {
$this->addFlash('error', 'tfa_backup.no_codes_enabled');
@ -112,6 +116,10 @@ class UserSettingsController extends AbstractController
throw new RuntimeException('This controller only works only for Part-DB User objects!');
}
if ($user->isSamlUser()) {
throw new RuntimeException('You can not remove U2F keys from SAML users!');
}
if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) {
//Handle U2F key removal
if ($request->request->has('key_id')) {
@ -192,6 +200,10 @@ class UserSettingsController extends AbstractController
return new RuntimeException('This controller only works only for Part-DB User objects!');
}
if ($user->isSamlUser()) {
throw new RuntimeException('You can not remove U2F keys from SAML users!');
}
if ($this->isCsrfTokenValid('devices_reset'.$user->getId(), $request->request->get('_token'))) {
$user->invalidateTrustedDeviceTokens();
$entityManager->flush();
@ -281,14 +293,14 @@ class UserSettingsController extends AbstractController
])
->add('old_password', PasswordType::class, [
'label' => 'user.settings.pw_old.label',
'disabled' => $this->demo_mode,
'disabled' => $this->demo_mode || $user->isSamlUser(),
'attr' => [
'autocomplete' => 'current-password',
],
'constraints' => [new UserPassword()],
]) //This constraint checks, if the current user pw was inputted.
->add('new_password', RepeatedType::class, [
'disabled' => $this->demo_mode,
'disabled' => $this->demo_mode || $user->isSamlUser(),
'type' => PasswordType::class,
'first_options' => [
'label' => 'user.settings.pw_new.label',
@ -307,7 +319,10 @@ class UserSettingsController extends AbstractController
'max' => 128,
])],
])
->add('submit', SubmitType::class, ['label' => 'save'])
->add('submit', SubmitType::class, [
'label' => 'save',
'disabled' => $this->demo_mode || $user->isSamlUser(),
])
->getForm();
$pw_form->handleRequest($request);
@ -327,7 +342,9 @@ class UserSettingsController extends AbstractController
}
//Handle 2FA things
$google_form = $this->createForm(TFAGoogleSettingsType::class, $user);
$google_form = $this->createForm(TFAGoogleSettingsType::class, $user, [
'disabled' => $this->demo_mode || $user->isSamlUser(),
]);
$google_enabled = $user->isGoogleAuthenticatorEnabled();
if (!$google_enabled && !$form->isSubmitted()) {
$user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret());
@ -335,7 +352,7 @@ class UserSettingsController extends AbstractController
}
$google_form->handleRequest($request);
if (!$this->demo_mode && $google_form->isSubmitted() && $google_form->isValid()) {
if (!$this->demo_mode && !$user->isSamlUser() && $google_form->isSubmitted() && $google_form->isValid()) {
if (!$google_enabled) {
//Save 2FA settings (save secrets)
$user->setGoogleAuthenticatorSecret($google_form->get('googleAuthenticatorSecret')->getData());
@ -369,7 +386,7 @@ class UserSettingsController extends AbstractController
])->getForm();
$backup_form->handleRequest($request);
if (!$this->demo_mode && $backup_form->isSubmitted() && $backup_form->isValid()) {
if (!$this->demo_mode && !$user->isSamlUser() && $backup_form->isSubmitted() && $backup_form->isValid()) {
$backupCodeManager->regenerateBackupCodes($user);
$em->flush();
$this->addFlash('success', 'user.settings.2fa.backup_codes.regenerated');

View file

@ -20,9 +20,11 @@
namespace App\Controller;
use App\Entity\UserSystem\User;
use App\Entity\UserSystem\WebauthnKey;
use Doctrine\ORM\EntityManagerInterface;
use Jbtronics\TFAWebauthn\Services\TFAWebauthnRegistrationHelper;
use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
@ -31,6 +33,13 @@ use function Symfony\Component\Translation\t;
class WebauthnKeyRegistrationController extends AbstractController
{
private bool $demo_mode;
public function __construct(bool $demo_mode)
{
$this->demo_mode = $demo_mode;
}
/**
* @Route("/webauthn/register", name="webauthn_register")
*/
@ -39,6 +48,20 @@ class WebauthnKeyRegistrationController extends AbstractController
//When user change its settings, he should be logged in fully.
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
if ($this->demo_mode) {
throw new RuntimeException('You can not do 2FA things in demo mode');
}
$user = $this->getUser();
if (!$user instanceof User) {
throw new RuntimeException('This controller only works only for Part-DB User objects!');
}
if ($user->isSamlUser()) {
throw new RuntimeException('You can not remove U2F keys from SAML users!');
}
//If form was submitted, check the auth response
if ($request->getMethod() === 'POST') {
$webauthnResponse = $request->request->get('_auth_code');

View file

@ -30,6 +30,7 @@ use App\Security\Interfaces\HasPermissionsInterface;
use App\Validator\Constraints\Selectable;
use App\Validator\Constraints\ValidPermission;
use App\Validator\Constraints\ValidTheme;
use Hslavich\OneloginSamlBundle\Security\User\SamlUserInterface;
use Jbtronics\TFAWebauthn\Model\LegacyU2FKeyInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Webauthn\PublicKeyCredentialUserEntity;
@ -60,7 +61,8 @@ use Jbtronics\TFAWebauthn\Model\TwoFactorInterface as WebauthnTwoFactorInterface
* @ORM\EntityListeners({"App\EntityListeners\TreeCacheInvalidationListener"})
* @UniqueEntity("name", message="validator.user.username_already_used")
*/
class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, TwoFactorInterface, BackupCodeInterface, TrustedDeviceInterface, WebauthnTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface
class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, TwoFactorInterface,
BackupCodeInterface, TrustedDeviceInterface, WebauthnTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface, SamlUserInterface
{
//use MasterAttachmentTrait;
@ -238,10 +240,16 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
/**
* @var DateTime the time until the password reset token is valid
* @ORM\Column(type="datetime", nullable=true)
* @ORM\Column(type="datetime", nullable=true, columnDefinition="DEFAULT NULL")
*/
protected $pw_reset_expires;
/**
* @var bool True if the user was created by a SAML provider (and therefore cannot change its password)
* @ORM\Column(type="boolean")
*/
protected bool $saml_user = false;
public function __construct()
{
parent::__construct();
@ -298,6 +306,10 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
if ($this->saml_user) {
$roles[] = 'ROLE_SAML_USER';
}
return array_unique($roles);
}
@ -860,4 +872,56 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
{
$this->webauthn_keys->add($webauthnKey);
}
/**
* Returns true, if the user was created by the SAML authentication.
* @return bool
*/
public function isSamlUser(): bool
{
return $this->saml_user;
}
/**
* Sets the saml_user flag.
* @param bool $saml_user
* @return User
*/
public function setSamlUser(bool $saml_user): User
{
$this->saml_user = $saml_user;
return $this;
}
public function setSamlAttributes(array $attributes)
{
//When mail attribute exists, set it
if (isset($attributes['email'])) {
$this->setEmail($attributes['email'][0]);
}
//When first name attribute exists, set it
if (isset($attributes['firstName'])) {
$this->setFirstName($attributes['firstName'][0]);
}
//When last name attribute exists, set it
if (isset($attributes['lastName'])) {
$this->setLastName($attributes['lastName'][0]);
}
if (isset($attributes['department'])) {
$this->setDepartment($attributes['department'][0]);
}
//Use X500 attributes as userinfo
if (isset($attributes['urn:oid:2.5.4.42'])) {
$this->setFirstName($attributes['urn:oid:2.5.4.42'][0]);
}
if (isset($attributes['urn:oid:2.5.4.4'])) {
$this->setLastName($attributes['urn:oid:2.5.4.4'][0]);
}
if (isset($attributes['urn:oid:1.2.840.113549.1.9.1'])) {
$this->setEmail($attributes['urn:oid:1.2.840.113549.1.9.1'][0]);
}
}
}

View file

@ -57,10 +57,11 @@ final class LoginSuccessSubscriber implements EventSubscriberInterface
$ip = $event->getRequest()->getClientIp();
$log = new UserLoginLogEntry($ip, $this->gpdr_compliance);
$user = $event->getAuthenticationToken()->getUser();
if ($user instanceof User) {
if ($user instanceof User && $user->getID()) {
$log->setTargetElement($user);
$this->eventLogger->logAndFlush($log);
}
$this->eventLogger->logAndFlush($log);
$this->flashBag->add('notice', $this->translator->trans('flash.login_successful'));
}

View file

@ -65,7 +65,7 @@ class UserAdminForm extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
/** @var AbstractStructuralDBElement $entity */
/** @var User $entity */
$entity = $options['data'];
$is_new = null === $entity->getID();
@ -164,7 +164,7 @@ class UserAdminForm extends AbstractType
'invalid_message' => 'password_must_match',
'required' => false,
'mapped' => false,
'disabled' => !$this->security->isGranted('set_password', $entity),
'disabled' => !$this->security->isGranted('set_password', $entity) || $entity->isSamlUser(),
'constraints' => [new Length([
'min' => 6,
'max' => 128,
@ -174,7 +174,7 @@ class UserAdminForm extends AbstractType
->add('need_pw_change', CheckboxType::class, [
'required' => false,
'label' => 'user.edit.needs_pw_change',
'disabled' => !$this->security->isGranted('set_password', $entity),
'disabled' => !$this->security->isGranted('set_password', $entity) || $entity->isSamlUser(),
])
->add('disabled', CheckboxType::class, [

View file

@ -57,7 +57,7 @@ class UserSettingsType extends AbstractType
$builder
->add('name', TextType::class, [
'label' => 'user.username.label',
'disabled' => !$this->security->isGranted('edit_username', $options['data']) || $this->demo_mode,
'disabled' => !$this->security->isGranted('edit_username', $options['data']) || $this->demo_mode || $options['data']->isSamlUser(),
])
->add('first_name', TextType::class, [
'required' => false,

View file

@ -45,10 +45,16 @@ class NamedDBElementRepository extends DBElementRepository
$node->setId($entity->getID());
$result[] = $node;
if ($entity instanceof User && $entity->isDisabled()) {
//If this is an user, then add a badge when it is disabled
$node->setIcon('fa-fw fa-treeview fa-solid fa-user-lock text-muted');
if ($entity instanceof User) {
if ($entity->isDisabled()) {
//If this is an user, then add a badge when it is disabled
$node->setIcon('fa-fw fa-treeview fa-solid fa-user-lock text-muted');
}
if ($entity->isSamlUser()) {
$node->setIcon('fa-fw fa-treeview fa-solid fa-house-user text-muted');
}
}
}
return $result;

View file

@ -89,4 +89,26 @@ final class UserRepository extends NamedDBElementRepository implements PasswordU
$this->getEntityManager()->flush();
}
}
/**
* Returns the list of all local users (not SAML users).
* @return User[]
*/
public function onlyLocalUsers(): array
{
return $this->findBy([
'saml_user' => false,
]);
}
/**
* Returns the list of all SAML users.
* @return User[]
*/
public function onlySAMLUsers(): array
{
return $this->findBy([
'saml_user' => true,
]);
}
}

View file

@ -0,0 +1,63 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Security;
use App\Entity\UserSystem\User;
use Hslavich\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
use Symfony\Contracts\Translation\TranslatorInterface;
class EnsureSAMLUserForSAMLLoginChecker implements EventSubscriberInterface
{
private TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public static function getSubscribedEvents()
{
return [
AuthenticationSuccessEvent::class => 'onAuthenticationSuccess',
];
}
public function onAuthenticationSuccess(AuthenticationSuccessEvent $event): void
{
$token = $event->getAuthenticationToken();
$user = $token->getUser();
//If we are using SAML, we need to check that the user is a SAML user.
if ($token instanceof SamlToken) {
if ($user instanceof User && !$user->isSAMLUser()) {
throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_local_user_per_saml', [], 'security'));
}
} else { //Ensure that you can not login locally with a SAML user (even if this should not happen, as the password is not set)
if ($user instanceof User && $user->isSamlUser()) {
throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_saml_user_locally', [], 'security'));
}
}
}
}

View file

@ -0,0 +1,159 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Security;
use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User;
use Doctrine\ORM\EntityManagerInterface;
use Hslavich\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken;
use Hslavich\OneloginSamlBundle\Security\User\SamlUserFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\User\UserInterface;
class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterface
{
private EntityManagerInterface $em;
private array $saml_role_mapping;
private bool $update_group_on_login;
public function __construct(EntityManagerInterface $entityManager, ?array $saml_role_mapping, bool $update_group_on_login)
{
$this->em = $entityManager;
if ($saml_role_mapping) {
$this->saml_role_mapping = $saml_role_mapping;
} else {
$this->saml_role_mapping = [];
}
$this->update_group_on_login = $update_group_on_login;
}
public const SAML_PASSWORD_PLACEHOLDER = '!!SAML!!';
public function createUser($username, array $attributes = []): UserInterface
{
$user = new User();
$user->setName($username);
$user->setNeedPwChange(false);
$user->setPassword(self::SAML_PASSWORD_PLACEHOLDER);
//This is a SAML user now!
$user->setSamlUser(true);
//Update basic user information
$user->setSamlAttributes($attributes);
//Check if we can find a group for this user based on the SAML attributes
$group = $this->mapSAMLAttributesToLocalGroup($attributes);
$user->setGroup($group);
return $user;
}
/**
* This method is called after a successful authentication. It is used to update the group of the user,
* based on the new SAML attributes.
* @param AuthenticationSuccessEvent $event
* @return void
*/
public function onAuthenticationSuccess(AuthenticationSuccessEvent $event): void
{
if (! $this->update_group_on_login) {
return;
}
$token = $event->getAuthenticationToken();
$user = $token->getUser();
//Only update the group if the user is a SAML user
if (! $token instanceof SamlToken || ! $user instanceof User) {
return;
}
//Check if we can find a group for this user based on the SAML attributes
$group = $this->mapSAMLAttributesToLocalGroup($token->getAttributes());
//If needed update the group of the user and save it to DB
if ($group !== $user->getGroup()) {
$user->setGroup($group);
$this->em->flush();
}
}
/**
* Maps the given SAML attributes to a local group.
* @param array $attributes The SAML attributes
* @return Group|null
*/
public function mapSAMLAttributesToLocalGroup(array $attributes): ?Group
{
//Extract the roles from the SAML attributes
$roles = $attributes['group'] ?? [];
$group_id = $this->mapSAMLRolesToLocalGroupID($roles);
//Check if we can find a group with the given ID
if ($group_id !== null) {
$group = $this->em->find(Group::class, $group_id);
if ($group !== null) {
return $group;
}
}
//If no group was found, return null
return null;
}
/**
* Maps a list of SAML roles to a local group ID.
* The first available mapping will be used (so the order of the $map is important, first match wins).
* @param array $roles The list of SAML roles
* @param array $map|null The mapping from SAML roles. If null, the global mapping will be used.
* @return int|null The ID of the local group or null if no mapping was found.
*/
public function mapSAMLRolesToLocalGroupID(array $roles, array $map = null): ?int
{
$map = $map ?? $this->saml_role_mapping;
//Iterate over the mapping (from first to last) and check if we have a match
foreach ($map as $saml_role => $group_id) {
//Skip wildcard
if ($saml_role === '*') {
continue;
}
if (in_array($saml_role, $roles, true)) {
return (int) $group_id;
}
}
//If no applicable mapping was found, check if we have a default mapping
if (array_key_exists('*', $map)) {
return (int) $map['*'];
}
//If no mapping was found, return null
return null;
}
public static function getSubscribedEvents(): array
{
return [
AuthenticationSuccessEvent::class => 'onAuthenticationSuccess',
];
}
}

View file

@ -63,7 +63,7 @@ final class UserChecker implements UserCheckerInterface
//Check if user is disabled. Then dont allow login
if ($user->isDisabled()) {
//throw new DisabledException();
throw new CustomUserMessageAccountStatusException($this->translator->trans('user.login_error.user_disabled'));
throw new CustomUserMessageAccountStatusException($this->translator->trans('user.login_error.user_disabled', [], 'security'));
}
}
}