README.md in critic-0.2.2 vs README.md in critic-0.2.3

- old
+ new

@@ -34,22 +34,44 @@ There are two types of methods: * *action* - determines if subject is authorized to perform a specific operation on the resource * *scope* - returns a list of resources available to the subject +The default scope is `index` but it can be overridden by specifying `.scope`. +```ruby +# app/policies/post_policy.rb +class PostPolicy + include Critic::Policy + + # set default scope + self.scope = :author_index + + # now default scope + def author_index + resource.where(author_id: subject.id) + end + + # no longer the default scope + def index + resource.order(:created_at) + end +end +``` + #### Actions The most basic actions return `true` or `false` to indicate the authorization status. ```ruby # app/policies/post_policy.rb class PostPolicy include Critic::Policy def update? - !resource.locked + !resource.locked? && + resource.published_at.present? end end ``` This policy will only allow updates if the post is not `locked`. @@ -62,10 +84,80 @@ PostPolicy.authorize(:update?, User.new, Post.new(false)).granted? #=> true PostPolicy.authorize(:update?, User.new, Post.new(true)).granted? #=> false ``` +#### Authorization Result + +Returning a String from your action is interpreted as a failure. The String is added to the messages of the authorization. + +```ruby +Post = Struct.new(:author_id) +User = Struct.new(:id) + +class PostPolicy + include Critic::Policy + + def destroy? + return true if resource.author_id == subject.id + "Cannot destroy Post: This post is authored by #{resource.author_id}" + end +end + +authorization = PostPolicy.authorize(destroy?, User.new(1), Post.new(2)) +authorization.granted? #=> false +authorization.messages #=> ["Cannot destroy Post: This post is authored by 2"'] +``` + +`halt` can be used to indicate early failure. The argument provided to `halt` becomes the result of the authorization. + +```ruby +Post = Struct.new(:author_id) +User = Struct.new(:id) + +class PostPolicy + include Critic::Policy + + def destroy? + if resource.author_id != subject.id + halt "Cannot destroy Post: This post is authored by #{resource.author_id}" + end + true + end +end + +authorization = PostPolicy.authorize(destroy?, User.new(1), Post.new(2)) +authorization.granted? #=> false +authorization.messages #=> ["Cannot destroy Post: This post is authored by 2"'] +``` + +`halt(true)` indicates immediate success. + +```ruby +Post = Struct.new(:author_id) +User = Struct.new(:id) + +class PostPolicy + include Critic::Policy + + def destroy? + check_ownership + false + end + + private + + def check_ownership + halt(true) if resource.author_id == subject.id + end +end + +authorization = PostPolicy.authorize(destroy?, User.new(1), Post.new(2)) +authorization.granted? #=> false +authorization.messages #=> ["Cannot destroy Post: This post is authored by 2"'] +``` + #### Scopes Scopes treat `resource` as a starting point and return a restricted set of associated resources. Policies can have any number of scopes. The default scope is `#index`. ```ruby @@ -77,10 +169,21 @@ resource.where(deleted_at: nil, author_id: subject.id) end end ``` +Verify authorization using `#authorize`. + +```ruby +Post = Class.new(ActiveRecord::Base) +User = Struct.new + +authorization = PostPolicy.authorize(index, User.new, Post.new(false)) +authorization.granted? #=> true +authorization.result #=> <#ActiveRecord::Relation..> +``` + #### Convention It can be a useful convention to add a `?` suffix to your action methods. This allows a clear separation between actions and scopes. All other methods should be `protected`, similar to Rails controller. ```ruby @@ -88,16 +191,16 @@ class PostPolicy include Critic::Policy # default scope def index - Post.where(published: true) + resource.where(published: true) end # custom scope def author_index - Post.where(author_id: subject.id) + resource.where(author_id: subject.id) end # action def show? (post.draft? && authored_post?) || post.published? @@ -115,10 +218,12 @@ ### Controller Controllers are the primary consumer of policies. Controllers ask the policy if an authenticated subject is authorized to perform a specific action on a specific resource. +#### Actions + In Rails, the policy action is inferred from `params[:action]` which corresponds to the controller action method name. When `authorize` fails, a `Critic::AuthorizationDenied` exception is raised with reference to the performed authorization. ```ruby @@ -148,22 +253,117 @@ include Critic::Controller error Critic::AuthorizationDenied do |exception| messages = exception.authorization.messages || exception.message - body {errors: [messages]} + body {errors: [*messages]} halt 403 end put '/posts/:id' do |id| post = Post.find(id) authorize post, :update post.to_json end end +``` +##### Gentle +Calling `authorized?` returns `true` or `false` instead of raising an exception. + +```ruby +# app/controllers/post_controller.rb +class PostController < Sinatra::Base + include Critic::Controller + + put '/posts/:id' do |id| + post = Post.find(id) + + halt(403) unless authorized?(post, :update) + + post.to_json + end +end +``` + +##### Verify authorization + +`verify_authorized` enforces that the request was authorized before the response is returned. A `Critic::AuthorizationMissing` error is raised in this case. A request is authorized if `authorized?`, `authorize` or `authorizing!` is called before the response is returned. + +```ruby +# app/controllers/post_controller.rb +class PostController < Sinatra::Base + include Critic::Controller + + verify_authorized + + error Critic::AuthorizationMissing do |exception| + # notify developers that something has gone horribly wrong + halt 503 + end + + put '/posts/:id' do |id| + post = Post.find(id) + + post.to_json + end +end +``` + +This check can be artificially skipped calling `authorizing!`. + +```ruby +# app/controllers/invitation_controller.rb +class InvitationController < Sinatra::Base + include Critic::Controller + + verify_authorized + + post '/invitation/accept/code' do |code| + invitation = Invitiation.find_by(code: code) + + invitation.accept! + authorizing! # Skip authorization check + + redirect '/' + end +end +``` + +#### Scopes + +Use `authorize_scope` and provide the base scope. The return value is the result. + +```ruby +# app/controllers/post_controller.rb +class PostController < Sinatra::Base + include Critic::Controller + + get '/customers/:customer_id/posts' do |customer_id| + posts = + authorize_scope(Post.where(customer_id: customer_id)) + + posts.to_json + end +end +``` + +Custom indexes can be used by passing an `action` parameter. + +```ruby +# app/controllers/post_controller.rb +class PostController < Sinatra::Base + include Critic::Controller + + get '/posts' d + posts = + authorize_scope(Post, action: :custom_index) + + posts.to_json + end +end ``` #### Custom subject By default, the policy's subject is referenced by `current_user`. Override `critic` to customize.