diff --git a/CHANGELOG.md b/CHANGELOG.md index fd73fb2da31a2a6dce288a92228bb2961a42a8c9..d2b58cae13ff7cda4eac45ed6cdcddc80cc4cbfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ Première version officielle sous ZF3. 3.2.0 ----- +- Configuration de la stratégie d'extraction d'un identifiant utile parmi les données d'authentification shibboleth + (config 'shib_user_id_extractor'). - 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. diff --git a/config/module.config.php b/config/module.config.php index fef6e9f1c89d4f0c91e56ff15b0e294771b6fd59..438010a5697f3cd93533db1777c899e099f98b6b 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -187,6 +187,44 @@ $settings = [ 'supannEtuId|supannEmpId', ], */ + + /** + * Configuration de la stratégie d'extraction d'un identifiant utile parmi les données d'authentification + * shibboleth. + * 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: '{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é, si le 1er est introuvable + 'name' => 'supannEmpId', + // pas de pattern donc valeur brute utilisée + 'preg_match_pattern' => null, + ], + [ + // 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', + ], + ], + ], ], /** diff --git a/config/unicaen-auth.local.php.dist b/config/unicaen-auth.local.php.dist index 0633455e8c791eadb78f2f5bea320d207f787c61..1b182e74cd1ef57b1aa4be36331bf8be75f69581 100644 --- a/config/unicaen-auth.local.php.dist +++ b/config/unicaen-auth.local.php.dist @@ -115,6 +115,42 @@ return [ // 'givenName', // 'supannEtuId|supannEmpId', //], + + /** + * Configuration de la stratégie d'extraction d'un identifiant utile parmi les données d'authentification + * shibboleth. + * 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: '{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' => [ + [ + 'name' => 'supannEmpId', + ], + [ + 'name' => 'supannEtuId', + ], + ], + ], ], /** diff --git a/src/UnicaenAuth/Authentication/Adapter/LocalAdapterFactory.php b/src/UnicaenAuth/Authentication/Adapter/LocalAdapterFactory.php index 2733ab3b9bc3797ae141fe31315305f6b7b0b064..04e1866f5926e8727b9b31979e67e6f0056ad270 100644 --- a/src/UnicaenAuth/Authentication/Adapter/LocalAdapterFactory.php +++ b/src/UnicaenAuth/Authentication/Adapter/LocalAdapterFactory.php @@ -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 diff --git a/src/UnicaenAuth/Entity/Shibboleth/ShibUser.php b/src/UnicaenAuth/Entity/Shibboleth/ShibUser.php index 8cba0fd40c89b2ef49a9552523008ea009998fd5..68617a3b1c08c7f0fd2a84a0d40e00a1ac2fa650 100644 --- a/src/UnicaenAuth/Entity/Shibboleth/ShibUser.php +++ b/src/UnicaenAuth/Entity/Shibboleth/ShibUser.php @@ -47,6 +47,18 @@ class ShibUser implements UserInterface */ protected $state = 1; + /** + * Retourne la partie domaine DNS de l'EPPN. + * Retourne par exemple "unicaen.fr" lorsque l'EPPN est "tartempion@unicaen.fr" + * + * @return string + */ + public function getEppnDomain() + { + $parts = explode('@', $this->getEppn()); + + return $parts[1]; + } /** * @return string diff --git a/src/UnicaenAuth/Service/ShibService.php b/src/UnicaenAuth/Service/ShibService.php index 27e3519543c4911e682960265c318e6a3f54bd8c..f4cce5a0d66cab3f046a6e840168722d0d8bd366 100644 --- a/src/UnicaenAuth/Service/ShibService.php +++ b/src/UnicaenAuth/Service/ShibService.php @@ -19,6 +19,10 @@ use Zend\Session\Container; */ class ShibService { + const SHIB_USER_ID_EXTRACTOR = 'shib_user_id_extractor'; + + const DOMAIN_DEFAULT = 'default'; + const KEY_fromShibUser = 'fromShibUser'; const KEY_toShibUser = 'toShibUser'; @@ -105,9 +109,9 @@ EOS; private function isAuthenticated(): bool { return - $this->getServerArrayVariable('REMOTE_USER') || - $this->getServerArrayVariable('Shib-Session-ID') || - $this->getServerArrayVariable('HTTP_SHIB_SESSION_ID'); + $this->getValueFromShibData('REMOTE_USER', $_SERVER) || + $this->getValueFromShibData('Shib-Session-ID', $_SERVER) || + $this->getValueFromShibData('HTTP_SHIB_SESSION_ID', $_SERVER); } /** @@ -195,6 +199,36 @@ 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 getShibUserIdExtractorConfigForDomain(string $domain): array + { + $key = self::SHIB_USER_ID_EXTRACTOR; + if (! array_key_exists($key, $this->shibbolethConfig)) { + throw new RuntimeException("Aucune config '$key' trouvée."); + } + + $config = $this->shibbolethConfig[$key]; + + if (! array_key_exists($domain, $config)) { + return []; + } + + return $config[$domain]; + } + /** * */ @@ -224,7 +258,6 @@ EOS; $this->assertRequiredAttributesExistInData($data); $eppn = $this->getValueFromShibData('eppn', $data); - $supannId = $this->getValueFromShibData('supannEmpId', $data) ?: $this->getValueFromShibData('supannEtuId', $data); $email = $this->getValueFromShibData('mail', $data); $displayName = $this->getValueFromShibData('displayName', $data); $givenName = $this->getValueFromShibData('givenName', $data); @@ -234,17 +267,22 @@ EOS; Assertion::contains($eppn, '@', "L'eppn '" . $eppn . "' n'est pas de la forme 'id@domaine' attendue (ex: 'tartempion@unicaen.fr')"); $shibUser = new ShibUser(); + // propriétés de UserInterface $shibUser->setEppn($eppn); - $shibUser->setId($supannId); + $shibUser->setUsername($eppn); + $domain = $shibUser->getEppnDomain(); // possible uniquement après $shibUser->setEppn($eppn) + $id = $this->extractShibUserIdValueForDomainFromShibData($domain, $data); + $shibUser->setId($id); $shibUser->setDisplayName($displayName); $shibUser->setEmail($email); + // autres propriétés $shibUser->setNom($surname); $shibUser->setPrenom($givenName); $shibUser->setCivilite($civilite); return $shibUser; } - + /** * Retourne true si les données stockées en session indiquent qu'une usurpation d'identité Shibboleth est en cours. * @@ -332,7 +370,7 @@ EOS; throw new RuntimeException("Anomalie: clé '$key' introuvable"); } - $this->simulateAuthenticatedUser($toShibUser, 'supannEmpId'); + $this->simulateAuthenticatedUser($toShibUser); return $this; } @@ -394,26 +432,15 @@ EOS; * qui se 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 function simulateAuthenticatedUser(ShibUser $shibUser, $keyForId = 'supannEmpId') + public function simulateAuthenticatedUser(ShibUser $shibUser) { // 'REMOTE_USER' (notamment) est utilisé pour savoir si un utilisateur est authentifié ou non $this->setServerArrayVariable('REMOTE_USER', $shibUser->getEppn()); -// // on s'assure que tous les attributs obligatoires ont une valeur -// foreach ($this->getShibbolethRequiredAttributes() as $requiredAttribute) { -// // un pipe permet d'exprimer un OU logique, ex: 'supannEmpId|supannEtuId' -// $attributes = array_map('trim', explode('|', $requiredAttribute)); -// foreach ($attributes as $attribute) { -// $this->setServerArrayVariable($attribute, 'qqchose'); -// } -// } - // pour certains attributs, on veut une valeur sensée! $this->setServerArrayVariable('eppn', $shibUser->getEppn()); - $this->setServerArrayVariable($keyForId, $shibUser->getId()); + $this->setServerArrayVariable('supannEmpId', $shibUser->getId()); // ou bien 'supannEtuId', peu importe. $this->setServerArrayVariable('displayName', $shibUser->getDisplayName()); $this->setServerArrayVariable('mail', $shibUser->getEppn()); $this->setServerArrayVariable('sn', $shibUser->getNom()); @@ -432,18 +459,20 @@ EOS; throw new RuntimeException('Des attributs Shibboleth obligatoires font défaut dans $_SERVER.', null, $e); } - $eppn = $this->getServerArrayVariable('eppn'); - $id = $this->getServerArrayVariable('supannEtuId') ?: $this->getServerArrayVariable('supannEmpId'); - $mail = $this->getServerArrayVariable('mail'); - $displayName = $this->getServerArrayVariable('displayName'); - $surname = $this->getServerArrayVariable('sn') ?: $this->getServerArrayVariable('surname'); - $givenName = $this->getServerArrayVariable('givenName'); - $civilite = $this->getServerArrayVariable('supannCivilite'); + $eppn = $this->getValueFromShibData('eppn', $_SERVER); + $mail = $this->getValueFromShibData('mail', $_SERVER); + $displayName = $this->getValueFromShibData('displayName', $_SERVER); + $surname = $this->getValueFromShibData('sn', $_SERVER) ?: $this->getValueFromShibData('surname', $_SERVER); + $givenName = $this->getValueFromShibData('givenName', $_SERVER); + $civilite = $this->getValueFromShibData('supannCivilite', $_SERVER); $shibUser = new ShibUser(); // propriétés de UserInterface - $shibUser->setId($id); + $shibUser->setEppn($eppn); $shibUser->setUsername($eppn); + $domain = $shibUser->getEppnDomain(); // possible uniquement après $shibUser->setEppn($eppn) + $id = $this->extractShibUserIdValueForDomainFromShibData($domain, $_SERVER); + $shibUser->setId($id); $shibUser->setDisplayName($displayName); $shibUser->setEmail($mail); $shibUser->setPassword(null); @@ -455,6 +484,40 @@ EOS; return $shibUser; } + /** + * @param string $domain + * @param array $data + * @return string|null + */ + private function extractShibUserIdValueForDomainFromShibData(string $domain, array $data): ?string + { + $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) { + 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]; + } + + return $value; + } + } + + return null; + } + /** * Retourne l'URL de déconnexion Shibboleth. * @@ -554,19 +617,4 @@ EOS; $_SERVER[$key] = $value; } - - /** - * @param $name - * @return string|null - */ - private function getServerArrayVariable($name): ?string - { - $key = $this->getShibbolethAliasFor($name) ?: $name; - - if (! array_key_exists($key, $_SERVER)) { - return null; - } - - return $_SERVER[$key]; - } } \ No newline at end of file