#192 Authorization with CanCan
- Download:
- source codeProject Files in Zip (107 KB)
- mp4Full Size H.264 Video (26.1 MB)
- m4vSmaller H.264 Video (17.3 MB)
- webmFull Size VP8 Video (43.9 MB)
- ogvFull Size Theora Video (36.9 MB)
Un po' di episodi fa abbiamo trattato Declarive Authorization. Anche se si tratta di un ottimo plugin per l’autorizzazione in applicazioni Rails, potrebbe rivelarsi un po’ troppo pesante per i siti più, semplici. Dopo avere scritto il Railscast su Declarative Authorization, Ryan Bates ha cercato una soluzione alternativa per lo stesso problema e, non trovandone una che soddisfasse le sue aspettative, ha deciso di scriverne una per conto suo, CanCan. CanCan è un semplice plugin per l’autorizzazione in cui Ryan ha cercato di mantenere ogni cosa il più semplice possibile mentre la scriveva. Diamogli un’occhiata.
In questo episodio lavoreremo con la solita semplice applicazione di blogging usata anche nell’episodio relativo a Declarative Authorization. L’applicazione ha una serie di articoli, ciascuno dei quali appartiene ad un utente e ciascun articolo può avere molti commenti associati ad esso.
Nello screenshot di sopra si noti che, anche se non c’è alcun utente autenticato, i link alla modifica e alla cancellazione degli articoli e dei commenti sono visibili. Vogliamo usare l’autorizzazione per gestire ciò che ciascun utente ha il diritto di compiere sul sito. Abbiamo già un meccanismo di autenticazione nella nostra applicazione, che permette agli utenti di registrarsi e accedere al sito ed abbiamo a suo tempo usato Authlogic per realizzare queste logiche, anche se alla fine una qualunque soluzione per l’autenticazione andrebbe bene.
Nella pagina di registrazione, abbiamo una serie di checkbox per permettere ad un utente di scegliere il ruolo al quale questi vogliono essere associati. Ad un amministratore sarà permesso tutto, ad un moderatore la sola modifica dei commenti di chiunque e ad un autore la creazione di articoli e l’aggiornamento dei proprio. Gli utenti che non appartengono ad alcun ruolo possono solamente creare nuovi commenti e modificare quelli scritti da loro:
Installazione di CanCan
CanCan è distribuito come gem. Per aggiungerlo alla nostra applicazione dobbiamo aggiungere la seguente linea nel blocco Rails::Initializer.run
del nostro file /config/environment.rb
:
config.gem "cancan"
Possiamo assicurarci che il gem sia installato, lanciando:
sudo rake gems:install
Se avete già installato CanCan in precedenza, dovete sincerarvi di essere aggiornati all’ultima versione, dal momento che ci sono alcune nuove funzionalità disponibili solo a partire dalla versione 1.0.0.
Utilizzo di CanCan
Per usare CanCan dobbiamo creare una nuova classe chiamata Ability
, che metteremo nella cartella /app/models
. La classe deve includere il module CanCan::Ability
oltre che un metodo initialize
che prende un oggetto user come parametro. E’ proprio in questo metodo che definiamo i diritti per ogni tipo di utente:
class Ability include CanCan::Ability def initialize(user) end end
I diritti sono definiti attraverso il metodo a tre lettere che è il cuore di CanCan. Questo metodo accetta due parametri: il primo è la action che vogliamo eseguire ed il secondo è la classe di modello a cui si applica la action. In alternativa, per applicare una action a tutti i modelli, possiamo passare :all
. Se vogliamo che tutti gli utenti possano solamente leggere tutti i modelli, possiamo scrivere questo:
class Ability include CanCan::Ability def initialize(user) can :read, :all end end
Per come abbiamo impostato ora i diritti, nessuno può modificare o cancellare articoli o commenti, tuttavia ci sono ancora i link alle action edit
e destroy
nella pagina di ciascun articolo. Nel codice della vista relativa alla action show
dell’articolo possiamo usare can?
(notate il punto interrogativo) per stabilire se l’utente corrente sia autorizzato ad eseguire la action alla quale ciascun link è associato. Mentre il metodo can
determina i diritti, il metodo can?
è un metodo booleano che indica se l’utente corrente abbia o meno tali diritti. Come can
, can?
accetta due parametri, una action e un modello, in questo caso un Article
. Usiamo can?
nella vista affinchè i link edit e destroy siano nascosti a meno che l’utente corrente non abbia un opportuno diritto. Per fare ciò, racchiudiamo i link degli articoli in una guardia if
, che li mostra solo se l’utente corrente può intraprendere l’opportuna action sull’articolo:
<p> <% if can? :update, @article %> <%= link_to "Edit", edit_article_path(@article) %> | <% end %> <% if can? :destroy, @article %> <%= link_to "Destroy", @article, :method => :delete, :confirm => "Are you sure?" %> | <% end %> <%= link_to "Back to Articles", articles_path %> </p>
Un po’ più in basso nella stessa vista, facciamo una modifica analoga per ciascun link di commento:
<p> <% if can? :update, comment %> <%= link_to "Edit", edit_comment_path(comment) %> | <% end %> <% if can? :destroy, comment %> <%= link_to "Destroy", comment, :method => :delete, :confirm => "Are you sure?" %> <% end %> </p>
Possiamo ora caricare la pagina degli articoli e vedere come i link edit e delete per gli articoli siano scomparsi, dal momento che non abbiamo i diritti adeguati alle azioni di modifica o cancellazione:
Proteggere i controller
Anche se abbiamo rimosso i link per la modifica degli articoli, e dei commenti, le action di per sè sono ancora disponibili sul controller e se vengono visitate direttamente, funzionano. Per questa ragione, occorre che bonifichiamo anche il codice dei controller, oltre che le viste, in modo che gli utenti possano avere accesso esclusivamente ad action per le quali sono autorizzati. Ci sono due modi per fare tutto questo in CanCan. Il primo lavora a livello di action: ci avvarremo della action edit dell’ArticleController
per mostrarne un esempio di uso:
def edit @article = Article.find(params[:id]) unauthorized! if cannot? :edit, @article end
Per interrompere l’esecuzione della action chiamiamo il metodo unauthorized!
che solleva un’eccezione. Ovviamente vogliamo che questa eccezione sia lanciata solo se l’utente non possiede i diritti adeguati. Per verificare ciò, possiamo usare il solito metodo can?
usato già nella vista o, visto che ci siamo, il metodo cannot?
.
Se proviamo ad accedere direttamente alla action edit ora, verremo fermati da un errore (l’eccezione):
Possiamo replicare questa logica fra tutte le action dei nostri controller, ma per fortuna stiamo usando la convenzione RESTful, per cui ci viene fornito un modo molto più semplice per fare la stessa cosa. In cima al controller possiamo infatti invocare la load_and_authorize_resource
, che caricherà e autorizzerà l’opportuna risorsa in una before filter. Dal momento che questo metodo carica la risorsa necessaria per noi, basandosi sulla action, possiamo togliere le linee di codice che impostano la variabile di istanza in ogni action (in questo caso @article
) rendendo il codice del nostro ArticlesController
simile a questo:
class ArticlesController < ApplicationController load_and_authorize_resource def index @articles = Article.all end def show @comment = Comment.new(:article => @article) end def new end def create @article.user = current_user if @article.save flash[:notice] = "Successfully created article." redirect_to @article else render :action => 'new' end end def edit end def update if @article.update_attributes(params[:article]) flash[:notice] = "Successfully updated article." redirect_to @article else render :action => 'edit' end end def destroy @article.destroy flash[:notice] = "Successfully destroyed article." redirect_to articles_url end end
Sistemata questa cosa, gli utenti non potranno creare, modificare o cancellare alcun articolo.
Ci occorre fare modifiche simili anche al CommentsController
per limitare l’accesso anche alle sue action. Nuovamente, usiamo load_and_authorize_resources
per caricare la risorsa Comment
e verificare i diritti per questa. Se Comment
fosse una nested resource sotto ad Article
negli instradamenti, potremmo usare :nested
con load_and_authorize_resources
per caricare i commenti attraverso la risorsa Article
:
load_and_authorize_resource :nested => :article
In questo caso, comunque, non usiamo l’innestamento, per cui non abbiamo bisogno di fare così.
Aggiunta dei diritti (Abilities)
Ora che la nostra applicazione è sicura, possiamo cominciare a definire i diritti che ciascun ruolo comporta. Ciò viene fatto ritornando alla classe Ability
che abbiamo creato prima. I diritti che definiamo nel metodo initialize
verranno riflessi su tutta la nostra applicazione:
class Ability include CanCan::Ability def initialize(user) can :read, :all end end
Passiamo l’utente corrente al metodo initialize
, in modo tale da poter cambiare i diritti a seconda di chi sia l’utente autenticato correntemente. Cominciamo con gli utenti di ruolo amministrativo, che dovrebbero poter fare tutto.
L’utente passato al metodo initialize
può essere un oggetto di qualsiasi tipo, in quanto l’autenticazione è completamente disaccoppiata dall’autorizzazione. Ciò che definisce un utente come, per esempio, amministratore, dipende esclusivamente dal sistema di autenticazione usato. Potermmo, per esempio, avere un campo booleano admin?
nel nostro modelle User
. Nella nostra applicazione un utente può avere più di un ruolo e c’è un metodo role?
che ci dice se un utente è membro di un certo ruolo. Useremo quel metodo per impostare i diritti:
class Ability include CanCan::Ability def initialize(user) if user.role? :admin can :manage, :all else can :read, :all end end end
Il nostro codice ora verifica se l’utente corrente è un amministratore e in tal caso gli dà il permesso di gestire completamente tutti i modelli. Il passaggio di :manage
come action indica proprio che l’utente può eseguire tutte le azioni possibili sul modello.
Dobbiamo ancora definire il metodo role?
nella classe di modello User
. Qui abbiamo impostato i ruoli allo stesso modo in cui avevamo fatto nell’episodio 189 [guardalo, leggilo], ma non importa come configuriate i vostri ruoli, fintanto che sarete in grado di stabilire a quale ruolo o ruoli appartiene un determinato utente.
class User < ActiveRecord::Base acts_as_authentic has_many :articles has_many :comments named_scope :with_role, lambda { |role| {:conditions => "roles_mask & #{2**ROLES.index(role.to_s)} > 0 "} } ROLES = %w[admin moderator author editor] def roles=(roles) self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.sum end def roles ROLES.reject { |r| ((roles_mask || 0) & 2**ROLES.index(r)).zero? } end def role?(role) roles.include? role.to_s end end
Il metodo role?
aggiunto qui controlla che il ruolo passato sia incluso fra quelli dell’utente.
Tornando alla classe Ability
, dobbiamo fare ancora una ulteriore modifica. Nel metodo initialize
controlliamo se l’utente appartiene ad un ruolo, ma per gli utenti anonimi, ossia che non si sono autenticati al sito, user
sarà nil
. Potremmo controllare che l’utente non sia nil
prima di provare a verificarne i ruoli, ma decidiamo in questo caso, piuttosto, di creare un utente "ospite" nel caso in cui l’utente passato sia nil
. In questo modo possiamo continuare a richiamare i metodi tipo role?
anche per gli utenti che non hanno ancora impostato un account:
class Ability include CanCan::Ability def initialize(user) user ||= User.new # User ospite if user.role? :admin can :manage, :all else can :read, :all end end end
Se ritorniamo nuovamente alla nostra applicazione, continueremo a non poter modificare o cancellare i commenti, ma se ci autentichiamo con un utente di ruolo amministrativo, allora i link verranno correttamente mostrati:
Ora che abbiamo fatto in modo che l’autorizzazione funzioni per gli amministratori, non ci resta che impostare i diritti per gli altri ruoli. Cominciamo dagli utenti ospiti, quelli che non hanno alcun ruolo associato. Questi dovrebbero poter creare dei commenti ed aggiornare i commenti che sono stati scritti da loro stessi. Per far ciò modifichiamo la nostra classe Ability
:
class Ability include CanCan::Ability def initialize(user) user ||= User.new if user.role? :admin can :manage, :all else can :read, :all can :create, Comment can :update, Comment do |comment| comment.try(:user) == user end end end end
Scrivere il codice per permettere agli utenti di creare commenti è semplice, ma il codice per permettere la modifica dei commenti è un po’ più insidioso, in quanto gli utenti dovrebbero poter modificare solo i commenti scritti da loro. Per ottenere questo comportamento passiamo a can un blocco che passerà l’istanza del modello che stiamo controllando. Il blocco deve restituire true
o false
a seconda che la action debba essere consentita o meno, per cui nel blocco controlliamo che l’utente che ha scritto il commento sia lo stesso che sta cercando di modificarlo / cancellarlo. C’è la possibilità che il commento possa essere nil
, per cui usiamo il metodo di Rails try
per leggere gli attributi dell’utente affinchè sia restituito nil
nel caso in cui il commento sia nil
, piuttosto che sollevare un’eccezione.
Se ci autentichiamo come utente privo di ruoli, potremo ora aggiungere nuovi commenti, aggiornarli, ma non fare altrettanto per commenti non inseriti da noi.
Adesso modifichiamo ancora il codice per aggiungere i diritti dei moderatori. I moderatori dovrebbero poter modificare tutti i commenti, per cui aggiorniamo il diritto di aggiornamento di commenti per seguire questo comportamento:
can :update, Comment do |comment| comment.try(:user) == user || user.role?(:moderator) end
Ci manca un ultimo ruolo da coprire, :author
. Gli autori dovrebbero poter creare nuovi articoli e cambiare tutti quelli che hanno scritto loro. Per aggiungere questi diritti, dobbiamo semplicemente aggiungere il seguente codice alla classe Ability
:
class Ability include CanCan::Ability def initialize(user) user ||= User.new if user.role? :admin can :manage, :all else can :read, :all can :create, Comment can :update, Comment do |comment| comment.try(:user) == user || user.role?(:moderator) end if user.role?(:author) can :create, Article can :update, Article do |article| article.try(:user) == user end end end end end
Come già fatto per gli utenti ospiti relativamente ai commenti, passiamo l’articolo corrente al blocco nel diritto di update dell’articolo e controlliamo che l’utente dell’articolo sia l’utente corrente.
Ora abbiamo definito tutti i diritti per ciascun ruolo. La cosa bella di CanCan è che ci permette di definire tutti i diritti in un posto solo ed il resto dell’applicazione si adeguerà automaticamente a questi cambiamenti.
Una pagina di errore più carina
Se un utente chiamasse una action per la quale non avesse il diritto, vedrebbe una pagina di errore piuttosto bruttina, mostrante una eccezione di tipo AccessDenied
. Cambiamo questo comportamento in modo tale che in casi simili sia mostrata una pagina di errore migliore.
Rails fornisce un metodo denominato rescue_from
che possiamo sfruttare per i nostri scopi nell’ApplicationController
. Possiamo passare a tale metodo una eccezione ed un metodo o un blocco. Nel nostro caso, passiamo un blocco dentro al quale facciamo mostrare all’applicazione un messaggio di errore flash e ridirigiamo alla home page.
rescue_from CanCan::AccessDenied do |exception| flash[:error] = "Access denied!" redirect_to root_url end
Dopo questa modifica, quando un utente privo di ruoli prova a modificare un articolo, digitando direttamente l’URL per la action di edit nel browser, viene rimandato alla home page e avvisato che non può fare una cosa simile.
E’ tutto per questo episodio. Per ulteriori dettagli o per segnalare anomalie su CanCan, visitate la pagina GitHub del progetto di Ryan.