[Tutorial] Autenticação com Authlogic + autorização com Acl9

[Tutorial] Autenticação com Authlogic + autorização com Acl9

Introdução

Autorização é diferente de autenticação e segundo Oleg Dashevskii (criador do Acl9):

Ambas palavras iniciam com “aut”, mas tem significados diferentes! Autenticação é basicamente um mapeamento de credenciais (login e senha) ou OpenID para especificar a conta do usuário no sistema. E autorização é uma permissão de um usuário autenticado para executar alguma ação específica em algum lugar do sistema.

Hoje vou escrever dois tutoriais em um mesmo post, mas eles podem ser usados separadamente. O primeiro é sobre como usar o Authlogic pra fazer autenticação e o segundo é sobre como usar o Acl9 para gerenciar autorizações.

 

Pré-requisitos

Utilizei Ruby v1.8.6 com as seguintes gems: Rails v2.3.3, Authlogic v2.1.1, Acl9 v0.10.0 e Nifty-generators 0.3.0.

Instalação das gems

[sudo] gem sources -a http://gems.github.com
[sudo] gem install authlogic
[sudo] gem install be9-acl9
[sudo] gem install nifty-generators

 

Autenticação com AuthLogic

Esta parte do tutorial é praticamente a forma escrita do episódio #160 do RailsCasts.
Atenção: Nós iremos usar o nifty-generators pra facilitar, mas isso não é mandatório para poder utilizar authlogic ou acl9.

Preprarando um simples conteúdo estático

Crie um novo projeto rails:

rails authlogic_acl9

Antes de mais nada, gere um nifty_layout:

script/generate nifty_layout

Inclua no arquivo config/environment.rb a seguinte linha:

config.gem "authlogic"

Agora crie um controller para algumas páginas estáticas:

script/generate controller static_content index

Na página index.html.erb criada, eu escrevi:

<h1>Página inicial</h1>

Pra que esta seja a página inicial da app, apague o arquivo public/index.html e adicione a seguinte linha no config/routes.rb:

map.root :controller => "static_content", :action => "index"

User

Agora que já foi criado uma página estática simples, crie um scaffold para o User:

script/generate nifty_scaffold user username:string email:string password:string new edit

Para quem não conhece o nifty_scaffold, os últimos parametros que foram passados são os controllers (neste caso ‘new’ e ‘edit’ – com os quais ganhamos automáticamente o ‘create’ e ‘update’, por motivos óbvios).

Altere o migration da tabela Users para:

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :username
      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
end

Exitem outras colunas interessantes que podem ser usadas, como por exemplo login_count (integer) e current_login_ip (string). Veja mais colunas neste link.

Gere a tabela:

rake db:migrate

Inclua a seguinte linha no seu models/user.rb:

class User < ActiveRecord::Base
  acts_as_authentic
end

Altere o users/_form.html.erb, incluindo um campo de confirmação de senha:

<% form_for @user do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :username %><br />
    <%= f.text_field :username %>
  </p>
  <p>
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </p>
  <p>
    <%= f.label :password %><br />
    <%= f.password_field :password %>
  </p>
  <p>
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation %>
  </p>
  <p><%= f.submit "Enviar" %></p>
<% end %>

O interessante é que o AuthLogic já reconhece esse campo e não cadastra o usuário se as senhas não coencidirem, exibindo um erro de validação.

Agora altere o users_controller.rb:

class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(params[:user])
    if @user.save
      flash[:notice] = "Usuário cadastrado com sucesso."
      redirect_to root_url
    else
      render :action => 'new'
    end
  end

  def edit
    @user = current_user
  end

  def update
    @user = current_user
    if @user.update_attributes(params[:user])
      flash[:notice] = "Dados do usuário alterados com sucesso."
      redirect_to root_url
    else
      render :action => 'edit'
    end
  end
end

Session

Vamos agora começar a gerenciar as sessões:

script/generate session user_session
script/generate nifty_scaffold user_session --skip-model username:string password:string new destroy

