• 15 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 11/05/2020

Événements et autorisations

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Pour terminer ce cours, je vais évoquer dans ce dernier chapitre des points que je n'ai pas encore abordés et qui peuvent constituer des alternatives intéressantes par rapport au codage qu'on a vu.

Nous allons ainsi passer en revue les événements (events) et les autorisations (authorization).

Pour évoquer les points de ce chapitre je vous propose d'installer l'application d'exemple que j'ai déposée sur github. Elle vous donne l'occasion de voir un cas réaliste par rapport aux applications partielles qu'on a utilisées dans ce cours.

Explorez un peu l'application avant de poursuivre la lecture de ce chapitre.

Événements

Le design pattern Observateur (observer) établit une relation de type 1:n entre des objets.  Si l'objet côté 1 change d'état il en informe les objets côté n pour qu'ils puissent agir en conséquence. Ça implique une intendance et les objets doivent pouvoir s'inscrire et se mettre à l'écoute des événements du sujet.

Voici une visualisation de ce design pattern :

Le design pattern observateur
Le design pattern observateur

On a un sujet et des observateurs. Les observateurs doivent s'inscrire (on dit aussi s'abonner) auprès du sujet. Lorsque le sujet change d'état il notifie tous ses abonnés. Un observateur peut aussi se désinscrire, il ne recevra alors plus les notifications.

Vous pouvez trouver une description détaillée de ce design pattern ici.

Laravel implémente ce design pattern et le rend très simple d'utilisation comme nous allons le voir.

Événements du framework

On peut utiliser les événement existants déjà dans le framework qui en propose au niveau de l'authentification :

  • tentative de connexion (login),

  • connexion (login) réussie,

  • déconnexion (logout) réussie.

On trouve aussi des événements avec Eloquent :

  • creating,

  • created,

  • updating,

  • updated,

  • saving,

  • saved,

  • deleting,

  • deleted,

  • restoring, restored.

Le provider

Il y a un service provider dédié aux événements :

Le service provider pour les événements
Le service provider pour les événements

Avec ce code :

<?php namespace App\Providers;

use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider {

	/**
	 * The event handler mappings for the application.
	 *
	 * @var array
	 */
	protected $listen = [
		'Illuminate\Auth\Events\Login' => ['App\Listeners\LoginSuccess'],
		'Illuminate\Auth\Events\Logout' => ['App\Listeners\LogoutSuccess'],
		'App\Event\UserAccess' => ['App\Listeners\UserAccess']
	];

	/**
	 * Register any other events for your application.
	 *
	 * @param \Illuminate\Contracts\Events\Dispatcher $events
	 * @return void
	 */
	public function boot(DispatcherContract $events)
	{
		parent::boot($events);
	}

}

On a une propriété listen pour les abonnements. C'est un simple tableau qui prend l'événement (event) comme clé et l'observateur (listener) comme valeur.

Dans l'application d'exemple on veut savoir quand quelqu'un se connecte ou se déconnecte. On prévoit donc des observateurs dans le provider :

<?php
protected $listen = [
	'Illuminate\Auth\Events\Login' => ['App\Listeners\LoginSuccess'],
	'Illuminate\Auth\Events\Logout' => ['App\Listeners\LogoutSuccess'],
	...
];

Vous pouvez trouver ces événements dans la documentation.

Laravel prévoit de placer les observateurs (listeners) dans ce dossier en tant que classes :

Le dossier des observateurs

Pour simplifier la gestion le statut de l'utilisateur (son rôle) est enregistré dans la session. On a 3 rôles :

  • Administrateur (admin) : avec tous les droits,

  • Rédacteur (redac) : avec le droit de gérer des articles,

  • Utilisateur (user) : avec juste le droit de laisser des commentaires.

Donc dès que quelqu'un se connecte on regarde son rôle et on le mémorise dans la session, si on regarde l'observateur (listener) :

<?php

namespace App\Listeners;

use Illuminate\Auth\Events\Login;

class LoginSuccess extends ListenerBase
{
    /**
     * Handle the event.
     *
     * @param  Login  $login
     * @return void
     */
    public function handle(Login $login)
    {
        $this->statut->setLoginStatut($login);
    }
}

On a une propriété statut qui apparaît. Pour en trouver l'origine il faut voir que notre classe hérite de ListenerBase que voici :

<?php

namespace App\Listeners;

use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Services\Statut;

class ListenerBase
{
    /**
     * The Statut instance.
     *
     * @var App\Services\Statut
     */
    protected $statut;

    /**
     * Create the event listener.
     *
     * @param App\Services\Statut $statut  
     * @return void
     */
    public function __construct(Statut $statut)
    {
        $this->statut = $statut;
    }
}

