Как стать автором
Поиск
Написать публикацию
Обновить

Авторизация в Ruby on Rails

В системном программировании есть два понятия аутентификация (authentication) и авторизация (authorization). Аутентификация (authentication) — это процесс определения кем именно является пользователь системы. Самый простой и часто используемый способ — это использование логина и пароля. Существуют также распределённые системы: OpenId, Google API, Facebook API и т. п.



Всё это выглядит примерно следующим образом:



  • пользователь инициирует аутентификацию (authentication)
  • отдаёт необходимые данные, имя, пароль, отпечатки пальцев и т. п.
  • система в результате достаёт некие данные о пользователе: пол, адрес почты и т. п.
  • связь между пользователем и теми данными которые достала система где-то сохраняется, обычно используется мехонизм сессий (если не сохранять эту связь, то пользователю потребуется постоянно вводить логин и пароль).


Для реализации аутентификации в Ruby on Rails существует несколько плагинов: restful_authentication, authlogic. Я бы посоветовал authlogic. В последнем проекте я использую самописный механизм аутентификации, так как у меня legacy база данных и механизм аутентификации очень простой.



Я хочу написать об авторизации. Авторизация — это процесс проверки прав пользователя при выполнении определённых действий. При реализации авторизации возникают следующие вопросы:



  • как хранить права пользователя и какая у них структура.
  • как и на какой именно стадии проверять права пользователя.
  • как реагировать на недостаток прав пользователя.


Я перерыл множество плагинов рельс и понял, что не хочу использовать ничего из имеющегося, так как плагины слишком сложны и непрактичны для меня.



Мои ответы на эти вопросы следующие:




  • права пользователя — это просто поле role в табличке users с одной из констант.
  • права пользователя проверяются в before_filter'ах контроллеров
  • в случае недостатка прав пользователя кидается исключение


Начнём по порядку.



Для хранения прав пользователей заводится атрибут role:



class User < ActiveRecord::Base

  ...

  module Role
    ADMIN = ...
    MODERATOR = ...
    USER = ...
  end

  ...

  validates_presence_of :role
  validates_inclusion_of :role, :in => [Role::ADMIN, Role::MODERATOR, Role::USER]

  ...

  def admin?
    role == Role::ADMIN
  end

  def moderator?
    role == Role::MODERATOR
  end

  def user?
    role == Role::USER
  end
end



Права пользователей проверяются в контроллерах, при этом права пользователя зависят не только от самого пользователя, но и от конкретной ситуации. Например у пользователя есть группы и телефоны. В routes.rb это выглядит вот так:



map.resources :users do |user|
  user.resources :groups
  user.resources :phones
end



Но пользователь может просматривать только свои группы и телефоны. Это достигается следующим образом. GroupsController и PhonesController оба наследуются от UserResourceController.



class GroupsController < UserResourceController
  ...
end



class PhonesController < UserResourceController
  ...
end



А UserResourceController выглядит следующим образом



class UserResourceController < ApplicationController
  before_filter :init_user
  deny_all
  allow_any :admin?, :owner?
  
  private
  
  def init_user
    @user = User.find(params[:user_id])
  end

  def owner?
    current_user == @user
  end
end



Для авторизации здесь написаны две строчки, первая



deny_all



запрещает доступ к этому контроллеру для всех кто бы то ни был. Вторая



allow_any :admin?, :owner?



разрешает доступ либо админу (admin), либо хозяину (owner). Правила применяются в порядке следования с верху вниз. Возникает вопрос, как система авторизации поймёт admin это или owner. Передоваемые методу allow_any значения — это просто имена методов контроллера. Чтобы понять не является ли текущий пользователь хозяином ресурса вызывается метод owner? класса UserResourceController. В этом методе и происходит проверка. Как видно умение пользователя быть хозяином нигде не хранится, не соответствует никакой роли, это просто метод контроллера. Таким же образом метод admin? проверяет является ли пользователь администратором. Этот метод объявлен в родительском классе ApplicationController.



Можно обойтись без дополнительного метода, если передовать методу allow_any не символ, а Proc.



allow_any :owner?, lambda {|controller| controller.current_user.admin?}



Здесь уже используется роль, которая хранится в базе данных.



В случае если в системе есть несколько администраторов, но у них должен быть доступ только к своим ресурсам, а не к ресурсам друг друга, то для проверки прав можно использовать следующую инструкцию.



allow_all :admin?, :owner?



Для того чтобы пользователь получил доступ к такому ресурсу необходимо чтобы он был и администратором и хозяимном ресурса.



