Vos papiers rouges s'il vous plaît

GNU/Linux Magazine n° 126 | avril 2010 | Thomas Riboulet
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Ruby On Rails est devenue une femme magnifique, de nombreux bijoux (plugins) sont dans son boudoir, elle se voit offrir encore et encore de nouvelles robes et en plus, elle commence à se faire courtiser par des grands noms.

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