Na primeira linha, você vê um script (o qual faz parte do AuthLogic) que gera a session.
E na segunda, usamos novamente o nifty_scaffold, só que agora ignorando o model (com o parametro –skip-model), uma vez que ele já foi gerado pelo primeiro script (generate session).

Altere o user_sessions_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] = "Usuário logado com sucesso."
      redirect_to root_url
    else
      render :action => 'new'
    end
  end

  def destroy
    @user_session = UserSession.find
    @user_session.destroy
    flash[:notice] = "Sessão finalizada com sucesso."
    redirect_to root_url
  end
end

E user_sessions/new.html.erb será a página de login:

<% title "Login" %>

<% form_for @user_session do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :username %><br />
    <%= f.text_field :username %>
  </p>
  <p>
    <%= f.label :password %><br />
    <%= f.password_field :password %>
  </p>
  <p><%= f.submit "Enviar" %></p>
<% end %>

Adicione no routes.rb as rotas para login e logout:

map.login "login", :controller => "user_sessions", :action => "new"
map.logout "logout", :controller => "user_sessions", :action => "destroy"

No layouts application.html.erb, inclua um menu que irá gerenciar o usuário e as sessões:

<div id="user_nav">
  <% if current_user %>
    <%= link_to "Editar", edit_user_path(:current) %> |
    <%= link_to "Logout", logout_path %> |
    <span>Bem vindo <strong><%= current_user.username %></strong>!</span>
  <% else %>
    <%= link_to "Registrar", new_user_path %> |
    <%= link_to "Login", login_path %>
  <% end %>
</div>

Pra finalizar, inclua dois métodos ao application_controller.rb, os quais retornam o usuário autenticado:

class ApplicationController < ActionController::Base
  helper :all # include all helpers, all the time
  protect_from_forgery # See ActionController::RequestForgeryProtection for details

  #Authlogic
  filter_parameter_logging :password

  helper_method :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
end

Pronto! O AuthLogic já está gerenciando suas sessões :)

Uma opção bem interessante é traduzir as mensagens, campos e modelos do Authlogic para pt-BR. O Patrick Espake explica como fazer isso, neste post.

 

Autorização com Acl9

Nesta segunda parte do tutorial, vamos gerenciar as autorizações atravéz do Acl9.
Bem, como eu disse no início do post, são dois tutoriais independentes, mas irei usar a aplicação que criamos no primeiro tutorial pra adiantar as coisas.

Inclua mais uma linha no seu config/environment.rb:

config.gem "be9-acl9", :source => "http://gems.github.com", :lib => "acl9"

Irei usar algumas actions sem sentido, apenas para efeito didático. Vamos então recriar o controller static_content:

script/generate controller static_content index index2 index3 index4 denied

Adicione as novas actions no routes.rb:

map.root :controller => "static_content", :action => "index"
map.resources :static_content, :collection => {:index2 => :get, :index3 => :get, :index4 => :get, :denied => :get}

No final do layout application.html.erb, inserimos um rodapé com alguns links, pra testar se as permissões de fato funcionam corretamente:

<div id="footer">
  <%= link_to 'index', :controller => 'static_content', :action => 'index' %> |
  <%= link_to 'index2', :controller => 'static_content', :action => 'index2' %> |
  <%= link_to 'index3', :controller => 'static_content', :action => 'index3' %> |
  <%= link_to 'index4', :controller => 'static_content', :action => 'index4' %>
</div>

Vamos trabalhar o rescue ‘AccessDenied’ no application_controller.rb:

rescue_from 'Acl9::AccessDenied', :with => :access_denied

def access_denied
  if current_user
    render :template => 'static_content/denied'
  else
    flash[:notice] = 'Acesso negado. Você precisa estar logado.'
    redirect_to login_path
  end
end

Este método faz o seguinte: se existir uma sessão de usuário e ocorrer esta excessão, significa que este usuário específico não tem permissão pra acessar tal recurso, então é renderizado o ‘static_content/denied’. Caso ele não esteja logado, aparece uma mensagem flash dizendo que ele precisa se logar e redireciona-o para tela de login.

