Controle De Acesso Em Rails

janeiro, 21 - 2009

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={}, &amp;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
&#91;/sourcecode&#93;

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) &amp; @roles).size > 0) or
      (@mode.exclusive? and (roles_of(user) &amp; @roles).empty?)) and
      @check.bind(controller).call;
    else true; end
  end
  
  private
  def roles_of(user)
    [user.send(@@options[:roles])].flatten.map(&amp;@@options[:map].to_sym).map(&:to_sym);
  end
  
  def set_check_for(user)
    #@check.flatten_keys!; 
    @check = @check[(@check.keys &amp; 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

Anúncios

Novidades Rails E Mootools

julho, 4 - 2008

Pessoal, está muito dificil continuar mantendo o blog, estou totalmente sem tempo, as postagens já diminuiram consideravelmente. Peço desculpas e paciência a todos que lêem (liam) o blog, um dia voltarei ao ritmo de antes. Mas para não deixar passar, vou deixar links para dois assuntos sobre os quais eu queria falar mas não consegui:


RadRails 1.0 Lançado

março, 17 - 2008

Há algum tempo foi lançada a versão 1.0 do RadRails, que agora faz parte do projeto Aptana. A vantagem é que o Aptana já era uma ótima IDE para XHTML, CSS e Javascript, depois que integrou o RadRails e suporte a PHP ficou melhor ainda. Nessa nova versão do RadRails vieram algumas melhorias e acertos, além de uma interface um pouco melhorada. Algumas das novas funcionalidades:

  • Suporte ao Rails 2.0
  • Suporte a JRuby nativo (viva!)
  • Aba de vizualização RDoc
  • Code Completion agora sugere argumentos de métodos

O suporte, via IDE, dos servidores foi melhorado, agora eles iniciam automáticamente com a criação do projeto. Falando nisso, a tela de criação de projeto melhorou, foram adicionadas novas opções muito úteis como a versão do Rails que vai ser usada no projeto, o tipo de banco de dados (fim do sqlite como default), e os servidores que serão criados. Novo Rails Shell, para quem prefere usar a linha de comando às janelas da IDE. Vale a pena conferir!

Para instalar/atualizar o RadRails no Aptana: Help > Software Updates > Find and Install… > Search for new features to install > Next > Selecione “Aptana: RadRails Development Enviroment” > Finish


Rails 2.0 Chegou

dezembro, 7 - 2007

Finalmente chegou o tão esperado Rails 2.0, na versão 2.0.1 com algumas correções de última hora. Muitas novidades chegaram, confira algumas aqui mesmo ou veja alguns vídeos sobre o Rails 2.0 no Railscasts (Simplificando Views com Rails 2.0, Fixtures no Rails 2.0 e Autenticação HTTP). Para instalar é só rodar:

gem install rails -y

Ou se o de cima não funcionar:

gem install rails -y --source http://gems.rubyonrails.org

Se a versão do RubyGems não for a mais nova pode estar dando erros (como aqui), então rode antes:

gem update --system

Prometo que voltarei a postar mais regularmente.

Powered by ScribeFire.


Rails 2.0 Chegando

outubro, 1 - 2007

Acabou de sair do Riding Rails o Preview Release do Rails 2.0. Antes de se lançarem a versão 2.0 final eles lançarão o Rails 1.2.4, que vai ter muitos bugs consertados e warnings de métodos deprecados para sua aplicação estar prontar para o novo Rails.
Nessa nova versão tivemos mudanças no ActionPack, não tantas no ActiveRecord, o ActiveWebService saiu e entrou ActiveResource, e etc. Praticamente o framework ficou mais limpo, mais coeso. Vou listar algumas delas que achei interessante la do post, lembrando que não testei nenhuma delas:

ActionPack

Organização Da View

Agora os templates tem um novo formato para serem nomeados, ele é action.formato.renderizador. Exemplos:

  • Páginas que serão renderizadas para HTML com o ERB: action.html.erb.
  • Usar o Builder que renderiza aquele XML para um feed: action.atom.builder.
  • Renderizar usando outro renderizador como o HAML: action.html.haml.

Tratamento de erros

Antes você redefinia um método para tratar os erros que ocorriam, tendo que fazer um case para identificar o tipo de cada erro. Agora você ganhou um método declarativo nos controles chamado rescue_from, que chama um determinado método para cada tipo de erro. Exemplo:

class PostsController < ApplicationController
  rescue_from User::NotAuthorized, :with => :deny_access

  protected
  def deny_access
    #...
  end
end

Sai O Paginator Clássico

O paginator clássico agora só está disponível como um plugin (classic_pagination), foi retirado do core do Rails. Aproveitando que ele saiu, aconselho vocês a usarem o plugin will_paginate.

ActiveRecord

Migrations Mais Atraentes

Agora ao invés de ter que ficar chamando column para cada novo atributo na coluna, você pode chamar o tipo do atributo (string, integer, datetime, etc.). Isso já implorava por um method_missing há um tempo, mais Ruby/Rails Like.
Antes você usava:

create_table :posts do |t|
  t.column :body, :string, :null => false;
  t.column :user_id, :integer;
  t.column :created_at, :datetime;
end

Agora pode usar:

create_table :posts do |t|
  t.string   :body, :null => false;
  t.integer  :user_id;
  t.datetime :created_at;
end

Alguns Quilos A Menos

Para deixar o ActiveRecord um pouco mais limpo removeram os acts_as_etc do core, eles estão disponíveis como plugins, para instalar o acts_as_list, por exemplo, é só rodar script/plugin install acts_as_list. Removeram também os adaptadores de bancos de dados comerciais, agora o Rails só vem com adaptadores para os bancos MySQL, SQLite e PostgreSQL. Mas se quiser usar os outros bancos, os adaptadores estão disponíveis como gems, para instalar o adaptador para o Oracle, por exemplo, é só dar um gem install activerecord-oracle-adapter.

Vale lembrar que ainda não é a versão final, algumas coisas ainda podem mudar. Ah, não precisa dizer que os métodos hoje deprecados serão eliminados. Essa nova versão promete ser mais estável, mais divertida e mais limpa.

Powered by ScribeFire.


RailsConf Europe 2007

setembro, 20 - 2007

Post rapidinho da semana, o tempo tá apertado, depois alivia e vem mais, prometo!

Hoje acabou a RailsConf Europe, ocorrida em Berlim, que começou dia 17 e durou até dia 19. Dessa vez não fui (huahuauha), mas muitos desenvolvedores foram, dizem que foi um sucesso.

Grandes nomes apareceram como David Hansson, Chad Fownler, Dave Thomas, etc. Mas a grande surpresa foi saber que se falou muito sobre JRuby e a Sun, que acabou de lançar o NetBeans 6 Beta.

Powered by ScribeFire.


Javascript, Ruby On Rails E O Blog

agosto, 27 - 2007

Pessoal,

O tempo tá meio apertado (ok, sempre a mesma desculpa), por isso a falta dos posts mais frequentes. Vou reiniciar falando de duas coisas, ou melhor, frameworks.

Quem acompanha o blog sabe que, há um tempo, eu estava desenvolvendo um framework Javascript que partiu de diversos códigos usados nos meus projetos. Então, a idéia foi pro ralo, um dos motivos da desistência foi o fato do único mantedor ter sido eu, além dos outros frameworks terem crescido em qualidade e funcionalidades, não dava pra acompanhar. Apesar de parecer um total fracasso, não foi, com esse framework que estava desenvolvendo, realmente aprendi Javascript, diversos conceitos, práticas, técnicas, integração com CSS, DOM, etc. Aprendi o que os frameworks atuais fazem por baixo dos panos, aprendi a usar melhor essa linguagem simples, e usa-la sem comprometer a acessibilidade. Depois disso passei um tempo sem mexer mais com Javascript. Voltei! E percebi que realmente há a necessidade de usar um framework ativo, testei (não intensamente) alguns deles, jQuery, Prototype (junto ao Scriptaculous), e Mootools. Sei que a maioria ama jQuery, fui ver, achei realmente muito simples, mas não gostei, é simples demais para o que quero. Prototype, já imaginava ser a minha escolhida, mas achei grande demais, com a Scriptaculous então, parecia um monstro, e sem muitas das facilidades dos novos frameworks. Mootools, pra mim a menos cotada, nem testaria não fosse o Júlio ter postado sobre ela, fui ver mais de perto, perfeita! Não é um monstro, é fácil, flexível, e o que mais me impressionou foram as animações (Fx!), muito bem acabadas, eventos próprios, o básico que as outras libs tinham e poder selecionar só o que preciso para baixar. Foi o bastante para tomar a decisão. Usarei Mootools de agora em diante. Sei muito pouco (quase nada) sobre ela ainda, terei que estudá-la, mas infelizmente não será imediatamente agora. É minha escolha por enquanto.

Agora sobre framework web, já faz algum tempo que venho estudando Ruby e Rails, mas só agora parece que adquiri o mínimo de conhecimento, me sentia muito inseguro nele, e grande parte por causa do PHP, ou melhor do modo como eu usava o PHP, scripts com consulta ao BD junto de HTML, códigos sujos, etc. Cheguei no Ruby com algum conhecimento de orientação a objetos grande parte por causa do Javascript (Objetos, Closures, etc.), mas ainda num território muito novo, eram muitas coisas novas, e o pior foi que parti direto pro Rails sem nem estudar Ruby antes, aí que me sentia perdido mesmo. Percebi que precisava aprender Ruby puro antes, comecei a fazer códigos Ruby, e me familiarizar, gostei bastante. Parti pro Rails e comecei a entender como as coisas funcionavam, fiz projetos de teste, etc. Agora já estou “contaminado”, pretendo estudar mais e usar bastante Rails de agora em diante.

Portanto o meu blog vai continuar sendo o que sempre foi, o bom e velho XHTML, CSS, Javascript (tradicionais scripts, Mootools e tentarei melhorar o Dynamic History) e tentarei postar mais um pouco de RoR.

Abraço pessoal! Flwss….

Powered by ScribeFire.