يوفر Pundit مجموعة من المساعدين الذين يرشدونك في الاستفادة من فئات Ruby العادية وأنماط التصميم الموجهة للكائنات لبناء نظام ترخيص مباشر وقوي وقابل للتطوير.
برعاية: فارفيت
يرجى ملاحظة أن الملف التمهيدي الموجود على GitHub دقيق مع أحدث التعليمات البرمجية الموجودة على GitHub . من المرجح أنك تستخدم إصدارًا تم إصداره من Pundit، لذا يرجى الرجوع إلى الوثائق الخاصة بأحدث إصدار من Pundit.
bundle add pundit
قم بتضمين Pundit::Authorization
في وحدة تحكم التطبيق الخاصة بك:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
end
اختياريًا، يمكنك تشغيل المولد، والذي سيقوم بإعداد سياسة التطبيق مع بعض الإعدادات الافتراضية المفيدة لك:
rails g pundit:install
بعد إنشاء سياسة التطبيق الخاصة بك، أعد تشغيل خادم Rails حتى يتمكن Rails من التقاط أي فئات في دليل app/policies/
الجديد.
يركز الناقد على مفهوم الطبقات السياسية. نقترح عليك وضع هذه الفئات في app/policies
. هذا مثال يسمح بتحديث منشور إذا كان المستخدم مسؤولاً، أو إذا كان المنشور غير منشور:
class PostPolicy
attr_reader :user , :post
def initialize ( user , post )
@user = user
@post = post
end
def update?
user . admin? || ! post . published?
end
end
كما ترون، هذه فئة روبي عادية. يقدم الناقد الافتراضات التالية حول هذه الفئة:
current_user
لاسترداد ما سيتم إرساله إلى هذه الوسيطةupdate?
. عادةً ما يتم تعيين هذا إلى اسم إجراء وحدة تحكم معين.هذا كل شيء حقا.
ستحتاج عادةً إلى الوراثة من سياسة التطبيق التي أنشأها المولد، أو إعداد الفئة الأساسية الخاصة بك للوراثة من:
class PostPolicy < ApplicationPolicy
def update?
user . admin? or not record . published?
end
end
في ApplicationPolicy
الذي تم إنشاؤه، يسمى كائن النموذج record
.
لنفترض أن لديك مثيلًا للفئة Post
، يتيح لك Pundit الآن القيام بذلك في وحدة التحكم الخاصة بك:
def update
@post = Post . find ( params [ :id ] )
authorize @post
if @post . update ( post_params )
redirect_to @post
else
render :edit
end
end
تستنتج طريقة التفويض تلقائيًا أن Post
سيكون له فئة PostPolicy
مطابقة، ويقوم بإنشاء مثيل لهذه الفئة، وتسليم المستخدم الحالي والسجل المحدد. ثم يستنتج من اسم الإجراء أنه يجب استدعاء update?
في هذا المثال من السياسة. في هذه الحالة، يمكنك أن تتخيل أن authorize
كان سيفعل شيئًا كهذا:
unless PostPolicy . new ( current_user , @post ) . update?
raise Pundit :: NotAuthorizedError , "not allowed to PostPolicy#update? this Post"
end
يمكنك تمرير وسيطة ثانية authorize
إذا كان اسم الإذن الذي تريد التحقق منه لا يتطابق مع اسم الإجراء. على سبيل المثال:
def publish
@post = Post . find ( params [ :id ] )
authorize @post , :update?
@post . publish!
redirect_to @post
end
يمكنك تمرير وسيطة لتجاوز فئة السياسة إذا لزم الأمر. على سبيل المثال:
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
إذا لم يكن لديك مثيل للوسيطة الأولى authorize
، فيمكنك اجتياز الفصل الدراسي. على سبيل المثال:
سياسة:
class PostPolicy < ApplicationPolicy
def admin_list?
user . admin?
end
end
المراقب المالي:
def admin_list
authorize Post # we don't have a particular post to authorize
# Rest of controller action
end
يقوم authorize
بإرجاع المثيل الذي تم تمريره إليه، بحيث يمكنك ربطه على النحو التالي:
المراقب المالي:
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
يمكنك بسهولة الحصول على مثيل للسياسة من خلال طريقة policy
في كل من العرض ووحدة التحكم. يعد هذا مفيدًا بشكل خاص لعرض الروابط أو الأزرار المشروطة في طريقة العرض:
<% if policy(@post).update? %>
<%= link_to "Edit post", edit_post_path(@post) %>
<% end %>
نظرًا لوجود سياسة بدون نموذج مطابق/فئة روبي، يمكنك استردادها عن طريق تمرير رمز.
# 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
لاحظ أن سياسة مقطوعة الرأس لا تزال بحاجة إلى قبول وسيطتين. ستكون الوسيطة الثانية هي الرمز :dashboard
في هذه الحالة، وهو ما يتم تمريره كسجل authorize
أدناه.
# In controllers
def show
authorize :dashboard , :show?
...
end
# In views
<% if policy(:dashboard).show? %>
<%= link_to 'Dashboard', dashboard_path %>
<% end %>
في كثير من الأحيان، سوف ترغب في الحصول على نوع من عرض سجلات القائمة التي يمكن لمستخدم معين الوصول إليها. عند استخدام Pundit، من المتوقع منك تحديد فئة تسمى نطاق السياسة. يمكن أن يبدو مثل هذا:
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
يقدم الناقد الافتراضات التالية حول هذه الفئة:
Scope
وهي متداخلة ضمن فئة السياسة.current_user
لاسترداد ما سيتم إرساله إلى هذه الوسيطة.ActiveRecord::Relation
، ولكنها قد تكون شيئًا آخر تمامًا.resolve
، والذي يجب أن يُرجع نوعًا ما من النتائج التي يمكن تكرارها. بالنسبة لفئات ActiveRecord، عادةً ما يكون هذا هو ActiveRecord::Relation
.ربما تريد أن ترث من نطاق سياسة التطبيق الذي أنشأه المولد، أو أن تنشئ فئة أساسية خاصة بك لترث منها:
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
يمكنك الآن استخدام هذه الفئة من وحدة التحكم الخاصة بك عبر طريقة policy_scope
:
def index
@posts = policy_scope ( Post )
end
def show
@post = policy_scope ( Post ) . find ( params [ :id ] )
end
كما هو الحال مع التابع Authorized، يمكنك أيضًا تجاوز فئة نطاق السياسة:
def index
# publication_class => Post
@publications = policy_scope ( publication_class , policy_scope_class : PublicationPolicy :: Scope )
end
في هذه الحالة هو اختصار للقيام بما يلي:
def index
@publications = PublicationPolicy :: Scope . new ( current_user , Post ) . resolve
end
يمكنك، وننصحك، باستخدام هذه الطريقة في طرق العرض:
<% policy_scope(@user.posts).each do |post| %>
< p > <%= link_to post . title , post_path ( post ) %> </ p >
<% end %>
عندما تقوم بتطوير تطبيق باستخدام Pundit، قد يكون من السهل أن تنسى السماح ببعض الإجراءات. الناس ينسون بعد كل شيء. نظرًا لأن Pundit يشجعك على إضافة استدعاء authorize
يدويًا إلى كل إجراء لوحدة التحكم، فمن السهل حقًا تفويت أي إجراء.
لحسن الحظ، لدى Pundit ميزة مفيدة تذكرك في حالة النسيان. يتتبع Pundit ما إذا كنت قد قمت بالاتصال authorize
في أي مكان في إجراء وحدة التحكم الخاصة بك. يضيف Pundit أيضًا طريقة إلى وحدات التحكم الخاصة بك تسمى verify_authorized
. ستثير هذه الطريقة استثناءً إذا لم يتم استدعاء authorize
بعد. يجب عليك تشغيل هذه الطريقة في ربط after_action
للتأكد من أنك لم تنسَ السماح بالإجراء. على سبيل المثال:
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
after_action :verify_authorized
end
وبالمثل، يضيف Pundit أيضًا verify_policy_scoped
إلى وحدة التحكم الخاصة بك. سيؤدي هذا إلى ظهور استثناء مشابه لـ verify_authorized
. ومع ذلك، فإنه يتتبع ما إذا تم استخدام policy_scope
بدلاً من authorize
. يعد هذا مفيدًا في الغالب لإجراءات وحدة التحكم مثل index
الذي يبحث عن مجموعات ذات نطاق ولا يسمح بمثيلات فردية.
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
آلية التحقق هذه موجودة فقط لمساعدتك أثناء تطوير تطبيقك، لذا لا تنس الاتصال authorize
. إنها ليست نوعًا من آلية الأمان أو آلية الترخيص. يجب أن تكون قادرًا على إزالة هذه المرشحات دون التأثير على كيفية عمل تطبيقك بأي شكل من الأشكال.
وجد بعض الأشخاص هذه الميزة مربكة، بينما وجدها كثيرون آخرون مفيدة للغاية. إذا كنت تندرج ضمن فئة الأشخاص الذين يجدون ذلك مربكًا، فلن تحتاج إلى استخدامه. سوف يعمل Pundit بشكل جيد دون استخدام verify_authorized
و verify_policy_scoped
.
إذا كنت تستخدم verify_authorized
في وحدات التحكم الخاصة بك ولكنك تحتاج إلى تجاوز التحقق بشكل مشروط، فيمكنك استخدام skip_authorization
. لتجاوز verify_policy_scoped
، استخدم skip_policy_scope
. تكون هذه مفيدة في الحالات التي لا تريد فيها تعطيل التحقق من الإجراء بأكمله، ولكن لديك بعض الحالات التي لا تنوي فيها عدم التفويض.
def show
record = Record . find_by ( attribute : "value" )
if record . present?
authorize record
else
skip_authorization
end
end
في بعض الأحيان قد ترغب في الإعلان بوضوح عن السياسة التي يجب استخدامها لفصل معين، بدلاً من السماح للناقد باستنتاج ذلك. يمكن القيام بذلك على النحو التالي:
class Post
def self . policy_class
PostablePolicy
end
end
بدلًا من ذلك، يمكنك الإعلان عن طريقة مثيل:
class Post
def policy_class
PostablePolicy
end
end
Pundit هي مكتبة صغيرة جدًا عن قصد، وهي لا تفعل أي شيء لا يمكنك القيام به بنفسك. ليس هناك صلصة سرية هنا. يفعل أقل ما يمكن، ومن ثم يخرج من طريقك.
من خلال عدد قليل من المساعدين الأقوياء المتاحين في Pundit، لديك القدرة على إنشاء نظام ترخيص جيد التنظيم ويعمل بكامل طاقته دون استخدام أي DSLs خاصة أو بناء جملة غير تقليدي.
تذكر أن جميع فئات السياسة والنطاق هي فئات روبي بسيطة، مما يعني أنه يمكنك استخدام نفس الآليات التي تستخدمها دائمًا لتجفيف الأشياء. قم بتغليف مجموعة من الأذونات في وحدة نمطية وإدراجها في سياسات متعددة. استخدم alias_method
لجعل بعض الأذونات تتصرف بنفس الطريقة التي تعمل بها الأذونات الأخرى. وراثة من مجموعة أساسية من الأذونات. استخدم البرمجة الوصفية إذا كنت مضطرًا لذلك حقًا.
استخدم المولد المتوفر لإنشاء السياسات:
rails g pundit:policy post
في العديد من التطبيقات، لا يتمكن سوى المستخدمين الذين قاموا بتسجيل الدخول من فعل أي شيء. إذا كنت تقوم ببناء مثل هذا النظام، فقد يكون من المرهق التحقق من أن المستخدم في السياسة ليس nil
لكل إذن فردي. وبصرف النظر عن السياسات، يمكنك إضافة هذا الاختيار إلى الفئة الأساسية للنطاقات.
نقترح عليك تحديد عامل تصفية يعيد توجيه المستخدمين غير المصادقين إلى صفحة تسجيل الدخول. كدفاع ثانوي، إذا قمت بتعريف ApplicationPolicy، فقد يكون من الجيد رفع استثناء إذا تمكن مستخدم غير مصادق عليه بطريقة أو بأخرى. بهذه الطريقة يمكنك أن تفشل بأمان أكبر.
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
لدعم نمط كائن فارغ قد تجد أنك تريد تنفيذ NilClassPolicy
. قد يكون هذا مفيدًا عندما تريد توسيع ApplicationPolicy الخاص بك للسماح ببعض التسامح، على سبيل المثال، مع الارتباطات التي قد تكون 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 خطأ Pundit::NotAuthorizedError
الذي يمكنك إنقاذه من ApplicationController
لديك. يمكنك تخصيص طريقة user_not_authorized
في كل وحدة تحكم.
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
وبدلاً من ذلك، يمكنك التعامل مع Pundit::NotAuthorizedError بشكل عام من خلال جعل Rails يتعامل معها كخطأ 403 وتقديم صفحة خطأ 403. أضف ما يلي إلى application.rb:
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
توفر NotAuthorizedError
معلومات حول الاستعلام (على سبيل المثال :create?
)، والسجل (على سبيل المثال، مثيل Post
)، والسياسة (على سبيل المثال، مثيل PostPolicy
) التي تسببت في ظهور الخطأ.
تتمثل إحدى طرق استخدام خصائص query
record
policy
في توصيلها بـ I18n
لإنشاء رسائل خطأ. وإليك كيف يمكنك القيام بذلك.
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! '
هذا مثال. لا يعرف الناقد كيفية تنفيذ رسائل الخطأ الخاصة بك.
في بعض الأحيان تريد استرداد سياسة لسجل خارج وحدة التحكم أو العرض. على سبيل المثال، عند تفويض الأذونات من سياسة إلى أخرى.
يمكنك بسهولة استرداد السياسات والنطاقات مثل هذا:
Pundit . policy! ( user , post )
Pundit . policy ( user , post )
Pundit . policy_scope! ( user , Post )
Pundit . policy_scope ( user , Post )
سوف تثير أساليب الانفجار استثناءً في حالة عدم وجود السياسة، في حين أن الأساليب التي لا تحتوي على الانفجارات ستعود صفرًا.
في بعض الأحيان، قد لا تتمكن وحدة التحكم الخاصة بك من الوصول إلى current_user
، أو قد لا تكون الطريقة التي يجب أن يستدعيها Pundit هي current_user
. لمعالجة هذه المشكلة، يمكنك تحديد طريقة في وحدة التحكم الخاصة بك باسم pundit_user
.
def pundit_user
User . find_by_other_means
end
في بعض الحالات، قد يكون من المفيد وجود سياسات متعددة تخدم سياقات مختلفة للمورد. ومن الأمثلة الرئيسية على ذلك الحالة التي تختلف فيها سياسات المستخدم عن سياسات المسؤول. للتفويض باستخدام سياسة مساحة الاسم، قم بتمرير مساحة الاسم إلى مساعد authorize
في مصفوفة:
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
إذا كنت تستخدم سياسات مساحة الاسم لشيء مثل طرق عرض المسؤول، فقد يكون من المفيد تجاوز policy_scope
authorize
المساعدين في AdminController
لتطبيق مساحة الاسم تلقائيًا:
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 بشدة على تصميم تطبيقك بطريقة تجعل السياق الوحيد الذي تحتاجه للحصول على الترخيص هو كائن المستخدم ونموذج المجال الذي تريد التحقق من الترخيص له. إذا وجدت نفسك بحاجة إلى سياق أكثر من ذلك، ففكر فيما إذا كنت تسمح بنموذج المجال الصحيح، فربما يوفر نموذج مجال آخر (أو غلاف حول نماذج المجال المتعددة) السياق الذي تحتاجه.
لا يسمح لك Pundit بتمرير حجج إضافية إلى السياسات لهذا السبب بالتحديد.
ومع ذلك، في حالات نادرة جدًا، قد تحتاج إلى الترخيص استنادًا إلى سياق أكثر من مجرد المستخدم الذي تمت مصادقته حاليًا. لنفترض على سبيل المثال أن التفويض يعتمد على عنوان IP بالإضافة إلى المستخدم الذي تمت مصادقته. في هذه الحالة، أحد الخيارات هو إنشاء فئة خاصة تغطي كلاً من المستخدم وIP وتمريرها إلى السياسة.
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
في ريلز، تتم معالجة حماية التعيينات الجماعية في وحدة التحكم. باستخدام Pundit، يمكنك التحكم في السمات التي يمكن للمستخدم الوصول إليها لتحديثها عبر سياساتك. يمكنك إعداد طريقة permitted_attributes
في سياستك مثل هذا:
# 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
يمكنك الآن استرداد هذه السمات من السياسة:
# 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
ومع ذلك، هذا الأمر مرهق بعض الشيء، لذا يوفر Pundit طريقة مساعدة مريحة:
# 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
إذا كنت تريد السماح بسمات مختلفة بناءً على الإجراء الحالي، فيمكنك تحديد طريقة permitted_attributes_for_#{action}
في سياستك:
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes_for_create
[ :title , :body ]
end
def permitted_attributes_for_edit
[ :body ]
end
end
إذا قمت بتحديد طريقة خاصة بالإجراء في سياستك للإجراء الحالي، فسوف يستدعيها المساعد permitted_attributes
بدلاً من ذلك