Commit dd5c8b60 authored by Bertrand Gauthier's avatar Bertrand Gauthier
Browse files

Merge branch 'release-3.2.0'

parents ff93d62a 06ac47cc
Pipeline #9952 passed with stage
in 14 seconds
......@@ -45,3 +45,6 @@ Première version officielle sous ZF3.
- Possibilité de connaître la source de l'authentification (db, ldap, cas, shib).
- Possibilité de stopper l'usurpation en cours pour revenir à l'identité d'origine.
- [FIX] Usurpation d'un compte local en BDD.
- [FIX] Lorsque l'usurpateur stoppait l'usurpation, il ne récupérait pas son dernier endossé
- [FIX] En cas d'usurpation puis de déconnexion puis de reconnexion, on revenait dans la peau de l'usurpé sans pouvoir
stopper l'usurpation
\ No newline at end of file
......@@ -194,22 +194,33 @@ $settings = [
* Ex: identifiant de l'usager au sein du référentiel établissement, transmis par l'IDP via le supannRefId.
*/
'shib_user_id_extractor' => [
/*
// domaine (ex: 'unicaen.fr') de l'EPPN (ex: hochonp@unicaen.fr')
'unicaen.fr' => [
[
// nom du 1er attribut recherché
'name' => 'supannRefId', // ex: '{REFERENTIEL}1234;{ISO15693}044D1AZE7A5P80'
'name' => 'supannRefId', // ex: '{OCTOPUS:ID}1234;{ISO15693}044D1AZE7A5P80'
// pattern éventuel pour extraire la partie intéressante
'preg_match_pattern' => '|\{REFERENTIEL\}(\d+)|', // ex: permet d'extraire '1234'
'preg_match_pattern' => '|\{OCTOPUS:ID\}(\d+)|', // ex: permet d'extraire '1234'
],
[
// nom du 2e attribut recherché
// nom du 2e attribut recherché, si le 1er est introuvable
'name' => 'supannEmpId',
// pas de pattern donc valeur directement utilisable
// pas de pattern donc valeur brute utilisée
'preg_match_pattern' => null,
],
[
// nom du 3e attribut recherché
// nom du 3e attribut recherché, si le 2e est introuvable
'name' => 'supannEtuId',
],
],
*/
// config de repli pour tous les autres domaines
'default' => [
[
'name' => 'supannEmpId',
],
[
'name' => 'supannEtuId',
],
],
......
......@@ -123,21 +123,30 @@ return [
*/
'shib_user_id_extractor' => [
// domaine (ex: 'unicaen.fr') de l'EPPN (ex: hochonp@unicaen.fr')
'unicaen.fr' => [
[
// nom du 1er attribut recherché
'name' => 'supannRefId', // ex: '{REFERENTIEL}1234;{ISO15693}044D1AZE7A5P80'
// pattern éventuel pour extraire la partie intéressante
'preg_match_pattern' => '|\{REFERENTIEL\}(\d+)|', // ex: permet d'extraire '1234'
],
// 'unicaen.fr' => [
// [
// // nom du 1er attribut recherché
// 'name' => 'supannRefId', // ex: '{OCTOPUS:ID}1234;{ISO15693}044D1AZE7A5P80'
// // pattern éventuel pour extraire la partie intéressante
// 'preg_match_pattern' => '|\{OCTOPUS:ID\}(\d+)|', // ex: permet d'extraire '1234'
// ],
// [
// // nom du 2e attribut recherché
// 'name' => 'supannEmpId',
// // pas de pattern donc valeur brute utilisée
// 'preg_match_pattern' => null,
// ],
// [
// // nom du 3e attribut recherché
// 'name' => 'supannEtuId',
// ],
// ],
// config de repli pour tous les autres domaines
'default' => [
[
// nom du 2e attribut recherché
'name' => 'supannEmpId',
// pas de pattern donc valeur directement utilisable
'preg_match_pattern' => null,
],
[
// nom du 3e attribut recherché
'name' => 'supannEtuId',
],
],
......
......@@ -28,11 +28,11 @@ class LocalAdapterFactory
// db adapter
if (array_key_exists('db', $localConfig)) {
$this->attachSubAdapter($localAdapter, $container, $localConfig['db']);
$this->attachSubAdapter($localAdapter, $container, $localConfig['db'], 20); // en 1er
}
// ldap adapter
if (array_key_exists('ldap', $localConfig)) {
$this->attachSubAdapter($localAdapter, $container, $localConfig['ldap']);
$this->attachSubAdapter($localAdapter, $container, $localConfig['ldap'], 10); // en 2e
}
return $localAdapter;
......@@ -42,8 +42,9 @@ class LocalAdapterFactory
* @param LocalAdapter $localAdapter
* @param ContainerInterface $container
* @param array $config
* @param int $priority
*/
private function attachSubAdapter(LocalAdapter $localAdapter, ContainerInterface $container, array $config)
private function attachSubAdapter(LocalAdapter $localAdapter, ContainerInterface $container, array $config, int $priority)
{
$enabled = isset($config['enabled']) && $config['enabled'] === true;
if (! $enabled) {
......@@ -52,7 +53,7 @@ class LocalAdapterFactory
/** @var AbstractAdapter $subAdapter */
$subAdapter = $container->get($config['adapter']);
$subAdapter->attach($localAdapter->getEventManager());
$subAdapter->attach($localAdapter->getEventManager(), $priority);
}
}
\ No newline at end of file
......@@ -54,4 +54,14 @@ class Shib extends AbstractStorage
return $this->shibService->getAuthenticatedUser();
}
/**
* @inheritDoc
*/
public function clear(ChainEvent $e)
{
parent::clear($e);
$this->shibService->deactivateUsurpation();
}
}
......@@ -6,6 +6,7 @@ use BjyAuthorize\Provider\Role\ProviderInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use UnicaenAuth\Entity\Db\Role;
/**
* User entity abstract mother class.
......@@ -71,6 +72,13 @@ abstract class AbstractUser implements UserInterface, ProviderInterface
*/
protected $roles;
/**
* @var AbstractRole
* @ORM\ManyToOne(targetEntity="UnicaenAuth\Entity\Db\Role")
* @ORM\JoinColumn(name="last_role_id", referencedColumnName="id")
*/
protected $lastRole;
/**
* Initialies the roles variable.
*/
......@@ -259,6 +267,25 @@ abstract class AbstractUser implements UserInterface, ProviderInterface
return $this;
}
/**
* @return AbstractRole|null
*/
public function getLastRole()
{
return $this->lastRole;
}
/**
* @param AbstractRole|null $lastRole
* @return self
*/
public function setLastRole(AbstractRole $lastRole = null)
{
$this->lastRole = $lastRole;
return $this;
}
/**
* Retourne true si cet utilisateur est local.
*
......
......@@ -2,11 +2,14 @@
namespace UnicaenAuth\Event\Listener;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use UnicaenApp\Service\EntityManagerAwareInterface;
use UnicaenApp\Service\EntityManagerAwareTrait;
use UnicaenAuth\Entity\Db\AbstractUser;
use UnicaenAuth\Event\UserAuthenticatedEvent;
use UnicaenAuth\Service\Traits\UserContextServiceAwareTrait;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\EventManager\ListenerAggregateTrait;
/**
* Classe abstraites pour les classes désirant scruter un événement déclenché lors de l'authentification
......@@ -14,16 +17,19 @@ use UnicaenAuth\Event\UserAuthenticatedEvent;
*
* Événements disponibles :
* - juste avant que l'entité utilisateur ne soit persistée.
* - juste après que l'entité utilisateur ait été persistée.
*
* @author Bertrand GAUTHIER <bertrand.gauthier at unicaen.fr>
* @see UserAuthenticatedEvent
*/
abstract class AuthenticatedUserSavedAbstractListener implements ListenerAggregateInterface, EntityManagerAwareInterface
{
use ListenerAggregateTrait;
use EntityManagerAwareTrait;
use UserContextServiceAwareTrait;
/**
* @var \Zend\Stdlib\CallbackHandler[]
* @var callable[]
*/
protected $listeners = [];
......@@ -31,20 +37,33 @@ abstract class AuthenticatedUserSavedAbstractListener implements ListenerAggrega
* Méthode appelée juste avant que l'entité utilisateur soit persistée.
*
* @param UserAuthenticatedEvent $e
* @return void
*/
public function onUserAuthenticatedPrePersist(UserAuthenticatedEvent $e)
{
/** @var AbstractUser $user */
$user = $e->getDbUser();
// Sélection du dernier rôle endossé.
$this->selectLastUserRole($user);
}
/**
* Méthode appelée juste après que l'entité utilisateur soit persistée.
*
* @param UserAuthenticatedEvent $e
* @return void
*/
public function onUserAuthenticatedPostPersist(UserAuthenticatedEvent $e)
{
// nop
}
protected function selectLastUserRole(AbstractUser $user)
{
if ($role = $user->getLastRole()) {
$this->serviceUserContext->setNextSelectedIdentityRole($role);
}
}
/**
......@@ -66,18 +85,4 @@ abstract class AuthenticatedUserSavedAbstractListener implements ListenerAggrega
[$this, 'onUserAuthenticatedPostPersist'],
100);
}
/**
* Detach all previously attached listeners
*
* @param EventManagerInterface $events
*/
public function detach(EventManagerInterface $events)
{
foreach ($this->listeners as $index => $listener) {
if ($events->detach($listener)) {
unset($this->listeners[$index]);
}
}
}
}
\ No newline at end of file
......@@ -21,6 +21,8 @@ class ShibService
{
const SHIB_USER_ID_EXTRACTOR = 'shib_user_id_extractor';
const DOMAIN_DEFAULT = 'default';
const KEY_fromShibUser = 'fromShibUser';
const KEY_toShibUser = 'toShibUser';
......@@ -197,13 +199,21 @@ EOS;
return (array)$this->shibbolethConfig['required_attributes'];
}
/**
* @return array
*/
private function getShibUserIdExtractorDefaultConfig(): array
{
return $this->getShibUserIdExtractorConfigForDomain(self::DOMAIN_DEFAULT);
}
/**
* Retourne la config permettant d'extraire l'id à partir des attributs.
*
* @param string $domain
* @return array
*/
private function getShibUserIdExtractorForDomain(string $domain): array
private function getShibUserIdExtractorConfigForDomain(string $domain): array
{
$key = self::SHIB_USER_ID_EXTRACTOR;
if (! array_key_exists($key, $this->shibbolethConfig)) {
......@@ -213,7 +223,7 @@ EOS;
$config = $this->shibbolethConfig[$key];
if (! array_key_exists($domain, $config)) {
throw new RuntimeException("Aucune config '$key' trouvée pour le domaine '$domain'.");
return [];
}
return $config[$domain];
......@@ -481,14 +491,23 @@ EOS;
*/
private function extractShibUserIdValueForDomainFromShibData(string $domain, array $data): ?string
{
$config = $this->getShibUserIdExtractorForDomain($domain);
$config = $this->getShibUserIdExtractorConfigForDomain($domain);
if (empty($config)) {
$config = $this->getShibUserIdExtractorDefaultConfig();
if (empty($config)) {
throw new RuntimeException("Aucune config trouvée ni pour le domaine '$domain' ni par défaut.");
}
}
foreach ($config as $array) {
$name = $array['name'];
$value = $this->getValueFromShibData($name, $data);
if ($value !== null) {
$pregMatchPattern = $array['preg_match_pattern'] ?? null;
if ($pregMatchPattern !== null && preg_match($pregMatchPattern, $value, $matches)) {
if ($pregMatchPattern !== null) {
if (preg_match($pregMatchPattern, $value, $matches) === 0) {
throw new RuntimeException("Le pattern '$pregMatchPattern' n'a pas permis d'extraire une valeur de '$name'.");
}
$value = $matches[1];
}
......
......@@ -2,7 +2,7 @@
namespace UnicaenAuth\Service;
use BjyAuthorize\Acl\Role;
use Doctrine\ORM\ORMException;
use UnicaenApp\Exception\RuntimeException;
use UnicaenApp\Traits\SessionContainerTrait;
use UnicaenAuth\Acl\NamedRole;
......@@ -10,6 +10,8 @@ use UnicaenAuth\Authentication\SessionIdentity;
use UnicaenAuth\Authentication\Storage\Auth;
use UnicaenAuth\Authentication\Storage\Usurpation;
use UnicaenAuth\Entity\Db\AbstractUser;
use UnicaenAuth\Entity\Db\AbstractRole;
use UnicaenAuth\Entity\Db\Role;
use UnicaenAuth\Entity\Ldap\People;
use UnicaenAuth\Entity\Shibboleth\ShibUser;
use UnicaenAuth\Event\UserRoleSelectedEvent;
......@@ -19,8 +21,6 @@ use UnicaenAuth\Provider\Identity\Chain;
use Zend\Authentication\AuthenticationService;
use Zend\EventManager\EventManagerAwareInterface;
use Zend\EventManager\EventManagerAwareTrait;
use Zend\Http\Request;
use Zend\Http\Response;
use Zend\Permissions\Acl\Role\RoleInterface;
use ZfcUser\Entity\UserInterface;
......@@ -335,11 +335,35 @@ class UserContext extends AbstractService implements EventManagerAwareInterface
unset($this->getSessionContainer()->selectedIdentityRole);
}
$role = $this->getSelectableIdentityRoles()[$role];
if ($role instanceof AbstractRole) {
$this->saveUserLastRole($role);
}
$this->triggerUserRoleSelectedEvent(UserRoleSelectedEvent::POST_SELECTION, $role);
return $this;
}
/**
* @param AbstractRole $role
*/
private function saveUserLastRole(AbstractRole $role)
{
/** @var AbstractUser $user */
$user = $this->getDbUser();
if (! $user) {
return;
}
$user->setLastRole($role);
try {
$this->getEntityManager()->flush($user);
} catch (ORMException $e) {
throw new RuntimeException("Erreur rencontrée lors de l'enregistrement en bdd", null, $e);
}
}
/**
* Retourne l'éventuel rôle spécifié en session devant être le prochain rôle sélectionné.
*
......@@ -372,6 +396,11 @@ class UserContext extends AbstractService implements EventManagerAwareInterface
unset($this->getSessionContainer()->nextSelectedIdentityRole);
}
$role = $this->getSelectableIdentityRoles()[$role];
if ($role instanceof AbstractRole) {
$this->saveUserLastRole($role);
}
$this->triggerUserRoleSelectedEvent(UserRoleSelectedEvent::POST_SELECTION, $role);
return $this;
......@@ -499,5 +528,10 @@ class UserContext extends AbstractService implements EventManagerAwareInterface
$sessionIdentity = SessionIdentity::newInstance($usurpateur->getUsername(), $this->getAuthenticationType());
$this->authenticationService->getStorage()->write($sessionIdentity);
// Sélection du dernier rôle endossé.
if ($role = $usurpateur->getLastRole()) {
$this->setNextSelectedIdentityRole($role);
}
}
}
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment