Il y a plein de belles choses à faire avec Rails et beaucoup de bons plans sont disponibles ici et là. Un des problèmes récurrents lors de la création de nouveaux projets est la gestion de l'authentification et de l'autorisation. Gardez en tête que l'authentification est le fait d'authentifier un utilisateur : vérifier son identité. D'ailleurs, je suppose que des lutins du jardin magique et d'autres barbus sont déjà en train de m'écrire un mail (pour une fois) pour me dire qu'il faut dire « identifier » en français, que je suis en train de tuer la langue française et que c'est à cause de moi si Jacques Toubon ne s'est finalement pas limité à la loi qui porte son nom. Donc authentification, identification, ce que vous voulez, tant qu'on parle de la même chose : savoir qui est l'utilisateur qui se connecte. L'autorisation est le fait de déterminer ce qu'un utilisateur identifié (ou non) a le droit de voir, faire, etc.
Si vous faites un rapide tour d'horizon, vous verrez vite qu'il y a de nombreuses solutions permettant de faire cela. Mais ici, je vais me limiter à vous présenter un système d'authentification qui a retenu mon attention et comment l'utiliser.
1. Préalable
Pour cet article, je considère que vous avez déjà une connaissance somme toute bonne de Rails : comment créer une application, comment utiliser des generators, faire des migrations, les appliquer, ...
Si vous voulez, vous pouvez utiliser les NiftyScaffold [1] pour vous faire gagner du temps, mais je vais donner la totalité du code à écrire de toute façon.
Voilà, vous êtes prêt : prenez une bonne bouteille (d'eau), un peu de chocolat et détachez votre tmux/screen le temps de lire cet article.
1.1 Ici, les entrées sont offertes
Afin de ne pas faire un article sur comment faire une application Rails, je vais vous inviter à récupérer une base de code simpliste, sous licence MIT. Soit par git [2], soit directement en tarball [3]. Si vous n'avez pas Internet au moment où vous lisez ces lignes et que vous savez faire rapidement une application Rails avec un contenu type Articles, faites-vous plaisir. Enfin, si vous cherchez juste à lire cet article pour pouvoir intégrer ça dans une application existante, je pense que vous êtes assez grand pour savoir quoi faire.
Donc débrouillez-vous comme vous voulez, mais obtenez une application Rails qui tourne et qui permette de créer, supprimer, éditer, lister et afficher des articles, enfin des items quoi...
Il vous faudra juste créer un fichier de configuration pour la base de données. Voici de quoi faire vite avec SQLite3 :
# config/database.yml
development:
adapter: sqlite3
database: db/dev.sqlite
encoding: utf8
1.2 Problématique
Nous voulons pouvoir identifier des utilisateurs-auteurs et permettre l'accès à certaines actions en fonction de l'état connecté ou non de l'utilisateur. Nous commencerons donc par ajouter de quoi identifier les utilisateurs, puis nous ferons en sorte que ceux-ci ne puissent accéder aux fonctions d'édition des articles seulement s'ils en sont les auteurs.
En résumé :
- identification des utilisateurs-auteurs ;
- limitation des accès aux fonctions d'édition.
2. Contrôle d'identité
Le plugin d'identification que je vais vous présenter est Authlogic. Authlogic a été écrit par Ben Johnson (binarylogic, [4]) et se révèle particulièrement simple à mettre en place et à utiliser. Il est publié sous la licence MIT et accessible sur GitHub [5].
Je pars du principe que vous avez récupéré mon code et j'utiliserai donc le terme « Posts » pour désigner les items de cette application. Notez bien que, si vous avez cloné mon dépôt [2], vous devriez avoir deux branches : master et base. Celle qui nous intéresse ici est base, master vous sera utile en fin de lecture pour voir le code obtenu à la fin.
2.1 Installation
Authlogic est disponible sous la forme d'un RubyGem. Pour l'installer, il vous suffit donc d’utiliser :
> sudo gem install authlogic
# ou
> sudo gem install binarylogic-authlogic
Dans ce cas-là, il faut ajouter la ligne suivante au fichier config/environment.rb :
# Specify gems that this application depends on and have them installed with rake gems:install
config.gem "authlogic"
Il est aussi disponible comme plugin, ce qui peut servir :
> script/plugin install git://github.com/binarylogic/authlogic.git
Voilà, maintenant, nous pouvons passer aux choses sérieuses.
2.2 Top Model
Commençons donc par créer notre modèle User :
> script/generate model User
Remplissons un peu sa table : vous trouverez dans le Readme de Authlogic [6] les infos concernant les champs minimaux à créer dans la table users, mais voici une migration qui crée le minimum nécessaire (bon d'accord, il y a un peu plus).
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.string "login"
t.string "firstname"
t.string "lastname"
t.string "email"
t.string "crypted_password"
t.string "password_salt"
t.string "persistence_token"
t.timestamps
end
end
def self.down
drop_table :users
end
Quelques explications :
- login : le login de l'utilisateur (si si) ;
- firstname : le prénom de l'utilisateur (optionnel) ;
- lastname : le nom de l'utilisateur (oula, c'est corsé) (optionnel aussi) ;
- email : une adresse e-mail, si le login est absent, elle peut aussi servir pour identifier l'utilisateur (donc l'un ou l'autre ou les deux peuvent être présents dans la table) ;
- crypted_password : le mot de passe chiffré ;
- password_salt : le sel (fin) utilisé pour saler le mot de passe pour le chiffrement ;
- persistence_token : utile à des fins que l'on verra dans un autre article ;
Vous pouvez passer la migration (rake db:migrate).
Pour activer l'utilisation d’AuthLogic, il nous faut ajouter la ligne suivante au modèle User :
class User < ActiveRecord::Base
acts_as_authentic
end
Une fois cela fait, relancez le serveur.
2.3 Le Gendarme
Passons à la création du contrôleur. Personnellement, j'utilise le Nifty Scaffold generator de RyanB, mais j'y trouve quelques soucis, un de ces 4, il faudra que je fork son code. Après génération et quelques modifications, voici le code qui nous intéresse :
# controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.all
end
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(params[:user])
if @user.save
flash[:notice] = "Utilisateur créé avec succès."
redirect_to @user
else
render :action => 'new'
end
end
def edit
@user = User.find(params[:id])
end
def update
@user = User.find(params[:id])
if @user.update_attributes(params[:user])
flash[:notice] = "Utilisateur mis à jour avec succès."
redirect_to @user
else
render :action => 'edit'
end
end
def destroy
@user = User.find(params[:id])
@user.destroy
flash[:notice] = "Utilisateur détruit avec succès."
redirect_to users_url
end
end
Rien de bien sorcier ici, tout est relativement classique. Nous retrouvons les méthodes index, show, edit et new habituelles. Vient ensuite la méthode create, qui crée un user en utilisant les paramètres passées par le form (issu de l'action new). Cette méthode redirige vers l'action show pour ce nouveau user ou à nouveau vers l'action new s'il y a eu un problème de création de l'objet User (il y a un certain nombre de validations incluses par défaut avec AuthLogic).
La méthode update fait sensiblement la même chose, sauf qu'au lieu de créer un objet User, elle se contente de le mettre à jour avec les paramètres passés par le form (action edit). Cette méthode-ci renvoie vers l'action show ou à nouveau l'action edit.
La méthode destroy se contente de trouver l'objet User et de le supprimer.
Il nous faut maintenant nous occuper des vues pour réaliser ces actions :
# views/users/edit.html.erb
<h1>Edition des préférences</h1>
<%= render :partial => 'form' %>
<p>
<%= link_to "Voir", @user %> |
<%= link_to "Voir tous", users_path %>
</p>
# views/users/_form.html.erb
<% form_for @user do |f| %>
<%= f.error_messages %>
<p> Login : <%= f.text_field :login %></p>
<p>Prénom : <%= f.text_field :firstname %></p>
<p>Nom : <%= f.text_field :lastname %></p>
<p>Email : <%= f.text_field :email %></p>
<p>Mot de passe : <%= f.password_field :password %></p>
<p>Mot de passe (vérification) : <%= f.password_field :password_confirmation %></p>
<p><%= f.submit "Valider" %></p>
<% end %>
# views/users/index.html.erb
<h1>Utilisateurs</h1>
<table>
<tr>
<th>Login</th><th>Prénom</th>
<th>Nom</th><th>Email</th>
</tr>
<% for user in @users %>
<tr>
<td><%= link_to user.login, user %></td>
<td><%=h user.firstname %></td>
<td><%=h user.lastname %></td>
<td><%=h user.email %></td>
<td><%= link_to "Edit", edit_user_path(user) %></td>
<td><%= link_to "Destroy", user, :confirm => 'Etes vous sûr ?', :method => :delete %></td>
</tr>
<% end %>
</table>
<p><%= link_to "Nouvel utilisateur", new_user_path %></p>
# views/users/new.html.erb
<h1>Inscription</h1>
<%= render :partial => 'form' %>
<p><%= link_to "Retour à la liste", users_path %></p>
# views/users/show.html.erb
<h1><%= @user.login %></h1>
<p><strong>Login :</strong><%= @user.login %></p>
<p><strong>Prénom :</strong><%= @user.firstname %></p>
<p><strong>Nom :</strong><%=h @user.lastname %></p>
<p><strong>Email :</strong><%=h @user.email %></p>
<p>
<%= link_to "Editer", edit_user_path(@user) %> |
<%= link_to "Détruire", @user, :confirm => 'Etes vous sûr?', :method => :delete %> |
<%= link_to "Voir tous", users_path %>
</p>
Rien de bien complexe donc.
Nous avons donc de quoi créer, éditer, lister et détruire des objets users, mais cela reste relativement simpliste et loin d'un véritable système d'identification. Pour le moment, c'est simplement une gestion de contenu de plus. Mais vous pouvez déjà voir quelques points d'intérêt d’AuthLogic : les validateurs des différents champs. Par exemple, essayez de rentrer une adresse e-mail mal formatée, il va vous envoyer bouler.
Pour cela, il nous faut ajouter une gestion de sessions, car en fait, une gestion de l'identification passe par la vérification de l'identité de l'utilisateur et une méthode de suivi de sa connexion : une session. AuthLogic fournit un générateur particulier pour cela : session.
2.4 La chèvre
Nous allons donc créer un modèle UserSession :
> script/generate session user_session
Et il nous faut aussi un contrôleur, utilisons le NiftyScaffold generator :
> script/generate nifty_scaffold user_session --skip-model login:string password:string new destroy
Le NiftyScaffold generator va créer un contrôleur user_sessions sans créer un modèle (--skip-model, puisqu'on l'a créé au paragraphe précédent). Nous lui passons en paramètres supplémentaires deux variables : login et password associés à leur type (champs qui seront utilisés pour identifier l'utilisateur) et deux méthodes : new et destroy (correspondant respectivement aux actions de login et logout).
Nous pourrions remplacer le champ login par le champ email car AuthLogic est capable d'utiliser l'un ou l'autre comme identifiant de l'utilisateur.
Après quelques modifications linguistiques et esthétiques, nous obtenons le contrôleur suivant :
# app/controller/user_session_controller.rb
class UserSessionsController < ApplicationController
def new
@user_session = UserSession.new
end
def create
@user_session = UserSession.new(params[:user_session])
if @user_session.save
flash[:notice] = "Connecté."
redirect_to root_url
else
render :action => 'new'
end
end
def destroy
@user_session = UserSession.find(params[:id])
@user_session.destroy
flash[:notice] = "Déconnecté."
redirect_to root_url
end
end
Nous éditons rapidement la vue qui servira d'écran de login (app/views/user_sessions/new.html.erb) afin de traduire l'interface et surtout de passer le champ password en password_field.
# app/views/user_sessions/new.html.erb
<h1>Login</h1>
<% form_for @user_session do |f| %>
<%= f.error_messages %>
<p><%= f.label :login %><br /><%= f.text_field :login %></p>
<p><%= f.label :password %><br /><%= f.password_field :password %></p>
<p><%= f.submit "Login" %></p>
<% end %>
Bien, maintenant, pour accéder rapidement à ces pages, nous allons créer des routes particulières.
2.5 On the Road
Tout d'abord, ajoutons les routes login et logout, et en même temps, vérifions que nous mappons bien les ressources des contrôleurs users, posts et user_sessions.
# config/routes.rb
map.login "login", :controller => "user_sessions", :action => "new"
map.logout "logout", :controller => "user_sessions", :action => "destroy"
map.resources :users, :posts, :user_sessions
Une fois le fichier édité, n'oubliez pas de relancer le serveur pour qu'il prenne en compte ces modifications.
2.6 In, out
Nous pouvons maintenant ajouter les liens login et logout dans notre layout par défaut :
# app/views/layouts/application.html.erb
<div class="login">
<%= link_to "Login", login_path %> :: <%= link_to "Logout", logout_path %>
</div>
<% if flash[:warning] or flash[:notice] %>
<div id="notice" <% if flash[:warning] %>class="warning"<% end %>>
<%= flash[:warning] || flash[:notice] %>
</div>
<script type="text/javascript">
setTimeout("new Effect.Fade('notice');", 15000)
</script>
<% end %>
<%= yield %>
Ce layout comprend nos deux liens ainsi que de quoi afficher les erreurs. Si vous testez le lien login avec un utilisateur que vous avez créé, vous verrez ainsi que vous arrivez à vous connecter et à vous déconnecter.
Nous commençons à nous approcher de quelque chose d'utilisable. Hélas, ce n'est pas encore tout à fait cela.
2.7 George
Pour que ça devienne vraiment utilisable, il faut que les liens login et logout s'affichent en fonction du statut de l'utilisateur (connecté, non connecté) et si possible, que nous puissions accéder à des liens pratiques pour éditer nos préférences (si nous sommes connectés) et pour s'enregistrer (si nous ne le sommes pas).
Pour cela, il nous faut ajouter deux helpers dans application_helper.rb :
# app/controllers/application_controller.rb
helper_method :current_user_session, :current_user
private
def current_user_session
return @current_user_session if defined?(@current_user_session)
@current_user_session = UserSession.find
end
def current_user
return @current_user if defined?(@current_user)
@current_user = current_user_session && current_user_session.record
end
Nous pouvons désormais ajouter un test en utilisant cette méthode current_user. Modifions donc un peu notre layout pour obtenir ceci :
# app/views/layouts/application.html.erb
<div class="login">
<% if current_user %>
<%= link_to "Préférences", edit_user_path(current_user) %> ::
<%= link_to "Logout", logout_path %>
<% else %>
<%= link_to "Login", login_path %> ::
<%= link_to "S'enregistrer", new_user_path %>
<% end %>
</div>
Rechargez votre page, et vous pouvez tester : le menu de login change désormais en fonction du status de connexion. Nous avons donc résolu une partie de notre problématique : identifier les utilisateurs. Reste maintenant à filtrer les actions accessibles en fonction du status de connexion.
2.8 Peter & Steven
Nous pouvons donc utiliser le même principe pour limiter aux seuls utilisateurs connectés la possibilité d'éditer les articles ou d'en créer. Pour cela, rien de plus simple : il suffit de refaire un test sur current_user et d'afficher ou non les liens. Les lignes 9 et 10 de app/views/posts/index.html.erb deviennent donc :
# app/views/posts/index.html.erb
<td><%= link_to "Editer", edit_post_path(post) if current_user %></td>
<td><%= link_to "Supprimer", post, :confirm => 'Etes vous sûr ?', :method => :delete if current_user %></td>
La dernière ligne (la 16, si je ne me trompe pas) devient quant à elle :
# app/views/posts/index.html.erb
<p><%= link_to "Nouveau Post", new_post_path if current_user %></p>
Il ne faut pas oublier la vue show en modifiant le p :
<p>
<%= link_to "Edit", edit_post_path(@post) if current_user %> |
<%= link_to "Supprimer", @post, :confirm => 'Etes vous sûre ?', :method => :delete if current_user %> |
<%= link_to "Voir tous", posts_path %>
</p>
On va en profiter pour appliquer la même chose pour les actions liées aux utilisateurs car, comme me l'a fait remarqué un lutin malin, on peut éditer ou supprimer un utilisateur même si on est pas logué.
# views/users/show.html.erb
<p>
<%= link_to "Editer", edit_user_path(@user) if current_user && current_user == @user %> |
<%= link_to "Détruire", @user, :confirm => 'Etes vous sûr?', :method => :delete if current_user && current_user == @user %> |
<%= link_to "Voir tous", users_path %>
</p>
Vous voyez que là, on ajoute un test similaire au précédent pour n'autoriser que les utilisateurs connectés à voir le lien, mais on ajoute une condition : il faut que l'utilisateur connecté soit l'utilisateur affiché pour que celui-ci puisse accéder aux liens d'édition et de suppression.
Il vous suffit de faire de même dans la vue views/users/index.html.erb (en copiant/collant le test).
Rechargez et hop !
3. Qui que quoi donc où ?
Maintenant que nous pouvons identifier les auteurs, l'étape suivante est de savoir qui a écrit chaque article. Pour cela, il nous faut ajouter une association entre les users et les posts.
3.1 Comme les oies en hiver
Il nous faut donc faire une petite migration :
> script/generate migration AddPostsUsers
# db/migrate/..._add_posts_users.rb
class AddPostsUsers < ActiveRecord::Migration
def self.up
add_column :posts, :user_id, :integer
end
def self.down
remove_column :posts, :user_id
end
end
Rien de sorcier donc, et nous continuons en ajoutant les associations dans les modèles User et Post :
# app/models/user.rb
class User < ActiveRecord::Base
acts_as_authentic
has_many :posts
end
# app/models/posts.rb
class Post < ActiveRecord::Base
attr_accessible :title, :content
belongs_to :user
end
N'oublions pas de migrer la base.
3.2 De la couture
Maintenant, nous pouvons faire le lien entre les posts et les users en éditant le posts_controller, de façon à associer à chaque post l'auteur connecté lorsqu'il est créé. Pour cela, nous allons à nouveau utiliser la méthode current_user en modifiant la méthode create comme suit :
# app/controllers/posts_controller.rb
def create
@post = Post.new(params[:post])
@post.user = current_user if current_user
if @post.save
flash[:notice] = "Post créé avec succès."
redirect_to @post
else
render :action => 'new'
end
end
Nous associons donc l'utilisateur courant (s'il existe) au post. On pourrait pousser le vice jusqu'à rediriger vers le post sans sauver les modifications si l'utilisateur n'est pas connecté, mais je vous laisse deviner comment faire.
Afin de vérifier que cela fonctionne, il nous suffit d’ajouter une colonne dans notre tableau de la vue index. Un th Auteur et un td contenant un link_to vers l'utilisateur combleront donc parfaitement nos besoins.
# app/views/posts/index.html.erb
<table>
<tr>
<th>Titre</th>
<th>Auteur</th>
</tr>
<% for post in @posts %>
<tr>
<td><%= link_to post.title, post %></td>
<td><%= link_to post.user.login, post.user %></td>
<td><%= link_to "Editer", edit_post_path(post) if current_user %></td>
<td><%= link_to "Supprimer", post, :confirm => 'Etes vous sûr ?', :method => :delete if current_user %></td>
</tr>
<% end %>
</table>
Nous ajoutons ensuite un petit p dans app/views/posts/show.html.erb pour afficher le login de l'auteur, dans un link_to à nouveau.
# app/views/posts/show.html.erb
<p>
<strong>Auteur:</strong>
<%= link_to @post.user.login, @post.user %>
</p>
Maintenant, vous devriez pouvoir sans souci voir qui a écrit chaque article. Bon, vous allez me dire « rien de bien neuf », certes. Nous pouvons donc ajouter dans la foulée de quoi interdire l'édition ou la suppression de chaque post si l'utilisateur connecté n'est pas l'auteur. Pour cela, il suffit de reprendre les tests précédemment utilisés et ajouter la condition suivante :
# app/views/posts/index.html.erb
&& current_user == post.user
# soit (par exemple)
<td><%= link_to "Editer", edit_post_path(post) if current_user && current_user == post.user %></td>
# app/views/posts/show.html.erb
&& current_user == @post.user
# soit (par exemple)
<%= link_to "Supprimer", @post, :confirm => 'Etes vous sûre ?', :method => :delete if current_user && current_user == @post.user %>
Là encore, rien de sorcier, nous nous contentons de comparer l'utilisateur connecté à l'utilisateur associé au post.
4. Are you well sir ?
Désormais, nous avons une application qui répond à notre problématique. Pour que cela soit vraiment convivial, il nous faudrait de quoi gérer les utilisateurs par groupe et de quoi gérer plus finement les droits d'accès. Nous verrons donc cela dans un prochain article, car il y a beaucoup à dire sur ce sujet.
Vu que le code est disponible sur GitHub, n'hésitez pas à forker, patcher et me faire des retours directement là-bas.
Nous finissons en fliquant un peu plus (c'est la mode). Il reste un problème, tous les utilisateurs ont la possibilité d'éditer les posts ou de les supprimer. C'est une fonctionnalité qui peut être utile pour un wiki, mais ici, ce n'est pas vraiment ce que l'on veut. Il nous faut donc ajouter une condition pour tester la propriété du post avant d'afficher les liens d'édition ou de suppression.
Nous avons déjà fait quelque chose de similaire précédemment pour les liens d'édition ou de suppression des utilisateurs. Il nous suffit de reprendre le même principe et donc de transformer les liens concernant les articles de la manière suivante :
<td><%= link_to "Editer", edit_post_path(post) if current_user && current_user == post.user %></td>
Pour finir
J'ai largement appris AuthLogic au travers d'un Rails Cast sur ce plugin, je vous invite donc à le consulter si vous êtes anglophone.
Liens
[1] Nifty Scaffold : http://github.com/ryanb/nifty-generators
[2] Rails Cast : AuthLogic : http://railscasts.com/episodes/160-authlogic
[3] Tarball du code de base : http://github.com/mcansky/Article-Rails-Auth/archives/base
[4] binarylogic : http://www.binarylogic.com/
[5] AuthLogic @ GitHub : http://github.com/binarylogic/authlogic
[6] Readme Authlogic : http://github.com/binarylogic/authlogic
[7] Page GitHub pour le code de l'article : http://github.com/mcansky/Article-Rails-Auth (deux branches : master est le code final, base est le code de départ)
[8] Tarball du code final : http://github.com/mcansky/Article-Rails-Auth/archives/master