Commit 30760bf8 authored by Bertrand Gauthier's avatar Bertrand Gauthier
Browse files

Merge branch 'usurpation'

parents 058ae8f8 f90a09b9
...@@ -5,6 +5,7 @@ use UnicaenAuth\Service\ShibService; ...@@ -5,6 +5,7 @@ use UnicaenAuth\Service\ShibService;
use UnicaenAuth\Service\ShibServiceFactory; use UnicaenAuth\Service\ShibServiceFactory;
use UnicaenAuth\Service\UserContextFactory; use UnicaenAuth\Service\UserContextFactory;
use UnicaenAuth\View\Helper\ShibConnectViewHelperFactory; use UnicaenAuth\View\Helper\ShibConnectViewHelperFactory;
use UnicaenAuth\View\Helper\UserUsurpationHelperFactory;
$settings = [ $settings = [
/** /**
...@@ -432,6 +433,7 @@ return [ ...@@ -432,6 +433,7 @@ return [
'userInfo' => 'UnicaenAuth\View\Helper\UserInfoFactory', 'userInfo' => 'UnicaenAuth\View\Helper\UserInfoFactory',
'userProfileSelect' => 'UnicaenAuth\View\Helper\UserProfileSelectFactory', 'userProfileSelect' => 'UnicaenAuth\View\Helper\UserProfileSelectFactory',
'userProfileSelectRadioItem' => 'UnicaenAuth\View\Helper\UserProfileSelectRadioItemFactory', 'userProfileSelectRadioItem' => 'UnicaenAuth\View\Helper\UserProfileSelectRadioItemFactory',
'userUsurpation' => UserUsurpationHelperFactory::class,
'shibConnect' => ShibConnectViewHelperFactory::class, 'shibConnect' => ShibConnectViewHelperFactory::class,
], ],
'invokables' => [ 'invokables' => [
......
...@@ -51,15 +51,57 @@ class Shib implements ChainableStorage, ServiceLocatorAwareInterface ...@@ -51,15 +51,57 @@ class Shib implements ChainableStorage, ServiceLocatorAwareInterface
*/ */
public function read(ChainEvent $e) public function read(ChainEvent $e)
{ {
/** @var ShibService $shib */ $shibUser = $this->getAuthenticatedUser();
$shib = $this->getServiceLocator()->get(ShibService::class);
$shibUser = $shib->getAuthenticatedUser();
$e->addContents('shib', $shibUser); $e->addContents('shib', $shibUser);
return $shibUser; return $shibUser;
} }
/**
* @return null|ShibUser
*/
private function getAuthenticatedUser()
{
if (! $this->isShibbolethEnabled()) {
return null;
}
if (null !== $this->resolvedIdentity) {
return $this->resolvedIdentity;
}
/** @var ShibService $shib */
$shib = $this->getServiceLocator()->get(ShibService::class);
$this->resolvedIdentity = $shib->getAuthenticatedUser();
return $this->resolvedIdentity;
}
/**
* @return bool
*/
private function isShibbolethEnabled()
{
$options = $this->getModuleOptions();
$shibboleth = $options->getShibboleth();
return isset($shibboleth['enable']) && (bool) $shibboleth['enable'];
}
/**
* @return ModuleOptions
*/
private function getModuleOptions()
{
if (null === $this->options) {
$this->options = $this->getServiceLocator()->get('unicaen-auth_module_options');
}
return $this->options;
}
/** /**
* Writes $contents to storage * Writes $contents to storage
* *
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace UnicaenAuth\Controller; namespace UnicaenAuth\Controller;
use UnicaenApp\Exception\RuntimeException; use UnicaenApp\Exception\RuntimeException;
use UnicaenAuth\Service\ShibService;
use UnicaenAuth\Service\Traits\ShibServiceAwareTrait; use UnicaenAuth\Service\Traits\ShibServiceAwareTrait;
use UnicaenAuth\Service\Traits\UserServiceAwareTrait; use UnicaenAuth\Service\Traits\UserServiceAwareTrait;
use Zend\Authentication\AuthenticationService; use Zend\Authentication\AuthenticationService;
...@@ -23,6 +24,16 @@ class AuthController extends AbstractActionController ...@@ -23,6 +24,16 @@ class AuthController extends AbstractActionController
use UserServiceAwareTrait; use UserServiceAwareTrait;
/** /**
* Cette action n'est exécutée qu'une fois l'authentification Shibboleth réalisée avec succès.
*
* Lorsque l'authentification Shibboleth est activée (unicaen-auth.shibboleth.enable === true),
* et que la config Apache est correcte, une requête à l'adresse correspondant à cette action
* (suite au clic sur le bouton "Authentification Shibboleth, typiquement)
* est détournée pour réaliser l'authentification.
* Ce n'est qu'une fois l'authentification réalisée avec succès que cette action entre en jeu.
*
* @see ShibService::apacheConfigSnippet()
*
* @return Response|array * @return Response|array
*/ */
public function shibbolethAction() public function shibbolethAction()
...@@ -36,9 +47,14 @@ class AuthController extends AbstractActionController ...@@ -36,9 +47,14 @@ class AuthController extends AbstractActionController
$this->zfcUserAuthentication()->getAuthService()->clearIdentity(); $this->zfcUserAuthentication()->getAuthService()->clearIdentity();
// déconnexion Shibboleth le cas échéant // déconnexion Shibboleth le cas échéant
if ($this->shibService->isShibbolethEnable()) { if ($this->shibService->isShibbolethEnabled()) {
// désactivation de l'usurpation d'identité éventuelle
$this->shibService->deactivateUsurpation();
// URL par défaut vers laquelle on redirige après déconnexion : accueil
$homeUrl = $this->url()->fromRoute('home', [], ['force_canonical' => true]); $homeUrl = $this->url()->fromRoute('home', [], ['force_canonical' => true]);
$returnAbsoluteUrl = $this->params()->fromQuery('return', $homeUrl); $returnAbsoluteUrl = $this->params()->fromQuery('return', $homeUrl);
return $this->redirect()->toUrl($this->shibService->getLogoutUrl($returnAbsoluteUrl)); return $this->redirect()->toUrl($this->shibService->getLogoutUrl($returnAbsoluteUrl));
} else { } else {
return []; // une page d'aide s'affichera return []; // une page d'aide s'affichera
...@@ -65,9 +81,4 @@ class AuthController extends AbstractActionController ...@@ -65,9 +81,4 @@ class AuthController extends AbstractActionController
return $this->redirect()->toUrl($redirectUrl); return $this->redirect()->toUrl($redirectUrl);
} }
public function shibboleth()
{
}
} }
\ No newline at end of file
...@@ -6,8 +6,6 @@ use Zend\Http\Request; ...@@ -6,8 +6,6 @@ use Zend\Http\Request;
use Zend\Mvc\Controller\AbstractActionController; use Zend\Mvc\Controller\AbstractActionController;
/** /**
*
*
* @author Bertrand GAUTHIER <bertrand.gauthier at unicaen.fr> * @author Bertrand GAUTHIER <bertrand.gauthier at unicaen.fr>
*/ */
class UtilisateurController extends AbstractActionController class UtilisateurController extends AbstractActionController
......
...@@ -55,6 +55,16 @@ class ShibUser implements UserInterface ...@@ -55,6 +55,16 @@ class ShibUser implements UserInterface
return $this->getUsername(); return $this->getUsername();
} }
/**
* Set eduPersoPrincipalName (EPPN).
*
* @param string $eppn eduPersoPrincipalName (EPPN), ex: 'gauthierb@unicaen.fr'
*/
public function setEppn($eppn)
{
$this->setUsername($eppn);
}
/** /**
* Get id. * Get id.
* *
......
...@@ -2,10 +2,14 @@ ...@@ -2,10 +2,14 @@
namespace UnicaenAuth\Service; namespace UnicaenAuth\Service;
use Assert\Assertion;
use Assert\AssertionFailedException;
use UnicaenApp\Exception\LogicException;
use UnicaenApp\Exception\RuntimeException; use UnicaenApp\Exception\RuntimeException;
use UnicaenAuth\Entity\Shibboleth\ShibUser; use UnicaenAuth\Entity\Shibboleth\ShibUser;
use UnicaenAuth\Options\ModuleOptions; use UnicaenAuth\Options\ModuleOptions;
use Zend\Mvc\Router\Http\TreeRouteStack; use Zend\Mvc\Router\Http\TreeRouteStack;
use Zend\Session\Container;
/** /**
* Shibboleth service * Shibboleth service
...@@ -44,10 +48,47 @@ EOS; ...@@ -44,10 +48,47 @@ EOS;
return $text; return $text;
} }
/**
* @return ShibUser|null
*/
public function getAuthenticatedUser()
{
if (! $this->isShibbolethEnabled()) {
return null;
}
if ($this->authenticatedUser === null) {
if (! $this->getShibbolethSimulate() && ! isset($_SERVER['REMOTE_USER'])) {
try {
Assertion::keyIsset($_SERVER, 'REMOTE_USER');
} catch (AssertionFailedException $e) {
throw new RuntimeException("La clé suivante est introuvable ou sans valeur dans \$_SERVER : 'REMOTE_USER'.");
}
}
// gestion de l'usurpation éventuelle
if ($this->isUsurpationActive()) {
$this->handleUsurpation();
}
if (empty($_SERVER['REMOTE_USER'])) {
if ($this->isSimulationActive()) {
$this->handleSimulation();
} else {
return null;
}
}
$this->authenticatedUser = $this->createShibUserFromServerArrayData();
}
return $this->authenticatedUser;
}
/** /**
* @return boolean * @return boolean
*/ */
public function isShibbolethEnable() public function isShibbolethEnabled()
{ {
$options = $this->options->getShibboleth(); $options = $this->options->getShibboleth();
...@@ -55,24 +96,160 @@ EOS; ...@@ -55,24 +96,160 @@ EOS;
} }
/** /**
* @return ShibUser|null * @return array
*/ */
public function getAuthenticatedUser() public function getShibbolethSimulate()
{ {
if ($this->authenticatedUser === null) { $options = $this->options->getShibboleth();
if (empty($_SERVER['REMOTE_USER'])) {
return null; if (! array_key_exists('simulate', $options) || ! is_array($options['simulate'])) {
} return [];
$this->authenticatedUser = $this->createShibUser();
} }
return $this->authenticatedUser; return $options['simulate'];
}
/**
* Retourne true si la simulation d'un utilisateur authentifié via Shibboleth est en cours.
*
* @return bool
*/
private function isSimulationActive()
{
$options = $this->options->getShibboleth();
if (array_key_exists('simulate', $options) && is_array($options['simulate']) && ! empty($options['simulate'])) {
return true;
}
return false;
}
/**
* @return $this
*/
private function handleSimulation()
{
$simulate = $this->getShibbolethSimulate();
try {
Assertion::keyIsset($simulate, 'eppn',
"Clé 'eppn' introuvable ou sans valeur dans l'option 'unicaen-auth.shibboleth.simulate'");
Assertion::eq(count(array_intersect($keys = ['supannEmpId', 'supannEtuId'], array_keys($simulate))), 1,
"L'une ou l'autre de ces clés doit être présente dans l'option 'unicaen-auth.shibboleth.simulate': " .
implode(', ', $keys));
} catch (AssertionFailedException $e) {
throw new LogicException("Configuration erronée", null, $e);
}
$eppn = $simulate['eppn'];
$supannId = $simulate['supannEmpId'] ?: $simulate['supannEtuId'];
$email = isset($simulate['email']) ? $simulate['email'] : null;
$shibUser = new ShibUser();
$shibUser->setEppn($eppn);
$shibUser->setId($supannId);
$shibUser->setDisplayName("$eppn ($supannId)");
$shibUser->setEmail($email);
$shibUser->setNom('Shibboleth');
$shibUser->setPrenom('Simulation');
ShibService::simulateAuthenticatedUser($shibUser);
return $this;
}
/**
* Retourne true si les données stockées en session indiquent qu'une usurpation d'identité Shibboleth est en cours.
*
* @return bool
*/
private function isUsurpationActive()
{
return $this->getSessionContainer()->offsetExists('fromShibUser');
}
/**
* @param ShibUser $fromShibUser
* @param ShibUser $toShibUser
* @return self
*/
public function activateUsurpation(ShibUser $fromShibUser, ShibUser $toShibUser)
{
// le login doit faire partie des usurpateurs autorisés
if (! in_array($fromShibUser->getUsername(), $this->options->getUsurpationAllowedUsernames())) {
throw new RuntimeException("Usurpation non autorisée");
}
$session = $this->getSessionContainer();
$session->offsetSet('fromShibUser', $fromShibUser);
$session->offsetSet('toShibUser', $toShibUser);
return $this;
}
/**
* Suppression des données stockées en session concernant l'usurpation d'identité Shibboleth.
*
* @return self
*/
public function deactivateUsurpation()
{
$session = $this->getSessionContainer();
$session->offsetUnset('fromShibUser');
$session->offsetUnset('toShibUser');
return $this;
}
/**
* @return $this
*/
private function handleUsurpation()
{
$session = $this->getSessionContainer();
/** @var ShibUser|null $toShibUser */
$toShibUser = $session->offsetGet('toShibUser');
if ($toShibUser === null) {
throw new RuntimeException("Anomalie: 'toShibUser' introuvable");
}
static::simulateAuthenticatedUser($toShibUser, 'supannEmpId');
return $this;
}
/**
* @return Container
*/
private function getSessionContainer()
{
return new Container(ShibService::class);
}
/**
* Inscrit dans le tableau $_SERVER le nécessaire pour usurper l'identité d'un utilisateur
* qui ce serait authentifié via Shibboleth.
*
* @param ShibUser $shibUser Utilisateur dont on veut usurper l'identité.
* @param string $keyForId Clé du tableau $_SERVER dans laquelle mettre l'id de l'utilsateur spécifié.
* Ex: 'supannEmpId', 'supannEtuId'.
*/
public static function simulateAuthenticatedUser(ShibUser $shibUser, $keyForId = 'supannEmpId')
{
$_SERVER['REMOTE_USER'] = $shibUser->getEppn();
$_SERVER[$keyForId] = $shibUser->getId();
$_SERVER['displayName'] = $shibUser->getDisplayName();
$_SERVER['mail'] = $shibUser->getEmail();
$_SERVER['sn'] = $shibUser->getNom();
$_SERVER['givenName'] = $shibUser->getPrenom();
} }
/** /**
* @return ShibUser * @return ShibUser
*/ */
private function createShibUser() private function createShibUserFromServerArrayData()
{ {
$eppn = $_SERVER['REMOTE_USER']; $eppn = $_SERVER['REMOTE_USER'];
...@@ -81,7 +258,7 @@ EOS; ...@@ -81,7 +258,7 @@ EOS;
} elseif (isset($_SERVER['supannEmpId'])) { } elseif (isset($_SERVER['supannEmpId'])) {
$id = $_SERVER['supannEmpId']; $id = $_SERVER['supannEmpId'];
} else { } else {
throw new RuntimeException('Un au moins des attributs suivants doit exister dans $_SERVER : supannEtuId, supannEmpId.'); throw new RuntimeException('Un au moins des attributs Shibboleth suivants doit exister dans $_SERVER : supannEtuId, supannEmpId.');
} }
$mail = null; $mail = null;
...@@ -134,6 +311,10 @@ EOS; ...@@ -134,6 +311,10 @@ EOS;
*/ */
public function getLogoutUrl($returnAbsoluteUrl = null) public function getLogoutUrl($returnAbsoluteUrl = null)
{ {
if ($this->getShibbolethSimulate()) {
return '/';
}
$logoutRelativeUrl = '/Shibboleth.sso/Logout?return='; // NB: '?return=' semble obligatoire! $logoutRelativeUrl = '/Shibboleth.sso/Logout?return='; // NB: '?return=' semble obligatoire!
if ($returnAbsoluteUrl) { if ($returnAbsoluteUrl) {
......
...@@ -5,6 +5,7 @@ namespace UnicaenAuth\Service; ...@@ -5,6 +5,7 @@ namespace UnicaenAuth\Service;
use BjyAuthorize\Acl\Role; use BjyAuthorize\Acl\Role;
use UnicaenApp\Exception\RuntimeException; use UnicaenApp\Exception\RuntimeException;
use UnicaenApp\Traits\SessionContainerTrait; use UnicaenApp\Traits\SessionContainerTrait;
use UnicaenAuth\Entity\Shibboleth\ShibUser;
use UnicaenAuth\Event\UserRoleSelectedEvent; use UnicaenAuth\Event\UserRoleSelectedEvent;
use UnicaenAuth\Provider\Identity\Chain; use UnicaenAuth\Provider\Identity\Chain;
use Zend\EventManager\EventManagerAwareInterface; use Zend\EventManager\EventManagerAwareInterface;
...@@ -73,6 +74,24 @@ class UserContext extends AbstractService implements EventManagerAwareInterface ...@@ -73,6 +74,24 @@ class UserContext extends AbstractService implements EventManagerAwareInterface
/**
* Retourne l'éventuel utilisateur Shibboleth courant.
*
* @return ShibUser|null
*/
public function getShibUser()
{
if (($identity = $this->getIdentity())) {
if (isset($identity['shib']) && $identity['shib'] instanceof ShibUser) {
return $identity['shib'];
}
}
return null;
}
/** /**
* Retourne les données d'identité correspondant à l'utilisateur courant. * Retourne les données d'identité correspondant à l'utilisateur courant.
* *
...@@ -91,6 +110,27 @@ class UserContext extends AbstractService implements EventManagerAwareInterface ...@@ -91,6 +110,27 @@ class UserContext extends AbstractService implements EventManagerAwareInterface
} }
/**
* Retourne l'identifiant de connexion de l'utilisateur courant.
*
* @return string|null
*/
public function getIdentityUsername()
{
if ($user = $this->getShibUser()) {
return $user->getUsername();
}
if ($user = $this->getLdapUser()) {
return $user->getUsername();
}
if ($user = $this->getDbUser()) {
return $user->getUsername();
}
return null;
}
/** /**
* @param string $roleId * @param string $roleId
......
...@@ -34,7 +34,7 @@ class ShibConnectViewHelper extends AbstractHelper ...@@ -34,7 +34,7 @@ class ShibConnectViewHelper extends AbstractHelper
*/ */
private function render() private function render()
{ {
if (! $this->shibService->isShibbolethEnable()) { if (! $this->shibService->isShibbolethEnabled()) {
return ''; return '';
} }
......
...@@ -102,7 +102,11 @@ class UserInfo extends UserAbstract ...@@ -102,7 +102,11 @@ class UserInfo extends UserAbstract
} }
$out .= $aucuneAffDispo; $out .= $aucuneAffDispo;
} }
// formulaire d'usurpation d'identité
$userUsurpationHelper = $this->view->plugin('userUsurpation'); /* @var $userUsurpationHelper \UnicaenAuth\View\Helper\UserUsurpationHelper */
$out .= $userUsurpationHelper();
return $out; return $out;
} }
......
<?php
namespace UnicaenAuth\View\Helper;
use UnicaenApp\Form\View\Helper\FormControlGroup;
use UnicaenAuth\Options\ModuleOptions;
use Zend\Form\Element\Submit;
use Zend\Form\Element\Text;
use Zend\Form\Form;
use Zend\Form\View\Helper\Form as FormHelper;
use Zend\View\Renderer\PhpRenderer;
/**
* Aide de vue permettant de saisir et valider le login de l'utilisateur dont on veut usurper l'identité.
*
* @author Bertrand GAUTHIER <bertrand.gauthier@unicaen.fr>
*/