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

Finalisation de l'usurpation d'identité et aide de vue UserUsurpation...

Finalisation de l'usurpation d'identité et aide de vue UserUsurpation (intégrée dans UserInfo) + Simulation d'authentification shibboleth
parent 1b1a594e
......@@ -5,6 +5,7 @@ use UnicaenAuth\Service\ShibService;
use UnicaenAuth\Service\ShibServiceFactory;
use UnicaenAuth\Service\UserContextFactory;
use UnicaenAuth\View\Helper\ShibConnectViewHelperFactory;
use UnicaenAuth\View\Helper\UserUsurpationHelperFactory;
$settings = [
/**
......@@ -122,7 +123,6 @@ return [
['controller' => 'UnicaenApp\Controller\Application', 'action' => 'informatique-et-libertes', 'roles' => []],
['controller' => 'UnicaenApp\Controller\Application', 'action' => 'refresh-session', 'roles' => []],
['controller' => 'UnicaenAuth\Controller\Utilisateur', 'action' => 'selectionner-profil', 'roles' => []],
['controller' => 'UnicaenAuth\Controller\Utilisateur', 'action' => 'usurper-identite', 'roles' => []],
['controller' => 'UnicaenAuth\Controller\Auth', 'action' => 'shibboleth', 'roles' => []],
],
......@@ -433,6 +433,7 @@ return [
'userInfo' => 'UnicaenAuth\View\Helper\UserInfoFactory',
'userProfileSelect' => 'UnicaenAuth\View\Helper\UserProfileSelectFactory',
'userProfileSelectRadioItem' => 'UnicaenAuth\View\Helper\UserProfileSelectRadioItemFactory',
'userUsurpation' => UserUsurpationHelperFactory::class,
'shibConnect' => ShibConnectViewHelperFactory::class,
],
'invokables' => [
......
......@@ -51,15 +51,57 @@ class Shib implements ChainableStorage, ServiceLocatorAwareInterface
*/
public function read(ChainEvent $e)
{
/** @var ShibService $shib */
$shib = $this->getServiceLocator()->get(ShibService::class);
$shibUser = $shib->getAuthenticatedUser();
$shibUser = $this->getAuthenticatedUser();
$e->addContents('shib', $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
*
......
......@@ -3,6 +3,7 @@
namespace UnicaenAuth\Controller;
use UnicaenApp\Exception\RuntimeException;
use UnicaenAuth\Service\ShibService;
use UnicaenAuth\Service\Traits\ShibServiceAwareTrait;
use UnicaenAuth\Service\Traits\UserServiceAwareTrait;
use Zend\Authentication\AuthenticationService;
......@@ -23,6 +24,16 @@ class AuthController extends AbstractActionController
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
*/
public function shibbolethAction()
......@@ -36,9 +47,14 @@ class AuthController extends AbstractActionController
$this->zfcUserAuthentication()->getAuthService()->clearIdentity();
// 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]);
$returnAbsoluteUrl = $this->params()->fromQuery('return', $homeUrl);
return $this->redirect()->toUrl($this->shibService->getLogoutUrl($returnAbsoluteUrl));
} else {
return []; // une page d'aide s'affichera
......@@ -65,9 +81,4 @@ class AuthController extends AbstractActionController
return $this->redirect()->toUrl($redirectUrl);
}
public function shibboleth()
{
}
}
\ No newline at end of file
......@@ -2,78 +2,14 @@
namespace UnicaenAuth\Controller;
use UnicaenAuth\Entity\Db\UserInterface;
use UnicaenAuth\Entity\Ldap\People;
use UnicaenAuth\Entity\Shibboleth\ShibUser;
use UnicaenAuth\Options\ModuleOptions;
use Zend\Authentication\AuthenticationService;
use Zend\Http\Request;
use Zend\Http\Response;
use Zend\Mvc\Controller\AbstractActionController;
/**
*
*
* @author Bertrand GAUTHIER <bertrand.gauthier at unicaen.fr>
*/
class UtilisateurController extends AbstractActionController
{
/**
* Usurpe l'identité d'un autre utilisateur.
*
* @return Response
*/
public function usurperIdentiteAction()
{
$request = $this->getRequest();
if (! $request instanceof Request) {
exit(1);
}
$redirection = $this->redirect()->toRoute('home');
$newIdentity = $request->getQuery('identity', $request->getPost('identity'));
if (! $newIdentity) {
return $redirection;
}
/** @var AuthenticationService $authenticationService */
$authenticationService = $this->getServiceLocator()->get(AuthenticationService::class);
/** @var ModuleOptions $options */
$options = $this->getServiceLocator()->get('unicaen-auth_module_options');
$currentIdentity = $authenticationService->getIdentity();
if (! $currentIdentity) {
return $redirection;
}
if (! is_array($currentIdentity)) {
return $redirection;
}
if (isset($currentIdentity['shib'])) {
/** @var ShibUser $currentIdentity */
$currentIdentity = $currentIdentity['shib'];
} elseif (isset($currentIdentity['ldap'])) {
/** @var People $currentIdentity */
$currentIdentity = $currentIdentity['ldap'];
} elseif (isset($currentIdentity['db'])) {
/** @var UserInterface $currentIdentity */
$currentIdentity = $currentIdentity['db'];
} else {
return $redirection;
}
$currentIdentity = $currentIdentity->getUsername();
if (! in_array($currentIdentity, $options->getUsurpationAllowedUsernames())) {
return $redirection;
}
$authenticationService->getStorage()->write($newIdentity);
return $redirection;
}
/**
* Traite les requêtes AJAX POST de sélection d'un profil utilisateur.
* La sélection est mémorisé en session par le service AuthUserContext.
......
......@@ -55,6 +55,16 @@ class ShibUser implements UserInterface
return $this->getUsername();
}
/**
* Set eduPersoPrincipalName (EPPN).
*
* @param string $eppn eduPersoPrincipalName (EPPN), ex: 'gauthierb@unicaen.fr'
*/
public function setEppn($eppn)
{
$this->setUsername($eppn);
}
/**
* Get id.
*
......
......@@ -2,10 +2,14 @@
namespace UnicaenAuth\Service;
use Assert\Assertion;
use Assert\AssertionFailedException;
use UnicaenApp\Exception\LogicException;
use UnicaenApp\Exception\RuntimeException;
use UnicaenAuth\Entity\Shibboleth\ShibUser;
use UnicaenAuth\Options\ModuleOptions;
use Zend\Mvc\Router\Http\TreeRouteStack;
use Zend\Session\Container;
/**
* Shibboleth service
......@@ -44,10 +48,47 @@ EOS;
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
*/
public function isShibbolethEnable()
public function isShibbolethEnabled()
{
$options = $this->options->getShibboleth();
......@@ -55,24 +96,160 @@ EOS;
}
/**
* @return ShibUser|null
* @return array
*/
public function getAuthenticatedUser()
public function getShibbolethSimulate()
{
if ($this->authenticatedUser === null) {
if (empty($_SERVER['REMOTE_USER'])) {
return null;
$options = $this->options->getShibboleth();
if (! array_key_exists('simulate', $options) || ! is_array($options['simulate'])) {
return [];
}
$this->authenticatedUser = $this->createShibUser();
return $options['simulate'];
}
return $this->authenticatedUser;
/**
* 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
*/
private function createShibUser()
private function createShibUserFromServerArrayData()
{
$eppn = $_SERVER['REMOTE_USER'];
......@@ -81,7 +258,7 @@ EOS;
} elseif (isset($_SERVER['supannEmpId'])) {
$id = $_SERVER['supannEmpId'];
} 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;
......@@ -134,6 +311,10 @@ EOS;
*/
public function getLogoutUrl($returnAbsoluteUrl = null)
{
if ($this->getShibbolethSimulate()) {
return '/';
}
$logoutRelativeUrl = '/Shibboleth.sso/Logout?return='; // NB: '?return=' semble obligatoire!
if ($returnAbsoluteUrl) {
......
......@@ -5,6 +5,7 @@ namespace UnicaenAuth\Service;
use BjyAuthorize\Acl\Role;
use UnicaenApp\Exception\RuntimeException;
use UnicaenApp\Traits\SessionContainerTrait;
use UnicaenAuth\Entity\Shibboleth\ShibUser;
use UnicaenAuth\Event\UserRoleSelectedEvent;
use UnicaenAuth\Provider\Identity\Chain;
use Zend\EventManager\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.
*
......@@ -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
......
......@@ -34,7 +34,7 @@ class ShibConnectViewHelper extends AbstractHelper
*/
private function render()
{
if (! $this->shibService->isShibbolethEnable()) {
if (! $this->shibService->isShibbolethEnabled()) {
return '';
}
......
......@@ -103,6 +103,10 @@ class UserInfo extends UserAbstract
$out .= $aucuneAffDispo;
}
// formulaire d'usurpation d'identité
$userUsurpationHelper = $this->view->plugin('userUsurpation'); /* @var $userUsurpationHelper \UnicaenAuth\View\Helper\UserUsurpationHelper */
$out .= $userUsurpationHelper();
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>
*/
class UserUsurpationHelper extends UserAbstract
{
/**
* @var PhpRenderer
*/
protected $view;
/**
* @var ModuleOptions
*/
protected $moduleOptions;
/**
* @var string
*/
private $url;
/**
* @var bool
*/
private $usurpationEnabled = false;
/**
* Point d'entrée.
*
* @return self
*/
public function __invoke()
{
return $this;