Continuando, nós precisaremos de um model Role:

script/generate model Role name:string authorizable_type:string authorizable_id:integer

Altere a migration Roles para:

class CreateRoles < ActiveRecord::Migration
  def self.up
    create_table "roles", :force => true do |t|
        t.string   "name",              :limit => 40
        t.string   "authorizable_type", :limit => 40
        t.integer  "authorizable_id"
        t.timestamps
      end
  end

  def self.down
    drop_table :roles
  end
end

E o models/role.rb para:

class Role < ActiveRecord::Base
  acts_as_authorization_role
end

Assim, o Acl9 entende que está é a classe das regras de autorização.

Embora não seja necessário alterar nada na tabela Users, é necessário uma tabela intermediária dele com as regras, então crie a migration roles_users:

script/generate migration roles_users

E edite esta nova migration:

class RolesUsers < ActiveRecord::Migration
  def self.up
    create_table "roles_users", :id => false, :force => true do |t|
      t.integer  "user_id"
      t.integer  "role_id"
      t.datetime "created_at"
      t.datetime "updated_at"
    end
  end

  def self.down
  end
end

Não esqueça de gerar as tabelas:

rake db:migrate

Assim como fizemos no model Role, é necessário dizer ao Acl9 quem são os usuários, inserindo ‘acts_as_authorization_subject’ ao models/user.rb:

class User < ActiveRecord::Base
  acts_as_authentic
  acts_as_authorization_subject
end

 

Brincando com as autorizações

Bem, está tudo pronto! Aqui começa a diversão :)

No controller static_content_controller.rb, adicione no início da classe:

class StaticContentController < ApplicationController
  access_control do
    allow all
  end

  #...

Neste ponto, nada deve ter mudado (a não ser que algo tenha dado errado na instalação do Acl9), simplesmente porque estamos dando permissão de todas as actions para todos os usuários.

Vamos restringir algumas coisas:

access_control do
  allow all, :to => [:index]
  allow anonymous, :to => [:index2]
  allow logged_in, :to => [:index2, :index3]
end

Na linha allow all, :to => [:index], estamos dizendo que todos podem acessar a index. Na linha allow anonymous, :to => [:index2], somente usuários não logados podem acessar a index2, ou seja, se você estiver logado, talvez você não acesse a index2, exceto se existir uma regra explicita dizendo isso. Neste exemplo isso acontece, como você verica na última linha deste bloco: allow logged_in, :to => [:index2, :index3], claramente é notável que os usuários logados podem acessar a index2 e index3. Crie um usuário e você vai verificar que só quando você estiver logado você acessa a index3.

Ok, mas o que realmente queremos é criar regras diferentes para usuários diferentes e não apenas usuários logados e não-logados. Para isso:

access_control do
  allow all, :to => [:index]
  allow anonymous, :to => [:index2]
  allow logged_in, :to => [:index2, :index3]
  allow :admin, :to => [:index4]
end

Aqui fica interessante! Só o usuário que tiver a regra ‘admin’ vai acessar. E para fazer isso, basta associar esta regra à um usuário:

User.find(2).has_role! :admin

E se por algum motivo você precisar verificar se o usuário tem determina regra ou não, basta usar:

User.find(2).has_role? :admin
 #Retorna um boolean.

Bom, é isso.

Se tiverem dúvidas, postem nos comentários ou acessem o grupo oficial do Acl9.

