Простенькая реализация ролей на примере Ruby

в 23:07, , рубрики: ruby, ruby on rails, авторизация, метки: ,

Всем привет. Я долгое время программировал на 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";

Таким образом мы получаем таблицу с тремя полями:

  1. id — первичный ключ
  2. name — строка (название роли)
  3. 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js