Tutorial 3: Securing INVO ========================= In this chapter, we continue explaining how INVO is structured, we'll talk about the implementation of authentication, authorization using events and plugins and an access control list (ACL) managed by Phalcon. Se connecter à l'application ---------------------------- Se connecter va nous premettre de travailler sur les controlleurs du backend. La séparation entre les controlleurs du backend et du frontend sont purement d'ordre logique, car tous les contrôleurs sont localisés dans le même dossier (app/controllers/). Pour se connecter il faut un nom d'utilsateur et un mot de passe valide. Les utilisateurs sont stockés dans la table "users" de la base de données "invo". Avant de pouvoir commencer une session, nous devons configurer la connexion à la base de données. Un service appelé "db" est utilisé dans le conteneur de service avec cette information. Pour ce qui est de l'autoloader, on prends en paramètres les informations du fichier de configuration de manière à configurer le service : .. code-block:: php set( "db", function () use ($config) { return new DbAdapter( [ "host" => $config->database->host, "username" => $config->database->username, "password" => $config->database->password, "dbname" => $config->database->name, ] ); } ); Ici on retourne une instance de l'adaptateur de connexion à MySQL. Si nécessaire on pourrait faire des actions supplémentaire tel qu'ajouter un logger, un profileur ou changer l'adaptateur, ... Le formulaire (app/views/session/index.volt) demande les informations de connexion. Certaines lignes HTML ont été supprimés dans l'extrait suivant pour rendre l'exemple plus concis: .. code-block:: html+jinja {{ form("session/start") }}
{{ text_field("email") }}
{{ password_field("password") }}
{{ submit_button("Login") }}
{{ endForm() }} Instead of using raw PHP as the previous tutorial, we started to use :doc:`Volt `. This is a built-in template engine inspired in Jinja_ providing a simpler and friendly syntax to create templates. It will not take too long before you become familiar with Volt. Le :code:`SessionController::startAction` (app/controllers/SessionController.php) a pour tâche de valider les données entrées à la recherche d'un utilisateur valide dans la base de données : .. code-block:: php session->set( "auth", [ "id" => $user->id, "name" => $user->name, ] ); } /** * This action authenticate and logs a user into the application */ public function startAction() { if ($this->request->isPost()) { // Get the data from the user $email = $this->request->getPost("email"); $password = $this->request->getPost("password"); // Find the user in the database $user = Users::findFirst( [ "(email = :email: OR username = :email:) AND password = :password: AND active = 'Y'", "bind" => [ "email" => $email, "password" => sha1($password), ] ] ); if ($user !== false) { $this->_registerSession($user); $this->flash->success( "Welcome " . $user->name ); // Forward to the 'invoices' controller if the user is valid return $this->dispatcher->forward( [ "controller" => "invoices", "action" => "index", ] ); } $this->flash->error( "Wrong email/password" ); } // Forward to the login form again return $this->dispatcher->forward( [ "controller" => "session", "action" => "index", ] ); } } Pour des raisons de simplicité, nous avons utilisé "sha1_" pour stocker le mot de passe hashé dans la base de données, cependant cet algorithme n'est pas recommandé pour une vraie application, il est préférable d'utiliser ":doc:`bcrypt `" à la place. Veuillez noter que plusieurs attributs public sont accessibles dans le contrôleur avec :code:`$this->flash`, :code:`$this->request` ou :code:`$this->session`. Ceux-ci sont des servies défini dans le conteneur de service de tout à l'heure (app/config/services.php). Quand ils sont accédés pour la première fois, ils sont insérés dans le controlleur. Ces services sont partagés, ce qui signifie qu'on accéde à la même instance sans tenir compte de l'endroit où on les a créés. Par exemple, ici on créé le service de sessions et on enregistre l'identité de utilisateur dans la variable "auth": .. code-block:: php session->set( "auth", [ "id" => $user->id, "name" => $user->name, ] ); Another important aspect of this section is how the user is validated as a valid one, first we validate whether the request has been made using method POST: .. code-block:: php request->isPost()) { Then, we receive the parameters from the form: .. code-block:: php request->getPost("email"); $password = $this->request->getPost("password"); Now, we have to check if there is one user with the same username or email and password: .. code-block:: php [ "email" => $email, "password" => sha1($password), ] ] ); Note, the use of 'bound parameters', placeholders :email: and :password: are placed where values should be, then the values are 'bound' using the parameter 'bind'. This safely replaces the values for those columns without having the risk of a SQL injection. If the user is valid we register it in session and forwards him/her to the dashboard: .. code-block:: php _registerSession($user); $this->flash->success( "Welcome " . $user->name ); return $this->dispatcher->forward( [ "controller" => "invoices", "action" => "index", ] ); } If the user does not exist we forward the user back again to action where the form is displayed: .. code-block:: php dispatcher->forward( [ "controller" => "session", "action" => "index", ] ); Sécuriser le Backend -------------------- Le backend est une zone privé où seul les personnes enregistrés ont accès. Par conséquent il est nécessaire de vérifier que seul les utilisateurs enregistrés ont accés à ces contrôleurs. Si vous n'êtes pas connectés à l'application et que vous essayez d'accéder au contrôleur product, par exemple, vous verrez le message suivant : .. figure:: ../_static/img/invo-2.png :align: center A chaque fois que quelqu'un essaye d'accéder à n'importe quel contrôleur/action, l'application va vérifier que le rôle de l'utilisateur (en session) lui permet d'y accéder, sinon il affiche un message comme celui du dessus et transfert le flux à la page d'accueil. Maintenant, découvrons comment l'application fait cela. La première chose à savoir est qu'il y a un composant appelé :doc:`Dispatcher `. Il est informé de la route trouvé par le composant :doc:`Routing `. Puis, il est responsable de charger le contrôleur approprié et d'exécuter l'action correspondante. En temps normal, le framework créé le dispatcher automatiquement. Dans notre cas, nous voulons faire une vérification avant d'exécuter l'action requise, vérifier si l'utilisateur y a accès ou pas. Pour faire cela, nous avons remplacé le composant en créant une fonction dans le bootstrap (public/index.php): .. code-block:: php set( "dispatcher", function () { // ... $dispatcher = new Dispatcher(); return $dispatcher; } ); Nous avons maintenant un contrôle complet sur le dispatcher utilisé dans notre application. Plusieurs composants du framework déclenchent des évènements qui nous autorisent à modifier le flux interne des opérations. Comme l'injecteur de dépendances agit comme une "colle" pour composants, un nouveau composant appelé :doc:`EventsManager ` nous aide à intercepter les évènements produits par un composant routant les évènements aux listeners. Gestion des évènements ^^^^^^^^^^^^^^^^^^^^^^ Un :doc:`EventsManager ` (gestionnaire d'évènement) nous permet d'attacher un ou plusieurs listeners à un type particulier d'évènement. Le type d'évènement qui nous intéresse actuellement est le "dispatch", la code suivant filtre tous les évènements produit par le dispatcher : .. code-block:: php set( "dispatcher", function () { // Create an events manager $eventsManager = new EventsManager(); // Listen for events produced in the dispatcher using the Security plugin $eventsManager->attach( "dispatch:beforeExecuteRoute", new SecurityPlugin() ); // Handle exceptions and not-found exceptions using NotFoundPlugin $eventsManager->attach( "dispatch:beforeException", new NotFoundPlugin() ); $dispatcher = new Dispatcher(); // Assign the events manager to the dispatcher $dispatcher->setEventsManager($eventsManager); return $dispatcher; } ); When an event called "beforeExecuteRoute" is triggered the following plugin will be notified: .. code-block:: php attach( "dispatch:beforeExecuteRoute", new SecurityPlugin() ); When a "beforeException" is triggered then other plugin is notified: .. code-block:: php attach( "dispatch:beforeException", new NotFoundPlugin() ); Le plugin de sécurité est une classe situé dans (app/plugins/SecurityPlugin.php). Cette classe implémente une méthode "beforeExecuteRoute". C'est le même nom qu'un des évènement produit dans le dispatcer : .. code-block:: php `, mais en faisant ainsi on a un accès facilité aux services disponibles de l'application. Maintenant nous allons vérifier le rôle de la session courrante, vérifier si l'utilisateur a accès en utilisant les listes ACL (access control list). S'il/elle n'a pas accès, il/elle sera redirigé(e) vers la page d'accueil comme expliqué précédemment. .. code-block:: php session->get("auth"); if (!$auth) { $role = "Guests"; } else { $role = "Users"; } // Take the active controller/action from the dispatcher $controller = $dispatcher->getControllerName(); $action = $dispatcher->getActionName(); // Obtain the ACL list $acl = $this->getAcl(); // Check if the Role have access to the controller (resource) $allowed = $acl->isAllowed($role, $controller, $action); if (!$allowed) { // If he doesn't have access forward him to the index controller $this->flash->error( "You don't have access to this module" ); $dispatcher->forward( [ "controller" => "index", "action" => "index", ] ); // Returning "false" we tell to the dispatcher to stop the current operation return false; } } } Fournir une liste ACL ^^^^^^^^^^^^^^^^^^^^^ Dans l'exemple précédent, nous avons obtenu les ACL en utilisant la méthode :code:`$this->getAcl()`. Cette méthode est aussi implémentée dans Plugin. Maintenant nous allons expliquer étape par étape comment nous avons construit les ACL (access control list) : .. code-block:: php setDefaultAction( Acl::DENY ); // Register two roles, Users is registered users // and guests are users without a defined identity $roles = [ "users" => new Role("Users"), "guests" => new Role("Guests"), ]; foreach ($roles as $role) { $acl->addRole($role); } On défini les ressources pour chaque zone. Le nom des contrôleurs sont des ressources et leurs actions sont accédées pour les ressources : .. code-block:: php ["index", "search", "new", "edit", "save", "create", "delete"], "products" => ["index", "search", "new", "edit", "save", "create", "delete"], "producttypes" => ["index", "search", "new", "edit", "save", "create", "delete"], "invoices" => ["index", "profile"], ]; foreach ($privateResources as $resourceName => $actions) { $acl->addResource( new Resource($resourceName), $actions ); } // Public area resources (frontend) $publicResources = [ "index" => ["index"], "about" => ["index"], "register" => ["index"], "errors" => ["show404", "show500"], "session" => ["index", "register", "start", "end"], "contact" => ["index", "send"], ]; foreach ($publicResources as $resourceName => $actions) { $acl->addResource( new Resource($resourceName), $actions ); } Les ACL ont maintenant connaissance des contrôleurs et de leurs actions. Le rôle "Users" a accès à toutes les ressources du backend et du frontend. Le rôle "Guest" en revanche n'a accès qu'à la partie publique : .. code-block:: php $actions) { $acl->allow( $role->getName(), $resource, "*" ); } } // Grant access to private area only to role Users foreach ($privateResources as $resource => $actions) { foreach ($actions as $action) { $acl->allow( "Users", $resource, $action ); } } Hooray!, les ACL sont maintenant terminés. In next chapter, we will see how a CRUD is implemented in Phalcon and how you can customize it. .. _jinja: http://jinja.pocoo.org/ .. _sha1: http://php.net/manual/fr/function.sha1.php