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


Voltando Aos Poucos

janeiro, 15 - 2009

De volta! Não completamente, mas pretendo voltar às atividades aos poucos, ainda que os posts continuem raros, preciso de um tempo pra voltar à blogosfera, 2008 foi um ano muito lotado pra mim, não sobrou muito tempo para o blog. Peço desculpas a todos, principalmente àqueles que não retiraram meu feed do seu leitor ;D. Anyway, pretendo correr atrás do tempo perdido em se tratando de Ruby, Rails e Javascript. Estou lendo os milhões de artigos marked as starred no Google Reader que não tinha tempo de ler, a principio o objetivo é me atualizar, quem sabe aprender novos frameworks, novas linguagens? Qualquer dica é bem vinda, comente!
Pra piorar ainda mais a situação o meu desktop está com problemas, especificamente na placa de video, por enquanto estou me virando pra blogar. Sonho comprar um novo com o Core i7 da Intel lá pro meio do ano, sonho… Até consertar o meu deve demorar um pouco.
Apesar de estar me sentindo um pouco muito perdido, tenho planos para esse ano, pretendo fazer um projeto pessoal em Rails, por enquanto sem maiores detalhes =P. No mais, é isso. Obrigado a todos!


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:


Método Para Traduzir String

maio, 2 - 2008

Sempre pensei em um método que traduzisse uma String, na linguagem mesmo, encapsulado, sem precisar de classes de terceiros, etc. Há algum tempo atrás resolvi fazê-lo em Ruby e ficou guardado um bom tempo, até eu realmente precisar usá-lo, aproveito e mostro a vocês.

require "net/http";
require "uri";
require "hpricot";

class String
  
  def translate(from, to, opts={})
    params = {
      "text" => self, 
      "langpair" => "#{from}|#{to}", 
      "ie" => opts.delete(:encode) || "UTF-8", 
      "hl" => "pt-BR"
    }.merge(opts);
    text = Net::HTTP.post_form(URI.parse("http://translate.google.com/translate_t"), params).body;
    Hpricot(text).search("#result_box").inner_html;
  end
  
end

"A simple text to be translated".translate(:en, :pt);  #=> "Um simples texto a ser traduzido"

Ele faz uma consulta ao Google Tradutor e retorna o resultado traduzido. O primeiro parâmetro é a abreviação da língua na qual a String foi escrita e o segundo a língua para qual ela será traduzida, há também um terceiro, um hash com dados a serem enviados via POST. Ele usa a biblioteca Hpricot para buscar o trecho correspondente à tradução no documento HTML, poderia ser feito com expressões regulares, mas ficaria feio.


Diz Que Até Não É Um Mau Blog

março, 17 - 2008

O Júlio Greff nomeou meu blog como uma de suas referências. Valew Júlio, muito obrigado, e tomara que um dia meu blog chegue a altura do seu! Como parte do meme, tenho que indicar blogs dignos de tal “selo”:

Termino por aqui, esses blogs são muito bons, vale a pena dar uma conferida!


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


Efeitos Na Mootools

março, 12 - 2008

Voltei pessoal! E falando da Mootools, que andei estudando recentemente, mais especificamente sobre efeitos, uma parte muito divertida e legal.
Os efeitos na Mootools são divididos em algumas classes, que estão dentro de Fx, cada uma delas com um objetivo diferente. Falarei hoje nesse post da Fx.Style, Fx.Styles e inevitávelmente da Fx.Transitions, as duas primeiras basicamente mudam uma propriedade de estilo CSS gradualmente ao longo de um tempo de acordo com uma determinada transição, que está contida dentro da última classe referida. A principal diferença entre as duas classes é que a primeira só aplica efeito sobre uma propriedade e a segunda sobre mais de uma propriedade, as duas relacionadas a um elemento.

Transições

A classe Fx.Transitions nos fornece uma lista de transições, que alterarão a animação, para serem usadas em nossos efeitos. Transições são responsáveis por modificar os valores intermediarios entre o inicio e o fim de uma animação. Por exemplo, quando se quer modificar a largura de um elemento de 10 a 15 pixels, a transição será responsável por definir que, por exemplo, no 300º milissegundo da animação a largura terá 12 pixels. Cada transição, exceto a linear, contem 3 opções de easing:

  • easeIn: o efeito é mais intenso no ínicio da animação
  • easeOut: o efeito é mais intenso no final da animação
  • easeInOut: o efeito é mais intenso no ínicio e no final

