Pundit menyediakan seperangkat bantuan yang memandu Anda dalam memanfaatkan kelas Ruby reguler dan pola desain berorientasi objek untuk membangun sistem otorisasi yang lugas, kuat, dan terukur.
Disponsori oleh: Varvet
Harap dicatat bahwa README di GitHub akurat dengan kode terbaru di GitHub . Kemungkinan besar Anda menggunakan Pundit versi rilis, jadi silakan lihat dokumentasi Pundit versi rilis terbaru.
bundle add pundit
Sertakan Pundit::Authorization
di pengontrol aplikasi Anda:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
end
Secara opsional, Anda dapat menjalankan generator, yang akan menyiapkan kebijakan aplikasi dengan beberapa default yang berguna untuk Anda:
rails g pundit:install
Setelah membuat kebijakan aplikasi Anda, restart server Rails sehingga Rails dapat mengambil kelas apa pun di direktori app/policies/
yang baru.
Pakar fokus pada gagasan kelas kebijakan. Kami menyarankan Anda menempatkan kelas-kelas ini di app/policies
. Ini adalah contoh yang memungkinkan pembaruan postingan jika pengguna adalah admin, atau jika postingan tidak dipublikasikan:
class PostPolicy
attr_reader :user , :post
def initialize ( user , post )
@user = user
@post = post
end
def update?
user . admin? || ! post . published?
end
end
Seperti yang Anda lihat, ini adalah kelas Ruby biasa. Pundit membuat asumsi berikut tentang kelas ini:
current_user
untuk mengambil apa yang akan dikirim ke argumen iniupdate?
. Biasanya, ini akan dipetakan ke nama tindakan pengontrol tertentu.Itu benar.
Biasanya Anda ingin mewarisi kebijakan aplikasi yang dibuat oleh generator, atau menyiapkan kelas dasar Anda sendiri untuk mewarisi:
class PostPolicy < ApplicationPolicy
def update?
user . admin? or not record . published?
end
end
Dalam ApplicationPolicy
yang dihasilkan, objek model disebut record
.
Misalkan Anda memiliki instance kelas Post
, Pundit sekarang memungkinkan Anda melakukan ini di pengontrol Anda:
def update
@post = Post . find ( params [ :id ] )
authorize @post
if @post . update ( post_params )
redirect_to @post
else
render :edit
end
end
Metode otorisasi secara otomatis menyimpulkan bahwa Post
akan memiliki kelas PostPolicy
yang cocok, dan membuat instance kelas ini, menyerahkan pengguna saat ini dan catatan yang diberikan. Kemudian disimpulkan dari nama tindakan, bahwa ia harus memanggil update?
mengenai contoh kebijakan ini. Dalam hal ini, Anda dapat membayangkan bahwa authorize
akan melakukan hal seperti ini:
unless PostPolicy . new ( current_user , @post ) . update?
raise Pundit :: NotAuthorizedError , "not allowed to PostPolicy#update? this Post"
end
Anda dapat memberikan argumen kedua untuk authorize
jika nama izin yang ingin Anda periksa tidak cocok dengan nama tindakan. Misalnya:
def publish
@post = Post . find ( params [ :id ] )
authorize @post , :update?
@post . publish!
redirect_to @post
end
Anda dapat menyampaikan argumen untuk mengganti kelas kebijakan jika perlu. Misalnya:
def create
@publication = find_publication # assume this method returns any model that behaves like a publication
# @publication.class => Post
authorize @publication , policy_class : PublicationPolicy
@publication . publish!
redirect_to @publication
end
Jika Anda tidak memiliki instance untuk argumen pertama untuk authorize
, maka Anda dapat meneruskan kelas tersebut. Misalnya:
Kebijakan:
class PostPolicy < ApplicationPolicy
def admin_list?
user . admin?
end
end
Pengendali:
def admin_list
authorize Post # we don't have a particular post to authorize
# Rest of controller action
end
authorize
mengembalikan instance yang diteruskan ke sana, sehingga Anda dapat merangkainya seperti ini:
Pengendali:
def show
@user = authorize User . find ( params [ :id ] )
end
# return the record even for namespaced policies
def show
@user = authorize [ :admin , User . find ( params [ :id ] ) ]
end
Anda dapat dengan mudah mendapatkan contoh kebijakan melalui metode policy
di tampilan dan pengontrol. Hal ini sangat berguna untuk menampilkan tautan atau tombol secara kondisional dalam tampilan:
<% if policy(@post).update? %>
<%= link_to "Edit post", edit_post_path(@post) %>
<% end %>
Mengingat ada kebijakan tanpa model/kelas Ruby yang sesuai, Anda dapat mengambilnya dengan meneruskan simbol.
# app/policies/dashboard_policy.rb
class DashboardPolicy
attr_reader :user
# `_record` in this example will be :dashboard
def initialize ( user , _record )
@user = user
end
def show?
user . admin?
end
end
Perhatikan bahwa kebijakan tanpa kepala masih perlu menerima dua argumen. Argumen kedua dalam hal ini adalah simbol :dashboard
, yang diteruskan sebagai catatan untuk authorize
di bawah.
# In controllers
def show
authorize :dashboard , :show?
...
end
# In views
<% if policy(:dashboard).show? %>
<%= link_to 'Dashboard', dashboard_path %>
<% end %>
Seringkali, Anda ingin memiliki semacam catatan daftar tampilan yang dapat diakses oleh pengguna tertentu. Saat menggunakan Pundit, Anda diharapkan mendefinisikan kelas yang disebut cakupan kebijakan. Ini bisa terlihat seperti ini:
class PostPolicy < ApplicationPolicy
class Scope
def initialize ( user , scope )
@user = user
@scope = scope
end
def resolve
if user . admin?
scope . all
else
scope . where ( published : true )
end
end
private
attr_reader :user , :scope
end
def update?
user . admin? or not record . published?
end
end
Pundit membuat asumsi berikut tentang kelas ini:
Scope
dan ditempatkan di bawah kelas kebijakan.current_user
untuk mengambil apa yang akan dikirim ke argumen ini.ActiveRecord::Relation
, tetapi bisa juga berupa sesuatu yang lain sama sekali.resolve
, yang seharusnya mengembalikan beberapa jenis hasil yang dapat diulangi. Untuk kelas ActiveRecord, ini biasanya berupa ActiveRecord::Relation
.Anda mungkin ingin mewarisi cakupan kebijakan aplikasi yang dihasilkan oleh generator, atau membuat kelas dasar Anda sendiri untuk diwarisi:
class PostPolicy < ApplicationPolicy
class Scope < ApplicationPolicy :: Scope
def resolve
if user . admin?
scope . all
else
scope . where ( published : true )
end
end
end
def update?
user . admin? or not record . published?
end
end
Anda sekarang dapat menggunakan kelas ini dari pengontrol Anda melalui metode policy_scope
:
def index
@posts = policy_scope ( Post )
end
def show
@post = policy_scope ( Post ) . find ( params [ :id ] )
end
Seperti halnya metode otorisasi, Anda juga dapat mengganti kelas cakupan kebijakan:
def index
# publication_class => Post
@publications = policy_scope ( publication_class , policy_scope_class : PublicationPolicy :: Scope )
end
Dalam hal ini ini adalah jalan pintas untuk melakukan:
def index
@publications = PublicationPolicy :: Scope . new ( current_user , Post ) . resolve
end
Anda dapat, dan dianjurkan untuk, menggunakan metode ini dalam tampilan:
<% policy_scope(@user.posts).each do |post| %>
< p > <%= link_to post . title , post_path ( post ) %> </ p >
<% end %>
Saat Anda mengembangkan aplikasi dengan Pundit, Anda mungkin sering lupa untuk mengotorisasi beberapa tindakan. Lagipula, orang-orang memang pelupa. Karena Pundit menganjurkan Anda untuk menambahkan panggilan authorize
secara manual ke setiap tindakan pengontrol, sangat mudah untuk melewatkan satu tindakan.
Untungnya, Pundit memiliki fitur praktis yang mengingatkan Anda jika Anda lupa. Pundit melacak apakah Anda telah memanggil authorize
di mana pun dalam tindakan pengontrol Anda. Pundit juga menambahkan metode ke pengontrol Anda yang disebut verify_authorized
. Metode ini akan memunculkan pengecualian jika authorize
belum dipanggil. Anda harus menjalankan metode ini di hook after_action
untuk memastikan bahwa Anda tidak lupa untuk mengotorisasi tindakan tersebut. Misalnya:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
after_action :verify_authorized
end
Demikian pula, Pundit juga menambahkan verify_policy_scoped
ke pengontrol Anda. Ini akan memunculkan pengecualian yang mirip dengan verify_authorized
. Namun, ia melacak apakah policy_scope
digunakan dan bukan authorize
. Ini sebagian besar berguna untuk tindakan pengontrol seperti index
yang menemukan koleksi dengan cakupan dan tidak mengotorisasi masing-masing instance.
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
after_action :verify_pundit_authorization
def verify_pundit_authorization
if action_name == "index"
verify_policy_scoped
else
verify_authorized
end
end
end
Mekanisme verifikasi ini hanya ada untuk membantu Anda mengembangkan aplikasi, jadi jangan lupa menelepon authorize
. Ini bukan semacam mekanisme failsafe atau mekanisme otorisasi. Anda seharusnya dapat menghapus filter ini tanpa mempengaruhi cara kerja aplikasi Anda dengan cara apa pun.
Beberapa orang menganggap fitur ini membingungkan, sementara banyak orang lain menganggapnya sangat membantu. Jika Anda termasuk dalam kategori orang yang merasa bingung maka Anda tidak perlu menggunakannya. Pundit akan berfungsi dengan baik tanpa menggunakan verify_authorized
dan verify_policy_scoped
.
Jika Anda menggunakan verify_authorized
di pengontrol tetapi perlu melewati verifikasi secara kondisional, Anda dapat menggunakan skip_authorization
. Untuk melewati verify_policy_scoped
, gunakan skip_policy_scope
. Ini berguna dalam situasi di mana Anda tidak ingin menonaktifkan verifikasi untuk seluruh tindakan, namun ada beberapa kasus di mana Anda bermaksud untuk tidak mengizinkannya.
def show
record = Record . find_by ( attribute : "value" )
if record . present?
authorize record
else
skip_authorization
end
end
Terkadang Anda mungkin ingin mendeklarasikan secara eksplisit kebijakan mana yang akan digunakan untuk kelas tertentu, alih-alih membiarkan Pundit menyimpulkannya. Ini dapat dilakukan seperti ini:
class Post
def self . policy_class
PostablePolicy
end
end
Alternatifnya, Anda dapat mendeklarasikan metode instan:
class Post
def policy_class
PostablePolicy
end
end
Pundit sengaja dibuat menjadi perpustakaan yang sangat kecil, dan ia tidak melakukan apa pun yang tidak dapat Anda lakukan sendiri. Tidak ada saus rahasia di sini. Ia bertindak sesedikit mungkin, dan kemudian menghalangi Anda.
Dengan sedikit bantuan namun kuat yang tersedia di Pundit, Anda memiliki kekuatan untuk membangun sistem otorisasi yang terstruktur dengan baik dan berfungsi penuh tanpa menggunakan DSL khusus atau sintaksis yang funky.
Ingatlah bahwa semua kelas kebijakan dan ruang lingkup adalah kelas Ruby biasa, yang berarti Anda dapat menggunakan mekanisme yang sama yang selalu Anda gunakan untuk MENGERINGKAN segalanya. Rangkum serangkaian izin ke dalam modul dan sertakan dalam beberapa kebijakan. Gunakan alias_method
untuk membuat beberapa izin berperilaku sama dengan izin lainnya. Mewarisi dari kumpulan izin dasar. Gunakan metaprogramming jika Anda benar-benar harus melakukannya.
Gunakan generator yang disediakan untuk menghasilkan kebijakan:
rails g pundit:policy post
Di banyak aplikasi, hanya pengguna yang login yang benar-benar dapat melakukan apa pun. Jika Anda membangun sistem seperti itu, mungkin agak rumit untuk memeriksa apakah pengguna dalam kebijakan tidak nil
untuk setiap izin. Selain kebijakan, Anda dapat menambahkan pemeriksaan ini ke kelas dasar untuk cakupan.
Kami menyarankan Anda menentukan filter yang mengarahkan pengguna yang tidak diautentikasi ke halaman login. Sebagai pertahanan sekunder, jika Anda telah mendefinisikan ApplicationPolicy, mungkin ada baiknya untuk mengajukan pengecualian jika pengguna yang tidak diautentikasi berhasil lolos. Dengan cara ini Anda bisa gagal dengan lebih anggun.
class ApplicationPolicy
def initialize ( user , record )
raise Pundit :: NotAuthorizedError , "must be logged in" unless user
@user = user
@record = record
end
class Scope
attr_reader :user , :scope
def initialize ( user , scope )
raise Pundit :: NotAuthorizedError , "must be logged in" unless user
@user = user
@scope = scope
end
end
end
Untuk mendukung pola objek nol, Anda mungkin ingin menerapkan NilClassPolicy
. Ini mungkin berguna jika Anda ingin memperluas ApplicationPolicy Anda untuk memungkinkan beberapa toleransi, misalnya, asosiasi yang mungkin nil
.
class NilClassPolicy < ApplicationPolicy
class Scope < ApplicationPolicy :: Scope
def resolve
raise Pundit :: NotDefinedError , "Cannot scope NilClass"
end
end
def show?
false # Nobody can see nothing
end
end
Pundit memunculkan Pundit::NotAuthorizedError
yang dapat Anda save_from di ApplicationController
Anda. Anda dapat menyesuaikan metode user_not_authorized
di setiap pengontrol.
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
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_or_to ( root_path )
end
end
Alternatifnya, Anda dapat menangani Pundit::NotAuthorizedError secara global dengan meminta Rails menanganinya sebagai kesalahan 403 dan menyajikan halaman kesalahan 403. Tambahkan yang berikut ini ke application.rb:
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
NotAuthorizedError
memberikan informasi tentang kueri apa (misalnya :create?
), catatan apa (misalnya sebuah instance dari Post
), dan kebijakan apa (misalnya sebuah instance dari PostPolicy
) yang menyebabkan kesalahan tersebut dimunculkan.
Salah satu cara untuk menggunakan properti query
, record
, dan policy
ini adalah menghubungkannya dengan I18n
untuk menghasilkan pesan kesalahan. Inilah cara Anda melakukan hal itu.
class ApplicationController < ActionController :: Base
rescue_from Pundit :: NotAuthorizedError , with : :user_not_authorized
private
def user_not_authorized ( exception )
policy_name = exception . policy . class . to_s . underscore
flash [ :error ] = t " #{ policy_name } . #{ exception . query } " , scope : "pundit" , default : :default
redirect_back_or_to ( root_path )
end
end
en :
pundit :
default : ' You cannot perform this action. '
post_policy :
update? : ' You cannot edit this post! '
create? : ' You cannot create posts! '
Ini adalah sebuah contoh. Pundit tidak peduli dengan cara Anda menerapkan pesan kesalahan Anda.
Terkadang Anda ingin mengambil kebijakan untuk rekaman di luar pengontrol atau tampilan. Misalnya ketika Anda mendelegasikan izin dari satu kebijakan ke kebijakan lainnya.
Anda dapat dengan mudah mengambil kebijakan dan cakupan seperti ini:
Pundit . policy! ( user , post )
Pundit . policy ( user , post )
Pundit . policy_scope! ( user , Post )
Pundit . policy_scope ( user , Post )
Metode bang akan menimbulkan pengecualian jika kebijakan tidak ada, sedangkan metode tanpa bang akan menghasilkan nihil.
Terkadang, pengontrol Anda mungkin tidak dapat mengakses current_user
, atau metode yang harus dipanggil oleh Pundit mungkin bukan current_user
. Untuk mengatasinya, Anda dapat menentukan metode di pengontrol Anda yang bernama pundit_user
.
def pundit_user
User . find_by_other_means
end
Dalam beberapa kasus, mungkin berguna jika memiliki beberapa kebijakan yang melayani konteks berbeda untuk suatu sumber daya. Contoh utama dari hal ini adalah kasus ketika kebijakan Pengguna berbeda dengan kebijakan Admin. Untuk mengotorisasi dengan kebijakan namespace, teruskan namespace tersebut ke helper authorize
dalam array:
authorize ( post ) # => will look for a PostPolicy
authorize ( [ :admin , post ] ) # => will look for an Admin::PostPolicy
authorize ( [ :foo , :bar , post ] ) # => will look for a Foo::Bar::PostPolicy
policy_scope ( Post ) # => will look for a PostPolicy::Scope
policy_scope ( [ :admin , Post ] ) # => will look for an Admin::PostPolicy::Scope
policy_scope ( [ :foo , :bar , Post ] ) # => will look for a Foo::Bar::PostPolicy::Scope
Jika Anda menggunakan kebijakan dengan namespace untuk sesuatu seperti tampilan Admin, akan berguna untuk mengganti policy_scope
dan authorize
pembantu di AdminController
Anda untuk menerapkan namespace secara otomatis:
class AdminController < ApplicationController
def policy_scope ( scope )
super ( [ :admin , scope ] )
end
def authorize ( record , query = nil )
super ( [ :admin , record ] , query )
end
end
class Admin :: PostController < AdminController
def index
policy_scope ( Post )
end
def show
post = authorize Post . find ( params [ :id ] )
end
end
Pundit sangat menganjurkan Anda untuk memodelkan aplikasi Anda sedemikian rupa sehingga satu-satunya konteks yang Anda perlukan untuk otorisasi adalah objek pengguna dan model domain yang ingin Anda periksa otorisasinya. Jika Anda memerlukan konteks lebih dari itu, pertimbangkan apakah Anda mengotorisasi model domain yang tepat, mungkin model domain lain (atau gabungan beberapa model domain) dapat memberikan konteks yang Anda perlukan.
Pundit tidak mengizinkan Anda memberikan argumen tambahan terhadap kebijakan justru karena alasan ini.
Namun, dalam kasus yang sangat jarang terjadi, Anda mungkin perlu memberi otorisasi berdasarkan lebih banyak konteks daripada hanya pengguna yang saat ini diautentikasi. Misalkan misalnya otorisasi bergantung pada alamat IP selain pengguna yang diautentikasi. Dalam hal ini, salah satu pilihannya adalah membuat kelas khusus yang menggabungkan pengguna dan IP dan meneruskannya ke kebijakan.
class UserContext
attr_reader :user , :ip
def initialize ( user , ip )
@user = user
@ip = ip
end
end
class ApplicationController
include Pundit :: Authorization
def pundit_user
UserContext . new ( current_user , request . ip )
end
end
Di Rails, perlindungan penugasan massal ditangani di pengontrol. Dengan Pundit Anda dapat mengontrol atribut mana yang dapat diperbarui oleh pengguna melalui kebijakan Anda. Anda dapat menyiapkan metode permitted_attributes
dalam kebijakan Anda seperti ini:
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes
if user . admin? || user . owner_of? ( post )
[ :title , :body , :tag_list ]
else
[ :tag_list ]
end
end
end
Anda sekarang dapat mengambil atribut berikut dari kebijakan:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def update
@post = Post . find ( params [ :id ] )
if @post . update ( post_params )
redirect_to @post
else
render :edit
end
end
private
def post_params
params . require ( :post ) . permit ( policy ( @post ) . permitted_attributes )
end
end
Namun, ini agak rumit, jadi Pundit menyediakan metode bantuan yang mudah digunakan:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def update
@post = Post . find ( params [ :id ] )
if @post . update ( permitted_attributes ( @post ) )
redirect_to @post
else
render :edit
end
end
end
Jika Anda ingin mengizinkan atribut yang berbeda berdasarkan tindakan saat ini, Anda dapat menentukan metode permitted_attributes_for_#{action}
pada kebijakan Anda:
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes_for_create
[ :title , :body ]
end
def permitted_attributes_for_edit
[ :body ]
end
end
Jika Anda telah menentukan metode khusus tindakan pada kebijakan Anda untuk tindakan saat ini, helper permitted_attributes
akan memanggilnya o