422 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			422 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nur\b\authnz;
 | |
| 
 | |
| use nur\authz;
 | |
| use nur\b\IllegalAccessException;
 | |
| use nur\b\ValueException;
 | |
| use nur\config;
 | |
| use nur\cookie;
 | |
| use nur\F;
 | |
| use nur\msg;
 | |
| use nur\P;
 | |
| use nur\session;
 | |
| use nur\v\fo;
 | |
| use nur\v\ly;
 | |
| use nur\v\page;
 | |
| use nur\v\v;
 | |
| use nur\v\vo;
 | |
| use nur\v\vp\AInitAuthzPage;
 | |
| 
 | |
| /**
 | |
|  * Interface IAuthzManager: gestionnaire d'authentification et d'autorisation
 | |
|  */
 | |
| class AuthzManager {
 | |
|   #############################################################################
 | |
|   # Auth
 | |
| 
 | |
|   const COOKIE_KEY_AUTH = "auth:app:";
 | |
|   /** durée du cookie d'authentification: 17 jours par défaut */
 | |
|   const COOKIE_DURATION = 17 * 24 * 60;
 | |
| 
 | |
|   const SESSION_KEY_COOKIE = "authz:cookie";
 | |
|   const SESSION_KEY_STATUS = "authz:status";
 | |
|   const SESSION_KEY_USERNAME = "authz:username";
 | |
|   const SESSION_KEY_USER = "authz:user";
 | |
|   const SESSION_KEY_SULOGIN = "authz:sulogin";
 | |
| 
 | |
|   /** redirection vers la page de login pour une connexion standard */
 | |
|   const REASON_LOGIN = 1;
 | |
|   /** redirection vers la page de login pour cause de session expirée */
 | |
|   const REASON_SESSION = 2;
 | |
|   /** redirection vers la page de login pour accès interdit */
 | |
|   const REASON_UNAUTHORIZED = 3;
 | |
| 
 | |
|   /** pas de statut en particulier */
 | |
|   const STATUS_NONE = 0;
 | |
|   /** session fraichement démarrée */
 | |
|   const STATUS_INITIAL = 1;
 | |
|   /** session déconnecté */
 | |
|   const STATUS_DISCONNECTED = 2;
 | |
|   /** accès non autorisé */
 | |
|   const STATUS_UNAUTHORIZED = 3;
 | |
| 
 | |
|   protected function getCookieKey(): string {
 | |
|     return self::COOKIE_KEY_AUTH.config::get_appcode();
 | |
|   }
 | |
| 
 | |
|   protected function getCookieDuration(): int {
 | |
|     return static::COOKIE_DURATION;
 | |
|   }
 | |
| 
 | |
|   /** vérifier que le cookie d'authentification est posé et valide. */
 | |
|   function checkCookie(?string &$username=null, ?string &$authType=null): bool {
 | |
|     $value = cookie::get($this->getCookieKey(), false);
 | |
|     if ($value === false) return false;
 | |
|     if (!preg_match('/^(cas|form|ext|):(.*)$/', $value, $ms)) return false;
 | |
|     $authType = $ms[1];
 | |
|     $username = $ms[2];
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   protected function setCookie(string $username, string $authType): void {
 | |
|     cookie::setd($this->getCookieKey(), "$authType:$username", $this->getCookieDuration());
 | |
|   }
 | |
| 
 | |
|   protected function resetCookie(): void {
 | |
|     cookie::set($this->getCookieKey(), false);
 | |
|   }
 | |
| 
 | |
|   #############################################################################
 | |
|   # Session
 | |
| 
 | |
|   /** @var int */
 | |
|   private $status = self::STATUS_NONE;
 | |
| 
 | |
|   function getStatus(): int {
 | |
|     return $this->status;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Vérifier que la session est valide.
 | |
|    *
 | |
|    * comme raccourci, si la session est valide, initialiser $username avec le
 | |
|    * nom d'utilisateur *du cookie*
 | |
|    */
 | |
|   function checkSession(?string &$username=null, ?string &$authType=null): bool {
 | |
|     if (!$this->checkCookie($username, $authType)) return false;
 | |
|     session::start();
 | |
|     $this->status = session::get(self::SESSION_KEY_STATUS, self::STATUS_NONE);
 | |
|     return session::get(self::SESSION_KEY_COOKIE) === "$authType:$username";
 | |
|   }
 | |
| 
 | |
|   function isNewSession(): bool {
 | |
|     return session::get(self::SESSION_KEY_COOKIE) === null;
 | |
|   }
 | |
| 
 | |
|   function initSession(int $status): void {
 | |
|     $this->checkCookie($username, $authType);
 | |
|     session::start();
 | |
|     session::set(self::SESSION_KEY_COOKIE, "$authType:$username");
 | |
|     $this->status = $status;
 | |
|     session::set(self::SESSION_KEY_STATUS, $status);
 | |
|     session::set(self::SESSION_KEY_USERNAME, null);
 | |
|     session::set(self::SESSION_KEY_USER, null);
 | |
|   }
 | |
| 
 | |
|   function resetSession(int $status=self::STATUS_DISCONNECTED): void {
 | |
|     if ($this->checkCookie()) {
 | |
|       session::start();
 | |
|       session::destroy(true);
 | |
|       $this->initSession($status);
 | |
|     } elseif (session::started()) {
 | |
|       session::destroy(true);
 | |
|       $this->initSession($status);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /** @var string */
 | |
|   private $auth;
 | |
| 
 | |
|   /** tester si l'utilisateur est connecté */
 | |
|   function _isAuth(): bool {
 | |
|     if (!session::started() && session::started_once()) {
 | |
|       # la session a déjà été démarrée, pas besoin de la démarrer de nouveau
 | |
|     } else if (!$this->checkSession()) {
 | |
|       return false;
 | |
|     }
 | |
|     return session::get(self::SESSION_KEY_USERNAME) !== null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * obtenir le compte de l'utilisateur connecté
 | |
|    *
 | |
|    * @throws ValueException si aucun utilisateur n'est connecté
 | |
|    */
 | |
|   function _getAuth(): string {
 | |
|     if (!session::started() && session::started_once()) {
 | |
|       # la session a déjà été démarrée, pas besoin de la démarrer de nouveau
 | |
|       $username = session::get(self::SESSION_KEY_USERNAME);
 | |
|       if ($username !== null) return $username;
 | |
|     } elseif ($this->checkSession()) {
 | |
|       $username = session::get(self::SESSION_KEY_USERNAME);
 | |
|       if ($username !== null) return $username;
 | |
|     }
 | |
|     throw new ValueException("not authenticated");
 | |
|   }
 | |
| 
 | |
|   function isAuth(): bool {
 | |
|     $auth = $this->auth;
 | |
|     if ($auth !== null) return true;
 | |
|     else return $this->_isAuth();
 | |
|   }
 | |
| 
 | |
|   function getAuth(): string {
 | |
|     $auth = $this->auth;
 | |
|     if ($auth !== null) return $auth;
 | |
|     else return $this->_getAuth();
 | |
|   }
 | |
| 
 | |
|   #############################################################################
 | |
|   # Authz
 | |
| 
 | |
|   const USER_MANAGER_CLASS = SimpleUserManager::class;
 | |
| 
 | |
|   /** @var IUserManager */
 | |
|   private static $user_manager;
 | |
| 
 | |
|   protected function getUserManager(): IUserManager {
 | |
|     if (self::$user_manager === null) {
 | |
|       $class = static::USER_MANAGER_CLASS;
 | |
|       self::$user_manager = new $class();
 | |
|     }
 | |
|     return self::$user_manager;
 | |
|   }
 | |
| 
 | |
|   function redirect(int $reason, string $destUrl, string $loginUrl): void {
 | |
|     $params = ["d" => $destUrl];
 | |
|     switch ($reason) {
 | |
|     case self::REASON_LOGIN:
 | |
|       $params["a"] = 1; // autologin si possible
 | |
|       break;
 | |
|     case self::REASON_SESSION:
 | |
|       $this->resetSession(self::STATUS_DISCONNECTED);
 | |
|       break;
 | |
|     case self::REASON_UNAUTHORIZED:
 | |
|       session::set(self::SESSION_KEY_STATUS, self::STATUS_UNAUTHORIZED);
 | |
|       break;
 | |
|     default:
 | |
|       throw IllegalAccessException::unexpected_state();
 | |
|     }
 | |
|     page::redirect(page::bu($loginUrl, $params));
 | |
|   }
 | |
| 
 | |
|   function extLogin(string $username): bool {
 | |
|     # l'utilisateur doit exister
 | |
|     $user = $this->getUserManager()->getAuthzUser($username, null);
 | |
|     # ce doit être un utilisateur valide
 | |
|     if ($user !== null && $user->isValid()) {
 | |
|       $this->setCookie($username, "ext");
 | |
|       $this->initSession(self::STATUS_INITIAL);
 | |
|       session::set(self::SESSION_KEY_USERNAME, $username);
 | |
|       $this->onAuthOk($username);
 | |
|       session::set(self::SESSION_KEY_USER, $user);
 | |
|       $this->onAuthzOk($user);
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   function formLogin(string $username, string $password): bool {
 | |
|     # l'utilisateur doit exister
 | |
|     $user = $this->getUserManager()->getAuthzUser($username, null);
 | |
|     if ($user !== null) {
 | |
|       # ce doit être un utilisateur valide
 | |
|       if ($user->isValid()) {
 | |
|         $this->setCookie($username, "form");
 | |
|         # le cas échéant le mot de passe doit correspondre
 | |
|         if (config::is_devel() && !$password) $password = null;
 | |
|         if ($password === null || $user->validatePassword($password)) {
 | |
|           # c'est bon
 | |
|           $this->initSession(self::STATUS_INITIAL);
 | |
|           session::set(self::SESSION_KEY_USERNAME, $username);
 | |
|           $this->onAuthOk($username);
 | |
|           session::set(self::SESSION_KEY_USER, $user);
 | |
|           $this->onAuthzOk($user);
 | |
|           return true;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   function casLogin(string $username, ?array $overrides): bool {
 | |
|     $this->setCookie($username, "cas");
 | |
|     $this->initSession(self::STATUS_INITIAL);
 | |
|     session::set(self::SESSION_KEY_USERNAME, $username);
 | |
|     $this->onAuthOk($username);
 | |
|     # l'utilisateur doit exister
 | |
|     $user = $this->getUserManager()->getAuthzUser($username, $overrides);
 | |
|     # ce doit être un utilisteur valide
 | |
|     if ($user !== null && $user->isValid()) {
 | |
|       session::set(self::SESSION_KEY_USER, $user);
 | |
|       $this->onAuthzOk($user);
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * après les avoir chargées le cas échéant, retourner les informations
 | |
|    * d'autorisation de l'utilisateur spécifié ou une instance invalide s'il n'y
 | |
|    * en a pas. mettre à jour la session le cas échéant
 | |
|    *
 | |
|    * prendre par défaut l'utilisateur connecté
 | |
|    */
 | |
|   function _selectAuthz(?string $username=null): IAuthzUser {
 | |
|     # ici, la session a peut-être déjà été fermée, mais si elle a été ouverte au
 | |
|     # moins une fois, en tenir compte quand on accède aux informations
 | |
|     if ($username === null) {
 | |
|       if (!session::started_once() || !$this->isAuth()) return UnknownUser::with();
 | |
|       $username = $this->getAuth();
 | |
|       $user = session::get(self::SESSION_KEY_USER);
 | |
|     } else if (session::started_once() && $this->isAuth()) {
 | |
|       $user = session::get(self::SESSION_KEY_USER);
 | |
|     } else {
 | |
|       $user = null;
 | |
|     }
 | |
|     if ($user instanceof IAuthzUser && $user->getUsername() === $username) {
 | |
|       return $user;
 | |
|     }
 | |
|     $user = $this->getUserManager()->getAuthzUser($username, null);
 | |
|     if ($user === null) $user = UnknownUser::with($username);
 | |
|     session::set(self::SESSION_KEY_USERNAME, $username);
 | |
|     session::set(self::SESSION_KEY_USER, $user);
 | |
|     return $user;
 | |
|   }
 | |
| 
 | |
|   /** @var IAuthzUser */
 | |
|   private $authz;
 | |
| 
 | |
|   function setConnected(): void {
 | |
|     $this->auth = $this->_getAuth();
 | |
|     $this->authz = $this->_selectAuthz();
 | |
|   }
 | |
| 
 | |
|   function selectAuthz(?string $username=null): IAuthzUser {
 | |
|     $authz = null;
 | |
|     if ($username === null) $authz = $this->authz;
 | |
|     if ($authz === null) $authz = $this->_selectAuthz($username);
 | |
|     return $authz;
 | |
|   }
 | |
| 
 | |
|   /** obtenir les informations d'autorisation de l'utilisateur effectif */
 | |
|   function getUser(): IAuthzUser {
 | |
|     #XXX le cas échéant, ajouter les informations de l'utilisateur effectif
 | |
|     return $this->selectAuthz();
 | |
|   }
 | |
| 
 | |
|   function checkAuthz(?array $roles, ?array $perms): bool {
 | |
|     $authz = $this->selectAuthz();
 | |
|     if ($roles !== null && !$authz->isRole($roles)) return false;
 | |
|     if ($perms !== null && !$authz->isPerm($perms)) return false;
 | |
|     # sinon, au moins un rôle ou une permission doivent être définis
 | |
|     return $authz->isRole(IAuthzUser::ROLE_AUTHZ);
 | |
|   }
 | |
| 
 | |
|   /** la connexion actuelle a-t-elle été forcée par un administrateur? */
 | |
|   function isSulogin(): bool {
 | |
|     return boolval(session::get(self::SESSION_KEY_SULOGIN, false));
 | |
|   }
 | |
| 
 | |
|   /** marquer la connexion actuelle comme étant faite par un administrateur. */
 | |
|   function setSulogin(bool $sulogin=true): void {
 | |
|     if ($sulogin) {
 | |
|       $user = session::get(self::SESSION_KEY_SULOGIN, false);
 | |
|       if ($user === false) $user = session::get(self::SESSION_KEY_USER);
 | |
|       session::set(self::SESSION_KEY_SULOGIN, $user);
 | |
|     } else {
 | |
|       session::set(self::SESSION_KEY_SULOGIN, false);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   #############################################################################
 | |
|   # Méthodes surchargeables
 | |
| 
 | |
|   /** Traiter le cas où l'utilisateur s'est authentifié avec succès. */
 | |
|   function onAuthOk(string $username): void {
 | |
|   }
 | |
| 
 | |
|   /** Traiter le cas où l'utilisateur a été autorisé avec succès. */
 | |
|   function onAuthzOk(IAuthzUser $authz): void {
 | |
|   }
 | |
| 
 | |
|   #############################################################################
 | |
|   # Page login
 | |
| 
 | |
|   private $username = null;
 | |
|   private $authType = null;
 | |
| 
 | |
|   function beforeSetup(AInitAuthzPage $page): void {
 | |
|     # initialiser la session avant setup. ainsi, dans les fonction beforeSetup(),
 | |
|     # setup() et afterSetup(), la session est disponible
 | |
|     $username = P::get("u");
 | |
|     $password = P::get("p");
 | |
|     $destPage = F::get("d", $page->getMainUrl());
 | |
|     $page->_ensureFormLoginAndRedirect($username, $password, $destPage);
 | |
| 
 | |
|     $this->checkSession($this->username, $this->authType);
 | |
|   }
 | |
| 
 | |
|   function print(AInitAuthzPage $page): void {
 | |
|     page::no_cache();
 | |
|     $username = P::get("u");
 | |
|     $password = P::get("p");
 | |
| 
 | |
|     ly::row();
 | |
|     vo::h1(["class" => "text-center", q($page->TITLE())]);
 | |
| 
 | |
|     ly::col(["sm" => 6, "sm-push" => 3]);
 | |
|     $status = $this->getStatus();
 | |
|     switch ($status) {
 | |
|     case authz::DISCONNECTED:
 | |
|       msg::warning("Vous avez été déconnecté. Veuillez vous reconnecter");
 | |
|       break;
 | |
|     case authz::UNAUTHORIZED:
 | |
|       msg::error(["user" => [
 | |
|         "Connecté en tant que ",
 | |
|         v::b($this->getAuth()),
 | |
|         ", vous n'êtes pas autorisé à accéder à la page que vous avez demandé.",
 | |
|       ]]);
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     ly::panel("Connexion par identifiant/mot de passe");
 | |
|     fo::start([
 | |
|       "type" => "basic",
 | |
|       "action" => "",
 | |
|       "method" => "post",
 | |
|     ]);
 | |
|     fo::text("Identifiant", "u", $username?: $this->username, [
 | |
|       "accesskey" => "q",
 | |
|       "placeholder" => "Votre identifiant",
 | |
|     ]);
 | |
|     fo::password("Mot de passe", "p", $password, [
 | |
|       "placeholder" => "Votre mot de passe",
 | |
|     ]);
 | |
|     if ($username || $password) {
 | |
|       msg::error("$username: Votre identifiant et/ou votre mot de passe sont incorrects");
 | |
|     } elseif ($username === "") {
 | |
|       msg::error("Vous devez saisir votre identifiant");
 | |
|     } elseif ($password === "") {
 | |
|       msg::error("Vous devez saisir votre mot de passe");
 | |
|     }
 | |
|     fo::submit(["Connexion", "accesskey" => "r"]);
 | |
|     if ($this->isAuth() && $this->authType === "form") {
 | |
|       if ($status != authz::UNAUTHORIZED) {
 | |
|         msg::warning(["user" => [
 | |
|           "Connecté en tant que ",
 | |
|           v::b($this->getAuth()),
 | |
|           ", vous n'êtes pas autorisé à accéder à cette application.",
 | |
|         ]]);
 | |
|       }
 | |
|       fo::submit([
 | |
|         "Vous déconnecter", "accesskey" => "z",
 | |
|         "formmethod" => "get", "formaction" => $page->getLogoutUrl(),
 | |
|       ]);
 | |
|     }
 | |
|     fo::end();
 | |
| 
 | |
|     ly::end();
 | |
|   }
 | |
| }
 |