Escolhida a transição e o método easing, você pode obte-lá desse modo:

// Fx.Transitions.[tipo de transição].[método easing]
Fx.Transitions.Sine.easeInOut

A dica pra escolher a combinação certa é testando, verifique cada transição nesse demo de transições.

Alterando Uma Única Propriedade CSS Com Fx.Style

Primeiro nos precisamos decidir o elemento que receberá o efeito, depois disso temos dois meios de criar o efeito. O primeiro, clássico, usando o construtor, que aceita três argumentos: o primeiro o elemento em si, o segundo a propriedade CSS que será modificada e o terceiro uma lista de opções:

var obj = $("box");
var fx = new Fx.Style(obj, "height", {
	"transition": Fx.Transitions.Sine.easeInOut, // Transição a ser usada na animação
	"duration": 1000, // Duração em milissegundos
	"unit": "px", // Unidade usada para alterar a propriedade
	"wait": true, // Aguardar ou não a animação anterior de mesma instância para começar
	"fps": 50 // Quadros por segundo da animação, quanto maior melhor e mais lento
});

O segundo meio, que eu prefiro, é um atalho no próprio objeto (instância de Element), o método effect, que recebe os argumentos 2 e 3 iguais do construtor acima:

var obj = $("box");
var fx = obj.effect("height", {
	// Mesmas opções
});

Terminada a criação do efeito, temos que iniciá-lo efetivamente, para isso temos o método start. Ele aceita 1 ou 2 argumentos, no primeiro caso o primeiro argumento é o valor final da propriedade, o inicial será o valor atual:

fx.start(350); // O objeto terá a largura igual a 350 pixels no final

Já no segundo caso, o primeiro argumento é o valor inicial e o segundo o final:

fx.start(10, 350); // O objeto terá 10 pixels no ínicio do efeito e 350 depois de terminado

Dica: Se você for alterar a mesma propriedade várias vezes não crie uma nova instância a cada vez, use a que já foi criada e chame o método start pra cada efeito.
Pronto, bem mais legal do que mudar propriedades sem efeito, né?!

Alterando Várias Propriedades CSS Com Fx.Styles

Quase a mesma coisa que a classe Fx.Style, porém algumas diferenças. Continuamos com 2 meios de se obter a instância, ou pelo construtor, que aceita 2 argumentos iguais ao anterior: o elemento e as opções, a exeção é o nome da propriedade que será modificada, já que são mais de uma propriedades elas serão definidas na hora de iniciar o efeito:

var obj = $("box");
var fx = new Fx.Styles(obj, {
	// Mesmas opções
});

Ou pelo método effects do objeto:

var obj = $("box");
var fx = obj.effects({
	// Mesmas opções
});

Agora vamos iniciar o efeito, a diferença principal para Fx.Style é que é aqui que serão definidos os valroes das propriedades atrávez de um objeto (JSON):

fx.start({
	"height": 350, // Efeito iniciará com altura atual e terminará com 350 pixels
	"width": [10, 350], // Efeito iniciará com largura 10 pixels e terminará com 350 pixels
	"background-color": ["#FF0000", "#0000FF"], // Cor de fundo vermelha no início e azul no final
	"color": "#FFFFFF" // Cor do texto atual no início e branca no final
});

Como se pode perceber, se você fornecer um Array como valor da propriedade, o primeiro elemento será o valor para ser usado no início do efeito e o segundo no final, ou se fornecer somente um valor, esse será o valor final e o inicial será o valor atual da propriedade.
A mesma dica anterior também vale aqui, porém como aqui se modificam várias propriedades, a regra é: se você tiver que aplicar vários efeitos a um elemento várias vezes, use a mesma instância e chame o método start.

Viu como não é difícil brincar com a Mootools, gostei muito do suporte a efeitos, animações, esse tipo de coisa, é muito transparente, quem já implementou (ou tentou implementar) algo assim sabe como é difícil fazer um trabalho bem feito e ao mesmo tempo simples de se usar. Esse é um ponto forte da Mootools.

Flws pessoal, até a próxima!