Atualizado
Coloquei o projeto desenvolvido neste tutorial no GitHub:
http://github.com/lucascaton/authlogic_acl9_example/

  • Parabéns Lucas, muito bom o artigo eu estava procurando sobre um método de autorização e acho que achei :D.

  • Marcello de Souza

    Lucas, parabéns pelo trabalho.
    Mas śo uma coisa. como faço para no momento em q estou registrando um usuário, utilizando o authlogic, por exemplo, efino uma regra pra ele, eu não sei onde usar o has_role!.
    Sei o q ele faz.. mas não sei onde vou utilizá-lo, Vc poderia dar um exemplo disso? Grato.

  • Olá Marcelo, tudo bom?

    Você pode colocar o método ‘has_role!’ em um callback.
    Daí, nas views ‘new’ e ‘edit’ (ou em um ‘_form’), você poderia colocar um helper ’select’ (ou algo similar) com as regras deste usuário.

  • Marcello de Souza

    Pois é Lucas… é ai que tá o entrave…. isso é que eu não consigo fazer…. se puder me indicar onde eu posso conseguir um exemplo ….
    Mais uma vez obrigado e parabéns pelo trabalho.

  • Marcelo, tente algo assim, na sua view, você coloca:

      < %= f.label 'Privilégio' %>
      < %= select 'role', 'name', {     'Admin' => ‘admin’,
        ‘Estoque’ => ‘warehouse’,
        ‘Gerente’ => ‘manager’,
      } %>

    É legal usar atributos virtuais pra isso =)

    Você pode colocar o método ‘has_role!’ em um callback (after_create, after_save, etc.) e / ou limitar para que apenas aos usuários com as devidas permissões possam atribuir / editar estas permissões.

  • Se alguém precisar retirar regras de determinado usuário, existe um método para isso:
    has_no_roles!

    Exemplo de uso:
    User.find(1).has_no_roles! :admin

    Para mais informações, consulte os Rdocs do projeto:
    http://rdoc.info/projects/be9/acl9

  • Ivan

    1) Não seria interessante indexar os campos user_id e role_id da tabela ‘roles_users’?

  • Parabéns Lucas!

    Finalmente consegui ler seu artigo e realmente era o que estava precisando!

    Obrigado!

  • Dica rápida – como ignorar a validação de email no cadastro de usuários (inclusive permitir email vazio):

    Altere no seu ‘models/user.rb’:

    class User < ActiveRecord::Base   acts_as_authentic { |c| c.validate_email_field = false } end

  • Camilla

    Lucas eh necessário criar a Autorização com Acl9 quando fazemos a Autenticação com AuthLogic?

  • mairon

    opa fantastico esse tutorial só uma consulta eu vi que dentro do vendor um rails como faço para criar esse plugin ??

    abraçp

  • Coutinho

    Lucas o artigo tá muito bom, parabéns.
    Agora eu faço semelhante com o restfull_autenticated sendo que algumas coisas com me nos código do que você precisou.
    Aqui uso ele e nao preciso ficar dizendo em código o que cada perfil tem acesso, tenho somente um metodo no application controller que manda buscar as aitorizacoes de cada um no banco

  • Camila: eles trabalham perfeitamente de forma independente, ou seja, um não depende do outro.

    Mairon: tenta ‘rake rails:freeze:gems’.

    Coutinho: De fato, o seu jeito é melhor. Eu ainda vou dar uma estudada melhor, mas acredito que a melhor opção hoje seja a gem ‘declarative_authorization’: http://github.com/stffn/declarative_authorization

    Valeu por todos pelos comentários :)
    Abraços!

  • Valdir

    Valeu Rapaz! Muito bom o seu artigo. Recomendarei aos amigos.

    Obrigado!!!!

    1 abraço.

  • ótimo post cara

  • Opa esse seu tutorial é muito bom, mas estou com alguns problemas.
    Não estou conseguindo fazer o bloqueio de certas páginas/links, você poderia me explicar melhor como eu faço para bloquear o acesso?

    Tipo só ver quando tiver logado.

    Abraços.

  • Gabriel,
    Você pode incluir um método ‘allow logged_in’ no seu controller. Por exemplo:

    class MyController < ApplicationController access_control do allow logged_in, :to => :index, :show
    end

    def index; end
    def show; end
    end

    Desta forma, só quem estiver logado vai conseguir ver o ‘index’ e o ‘show’ :)

  • Thauan

    Otimo post cara, ajudou bastante. Estava procurando algo deste tipo.

  • Walter Luiz

    Show de bola esse tutorial. Caiu como uma luva para mim.
    Só não entendi uma coisa:
    – onde eu insiro essa regra ou como eu insiro: User.find(2).has_role? :admin

    Desde já agradeço e parabéns pelo blog.

  • Olá Walter. Obrigado pelos parabéns.

    Você pode usar esse método onde precisar :)
    Esse retorna true se determinado usuário possuir aquela permissão, por exemplo, usuário ‘lucas’ possui a permissão ‘admin’.

    Abraços!

  • Matheus

    Muito boa sua explicação, se minha aplicação tiver a necessidade de diversos tipos de autorização pode ter certeza que vou recorrer a este artigo por enquanto o Authlogic já está dando conta do recado. Abraços!

  • joao

    Catón, no controller estou fazendo com render :update do |page| para mudar apenas o login

    O problema é que ele tenta chamar o template. Dá um MissingTemplate

  • Eduardo

    Oi Lucas, gostei muito do seu tutorial, trabalho na área da saúde na unicamp e estamos preparando uma aplicação em ruby, temos a intenção de usar o authlogic e o acl9 e seu tutorial ajudou muito. Gostaria de saber se é possível dar permissões a nível de objeto, com um modelo de (usuários x grupos) (grupos x permissões). Se possível você poderia postar um exemplo de como criar estas interfaces usando o acl9 ou indicar onde posso conseguir mais exemplos?

    Desde já agradeço
    Eduardo

  • Glaz

    Oi Lucas,
    Meu destroy user não está funcionando.
    def destroy
    logger.info “testestestestes”
    @user = User.find(params[:id])
    if @user.destroy
    logger.info “teste”
    respond_to do |format|
    format.html { redirect_to(users_url) }
    format.xml { head :ok }
    end
    else
    logger.info “testestestestes”
    end

    end

    ======

    :delete %>
    ======

    o erro aparece assim:
    Unknown action

    No action responded to destroy. Actions: access_denied, create, current_user, current_user_session, edit, index, new, redirect_back_or_default, require_no_user, require_user, show, store_location, and update

    Sabe o que pode ser?
    Obrigada!

  • Carlos

    Oi Lucas

    Eu preciso inicialmente inserir as regras na tabela roles?
    Exemplo: devo inserir admin, manager etc e oq eu coloco no campo “authorizable_type…id”?
    Valeeeeeu

  • Eduardo

    Bom dia pessoal eu tenho seguinte código:

    access_control do
    #——————————–Permissões liberadas para
    administrador———————————–
    allow :Administrador
    #—————————————-Permissões para
    indicators—————————————-

    @roles = Permission.find(:all,
    :joins => :role,
    :joins => :interface,
    :conditions => {:interfaces => {:name =>
    params[:interface] }})
    @roles.map do |r|
    allow r.role.name.to_sym, :to => [:edit] if r.read
    allow r.role.name.to_sym, :to => [:new, :create, :live_search]
    if r.include
    allow r.role.name.to_sym, :to => [:edit, :update, :live_search]
    if r.write
    allow r.role.name.to_sym, :to => [:destroy] if r.exclude
    end

    #———————————————————————————————————-
    end

    que está dando o seguinte erro:

    undefined local variable or method `params’ for
    Acl9::Dsl::Generators::FilterLambda:0x4979430>

    Gostaria de saber se é possível passar parametros dentro do bloco do
    access control, já tentei com variáveis de classe mas vi que teria
    problemas de concorrencia. Não preciso necessáriamente usar o params,
    qq forma q servir para passar uma variável ali pra dentro resolveria
    (que não cause problemas de concorrêcnia).

    Desde já agradeço

  • You could certainly see your skills within the paintings you write. The arena hopes for more passionate writers like you who aren’t afraid to say how they believe. All the time follow your heart.

  • CakePHP Y Rails Lover

    Muito bom cara, estou fazendo meu primeiro trabalho profissa em Rails e to encantado com a ferramenta e com a comunidade.

  • Fantastic web site. A lot of useful information here. I am sending it to a few pals ans also sharing in delicious. And obviously, thanks for your effort!

Comments are closed.