業務システムなど、ユーザー管理を管理者のみ行えるようにしたい場面が度々あると思います。
私の場合はRailsで業務システムを開発しており、上記機能を作成する必要がありました。
Railsにおいて認証システムを開発する際は、Deviseというgemを使用するのが一般的かと思いますので、Deviseを改造して上記のシステム構築を行いました。
なお、今回は以下のgemを導入している想定で話を進めます。
gem 'devise'
gem 'cancancan'
gem 'rolify'
これらのgemの導入方法については、他にもたくさんの素晴らしい記事が公開されています。
今回の主題ではありませんのでスキップします。
また、認証で扱うモデル名はUser
とします。
手を入れるソース
Deviseのユーザー登録/編集処理はregistrations_controller.rbで定義されています。
このControllerをオーバーライドしてカスタマイズしていきます。
まずは、オーバーライドするためのファイルを作成します。
$ rails g devise:controllers users
Running via Spring preloader in process 94640
create app/controllers/users/confirmations_controller.rb
create app/controllers/users/passwords_controller.rb
create app/controllers/users/registrations_controller.rb
create app/controllers/users/sessions_controller.rb
create app/controllers/users/unlocks_controller.rb
create app/controllers/users/omniauth_callbacks_controller.rb
今回編集するのはapp/controllers/users/registrations_controller.rb
です。
中身を見てみると、以下のようになっています。
# frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController
# before_action :configure_sign_up_params, only: [:create]
# before_action :configure_account_update_params, only: [:update]
# GET /resource/sign_up
# def new
# super
# end
# POST /resource
# def create
# super
# end
# GET /resource/edit
# def edit
# super
# end
# PUT /resource
# def update
# super
# end
# DELETE /resource
# def destroy
# super
# end
# GET /resource/cancel
# Forces the session data which is usually expired after sign
# in to be expired now. This is useful if the user wants to
# cancel oauth signing in/up in the middle of the process,
# removing all OAuth session data.
# def cancel
# super
# end
# protected
# If you have extra params to permit, append them to the sanitizer.
# def configure_sign_up_params
# devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
# end
# If you have extra params to permit, append them to the sanitizer.
# def configure_account_update_params
# devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
# end
# The path used after sign up.
# def after_sign_up_path_for(resource)
# super(resource)
# end
# The path used after sign up for inactive accounts.
# def after_inactive_sign_up_path_for(resource)
# super(resource)
# end
end
登録
まずは、サインイン済みでもユーザー登録ができるようにします。
Devise::RegistrationsController
では、以下が定義されているため、ユーザーがサインインしている状態で:new
や:create
を行うことができません。
prepend_before_action :require_no_authentication, only: [:new, :create, :cancel]
そこで、オーバーライドして、サインインしていも:new
と:create
にアクセスできるようにしてやります。
--- a/app/controllers/users/registrations_controller.rb
+++ b/app/controllers/users/registrations_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController
+ prepend_before_action :require_no_authentication, only: [:cancel]
# before_action :configure_sign_up_params, only: [:create]
# before_action :configure_account_update_params, only: [:update]
次に、ユーザー登録後の自動サインインを回避します。
Devise::ReigistrationsControllerではsign_up
という関数の中でsign_in
という関数を呼んでいるだけなので、sign_up
をオーバーライドします。
--- a/app/controllers/users/registrations_controller.rb
+++ b/app/controllers/users/registrations_controller.rb
@@ -38,7 +38,7 @@ class Users::RegistrationsController < Devise::RegistrationsController
- # protected
+ protected
@@ -72,6 +72,10 @@ class Users::RegistrationsController < Devise::RegistrationsController
devise_parameter_sanitizer.permit(:account_update, keys: [:name])
end
+ def current_user_is_admin?
+ user_signed_in? && current_user.has_role?(:admin)
+ end
+
# The path used after sign up.
# def after_sign_up_path_for(resource)
# super(resource)
@@ -83,6 +87,8 @@ class Users::RegistrationsController < Devise::RegistrationsController
# end
def sign_up(resource_name, resource)
- sign_in(resource_name, resource)
+ if !current_user_is_admin?
+ sign_in(resource_name, resource)
+ end
end
end
このままだとどのユーザーでも登録できるようになっているので、権限チェックを行います。
--- a/app/controllers/users/registrations_controller.rb
+++ b/app/controllers/users/registrations_controller.rb
@@ -3,6 +3,6 @@ class Users::RegistrationsController < Devise::RegistrationsController
prepend_before_action :set_minimum_password_length, only: [:new, :edit]
+ before_action :creatable?, only: [:new, :create]
# GET /resource/sign_up
# def new
@@ -129,7 +130,14 @@ class Users::RegistrationsController < Devise::RegistrationsController
resource.update_without_password(params)
end
+ def creatable?
+ raise CanCan::AccessDenied unless user_signed_in?
+
+ if !current_user_is_admin?
+ raise CanCan::AccessDenied
+ end
+ end
+
編集
編集の場合は登録より複雑です。
まず、routeを定義するところからです。
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -157,7 +157,10 @@ Rails.application.routes.draw do
devise_for :users, :controllers => {
:registrations => 'users/registrations'
}
-
+ devise_scope :user do
+ get 'users/:id/edit' => 'users/registrations#edit', as: :edit_other_user_registration
+ match 'users/:id', to: 'users/registrations#update', via: [:patch, :put], as: :other_user_registration
+ end
resources :users, only: [:show, :index]
resources :csv_import, only: :index
続いて、edit
メソッドを変更します。
Devise::ReigistrationsControllerでは以下の定義がされています。
prepend_before_action :authenticate_scope!, only: [:edit, :destroy]
:authenticate_scope!はself.resource
にcurrent_userをぶち込んでしまいますので、:edit
と:update
は対象外にしておきます。
また、update関数をDevise::ReigistrationsControllerからコピーした後、adminユーザーの場合は現在のパスワードを入力せずとも編集できるようにコードを変更します。
--- a/app/controllers/users/registrations_controller.rb
+++ b/app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
prepend_before_action :require_no_authentication, only: [:cancel]
+ prepend_before_action :authenticate_scope!, only: [:update, :destroy]
@@ -19,17 +19,30 @@ class Users::RegistrationsController < Devise::RegistrationsController
# GET /resource/edit
def edit
- render :edit
+ if by_admin_user?(params)
+ self.resource = resource_class.to_adapter.get!(params[:id])
+ else
+ authenticate_scope!
+ super
+ end
end
# PUT /resource
- # We need to use a copy of the resource because we don't want to change
- # the current user in place.
def update
- self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
+ if by_admin_user?(params)
+ self.resource = resource_class.to_adapter.get!(params[:id])
+ else
+ self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
+ end
+
prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email)
- resource_updated = update_resource(resource, account_update_params)
+ if by_admin_user?(params)
+ resource_updated = update_resource_without_password(resource, account_update_params)
+ else
+ resource_updated = update_resource(resource, account_update_params)
+ end
+
yield resource if block_given?
if resource_updated
if is_flashing_format?
@@ -72,6 +85,10 @@ class Users::RegistrationsController < Devise::RegistrationsController
devise_parameter_sanitizer.permit(:account_update, keys: [:name])
end
+ def by_admin_user?(params)
+ params[:id].present? && current_user_is_admin?
+ end
+
def current_user_is_admin?
defined?(current_user) && current_user.present? && current_user.has_role?(:admin)
end
@@ -91,4 +108,8 @@ class Users::RegistrationsController < Devise::RegistrationsController
sign_in(resource_name, resource)
end
end
+
+ def update_resource_without_password(resource, params)
+ resource.update_without_password(params)
+ end
end
登録時と同じように権限チェックを行います。
--- a/app/controllers/users/registrations_controller.rb
+++ b/app/controllers/users/registrations_controller.rb
@@ -3,6 +3,6 @@ class Users::RegistrationsController < Devise::RegistrationsController
prepend_before_action :set_minimum_password_length, only: [:new, :edit]
+ before_action :editable?, only: [:edit, :update]
@@ -129,7 +130,14 @@ class Users::RegistrationsController < Devise::RegistrationsController
resource.update_without_password(params)
end
- def writable_check
+ def editable?
+ raise CanCan::AccessDenied unless user_signed_in?
+
+ if !current_user_is_admin?
+ raise CanCan::AccessDenied
+ end
+ end
+
管理者が他のユーザーのパスワードを変更できるようにします。
registrations_controller.rb
で利用するようにしたresource.update_without_password
はデフォルトではパスワードの更新ができないようになっています。
def update_without_password(params, *options)
params.delete(:password)
params.delete(:password_confirmation)
result = update_attributes(params, *options)
clean_up_passwords
result
end
これをUserモデルでオーバーライドします。
update_with_password
関数を参考に実装しますが、パスワードチェックは行わないようにします。
ただし、この関数が管理者以外から呼び出されることがあってはいけません。
今回のように呼び出し元で管理者権限を持っているかチェックするようにしましょう。
--- a/app/models/user.rb
+++ b/app/models/user.rb
+ # deviseのupdate_without_passwordは本来パスワードは変更できない
+ # 管理者しかこの関数を呼ばない前提で、パスワードがなくてもパスワードを変更できるようにオーバーラ
イドする
+ # update_with_password関数を参考にする
+ def update_without_password(params, *options)
+ # current_password = params.delete(:current_password)
+
+ if params[:password].blank?
+ params.delete(:password)
+ params.delete(:password_confirmation) if params[:password_confirmation].blank?
+ end
+
+ # result = if valid_password?(current_password)
+ # update_attributes(params, *options)
+ # else
+ # self.assign_attributes(params, *options)
+ # self.valid?
+ # self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
+ # false
+ # end
+
+ update_attributes(params, *options)
+
+ clean_up_passwords
+ result
+ end
成果物
Viewについてなど、いろいろと説明を省いてしまいましたが、Gistに最終的なソースをアップロードしました。
是非御覧ください。