Cette classe de base est prévue pour justement factoriser l'affectation de la propriété. Si on regarde la méthode finalement visée on voit qu'on mémorise l'information en session :

<?php
public function setLoginStatut($login)
{
	session()->put('statut', $login->user->role->slug);
}

Et évidemment en cas de déconnexion c'est le même processus avec le listener LogoutSuccess,  au final on rend l'utilisateur simple visiteur :

<?php
public function setVisitorStatut()
{
	session()->put('statut', 'visitor');
}

Mais il faut considérer un dernier cas : lorsque l'utilisateur accède à une page on va vérifier s'il y a un statut mémorisé en session et si ce n'est pas le cas en créer un.

Cette fois ce n'est pas un événement qui existe comme login ou logout, il nous faut donc le créer.

Laravel prévoit de placer les événements (events) dans ce dossier en tant que classes :

Le dossier des événements

Regardez dans le fichier app/Http/Middlewares/App.php ce code :

<?php
public function handle($request, Closure $next)
{
	...
	event(new UserAccess);
    ...
}

J'utilise l'helper event pour déclencher un événement. Sans l'helper il faudrait utiliser la façade :

<?php
Event::fire(new UserAccess);

Au niveau de l'inscription j'ai donc ceci :

<?php
protected $listen = [
	...
	'App\Event\UserAccess' => ['App\Listeners\UserAccess']
];

Voici la classe de l'événement :

<?php

namespace App\Events;

use App\Events\Event;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class UserAccess extends Event
{
    use SerializesModels;

    /**
     * Get the channels the event should be broadcast on.
     *
     * @return array
     */
    public function broadcastOn()
    {
        return [];
    }
}

Et le listener :

<?php

namespace App\Listeners;

use App\Events\UserAccess;

class UserAccess extends ListenerBase
{
    /**
     * Handle the event.
     *
     * @param  UserAccess  $event
     * @return void
     */
    public function handle(UserAccess $event)
    {
        $this->statut->setStatut();
    }
}

On appelle donc la méthode dans le service :

<?php
public function setStatut()
{
	if(!session()->has('statut')) 
	{
		session()->put('statut', auth()->check() ?  auth()->user()->role->slug : 'visitor');
	}
}

Créer un listener

Dans l'application tout est déjà en place. Comment fait-on pour créer un listener ? Prenons le cas de la connexion...

Il faut créer l'observateur avec Artisan :

php artisan make:listener LoginSuccess --event=Illuminate\Auth\Events\Login

On donne le nom de l'observateur et celui de l'événement que l'on veut observer. 

Ce qui crée cet observateur vierge :

<?php

namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class LoginSuccess
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  Login  $event
     * @return void
     */
    public function handle(Login $event)
    {
        //
    }
}

Il suffit alors d'ajouter la logique de l'observateur dans la méthode handle de l'observateur (ou appeler la méthode d'une autre classe) :

<?php
public function handle(Login $login)
{
    $this->statut->setLoginStatut($login);
}

Comme j'ai choisi d'utiliser une classe parente pour éviter de dupliquer du code la propriété est créée dans celle-ci.

Créer un événement

Prenons l'exemple de UserAccess. Avec Artisan je peux créer la classe :

php artisan make:event UserAccess

Et je me retrouve avec ce code de base :

<?php

namespace App\Events;

use App\Events\Event;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class UserAccess extends Event
{
    use SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the channels the event should be broadcast on.
     *
     * @return array
     */
    public function broadcastOn()
    {
        return [];
    }
}

Il suffit ensuite de compléter selon les besoin. Dans le cas de l'application il n'y a rien de spécial à effectuer. 

Vous pouvez trouver la documentation complète ici.

Autorisations

La sécurité

Lorsqu'on développe une application on prend plein de précautions, par exemple les utilisateurs doivent s'authentifier pour éviter des actions non autorisées. Dans le code on peut vérifier si la personne est authentifiée et quel est son degré d'habilitation. Dans l'application d'exemple on a vu ci-dessus qu'on met en session ce degré d'habilitation, il suffit ensuite de le vérifier. Par exemple dans les vues pour adapter ce qui est affiché :

@if(session('statut') == 'admin')
    {!! link_to_route('admin', trans('back/admin.administration'), [], ['class' => 'navbar-brand']) !!}
@else
    {!! link_to_route('blog.index', trans('back/admin.redaction'), [], ['class' => 'navbar-brand']) !!}
@endif

On peut aussi mettre en place des middlewares pour filtrer les accès. Dans l'application d'exemple on a le middleware isAdmin

<?php namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\RedirectResponse;

class IsAdmin {

	/**
	 * Handle an incoming request.
	 *
	 * @param  \Illuminate\Http\Request  $request
	 * @param  \Closure  $next
	 * @return mixed
	 */
	public function handle($request, Closure $next)
	{
		if (session('statut') === 'admin')
		{
			return $next($request);
		}
		return new RedirectResponse(url('/'));
	}

}

