# Rails 權限系統 - [source code](https://github.com/cellvinchung/demo_permission) ## ERD ![](https://i.imgur.com/pIdt8wd.png) ## gems - [Devise](https://github.com/heartcombo/devise) - [Pundit](https://github.com/varvet/pundit) ## models ### User ```ruby= class User < ApplicationRecord belongs_to :user_group has_many :enabled_permissions, through: :user_group # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable end ``` > 只管認證authentication,權限交由user_group處理 --- ### UserGroup ```ruby class UserGroup < ApplicationRecord has_many :users has_many :group_permissions, dependent: :destroy accepts_nested_attributes_for :group_permissions has_many :permission_resources, through: :group_permissions has_many :enabled_permissions, -> { enable }, class_name: 'GroupPermission' has_many :enabled_resources, through: :enabled_permissions end ``` > user_group設定權限,user指派user_group套用 --- ### GroupPermission ```ruby class GroupPermission < ApplicationRecord PRIORITY_OPTIONS = { disable: 0, readable: 1, writable: 2 } belongs_to :user_group belongs_to :permission_resource scope :enable, -> { where('group_permissions.priority > 0') } enum priority: PRIORITY_OPTIONS before_create :setup_priority private def setup_priority self.priority = permission_resource.default_priority if priority.nil? end end ``` > 群組與各資源間的關聯與權限設定 --- ### PermissionResource ```ruby class PermissionResource < ApplicationRecord has_many :group_permissions, dependent: :destroy enum default_priority: GroupPermission::PRIORITY_OPTIONS after_create :setup_group_sermissions private def setup_group_sermissions UserGroup.all.each do |user_group| user_group.permission_resources += [self] end end end ``` > 被管理的目標資源 ## Policies > by action設定權限 ### ApplicationPolicy ``` ruby # frozen_string_literal: true # frozen_string_literal: true class ApplicationPolicy attr_reader :user, :record def initialize(user, record) @user = user @record = record @record_model = @record.model_name.to_s end def index? readable? end def show? readable? end def create? writable? end def new? create? end def update? writable? end def edit? update? end def destroy? writable? end def super_admin? @super_admin ||= @user.user_group.super_admin? end def readable? super_admin? || permissions.where(permission_resources: { name: @record_model }).any? end def writable? super_admin? || permissions.writable.where(permission_resources: { name: @record_model }).any? end def permissions @permissions ||= @user.enabled_permissions.includes(:permission_resource) end class Scope def initialize(user, scope) @user = user @scope = scope end def resolve scope.all end private attr_reader :user, :scope end end ``` ## Controller ### ApplicationController ```ruby class ApplicationController < ActionController::Base include Pundit rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized flash[:alert] = "You are not authorized to perform this action." redirect_back(fallback_location: root_path) end end ``` ### AuthedController ```ruby class AuthedController < ApplicationController before_action :authenticate_user! before_action :setup_authorize private def setup_authorize # 預設model名為controller名的單數 authorize self.class.name.demodulize.to_s.gsub('Controller', '').singularize.constantize end end ``` > 需要納入權限的controller,這裡用authed ### UserGroupsController ```ruby class UserGroupsController < AuthedController helper_method :user_group def index @user_groups = UserGroup.all end def show @users = user_group.users end def new @user_group = UserGroup.new # 讓view產生畫面 PermissionResource.all.each do |permission_resource| @user_group.group_permissions.new(permission_resource: permission_resource, priority: permission_resource.default_priority) end end def create @user_group = UserGroup.new(permit_attribute) @user_group.save redirect_to url_for(action: :index) end def edit end def update user_group.update(permit_attribute) redirect_to url_for(action: :index) end def destroy user_group.destroy redirect_to url_for(action: :index) end private def user_group @user_group ||= UserGroup.includes(:permission_resources).find(params[:id]) end def permit_attribute params.require(:user_group).permit( :name, :super_admin, group_permissions_attributes: %i[id priority permission_resource_id] ) end end ``` ## View ### header ```htmlembedded= <nav class="navbar navbar-expand-lg navbar-light bg-light"> <div class="container-fluid"> <a class="navbar-brand" href="#">Navbar</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav me-auto mb-2 mb-lg-0"> <li class="nav-item"> <a class="nav-link active" aria-current="page" href="/">Home</a> </li> <% if user_signed_in? %> <li class="nav-item"> <%= link_to User.model_name.human, users_path, class: 'nav-link' if policy(User).index?%> </li> <li class="nav-item"> <%= link_to UserGroup.model_name.human, user_groups_path, class: 'nav-link' if policy(UserGroup).index?%> </li> <li class="nav-item"> <%= link_to PermissionResource.model_name.human, permission_resources_path, class: 'nav-link' if policy(PermissionResource).index?%> </li> <% else %> <li class="nav-item"> <%= link_to '登入', new_user_session_path, class: 'nav-link' %> </li> <% end %> </ul> </div> </div> </nav> ```