Architecture web des SI - partie 6 - année 2015 Didier FERMENT - Université de Picardie Symfony 2 : sécurité • • • Le processus d'authentification se nomme firewall dans Symfony 2 : il identifie un client comme déjà authentifié ou comme un anonyme ou comme un "user". Le processus d'autorisation d'exécution d'une action d'un contrôleur se nomme Access Control dans Symfony 2 : en fonction de l'authentification faite et des règles enregistrées ou tests de programmation, il autorise l'exécution ou non. Dans le cas négatif, une autre action peut être proposée ou une exception levée. Quelques ajouts préalable : • au routing QcmSalleTpBundle/Resources/config/routing.yml : qcm_salle_tp_homepage: path: / defaults: { _controller: QcmSalleTpBundle:Default:index } # page d'accueil sans authentification qcm_salle_tp_menu: pattern: /menu defaults: { _controller: QcmSalleTpBundle:Default:menu } # page d'accueil authentifié donc le menu des actions possibles • au DefaultController : class DefaultController extends Controller { public function indexAction() { return $this>render('QcmSalleTpBundle:Default:index.html.twig'); } public function menuAction() { return $this>render('QcmSalleTpBundle:Default:menu.html.twig'); } } • à la vue views/Default/index.html.twig : <!DOCTYPE html> <html> <head> <title>Login/logout</title> </head> <body> <h1>Gestion Salle Tp</h1> <p><a href="{{ path('qcm_salle_tp_menu') }}"> Menu principal</a></p> </body> </html> • créer la vue views/Default/menu.html.twig : ... <h1>Salle Tp</h1> <p>gestion de <a href="{{ path('salle') }}"> salle</a></p> <p>gestion de <a href="{{ path('ordinateur') }}">d'ordinateur</a></p> </body> </html> Security.yml • • Le mécanisme qui permet la gestion des "users" est un UserProvider : il mémorise des users. Par défaut, le fichier de configuration général de la sécurité app/config/security.yml fournit le userProvider "in-memory" • changeons-le en : memory indique que les users sont définis dans le fichier : c'est un userProvider pour la phase de développement. providers: mes_utilisateurs: memory: users: milou: { password: wouah, roles: [ 'ROLE_USER' ] } dupont: { password: dupont, roles: [ 'ROLE_USER' ] } tintin: { password: secret, roles: [ 'ROLE_ADMIN' ] } • les rôles : • Chaque user peut avoir 0 ou plusieurs rôles. • Un rôle se nomme ROLE_XXXXX et sert à préciser les autorisations. • L'accès à une ressource peut nécessiter 0 ou plusieurs rôles. • Une hiérarchie entre rôles définit des héritages • le ROLE_USER correspond aux users "normaux" • ROLE_ADMIN hérite des droits de ROLE_USER role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] • essayer http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/salleTp/menu : ça marche sans problème car il n'y a pour l'instant aucun contrôle. Salle Tp gestion de salle gestion de d'ordinateur • ajouter le lien logout à la vue views/Default/menu.html.twig : ... <p><a href="{{ path('logout') }}">Logout</a></p> firewalls • Le firewall (pare-feu) détermine, pour certaines parties du site, si un utilisateur doit ou ne doit pas être authentifié. Et s'il doit l'être, quelles sont les bonnes routes. • Effacer toute la section firewall et inscrire 3 règles : homepage : pas de restriction logguer : pas de restriction heureusement ! gestionSalleTp : accès seulement aux utilisateurs authentifiés • l'ordre de résolution des patterns de route est important ! firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false gestionSalleTp: pattern: ^/(menu|salle|ordinateur|login_check|logout) form_login: login_path: /login check_path: /login_check logout: path: /logout target: / homepage: pattern: ^/ anonymous: ~ l'authentification pour gestionSalleTp est assurée par un formulaire de login • la route login-path (celle de la page de login) ne doit pas être sécurisée par un firewall ou être accessible aux anonymes. • Le check est la soumission du formulaire : il doit être protégé par un firwall • logout assure la dé-authentification de l'utilisateur essayer http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/: Ça marche Gestion Salle Tp • • Menu principal • et http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/ menu: envoie sur http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/login avec Unable to generate a URL for the named route "login" as such route does not exist • Ajoutez les routes login et login_check ainsi que logout dans le routing général app/config/routing.yml : • la route de check-path doit être sécurisé par un firewall login: pattern: /login defaults: { _controller: QcmSecurityBundle:Security:login } login_check: pattern: /login_check logout: pattern: /logout • Créer un bundle SecurityBundle Le message d'erreur final sur les routes est du à ce qu'il existe déjà des routes $ php app/console generate:bundle Bundle namespace: Qcm/SecurityBundle ... • • supprimer Resources/views/Default renommer le controller Default en SecurityController et créer l'action login : use Symfony\Component\HttpFoundation\Response; … class SecurityController extends Controller { public function loginAction() { return new Response("<html><body>login !</body></html>"); } } • essayer http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/ menu: qui envoie sur http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/login : login ! L'authentification par formulaire • Créer le formulaire de login Resources/views/Security/login.html.twig • le formulaire de login doit forcément avoir : • action = chemin de check de login • l'input _username • l'input _password <!DOCTYPE html> <html> <head> <title>Salle</title> </head> <body> <h1>login</h1> {% if error %} <div>{{ error.message }}</div> {% endif %} <form action="{{ path('login_check') }}" method="post"> <label for="username">Login :</label> <input type="text" id="username" name="_username" value="{{ last_username }}" /> <label for="password">Mot de passe :</label> <input type="password" id="password" name="_password" /> <button type="submit">login</button> </form> </body> </html> • et modifiez l'action login du controller Security : • il n'y a pas d'action pour le check de login ni pour logout : c'est prédéfini dans Symfony use Symfony\Component\Security\Core\SecurityContext; use Symfony\Component\HttpFoundation\Request; ... class SecurityController extends Controller { public function loginAction(Request $request) $session = $request>getSession(); if ($request>attributes>has(SecurityContext::AUTHENTICATION_ERROR)) $error = $request>attributes>get(SecurityContext::AUTHENTICATION_ERROR); else { $error = $session>get(SecurityContext::AUTHENTICATION_ERROR); $session>remove(SecurityContext::AUTHENTICATION_ERROR); } return $this>render('QcmSecurityBundle:Security:login.html.twig', array( 'last_username' => $session>get(SecurityContext::LAST_USERNAME), 'error' => $error)); } • essayer http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/ menu: qui envoie sur http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/login : • et finalement pour l'utilisateur dupont mot de passe dupont No encoder has been configured for account "Symfony\Component\Security\Core\User\User". • Il reste à configurer un encoder pour le mot de passe : dans le fichier security.yml , ajoutez après la section providers : encoders: Symfony\Component\Security\Core\User\User : plaintext • on re-teste et ça passe pour dupont ! • Une fois le login passé, pas de PB pour l'utilisateur dupont : • re-essayer avec un utilisateur inconnu durand : L'utilisateur authentifié • Comment obtenir des informations sur l'utilisateur courant : • ajouter au DefaultController : • utiliser le service security.context public function menuAction() { $user = $this>get('security.context')>getToken()>getUser(); $chaineRoles = ''; foreach ($user>getRoles() as $groupe) $chaineRoles .= ' '.$groupe; return $this>render('QcmSalleTpBundle:Default:menu.html.twig', array('roles' => $chaineRoles)); } • ajoutez à la vue views/Default/menu.html.twig : ... <p><i>Qui suisje ? user : {{ app.user.username }} roles : {{ roles }} </i></p> • essayons http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/ menu: Salle Tp gestion de salle gestion de d'ordinateur Logout Qui suis-je ? user : dupont roles : ROLE_USER Les autorisations • • L'Acces_control du fichier security.yml : • = Un ensemble de règles générales d'autorisation • la 1ére satisfaisante, dans l'ordre, est exécutée • les régles peuvent porter sur un pattern de l'URLs ou sur l'IP du client ou sur https ….. ajoutons au fichier security.yml : access_control: { path: ^/(salle|ordinateur)/$, roles: ROLE_USER } { path: ^/(salle|ordinateur)/[09]+/show$, roles: ROLE_USER } { path: ^/(salle|ordinateur), roles: ROLE_ADMIN } • essayons http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/salle Tp/salle/ sous l'identité dupont simple user : Pas de problème ! • puis essayons d'afficher (show) une salle : Salle Id 1 Batiment B Etage 8 Numero 2 Back to the list Edit Delete • Pas de problème ! Car l'utilisateur dupont à le ROLE_USER requis pour l'action show • puis essayons d'éditer une salle : •• L'accès control a refusé car ROLE_USER n'est pas autorisé pour l'action edit • Faisons un logout : http://localhost/~ferment/symfony2015/web/app_dev.php/logout • Recommençons avec tintin de mot de passe « secret » qui a le ROLE_ADMIN : essayons la création de salle • grâce à la 3éme règle d'autorisation de l'access control • Réalisons un contrôle d’accès dans une action de controller • la page « index » de gestion d'ordinateur ne sera accessible qu'au seul administrateur : • Modifions l'indexAction dans OrdinateurController : public function indexAction() { if (false === $this>get('security.context')>isGranted('ROLE_ADMIN')) throw new AccessDeniedException(); else { $em = $this>getDoctrine()>getManager(); $entities = $em>getRepository('QcmSalleTpBundle:Ordinateur')>findAll(); return $this>render('QcmSalleTpBundle:Ordinateur:index.html.twig', array('entities' => $entities)); } } Essayer http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/ ordinateur en tant que tintin sur la page (ROLE_ADMIN) : Ordinateur list Id Ip Numero Actions 1 888.88.888.21 21 show edit 2 888.88.888.22 22 show …. • puis faire un logout • et essayer en tant que dupont (ROLE_USER) : Access Denied • 403 Forbidden 1 linked Exception • Réalisons un contrôle d’accès dans une vue (template) pour ne proposer à l'utilisateur que des liens "autorisés" • modifions views/Default/menu.html.twig ... <p>gestion de <a href="{{ path('salle') }}"> salle</a></p> <p>gestion de <a href="{{ path('ordinateur') }}">d'ordinateur</a></p> {% if is_granted('ROLE_ADMIN') %} <p>gestion de <a href="{{ path('ordinateur') }}">d'ordinateur</a></p> {% endif %} <p><a href="{{ path('logout') }}">Logout</a></p> ... • Essayer http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/menu en tant que dupont (ROLE_USER) : SalleTp gestion de salle Logout • puis logout et essayer en tant que tintin sur la page (ROLE_ADMIN) : SalleTp gestion de salle gestion de d'ordinateur Logout • faites un logout ! un UserProvider utilisant le stockage Base de Données • les utilisateurs seront stockés dans une table de la base de données • changer le fichier security.yml : providers: mes_utilisateurs: entity: class: QcmSecurityBundle:User property: username encoders: Qcm\SecurityBundle\Entity\User : plaintext • • il faut préciser l'encoder de mot de passe pour le nouveau user provider Générer l'entité User : $ php app/console generate:doctrine:entity The Entity shortcut name: QcmSecurityBundle:User New field name (press <return> to stop adding fields): username Field type [string]: Field length [255]: 20 New field name (press <return> to stop adding fields): password Field type [string]: Field length [255]: 15 ... • Faire hériter votre entité : • malheureusement, Doctrine ne sait pas le faire en Yaml : • il faut implémenter l'interface UserInterface donc 5 méthodes : • getUsername() • getPassword() • getSalt() pour "saler" l'encodage du password : on le laissera en clair ! • GetRoles() • eraseCredentials() efface les informations sensibles de l'utilisateur • et l'interface de sérialisation afin de mémoriser l'utilisateur authentifié dans la session • donc éditez User.php : <?php namespace Qcm\SecurityBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; class User implements UserInterface, \Serializable { private $id; private $username; private $password; public function getId() { return $this>id; } public function getUsername() { return $this>username; } public function getPassword() { return $this>password; } public function getSalt() { return ''; // sel vide ! } public function getRoles() { return array(); // pas de rôle pour l'instant } public function eraseCredentials() { } public function serialize() { return serialize(array( $this>id, $this>username, $this>password)); } public function unserialize($serialized) { list( $this>id, $this>username, $this>password) = unserialize($serialized); } } • générer la table associée à l'entité : $ php app/console doctrine:schema:update dumpsql CREATE TABLE User (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(20) NOT NULL, password VARCHAR(15) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB; $ php app/console doctrine:schema:update force • avec PhpMyAdmin, ajouter 2 utilisateurs dans votre base : INSERT INTO `mabase`.`User` (`id`, `username`, `password`) VALUES (NULL, 'haddock', 'tonnerredebrest'), (NULL, 'castafiore', 'ahjerie'); • essayer http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/ menu avec le username haddock et le password tonnerredebrest : Ça marche ! Salle Tp gestion de salle Logout Qui suis-je ? user : haddock roles : • sauf le rôle de haddock • Ajoutons la gestion des rôles donc une entité "role" associée à "user" par une relation manyToMany : • Créez le fichier Qcm/SecurityBundle/Entity/Role.orm.yml : Qcm\SecurityBundle\Entity\Role: type: entity table: null repositoryClass: Qcm\SecurityBundle\Entity\GroupeRepository id: id: type: integer id: true generator: strategy: AUTO fields: role: type: string length: '15' unique: true manyToMany: users: targetEntity: User mappedBy: roles lifecycleCallbacks: { } • Ajoutez la relation dans Qcm/SecurityBundle/Entity/User.orm.yml : manyToMany: roles: targetEntity: Role inversedBy: users • Générez/mettez à jour les entités : $ php app/console doctrine:generate:entities QcmSecurityBundle Generating entities for bundle "QcmSecurityBundle" > backing up User.php to User.php~ > generating Qcm\SecurityBundle\Entity\User > backing up Groupe.php to Groupe.php~ > generating Qcm\SecurityBundle\Entity\Groupe • modifier l'entité Groupe.php car il faut qu'elle implémente l'interface RoleInterface qui ne comporte que la méthode getRole() : use Symfony\Component\Security\Core\Role\RoleInterface; class Role implements RoleInterface • définir maintenant la méthode getRoles() de l'entité User.php : public function getRoles() { return $this>roles>toArray(); } • Synchronisez la base de données : $ php app/console doctrine:schema:update dumpsql CREATE TABLE user_groupe (user_id INT NOT NULL, groupe_id INT NOT NULL, INDEX IDX_61EB971CA76ED395 (user_id), INDEX IDX_61EB971C7A45358C (groupe_id), PRIMARY KEY(user_id, groupe_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB; CREATE TABLE Groupe (id INT AUTO_INCREMENT NOT NULL, role VARCHAR(15) NOT NULL, password VARCHAR(15) NOT NULL, UNIQUE INDEX UNIQ_315891757698A6A (role), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB; ALTER TABLE user_groupe ADD CONSTRAINT FK_61EB971CA76ED395 FOREIGN KEY (user_id) REFERENCES User (id) ON DELETE CASCADE; ALTER TABLE user_groupe ADD CONSTRAINT FK_61EB971C7A45358C FOREIGN KEY (groupe_id) REFERENCES Groupe (id) ON DELETE CASCADE; $ php app/console doctrine:schema:update force • avec PhpMyAdmin, ajouter les 2 roles dans la base et donnez le ROLE_ADMIN à haddock et ROLE_USER à castafiore : INSERT INTO `mabase`.`Role` (`id`, `role`) VALUES (NULL, 'ROLE_USER'), (NULL, 'ROLE_ADMIN'); INSERT INTO `mabase`.`user_role` (`user_id`, `role_id`) VALUES ('1', '2'), ('2', '1'); • Faites un logout • Essayer en tant que castafiore de password « ahjerie » (ROLE_USER) : http://10.1.17.78/~ferment/symfony2015/web/app_dev.php/ ordinateur Access Denied 403 Forbidden 1 linked Exception • puis en tant que haddock de password « tonnerredebrest» sur la page (ROLE_ADMIN) : Zone administrateur gestion de salle gestion de d'ordinateur Logout • si vous avez une erreur : Catchable Fatal Error: Object of class Qcm\SecurityBundle\Entity\Role could not be converted to string in DefaultController.php elle est due à l'affichage « Qui suis-je » de menuAction car getRoles() doit renvoyé un array de string ou d'objet Role … • il suffit d'ajouter une fonction __toString() à Role : public function __toString() { return $this>getRole(); } les ACLs • Les Acces Control précédents ne permettent pas de distinguer finement les autorisations : par exemple, que le créateur d'un enregistrement soit le seul à pouvoir le modifier ou supprimer, tandis que les autres n'ont qu'un droit de lecture dessus. • Les Access Control List (ACL), liste de contrôle d'accès, permettent de faire une gestion plus fine des droits d'accès aux ressources. • Une ACL est une liste d’Access Control Entry (ACE) ou entrée de contrôle d'accès donnant ou supprimant des droits d'accès à une personne ou un groupe. • Mettons en place les ACLs dans Symfony2 : • Ajoutez_les au fichier security.yml security: ... acl: connection: default • Puis générez les tables de base de données : $ php app/console init:acl Cela ajoute les tables acl_classes acl_entries acl_object_identities acl_object_identity_ancestors acl_security_identities • Ajoutons une ACE "seul l'utilisateur ayant crée un ordinateur peut l'éditer" • Modifions les actions new et edit du controler salle : • l'acl n'est pas "attachée" à l'entité directement mais à une identification de cet objet (ObjectIdentity) • De même, l'ace ne désigne pas le user directement mais une identification du user (UserSecurityIdentity) • l'ace que nous ajoutons concerne ce user et des autorisations regroupées dans un masque. use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Acl\Domain\ObjectIdentity; use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; use Symfony\Component\Security\Acl\Permission\MaskBuilder; ... public function createAction(Request $request) { $entity = new Ordinateur(); $form = $this>createForm(new OrdinateurType(), $entity); $form>bind($request); if ($form>isValid()) { $em = $this>getDoctrine()>getManager(); $em>persist($entity); $em>flush(); $aclProvider = $this>get('security.acl.provider'); $objectIdentity = ObjectIdentity::fromDomainObject($entity); $acl = $aclProvider>createAcl($objectIdentity); $securityContext = $this>get('security.context'); $user = $securityContext>getToken()>getUser(); $securityIdentity = UserSecurityIdentity::fromAccount($user); $acl>insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER); $aclProvider>updateAcl($acl); return $this>redirect($this>generateUrl('salle_show', array('id' => $entity>getId()))); } ... • Ajoutons un utilisateur tournesol de mot de passe « hein ? » et de rôle ROLE_ADMIN avec phpmyadmin et loggez-le • Essayer de créer un nouvel ordinateur • • avec Phpmyadmin, vous voyez que la table acl_entries a une ligne concernant un acl_object_identities et un acl_security_identities qui est tournesol • Modifiez l'action edit du controler salle : • la méthode isGranted() vérifie que le droit est accordé à l'utilisateur public function editAction($id) { $em = $this>getDoctrine()>getManager(); $entity = $em>getRepository('QcmSalleTpBundle:Ordinateur')>find($id); if (!$entity) { throw $this>createNotFoundException('Unable to find Ordinateur entity.'); } $securityContext = $this>get('security.context'); if (false === $securityContext>isGranted('EDIT', $entity)) { throw new AccessDeniedException(); } ... Voici quelques autres permissions : • VIEW, EDIT, CREATE, DELETE, UNDELETE, • OPERATOR = toutes les opérations ci-dessus, • MASTER = OPERATOR + le droit d'accorder à d'autres user, • OWNER essayez que tournesol « edit » l'ordinateur qui l'a crée : Ça marche ! • • • • Logout et reconnexion avec haddock puis essayez d’éditer l'ordinateur 666.66.666.66 : refusé comme prévu ! Access Denied 403 Forbidden - Exercice n Compléter l'action delete() pour qu'elle ne soit autorisée qu'au seul créateur
© Copyright 2025 ExpyDoc