Mise en place de la gestion des profils
Après avoir terminé la première étape en l’occurrence le système d’authentification, il est temps d’affiner la chose et de gérer les accès aux différents éléments composant l’interface, il va donc falloir gérer les droits d’un utilisateur en fonction de son profil, de la tâche souhaitée et de l’objet concerné.
J’ai repris une classe que j’avais réalisée il y a quelques années pour un projet similaire et qui avait montrer son efficacité, je l’ai couplée à mon AccessMiddleware afin de gérer les accès correctement.
Il existe un paquet pour Laravel qui permet de gérer les droits, mais après l’avoir installé, j’ai trouvé qu’il n’était pas satisfaisant, je l’ai donc retiré et suis revenu à mon système.
Tout d’abord, j’ai trois tables en plus de la table users :
- profiles : table qui gère les profils ainsi que le contexte (back / front) que j’ai détaillé dans le billet sur le système d’authentification.
- rights : table qui va définir les droits des différents objets.
- profiles_rights : table qui va définir un accès en fonction d’un profil et d’un droit.
Table profiles
Nous avons déjà vu cette table, je passe donc sur ce point.
Table rights
Elle est définie comme suit :
les champs type et context sont des enums car ils ne peuvent accepter que quelques valeurs de type chaine, pour le moment je n’ai que trois types de droits :
- controller : droits sur un contrôleur
- parameter : droits sur un paramètre d’un objet
- block : droits sur une fonctionnalité (un module)
Nous avons déjà parlé du contexte.
Le champ parent permet de déterminer si un objet a un parent (le paramètre d’un contrôleur par exemple)
Le champ object reçoit le nom du paramètre ou le nom de la classe d’un contrôleur par exemple.
Table profiles_rights
La table est définie comme suit :
Les champs profile_id et right_id sont des clés étrangères sur les tables profiles et rights
le champ rights est défini sous la forme d’un json qui répertorie les droits et leur valeur :
{"display": true, "read": true, "create": true, "delete": false, "update": false}
Le choix du json est parfait car son format est facilement utilisable dans PHP, de plus, si demain nous devons ajouter, retirer ou modifier une composante, ça pourra se faire sans trop de difficulté, de plus je n’aime pas les tables qui comportent trop de colonnes quand on peut faire autrement.
A noter que j’utilise le CRUD vu dans le billet sur la gestion des droits et profils auquel j’ajoute une validation pour l’affichage du contrôleur en lui même, ce qui induit qu’un utilisateur pourra par exemple afficher la liste des objets contenus dans un contrôleur droit display, mais pourra potentiellement ne pas accéder au détail.
Pour l’exemple ci-dessus, rien de plus simple à comprendre, l’utilisateur pourra afficher l’objet, en créer un, mais ne pourra ni modifier, ni supprimer un objet.
Une fois la structure créé dans la base il faut utiliser ces données afin d’autoriser ou non l’accès à l’utilisateur connecté, pour cela j’ai commencé par créer mon modèle Right, qui va gérer la récupération des données en base et renvoyer le résultat de la comparaison entre l’action demandée et ce qui est possible pour l’utilisateur.
Par exemple, si on souhaite vérifier qu’un utilisateur peut accéder à un contrôleur, voici la fonction :
public static function checkControllerRights($controller, $right_type = 1, $rights_coef = false, $context = 'back')
{
$return = false;
$profile_type = Auth::user()->profile->type;
if ($profile_type == 'all' || $profile_type == $context) {
$profile_id = Auth::user()->profile->id;
$rights = self::_getRights('controller', $controller, $profile_id);
if ($rights_coef) {
if (!empty($rights)) {
$return = (int)$rights['rights'];
} else {
$return = 0;
}
} else {
if (!empty($rights) && $rights[0]['rights'] >= $right_type) {
$return = true;
}
}
return $return;
}
Cette fonction reçoit en paramètres le contrôleur cible, le type de droit demandé, si on doit renvoyer le coéficient du droit ou le résultat de la comparaison et le contexte de la demande.
Au sujet du coéficient, les droits sont définis come suit :
- public static $_RIGHTS_DISPLAY_ = 1;
- public static $_RIGHTS_READ_ = 10;
- public static $_RIGHTS_CREATE_ = 100;
- public static $_RIGHTS_UPDATE_ = 1000;
- public static $_RIGHTS_DELETE_ = 10000;
Donc si un utilisateur pour un contrôleur a des droits en affichage, lecture et création, ça donnera un coéficient de 111 (1 + 10 + 100).
On commence par récupérer le type de profil de l’utilisateur, on vérifie qu’il corresponde à celui sur lequel on fait la recherche, sinon on retourne false.
Ensuite on récupère l’id du profil de l’utilisateur, puis on récupère les droits pour le profil et le contrôleur concernés, si on doit renvoyer le coéficient, on le retourne, s’il n’y a pas de droits, on renvoie 0.
Sinon on vérifie si l’utilisateur a des droits, si oui on compare le coéficient reçu en paramètre à celui récupéré de la base de données, si ce dernier est supérieur ou égal on renvoie true, l’utilisateur est autorisé, sinon c’est false.
La partie traitement de comparaison est maintenant terminée, passons à l’appel de cette comparaison, pour cela on va définir un middleware et l’appeler depuis le routage.
Je reprends donc mon AccessMiddleware et je lui ajoute les quatre droits possibles, en ayant ajouté au préalable un nouvel attribut $controller optionnel pour garder la compatibilité avec les appels ne nécessitant pas ce paramètre :
public function handle(Request $request, Closure $next, String $access, String $controller = null): Response
{
...
case 'display':
if (!Right::checkControllerRights($controller, Right::$_RIGHTS_DISPLAY_)) {
// TODO make deny action here
}
break;
Les quatre parties sont identiques, si ce n’est qu’elles sont appelées pour un type de droit différent, si le retour est false, on gérera la redirection ou autre.
Enfin depuis le routage, on va définir les routes ainsi que les appels de validation :
Route::controller(UserController::class)
->middleware('access:access-back')
–>group(function() {
Route::get('/'.Configuration::getValue('_CORE_BACK_URL_').'/users', 'index')
->name('users')
->middleware('access:display,UserController');
Voilà, donc si l’utilisateur appelle l’url /admin/users, nous vérifions qu’il a le droit display sur le contrôleur User, sinon on enverra l’action se trouvant dans le middleware à la place.
La partie interface de la gestion des droits se fera dans un prochain billet.