Sage - is a lightweight library for defining resource access policy rules.
Add this to your application's
dependencies: sage: github: imdrasil/sage
The core component of Sage is a policy class - it describes access policies to resource. That's why it is assumed you define a separate policy class for each resource you want to specify access restrictions.
Consider a simple example:
# It is not necessary to define application base policy class # but this allows to put all shared behavior and configs in one place abstract class ApplicationPolicy < Sage::Base end class PostPolicy < ApplicationPolicy constructor(User, Post) ability :edit? user.admin? || user.id == resource.id end ability :show? true end end
Now you can add authorization to your app:
abstract class ApplicationController include Sage::Behavior private def current_user User.current_user end end class PostsController def update @post = Post.find(params["id"]) authorize! :update?, @post # ... end end
In the above example Sage automatically refers policy class from the given
@post variable -
Post -> PostPolicy. The
user is automatically used from calling
sage_user method (which by default calls
When authorization is passed successfully (corresponding ability returned
true), nothing happens, but in case of an authorization failure
Sage::UnauthorizedError error is raised.
There are also an
unable? methods which return
able?(:update?, @post) unable?(:update?, @post)
Also you may specify exact policy class:
able?(:update, @post, within: EditorPostPolicy) authorize!(:update?, @post, within: EditorPostPolicy)
Policy class contains defined abilities (partially they are just a predicate methods) which are used to authorize activities.
Each policy record is instantiated with the target
resource : T object and authorization context
user : U. To avoid generics, they should define corresponding attribute types for themselves. As a plugin
constructor macro could be used for doing this:
class PostPolicy < Sage::Base constructor(User, Post) # This call is the same as getter user : User, resource : Post def initialize(@user, @resource) end end
#usermethod is abstract so should be defined by subclasses.
To define ability use corresponding macro
class PostPolicy < Sage::Base # ... ability :update? do user.admin? || user.id == resource.user_id end end
Calling other policies
It may be useful to call other resource policy from within a current one. For doing this you can use standard
class CommentPolicy < Sage::Policy # ... ability :update? do user.admin? || user.id == resource.id || able?(:update?, resource.post) end end
Policies can be tested as any other Crystal classes:
describe PostPolicy do described_class = PostPolicy describe "#update?" it "returns false when the user is not admin nor author" do user = User.new post = Post.new policy = described_class.new(user, post) policy.apply(:update?).should be_false end it "returns true when the user is admin" do user = User.new(:admin) post = Post.new policy = described_class.new(user, post) policy.apply(:update?).should be_true end it "returns true when the user is author" do user = User.new post = Post.new(user_id: user.id) policy = described_class.new(user, post) policy.apply(:update?).should be_true end end end
Sage allows you to add ability aliases. It may be useful when you rely on implicit rules in your code:
class PostController def edit # ... authorize! :edit?, @post # ... end def update # ... authorize! :update?, @post # ... end def destroy # ... authorize! :destroy?, @post # ... end end
In your policy you can create alias to avoid code duplication:
class PostPolicy < Sage::Base # ... alias_ability :update?, :edit?, to: :update? # ... end
alias_abilitydoesn't create aliased methods and resolve them only during
Sage::Base#applycall (which is under the hood of
When Sage can't resolve ability name it calls
Sage::Base#default_ability method which by default returns
false. You may override it to define another behavior.
Sometimes it happens that some of your abilities (or even all of them) starts with the same conditions. Example:
class PostPolicy < Sage::Base # ... ability :show? do user.admin? || resource.published? end ability :update? do user.admin? || user.id == resource.user_id end # ... end
You can separate the common parts from all abilities to a separate pre-checks:
class PostPolicy < Sage::Base # ... pre_check :admin? ability :show? do resource.published? end ability :update? do user.id == resource.user_id end private def admin? allow! if user.admin? end # ... end
Pre-checks are executed before ability invocation. They allow to halt the authorization process - just return
disallow! call value. Any other returned value is ignored.
- Fork it ( https://github.com/imdrasil/sage/fork )
- Create your feature branch (git checkout -b my-new-feature)
- Commit your changes (git commit -am 'Add some feature')
- Push to the branch (git push origin my-new-feature)
- Create a new Pull Request
- imdrasil Roman Kalnytskyi - creator, maintainer