. */ namespace App\Security; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use Doctrine\ORM\EntityManagerInterface; use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken; use Nbgrp\OneloginSamlBundle\Security\User\SamlUserFactoryInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; use Symfony\Component\Security\Core\User\UserInterface; /** * @see \App\Tests\Security\SamlUserFactoryTest */ class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterface { private readonly array $saml_role_mapping; public function __construct(private readonly EntityManagerInterface $em, ?array $saml_role_mapping, private readonly bool $update_group_on_login) { $this->saml_role_mapping = $saml_role_mapping ?: []; } final 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. */ 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 */ 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 instanceof Group) { 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 ??= $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', ]; } }