Il suffit ensuite de l'utiliser dans les routes :

<?php
Route::get('admin', [
	'uses' => 'AdminController@admin',
	'as' => 'admin',
	'middleware' => 'admin'
]);

Un autre lieu où on peut sécuriser l'application est au niveau des requêtes de formulaires. On a vu qu'il y a une méthode authorize. Dans l'application d'exemple cette méthode est située uniquement dans la classe Request qui est la base de toutes les autres :

<?php namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

abstract class Request extends FormRequest {

	public function authorize()
	{
		// Honeypot 
		return  $this->input('address') == '';
	}

}

En effet dans tous les formulaires il est prévu un contrôle caché. Par exemple dans le formulaire de contact on trouve ce code :

{!! Form::text('address', '', ['class' => 'hpet']) !!}	

La classe hpet se contente de rendre le contrôle invisible. Un humain ne voit pas ce contrôle et donc ne le complète pas, par contre un robot risque de l'utiliser. C'est ce qu'on appelle un pot de miel (pour attirer les abeilles virtuelles). On vérifie donc si ce contrôle est rempli et, si c'est le cas, on bloque le processus.

Les autorisations

En plus de ces possibilités Laravel nous offre un système complet d'autorisations. Le plus simple pour voir comment cela fonctionne est de prendre un exemple. On a le dossier app/Policies avec un fichier :

Le dossier des autorisations
Le dossier des autorisations

Voyons ce fichier : 

<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    /**
     * Grant all abilities to administrator.
     *
     * @param  \App\Models\User  $user
     * @param  string  $ability
     * @return bool
     */
	public function before(User $user, $ability)
	{
	    if (session('statut') === 'admin') {
	        return true;
	    }
	}

    /**
     * Determine if the given post can be changed by the user.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Post  $post
     * @return bool
     */
    public function change(User $user, Post $post)
    {
        return $user->id === $post->user_id;
    }

}

La méthode before est la première à être appelée. A ce niveau on vérifie si l'utilisateur est un administrateur, donc possède tous les droits, si c'est le cas on renvoie true.

On a ensuite la méthode change avec comme paramètres l'utilisateur et l'article. Si l'utilisateur est le créateur de l'article on renvoie true

Maintenant que ces autorisations sont en place il faut les déclarer. Regardez le fichier app/Providers/AuthServiceProvider :

<?php

namespace App\Providers;

use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

use App\Models\Post;
use App\Policies\PostPolicy;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        Post::class => PostPolicy::class,
    ];

    /**
     * Register any application authentication / authorization services.
     *
     * @param  \Illuminate\Contracts\Auth\Access\Gate  $gate
     * @return void
     */
    public function boot(GateContract $gate)
    {
        parent::registerPolicies($gate);
    }
}

Dans la propriété policies on a prévu d'ajouter la classe PostPolicy.

Il ne nous reste plus qu'à voir comment on l'utilise.

Par défaut le contrôleur de base utilise le trait AuthorizesRequests, donc tous les contrôleurs sont au courant de l'existence des autorisations enregistrées. 

Regardez ces deux méthodes du contrôleur BlogController :

<?php
/**
 * Show the form for editing the specified resource.
 *
 * @param  App\Repositories\UserRepository $user_gestion
 * @param  int  $id
 * @return Response
 */
public function edit(
	UserRepository $user_gestion, 
	$id)
{
	$post = $this->blog_gestion->getByIdWithTags($id);

	$this->authorize('change', $post);

	$url = config('medias.url');

	return view('back.blog.edit',  array_merge($this->blog_gestion->edit($post), compact('url')));
}

/**
 * Update the specified resource in storage.
 *
 * @param  App\Http\Requests\PostUpdateRequest $request
 * @param  int  $id
 * @return Response
 */
public function update(
	PostRequest $request,
	$id)
{
	$post = $this->blog_gestion->getById($id);

	$this->authorize('change', $post);

	$this->blog_gestion->update($request->all(), $post);

	return redirect('blog')->with('ok', trans('back/blog.updated'));		
}

Elles sont destinée pour la première à afficher le formulaire de modification d'un article et pour la seconde à traiter la soumission des modifications. Dans les deux cas on met en oeuvre l'autorisation :

<?php
$this->authorize('change', $post);

Donc s'il ne s'agit pas d'un administrateur ou du propriétaire de l'article ça va coincer ici.

Vous pouvez trouver la documentation complète ici.

En résumé

  • Laravel comporte un système complet et simple de gestion des événements.

  • Laravel comporte un système complet et simple de gestion des autorisations.

 

Exemple de certificat de réussite
Exemple de certificat de réussite