Pundit은 일반 Ruby 클래스와 객체 지향 디자인 패턴을 활용하여 간단하고 강력하며 확장 가능한 인증 시스템을 구축하도록 안내하는 도우미 세트를 제공합니다.
후원자: Varvet
GitHub 의 README는 GitHub의 최신 코드 와 정확합니다. 귀하는 Pundit의 출시된 버전을 사용하고 있을 가능성이 높으므로 최신 출시된 Pundit 버전에 대한 설명서를 참조하십시오.
bundle add pundit
애플리케이션 컨트롤러에 Pundit::Authorization
포함하세요.
class ApplicationController < ActionController :: Base
include Pundit :: Authorization
end
선택적으로, 몇 가지 유용한 기본값을 사용하여 애플리케이션 정책을 설정하는 생성기를 실행할 수 있습니다.
rails g pundit:install
애플리케이션 정책을 생성한 후 Rails 서버를 다시 시작하면 Rails가 새 app/policies/
디렉터리에서 모든 클래스를 선택할 수 있습니다.
Pundit은 정책 클래스의 개념에 초점을 맞추고 있습니다. 이러한 클래스를 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
보시다시피 이것은 일반 Ruby 클래스입니다. Pundit은 이 클래스에 대해 다음과 같은 가정을 합니다.
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
Authorize 메소드는 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
Pundit은 이 클래스에 대해 다음과 같은 가정을 합니다.
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
승인 메소드와 마찬가지로 정책 범위 클래스를 재정의할 수도 있습니다.
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
와 유사한 예외가 발생합니다. 그러나 authorize
대신 policy_scope
사용되는지 추적합니다. 이는 범위가 있는 컬렉션을 찾고 개별 인스턴스를 승인하지 않는 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
때로는 Pundit이 추론하도록 하는 대신 특정 클래스에 사용할 정책을 명시적으로 선언하고 싶을 수도 있습니다. 이는 다음과 같이 수행할 수 있습니다.
class Post
def self . policy_class
PostablePolicy
end
end
또는 인스턴스 메서드를 선언할 수 있습니다.
class Post
def policy_class
PostablePolicy
end
end
Pundit은 의도적으로 매우 작은 라이브러리이며 사용자가 스스로 할 수 없는 작업은 수행하지 않습니다. 여기에는 비밀 소스가 없습니다. 가능한 한 적은 작업을 수행한 다음 방해가 되지 않습니다.
Pundit에서 사용할 수 있는 소수이지만 강력한 도우미를 사용하면 특별한 DSL이나 펑키한 구문을 사용하지 않고도 잘 구조화되고 완벽하게 작동하는 인증 시스템을 구축할 수 있습니다.
모든 정책 및 범위 클래스는 일반 Ruby 클래스이므로 DRY 작업에 항상 사용하는 것과 동일한 메커니즘을 사용할 수 있습니다. 일련의 권한을 모듈로 캡슐화하고 이를 여러 정책에 포함합니다. 일부 권한이 다른 권한과 동일하게 작동하도록 하려면 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
null 객체 패턴을 지원하기 위해 NilClassPolicy
를 구현하고 싶을 수도 있습니다. 이는 예를 들어 nil
일 수 있는 연결에 대한 일부 허용을 허용하기 위해 ApplicationPolicy를 확장하려는 경우 유용할 수 있습니다.
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은 ApplicationController
에서 구제할 수 있는 Pundit::NotAuthorizedError
발생시킵니다. 모든 컨트롤러에서 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
또는 레일이 이를 403 오류로 처리하고 403 오류 페이지를 제공함으로써 Pundit::NotAuthorizedError를 전역적으로 처리할 수 있습니다. 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은 오류 메시지를 구현하는 방법에 대해 불가지론적입니다.
컨트롤러나 보기 외부의 레코드에 대한 정책을 검색하려는 경우가 있습니다. 예를 들어 한 정책에서 다른 정책으로 권한을 위임하는 경우입니다.
다음과 같이 정책과 범위를 쉽게 검색할 수 있습니다.
Pundit . policy! ( user , post )
Pundit . policy ( user , post )
Pundit . policy_scope! ( user , Post )
Pundit . policy_scope ( user , Post )
bang 메소드는 정책이 존재하지 않는 경우 예외를 발생시키는 반면, bang이 없는 메소드는 nil을 반환합니다.
경우에 따라 컨트롤러가 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
를 재정의하고 AdminController
의 도우미 authorize
네임스페이스를 자동으로 적용하는 것이 유용할 수 있습니다.
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
Rails에서는 대량 할당 보호가 컨트롤러에서 처리됩니다. 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
도우미가 대신 해당 메서드를 호출합니다.