Rev 1936 | Blame | Compare with Previous | Last modification | View Log | RSS feed
<?php/*** Service d'itentification d'un utilisateur.* Modifié pour fonctionner avec le SSO.** Encodage en entrée : utf8* Encodage en sortie : utf8** Cas d'utilisation :* Le service reçoit un jeton SSO (ou pas) dans le header "Authorization", le fait* vérifier par l'annuaire; s'il est valide, le décode puis retourne le profil utilisateur* associé; sinon retourne un profil anonyme.** 1: Aucun jeton ou jeton invalide transmis* 1: L'application retourne l'identifiant de session en cours (mode anonyme)** 2: Jeton valide transmis* 1 : Passe ou reste dans l'état "connecté"; le profil actif est retourné** En résumé, utilisation des URLs :* /CoelUtilisateur/identite : retour identifiant de session si jamais connecté, sinon retour de l'id (+ login et mot de passe)** @author Jean-Pascal MILCENT <jpm@tela-botanica.org>* @author David DELON <david.delon@clapas.net>* @author Mathias CHOUET <mathias@tela-botanica.org>* @license GPL v3 <http://www.gnu.org/licenses/gpl.txt>* @license CECILL v2 <http://www.cecill.info/licences/Licence_CeCILL_V2-en.txt>* @version $Id$* @copyright 2009*/class CoelUtilisateur extends Coel {protected $licence_acceptee = false;/*** Point d'entrée GET sans paramètres*/public function getRessource() {// par défaut, mode anonyme$infos = $this->getUtilisateurAnonyme();// recherche d'un utilisateur identifié en fonction du jeton SSO$infosIdentifie = $this->getUtilisateurSSO();if ($infosIdentifie != null) {$infos = $infosIdentifie;}// et vlan, passe-moi l'éponge$this->envoyer($infos);}/*** Point d'entrée GET avec paramètres*/public function getElement($ressources) {switch($ressources[0]) {case "setLicence" :$this->setLicence($ressources[1]);break;default:echo "action [" . $ressources[0] . "] inconnue";}}/*** Renvoie un profil utilisateur CoeL anonyme* (licence vide, id de session, non connecté)*/protected function getUtilisateurAnonyme() {$info = array("", session_id(), false);return $info;}/*** Recherche un jeton SSO dans l'entête HTTP "Authorization", vérifie ce* jeton auprès de l'annuaire et en cas de succès charge les informations* de l'utilisateur associé; si besoin, copie l'utilisateur dans la table* Personnes de CoeL** @return Array un profil utilisateur ou null*/protected function getUtilisateurSSO() {$utilisateur = null;// lecture du jeton$jeton = $this->lireJetonEntete();//echo "Jeton : "; var_dump($jeton);if ($jeton != null) {// validation par l'annuaire$valide = $this->verifierJeton($jeton);if ($valide === true) {// décodage du courriel utilisateur depuis le jeton$donneesJeton = $this->decoderJeton($jeton);if ($donneesJeton != null && $donneesJeton["sub"] != "") {// récupération de l'utilisateur$courriel = $donneesJeton["sub"];// lecture des infos dans l'annuaire (relou mais nécessaire pour la copie dans la table Personnes de CoeL)$infosAnnuaire = $this->obtenirInfosAnnuaire($courriel);$infosAnnuaire['courriel'] = $courriel;$this->setInfosAnnuaire($infosAnnuaire);//echo "Infos Annu: "; var_dump($infosAnnuaire);// lecture de l'utilisateur connu (ou pas) par CoeL (table Personnes)$utilisateur_existant = $this->chargerUtilisateur($courriel);//echo "UTIL EXIST: "; var_dump($utilisateur_existant);// Vérification de la nécessité de mettre à jour l'utilisateur COEL vis à vis de l'annuaire de Tela Botanicaif (!is_null($infosAnnuaire) && $this->avoirBesoinMiseAJour($utilisateur_existant)) {$this->debug[] = "Besoin d'une mise à jour";$presence_dans_coel = ($utilisateur_existant != false); // si on l'a trouvé juste au dessus$this->debug[] = "Presence: $presence_dans_coel";//$mot_de_passe_sha1 = $infosAnnuaire['pass'];//echo "MDP: " .$mot_de_passe_sha1;if ($presence_dans_coel) {// Nécessite de faire une mise à jour$this->debug[] = "Mise à jour de l'utilisateur {$infosAnnuaire['id']}";$this->mettreAJourUtilisateur($courriel, $infosAnnuaire);// ci-dessous : ne pas caster en int car GWT attend une String (le con !)$utilisateur_existant['licence'] = $this->recupererLicenceUtilisateur($infosAnnuaire['id']);} else {// Nécessite d'ajouter le nouvel utilisateur$this->debug[] = "Ajout d'une nouvel utilisateur";$this->ajouterUtilisateurACoel($infosAnnuaire);// rechargement après l'avoir ajouté (un peu nul)$utilisateur_existant = $this->chargerUtilisateur($courriel);}}// stockage de l'utilisateur en session$this->setUtilisateur($utilisateur_existant);// renvoi des infos$utilisateur = array($utilisateur_existant['licence'], $infosAnnuaire['id'], true, $this->getUtilisateurNomComplet(), $this->getUtilisateurPrenom(), $this->getUtilisateurNom(), $this->getParametre());}}}return $utilisateur;}protected function deconnecterUtilisateur() {$_SESSION['coel_utilisateur'] = '';$_SESSION['coel_infosAnnuaire'] = '';}/*** Retourne true si le compte utilisateur a été modifié dans l'annuaire, et que les* modifications ont besoin d'être répercutées dans la table Personnes de Coel*/protected function avoirBesoinMiseAJour($info_annuaire_coel) {$necessite_maj = false;if ($info_annuaire_coel == false) {// Le login et/ou le mot de passe a pu changer$necessite_maj = true;} else {$info_annuaire_distant = $this->getInfosAnnuaire();if ($this->comparerInfosAnnuairesDistantEtCoel($info_annuaire_distant, $info_annuaire_coel) == false) {$necessite_maj = true;}}return $necessite_maj;}protected function comparerInfosAnnuairesDistantEtCoel($annuaire_distant, $annuaire_coel) {$identique = true;$tableau_annuaire_distant = array('nom' => $annuaire_distant['nom'],'prenom' => $annuaire_distant['prenom'],'ville' => $annuaire_distant['ville'],'code_postal' => $annuaire_distant['code_postal']);$tableau_annuaire_coel = array('nom' => $annuaire_coel['nom'],'prenom' => $annuaire_coel['prenom'],'ville' => $annuaire_coel['ville'],'code_postal' => $annuaire_coel['code_postal']);foreach ($tableau_annuaire_distant as $cle => $valeur) {if ($tableau_annuaire_coel[$cle] != $valeur) {$identique = false;break;}}return $identique;}/*** Vérifie si un utilisateur est présent dans la table Personnes de CoeL*/protected function verifierPresenceUtilisateur($id) {$present = false;$requete = 'SELECT COUNT(cp_id_personne) AS nbre '.'FROM coel_personne '."WHERE cp_ce_annuaire = {$this->bdd->quote($id)} "." AND cp_ce_annuaire = cp_id_personne ";try {$nbre = $this->bdd->query($requete)->fetchColumn();if (0 == $nbre) {$this->debug[] = "Utilisateur NON présent dans l'annuaire de COEL.";} else if (1 == $nbre) {$this->debug[] = "Utilisateur présent dans l'annuaire de COEL.";$present = true;} else if (false === $nbre) {$this->debug[] = "Erreur dans la requête de vérification de présence dans l'annuaire de COEL.";}} catch (PDOException $e) {$this->messages[] = sprintf($this->getTxt('sql_erreur'), $e->getFile(), $e->getLine(), $e->getMessage());}return $present;}/*** Renvoie l'état d'acceptation de la licence pour un utilisateur donné :* 0 (non acceptée) ou 1 (acceptée)*/protected function recupererLicenceUtilisateur($id) {$requete = 'SELECT cp_mark_licence '.'FROM coel_personne '."WHERE cp_ce_annuaire = {$this->bdd->quote($id)} "." AND cp_ce_annuaire = cp_id_personne ";try {$licence = $this->bdd->query($requete)->fetchColumn();if ($licence === false) {$this->debug[] = "La licence n'a pas pu être récupérée.";return 0;} else {return $licence;}} catch (PDOException $e) {$this->messages[] = sprintf($this->getTxt('sql_erreur'), $e->getFile(), $e->getLine(), $e->getMessage());}}/*** Met à jour les données de l'utilisateur dans la table CoeL Personnes*/protected function mettreAJourUtilisateur($login, $infos) {try {$cp_fmt_nom_complet = $infos['prenom'].' '.$infos['nom'];$requete = ''. 'UPDATE coel_personne'. " SET cp_id_personne = '{$infos['id']}', ". " cp_fmt_nom_complet = '$cp_fmt_nom_complet', cp_prenom = '{$infos['prenom']}', cp_nom = '{$infos['nom']}', ". " cp_code_postal = '{$infos['code_postal']}', cp_ville = '{$infos['ville']}', cp_truk_courriel = '{$infos['courriel']}', ". " cp_login = '{$infos['courriel']}', cp_ce_annuaire = '{$infos['id']}' ". " WHERE cp_login = '$login' ";// Ajout des données$resultat = $this->bdd->exec($requete);if ($resultat === false) {$this->messages[] = "L'utilisateur n'a pas été mis à jour car la requête a échouée.";}} catch (PDOException $e) {$messages[] = sprintf($this->getTxt('sql_erreur'), $e->getFile(), $e->getLine(), $e->getMessage(), $requete);}}/*** Met à jour l'utilisateur d'id $idUtilisateur, et passe son acceptation de* la licence à "1"*/protected function setLicence($idUtilisateur = null) {if ($idUtilisateur == null) {$messages[] = "Id utilisateur null";$this->envoyer();exit;}// Mise à jour de l'utilisateurtry {$requete = 'UPDATE coel_personne '."SET cp_mark_licence = 1 "."WHERE cp_ce_annuaire = '$idUtilisateur' ";// Ajout des données$resultat = $this->bdd->exec($requete);if ($resultat === false) {$this->messages[] = "L'utilisateur n'a pas été mis à jour car la requête a échouée.";} else {// Confirmation attendue par l'interface$this->envoyer(array("1"));exit;}} catch (PDOException $e) {$messages[] = sprintf($this->getTxt('sql_erreur'), $e->getFile(), $e->getLine(), $e->getMessage(), $requete);$this->envoyer();}}/*** Ajoute une copie de l'utilisateur dans la table CoeL Personnes*/protected function ajouterUtilisateurACoel($infos) {try {// Construction de la requête d'ajout// Notes : pour rester compatibles avec l'annuaire de Tela, les utilisateurs sont ajoutés directement avec l'id// de l'annuaire Tela. Dans CoelPersonne, les personnes qui ne sont pas utilisateur sont ajoutés avec un id supérieur à 100 000// Notes 2017 : on ne devrait plus stocker les mots de passe dans la table coel_personne, et on ne devrait plus// considérer que la base de données de l'annuaire se trouve sur le même serveur.$cp_fmt_nom_complet = $infos['prenom'].' '.$infos['nom'];$cp_mark_licence = '0';// si un utilisateur a changé son email dans un compte TB existant, une ligne avec son ID existera déjà et /i// => on fait un REPLACE (attention à la compatibilité !)$requete = 'REPLACE INTO coel_personne '.' (cp_id_personne, cp_fmt_nom_complet, cp_prenom, cp_nom, cp_code_postal, '.' cp_ville, cp_truk_courriel, cp_login, cp_ce_annuaire, cp_mark_licence) '."VALUES ('{$infos['id']}', '$cp_fmt_nom_complet', '{$infos['prenom']}', '{$infos['nom']}', "."'{$infos['code_postal']}', '{$infos['ville']}', '{$infos['courriel']}', '{$infos['courriel']}', "."'{$infos['id']}', $cp_mark_licence) ";//$this->debug[] = $requete;// Ajout des donnéesvar_dump($requete);$resultat = $this->bdd->exec($requete);if ($resultat === false) {$this->debug[] = "Utilisateur NON ajouté dans coel_personne car la requête a échouée.";} else {$this->debug[] = "Utilisateur ajouté à coel_personne.";}} catch (PDOException $e) {//$this->debug[] = "EREEUR SQL: " . sprintf($this->getTxt('sql_erreur'), $e->getFile(), $e->getLine(), $e->getMessage(), $requete);$messages[] = sprintf($this->getTxt('sql_erreur'), $e->getFile(), $e->getLine(), $e->getMessage(), $requete);}}/*** Appelle l'annuaire pour connaître tous les détails de l'utilisateur de courriel $login* @TODO gérer le pseudo un jour !*/protected function obtenirInfosAnnuaire($login) {$url_annuaire = $this->config['coel']['urlAnnuaire'];$login_annuaire = $this->config['coel']['loginAnnuaire'];$mdp_annuaire = $this->config['coel']['mdpAnnuaire'];$posds = strpos($url_annuaire, "//");$protocole = substr($url_annuaire, 0, $posds+2);$reste_adresse = substr($url_annuaire, $posds+2);// auth HTTP basic cracra$url_annuaire = $protocole . $login_annuaire . ':' . $mdp_annuaire . "@" . $reste_adresse;$url_annuaire .= '/' . $login;$url_annuaire .= '/xml'; // @TODO un jour, faire en sorte que ça lise du JSON$resultat_annuaire = file_get_contents($url_annuaire);// valeurs par défaut pour éviter des warning$tableau_annuaire = array("nom" => "","prenom" => "","ville" => "","code_postal" => "","courriel" => "");if ($xml_utilisateur = simplexml_load_string($resultat_annuaire)) {// La fonction a retourné un objetforeach ($xml_utilisateur->children() as $key => $val) {if ((string) $val != '') {$tableau_annuaire[$key] = (String) $val;}}}return $tableau_annuaire;}/*** Enregistre le fait que la personne de login $login a accepté la licence de CoeL*/protected function accepterLicence($login) {$sortie = false;try {$requete = 'UPDATE coel_personne '.'SET cp_mark_licence = 1 '."WHERE cp_login = {$this->bdd->quote($login)} ";$resultat = $this->bdd->exec($requete);if ($resultat === false) {$this->debug[] = "La table Personne n'a pas été mise à jour car la requête a échoué";} else {$this->debug[] = "Création du cookie licence.";$_SESSION['coel_utilisateur']['licence'] = '1';// @TODO CHANGER//$this->setCookiePersistant('coel_licence', '1');$sortie = true;}} catch (PDOException $e) {$messages[] = sprintf($this->getTxt('sql_erreur'), $e->getFile(), $e->getLine(), $e->getMessage(), $requete);}return $sortie;}/*** Essaye de trouver un jeton JWT non vide dans l'entête HTTP "Authorization"** @return String un jeton JWT ou null*/protected function lireJetonEntete() {$jwt = null;$headers = apache_request_headers();if (isset($headers["Authorization"]) && ($headers["Authorization"] != "")) {$jwt = $headers["Authorization"];}return $jwt;}/*** Vérifie un jeton auprès de l'annuaire** @param String $jeton un jeton JWT* @return true si le jeton est vérifié, false sinon*/protected function verifierJeton($jeton) {$urlServiceVerification = $this->config['coel']['urlServiceBaseAuth'] . "verifierjeton";$urlServiceVerification .= "?token=" . $jeton;// file_get_contents râle si le certificat HTTPS est auto-signé//$retour = file_get_contents($urlServiceVerification);// curl avec les options suivantes ignore le pb de certificat (pour tester en local)$ch = curl_init();$timeout = 5;curl_setopt($ch, CURLOPT_URL, $urlServiceVerification);curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);// équivalent de "-k"curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);$data = curl_exec($ch);curl_close($ch);$retour = $data;$retour = json_decode($retour, true);return ($retour === true);}/*** Décode un jeton JWT (SSO) précédemment validé et retourne les infos* qu'il contient (payload / claims)* @param String $jeton un jeton JWT précédemment validé*/protected function decoderJeton($jeton) {$parts = explode('.', $jeton);$payload = $parts[1];$payload = base64_decode($payload);$payload = json_decode($payload, true);return $payload;}// accesseurs à deux rondsprotected function getUtilisateurId() {if ($utilisateur = $this->getUtilisateur()) {return $utilisateur['id'];} else {return '';}}protected function getUtilisateurLogin() {if ($utilisateur = $this->getUtilisateur()) {return $utilisateur['login'];} else {return '';}}protected function getUtilisateurNomComplet() {if ($utilisateur = $this->getUtilisateur()) {return $utilisateur['nom_complet'];} else {return '';}}protected function getUtilisateurPrenom() {if ($utilisateur = $this->getUtilisateur()) {return $utilisateur['prenom'];} else {return '';}}protected function getUtilisateurNom() {if ($utilisateur = $this->getUtilisateur()) {return $utilisateur['nom'];} else {return '';}}protected function getParametre() {if ($utilisateur = $this->getUtilisateur()) {return $utilisateur['parametre'];} else {return '';}}protected function getLicence() {if (!empty($_SESSION['coel_utilisateur'])) {return (string) $_SESSION['coel_utilisateur']['licence'];} else {return '';}}protected function getInfosAnnuaire() {if (!empty($_SESSION['coel_infosAnnuaire'])) {return $_SESSION['coel_infosAnnuaire'];} else {return '';}}protected function setInfosAnnuaire($infosAnnuaire) {$_SESSION['coel_infosAnnuaire'] = $infosAnnuaire;}}/*** Compatibilité avec nginx - merci http://php.net/manual/fr/function.getallheaders.php*/if (! function_exists('apache_request_headers')) {function apache_request_headers() {$headers = '';foreach ($_SERVER as $name => $value) {if (substr($name, 0, 5) == 'HTTP_') {$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;}}return $headers;}}?>