cours 6

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 suis­je ? 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)/[0­9]+/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 ­­dump­sql 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 ­­dump­sql 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