Recentemente precisei criar um meio de controlar o acesso de usuários de acordo com seus respectivos papéis no sistema. Foi aí que fiz uma pesquisa sobre o assunto e descobri em um post uma maneira bem simples e elegante de resolver o problema porém ainda faltava maior suporte. Pesquisei plugins e encontrei esse review, muito bom por sinal, nele o autor cria uma nova solução completa separando a lógica do acesso do resto do sistema. Mas como alguns sabem estou voltando aos poucos ao mundo Rails e resolvi encarar o problema como forma de me familiarizar novamente com a linguagem e o framework, solução: criar meu próprio sistema! Vou descrever o desenvolvimento passo a passo neste post.
Primeiro precisamos separar algumas coisas, muitos pensam que autenticação e autorização são a mesma coisa, mas não são. Autenticação de usuários é a identificação do usuário quando da entrada ao sistema, ou seja, ou você é um visitante ou é um usuário dentro da aplicação. Já autorização é um conceito um pouco mais amplo, dentro de uma aplicação geralmente cada usuário tem seu papel, em um jornal, por exemplo, temos leitores, editores, administradores e assim em diante, cada um com certas permissões. A autenticação pode ser considerada um nível de autorização já que restringimos a visualização de certas áreas a usuários identificados/logados no sistema.
O código que fiz cobre somente a parte de autorização (apesar de vermos algo relacionado à autenticação também), mas autenticação é algo muito trivial de se fazer, também existem plugins para isso, consulte o oráculo para maiores informações. Agora começa a parte interessante! =P
Definindo modelos
Vamos definir nossos modelos, como você verá ao longo do post, o código é bastante versátil, tudo poderá ser personalizado em uma única linha. De qualquer modo, vamos assumir que temos uma classe User e que cada usuário tem um papel, uma Role, ou seja Role has_many :users e User belongs_to :role.
Organização E Configuração Do Sistema
Criei um módulo chamado AccessControl, esse módulo será incluido no ApplicationController por meio de um include (não me diga…). Coloquei tudo dentro de um arquivo separado “access_control.rb” dentro de “app/controllers”. Depois criei um método para configurar o sistema, vejam abaixo:
module AccessControl Options = {:class => :User, :roles => :role, :map => :name, :user => :user}; def self.included(klass) klass.class_eval do extend AccessControl::ClassMethods; end end module ClassMethods def access_control_setup(opts) opts[:class] = opts[:a] || opts[:an] || opts[:class]; opts[:roles] = opts[:has_one] || opts[:has_many] || opts[:role] || opts[:roles]; opts[:map] = opts[:map_its] || opts[:map]; opts[:user] = opts[:class].to_s.downcase.to_sym; AccessControl::Options.merge!(opts); end end end
O código já está bem auto-explicativo. Em access_control_setup você deve ter reparado que existem diversos “sinônimos” para uma mesma opção, isso é para deixar a configuração mais intuitiva:
class ApplicationController < ActionController::Base include AccessControl; access_control_setup :an => User, :has_one => :role, :map_its => :name # Ou se a classe é Person e a relação com Role é do tipo many-to-many access_control_setup :a => Person, :has_many => :roles, :map => :name; #... end
Definindo Regras De Restrição
Meu objetivo é poder fazer as declarações de restrição a acesso das actions nos controllers, na própria classe mesmo, algo como:
class ArticlesController < ApplicationController # Nenhum visistante poderá vizualizar/criar/editar/deletar artigos restrict_access :from => [:guest], :message => "Efetue login primeiro"; # Somente editores e admins poderão criar artigos restrict_access :of => [:new], :to => [:editor, :admin]; # Somente admins poderão criar artigos, e editores seus próprios artigos restrict_access :of => [:edit], :to => [:admin, :editor], :check => { :editor => check_user_by(:user_id) }; #... end
Bem intuitivo, não? Agora vamos implementar o método restrict_access, também definido dentro de ClassMethods. Esse método deverá receber as actions que controlará o acesso (opção :of), para todas deve-se fornecer :all ou não especificar nenhuma action. Receberá também os tipos de usuário que poderão acessar (opção :to) ou os que não poderão (opção :from).
Além disso também aceita um bloco de código ou um Proc fornecido à opção :check, esse bloco será executado e decidirá se a action poderá seguir retornando true ou false, nesse caso o bloco será executado para todos os tipos de usuário. Mas também pode-se fornecer um Hash para a respectiva opção, e nele as chaves serão os tipos de usuário sobre os quais os Procs (os valores) serão executados. Essa opção check é útil para testar, por exemplo, se determinado editor é dono de um artigo. Opções extras poderão ser recuperadas mais tarde em um método access_denied, útil para definir mensagens ou endereços de redirecionamento.
def restrict_access(opts={}, &check) opts = {:of => :all}.merge(opts); check ||= opts.delete(:check); (@restrictions ||= []) << AccessControl::Restriction.new(opts.delete(:of), opts.delete(:to), opts.delete(:from), check, opts); before_filter(:check_authorization) unless before_filters.include?(:check_authorization); end [/sourcecode] Ele adiciona a uma variável de instância um objeto do tipo <b>Restriction</b>, depois define um filtro para ser executado antes da action caso já não exista. Precisamos criar um método <b>restrictions</b> para que o filtro possa acessar a lista de restrições, dentro de <b>ClassMethods</b> também: def restrictions; @restrictions; end
Lógica Das Restrições
A classe Restriction é responsável pela lógica por trás da autorização de execução das actions, é ela que decide se poderá prosseguir de acordo com regras definidas. Sua definição também está dentro do modulo AccessControl.
class Restriction attr_reader :actions, :options; CheckDefault = lambda{true;}; @@options = AccessControl::Options; def initialize(actions, included_roles, excluded_roles, check=nil, opts={}) @actions = (actions == :all) ? actions : [actions].flatten.map(&:to_sym); @roles = [(included_roles || excluded_roles)].flatten.map(&:to_sym); @mode = ActiveSupport::StringInquirer.new((included_roles) ? "inclusive" : "exclusive"); @check = check || CheckDefault; @options = opts; end def ok?(user, action, controller) # Could be in one line, but it'd be a mess if @actions == :all or @actions.include?(action) set_check_for(user) if @check.kind_of?(Hash); ((@mode.inclusive? and (roles_of(user) & @roles).size > 0) or (@mode.exclusive? and (roles_of(user) & @roles).empty?)) and @check.bind(controller).call; else true; end end private def roles_of(user) [user.send(@@options[:roles])].flatten.map(&@@options[:map].to_sym).map(&:to_sym); end def set_check_for(user) #@check.flatten_keys!; @check = @check[(@check.keys & roles_of(user)).first] || CheckDefault; end end
Ao iniciar ela armazena as opções, as actions e define se o modo de restrição é do tipo inclusivo ou exclusivo. O método set_check_for determina se o bloco de código deverá ser executado para o tipo de usuário atual. A lógica que determina o acesso a action esta dentro do método ok?.
Filtrando As Actions
O filtro check_authorization verifica se alguma restrição não permite o acesso a action, caso exista chama o método access_denied, que deverá ser implementado, com as opções extras fornecidas (veja o método restric_access).
def check_authorization action = request.path_parameters[:action].to_sym; restriction = self.class.restrictions.detect do |restriction| !restriction.ok?(current_user || AccessControl.guest, action, self); end access_denied(restriction.options) if restriction; return !restriction; end
Assumi que você tem um método current_user que retorna o usuário logado atualmente ou nil caso seja um visitante. Perceba ali na linha 4 que se o usuário for um visitante ele vai fornecer um objeto que atuará como usuário mas do tipo visitante (portando você não precisa definir uma Role guest), um Mock, esse objeto é devolvido pelo método guest:
def self.guest # Simple mock object Struct.new(Options[:roles]).new(Struct.new(Options[:map]).new(:guest)); end
Acesso Negado
Pronto, agora só falta a implementação do método access_denied, que pode ser feita no próprio módulo AccessControl ou dentro de ApplicationController como um método protegido. Veja um modelo abaixo:
def access_denied(opts) flash[:warning] = opts[:message] || "Você não tem permissão para acessar este recurso."; session[:redirect_to] = request.path; redirect_to(opts[:redirect_url] || login_path); end
Ele é útil pois pode-ser fornecer uma mensagem (com a opção :message) e um endereço (:redirect_url) em restrict_access que serão usados aqui ou caso nenhum seja fornecido ele exibe uma mensagem padrão e redireciona para a tela de login se existir.
Checks Automatizados
Você deve ter reparado um método chamado check_user_by, ele retorna um Proc para automatizar o teste de usuário, veja dois métodos desse tipo:
def check_user_by(param) lambda{current_user.id == params[param.to_sym].to_i;}; end def check_user; check_user_by(:id); end
O segundo assume o parametro :id como default. Ambos devem ser definidos dentro de ClassMethods.
É isso pessoal, para utilizar é só dar uma olhada nos exemplos do próprio post. Não implementei helpers para serem usados nas views, mas não é uma tarefa muito dificil. Quem quiser pode também baixar o arquivo com o código. Não tenho certeza ainda, mas talvez eu porte para um plugin, mas ainda preciso fazer os testes (não, não fiz teste nenhum, eu sei, eu sei…). Espero que tenham gostado, críticas são bem vindas! Flwss