Так же в инструкциях allow и deny можно использовать опции :only и :except, которые работают абсолютно так же как в инструкции before_filter:



  deny :owner?, :only => [:published, :accountable, :account_all]
  allow :moderator?, :only => [:show, :index, :accept, :decline, :published]



Как же обрабатывать недостаток прав пользователя. Для этого просто пишется обработчик исключения в ApplicationController'е



class ApplicationController < ActionController::Base
  ...
  rescue_from AuthorizationSystem::AccessDenied, :with => :render_forbidden
  ...
  private
  ...
  def render_forbidden(exception)
    log_error(exception)
    respond_to do |type|
      type.html { render :template => 'errors/forbidden', :layout => 'application', :status => :forbidden }
      type.all  { render :nothing => true, :status => :forbidden }
    end
  end
end



В результате при обращении пользователя к ресурсу на который у него нет прав ему показывается страница с соответствующим текстом, а Веб-сервер возвращает код 403.



Для того, чтобы пользоваться данной системой нужно просто включить в ApplicationController соответствующий модуль. ApplicationController приобретёт следующий вид:



class ApplicationController < ActionController::Base

  ...

  include AuthorizationSystem
  rescue_from AuthorizationSystem::AccessDenied, :with => :render_forbidden
  deny_all # Доступ по умолчанию

  ...

  private

  ...

  def render_forbidden(exception)
    log_error(exception)
    respond_to do |type|
      type.html { render :template => 'errors/forbidden', :layout => 'mmsportal_legacy', :status => :forbidden }
      type.all  { render :nothing => true, :status => :forbidden }
    end
  end

  ...

end



Для того чтобы всё это работало я положил в каталог lib проекта на Ruby on Rails файл authorization_system.rb содержание которого приводится ниже. Указанная система авторизации полностью независима от аутентификации, вы можете использовать любой плагин, какой только захотите. Кроме того структура прав пользователя тоже может быть произвольной. Всё что необходимо сделать — просто написать необходимые функции-тесты в контроллерах.



А вот и сам модуль



module AuthorizationSystem
  class AccessDenied < StandardError
  end

  def self.included(base)
    base.send :include, InstanceMethods
    base.send :extend, ClassMethods
  end

  module InstanceMethods
    def access_denied
      raise AccessDenied
    end

    private

    def check_access
      @access || access_denied
    end
  end

  module ClassMethods
    # Чтобы получить доступ пользователь должен обладать всеми указанными правами
    def allow_all(*roles)
      process_roles(roles, true, :&.to_proc, method(:allow_combiner))
    end

    # Пользователь не получит доступ только если обладает всеми указанными правами
    def deny_all(*roles)
      process_roles(roles, true, :&.to_proc, method(:deny_combiner))
    end

    # Чтобы получить доступ пользователь должен обладать любым из указанных прав
    def allow_any(*roles)
      process_roles(roles, false, :|.to_proc, method(:allow_combiner))
    end

    # Пользователь не получит доступ если обладает хотябы одним из указанных прав
    def deny_any(*roles)
      process_roles(roles, false, :|.to_proc, method(:deny_combiner))
    end

    # Пользователь с данным правом получит доступ
    def allow(role, opts = {})
      allow_all(role, opts)
    end

    # Пользователь с данным правом не получит доступ
    def deny(role, opts = {})
      deny_all(role, opts)
    end

    # Только пользователь с данным правом будет получать доступ
    def allow_only(role, opts = {})
      deny_all(opts)
      allow(role, opts)
    end

    # Только пользователь с данным правом не будет получать доступ
    def deny_only(role, opts = {})
      allow_all(opts)
      deny(role, opts)
    end

    private

    def allow_combiner(old_access, match)
      old_access || match
    end

    def deny_combiner(old_access, match)
      old_access && !match
    end

    def process_roles(roles, match_initial_value, match_combiner, access_combiner)
      opts = roles.extract_options!
      modify_access opts do |controller|
        controller.instance_eval do
          match = match_initial_value
          roles.each do |role|
            case role
            when Symbol then
              match = match_combiner.call(match, send(role))
            when Proc then
              match = match_combiner.call(match, role.call(controller))
            else
              match = match_combiner.call(match, role)
            end
          end
          @access = access_combiner.call(@access, match)
        end
      end
    end

    def modify_access(opts = {}, &block)
      skip_before_filter :check_access
      before_filter(opts, &block)
      before_filter :check_access
    end
  end

end

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.