Всем привет. Я долгое время программировал на PHP и использовал Zend 1. Работал над крупным проектом платежной системы. Система авторизации подразумевала пользователей, их авторизацию и разделение по ролям. Разделение по ролям было довольно обширным и ветвистым. Вообще в большинстве проектов если уж требуется авторизация, то, наверняка, потребуется хотя бы минимальное разделение по ролям.
Совсем недавно я начал проект на Ruby и подыскивал гем для авторизации. Но толком так и не нашел красивого и четкого гема, реализующего разделения по ролям, а может плохо искал. Теперь хочу рассказать о своем методе решения этой проблемы.
Задача.
Реализовать разделение по ролям с возможностью наследования малыми силами, не создавая кучу таблиц и лишних полей.
Реализация:
1. Для начала поговорим о БД.
Первым делом необходимо создать таблицу ролей. Раз уж нам необходимо использовать наследование ролей, то очевидно, что придется хранить древовидную структуру. Я использовал реляционную БД (Postgresql). В реляционных БД хранить деревья не очень удобно, но методов для такого хранения предостаточно. Я использовал «material path». Этот метод крайне прост и заключается в том, что в некотором поле (например path) мы храним путь данной строки таблицы относительно корня дерева из id элементов, которые являются предками для данного элемента. Например запись 0/12/23/97 означает, что текущий элемент c id 97 вложен в элемент 23, который вложен в 12. А 12 — является корневым. Ну да не буду подробно останавливаться на методах хранения древовидных структур.
Итак Ruby on Rails использует миграции для внесения изменений в БД, поэтому привожу пример миграции, которую я использовал для таблицы ролей:
class CreateRoles < ActiveRecord::Migration
def up
create_table :roles do |t|
t.string :name, :unique => true
t.string :path
end
execute <<SQL
ALTER TABLE roles ADD CONSTRAINT uniq_role_name UNIQUE(name);
INSERT INTO roles (name,path) VALUES ('registred',':registred');
INSERT INTO roles (name,path) VALUES ('admin',':registred:admin');
SQL
end
def down
drop_table :roles
end
end
Сразу оговорюсь, что я использую в миграция SQL код для того, чтобы на уровне БД организовывать Foreign key. Предполагаю, что многие сочтут это неправильным. Не могу привести никаких доводов ни за ни против этого метода, а потому прошу не заострять на этом внимание.
Также приведу SQL для создания таблицы (для тех, кто не на руби или не использует миграции):
CREATE TABLE "public"."roles" (
"id" int4 DEFAULT nextval('roles_id_seq'::regclass) NOT NULL,
"name" varchar(255),
"path" varchar(255),
CONSTRAINT "roles_pkey" PRIMARY KEY ("id"),
CONSTRAINT "uniq_role_name" UNIQUE ("name")
)
WITH (OIDS=FALSE)
;
ALTER TABLE "public"."roles" OWNER TO "nalogovaya.ru";
Таким образом мы получаем таблицу с тремя полями:
- id — первичный ключ
- name — строка (название роли)
- path — путь до роли во мнимом дереве ролей.
И тут же вставляю 2 роли.
Раз у нас присутствует авторизация — значит есть некая таблица users в которой хранится информация о пользователе. В ней необходимо создать поле-ключ для указания роли пользователя.
Привожу пример миграции:
class AddRoleToUser < ActiveRecord::Migration
def up
execute <<SQL
ALTER TABLE users ADD COLUMN role_id integer DEFAULT 1;
ALTER TABLE users ADD CONSTRAINT "role_relation" FOREIGN KEY ("role_id")
REFERENCES "public"."roles" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
SQL
end
def down
execute <<SQL
ALTER TABLE users DROP COLUMN role_id;
SQL
end
end
Итак! Мы имеем таблицу ролей в БД и указатель роли в таблице пользователей. Работа с БД на этом завершена.
2. Исходный код
Для начала создадим модель ролей. Приведу исходный код файла role.rb а затем поясню:
#!/bin/env ruby
# encoding: utf-8
class Role < ActiveRecord::Base
extend ActiveModel::Callbacks
has_many :users
ROLES = Role.all
def self.find_by_name(name)
ROLES.select{|item| item.name==name}.first
end
def >= (value)
value = checkRole(value)
!self.path.index(value.path).nil?
end
def <= (value)
value = checkRole(value)
!self.>=(value)
end
def == (value)
value = checkRole(value)
self.name==value.name
end
protected
def checkRole(value)
value.instance_of?(String) ? Role.find_by_name(value) : value
value
end
end
ROLES = Role.all — сомнительный ход для уменьшения количества запросов к БД. При старте рельс сразу подгружаются все роли, и к БД запросов больше не будет.
Метод checkRole — (назовите как вам больше понравится) служит исключительно для удобства, чтобы роли можно было сравнивать просто по названию, а не только по обьектам класса Role.
Остальные методы — просто функции сравнения ролей.
Теперь перейдем к пользователям. Для начала укажем связку:
belongs_to :role
Если вы используете Rails 4 и гем protected_attributes, то не забудьте еще добавить
attr_accessible :role
3. Использование.
Самая приятная часть. Если, конечно, первых 2 пункта — это то что Вам было нужно =)
Пример:
<% if current_user.role>='admin' #если роль пользователя "админ" или старше, что вряд ли, показываем ему что-то %>
<%= link_to 'Удалить всю входящую почту!', requests_path, :method => :delete %>
<% end %>
Спасибо за внимание, надеюсь кому-нибудь понадобится такая реализация ролей.
Автор: mao_z