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.