В системном программировании есть два понятия аутентификация (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