KittyPolicy
Minimalistic authorization library extracted from Product Hunt.
Features:
- small DSL for defining authorization abilities
- not class initializations when performing abilities check
- integrations with GraphQL gem.
Installation
Add this line to your application's Gemfile:
gem 'kitty_policy'
And then execute:
$ bundle
Or install it yourself as:
$ gem install kitty_policy
Usage
Step 1 - Define policy object
module ApplicationPolicy
extend KittyPolicy::DSL
# generates a method named `can_moderate?`
# example: no subject, just action
can :moderate do |user|
user.admin?
end
# generates a method named `can_start_trial?`
# example: `allow_guest` access
can :start_trial, allow_guest: true do |user, _subscription|
!user || user.trial_used?
end
# generates a method named `can_create_chat_room?`
# example: subject as symbol
can :create, :chat_room do |user|
user.admin?
end
# generates a method named `can_create_post?`
# example: subject as class, instance not used
can :create, Post do |user|
user.can_post?
end
# generates a method named `can_edit_post?`
# example: subject as class, passing subject instance
can :edit, Post do |user, post|
user.admin? || user == post.author
end
# generates a method named `can_manage_account?`
# example: using a private helper method
can :manage, Account do |user, account|
user.admin? || member?(user, account)
end
private
# you can extract private helper methods
def member?(user, account)
# ...
end
end
can
is just a convince helper to create methods on a module:
ApplicationPolicy.can_moderate?
ApplicationPolicy.can_start_trial?
ApplicationPolicy.can_create_post?
ApplicationPolicy.can_edit_post?
ApplicationPolicy.can_manage_account?
Step 2 - Use policy object
# answers if user can perform certain action
ApplicationPolicy.can?(user, :create, Post)
ApplicationPolicy.can?(user, :create, Post.new)
ApplicationPolicy.can?(user, :create, post)
ApplicationPolicy.can?(user, :start_trial)
# raises `KittyPolicy::AccessDenied` when user can't perform certain action
ApplicationPolicy.authorize!(user, :create, Post)
ApplicationPolicy.authorize!(user, :create, Post.new)
ApplicationPolicy.authorize!(user, :create, post)
ApplicationPolicy.authorize!(user, :start_trial)
(Optional Step) - Group policies into separate files
You can split your logic into multiple policy files:
module Posts::Policy
extend KittyPolicy::DSL
# ... define abilities
end
Then you can group them together.
module ApplicationPolicy
extend Posts::Policy
extend Ship::Policy
end
Testing with RSpec
require 'spec_helper'
require 'kitty_policy/rspec'
describe ApplicationPolicy do
include KittyPolicy::RSpec
describe 'can_moderate?' do
it 'returns true for admin' do
expect(User.new(admin: true)).to be_able_to :moderate
end
it 'returns false for everyone else' do
expect(User.new(admin: false)).not_to be_able_to :moderate
end
end
end
Delegating abilities
module ApplicationPolicy
extend KittyPolicy::DSL
can :edit, Post do |user, post|
user.id == post.user_id
end
# users who can edit post, should edit or delete its media
can :edit, PostMedia do |user, media|
can? user, :edit, media.post
end
can :destroy, PostMedia do |user, media|
can? user, :edit, media.post
end
# this can be expressed with `delegate_ability` helper
delegate_ability :edit, PostMedia, to: :post
delegate_ability :destroy, PostMedia, to: :post, to_ability: :edit
Integration with GraphQL
Field level authorization
# Manually import graphql plugin
require 'kitty_policy/graphql/field_authorization'
class ProductHuntSchema < GraphQL::Schema
# setup authorization per field
instrument :field, KittyPolicy::GraphQL::FieldAuthorization.new(
policy: ApplicationPolicy, # required
current_user_key: :current_user, # optional, default: :current_user
)
# ...
end
module Types
class PostType < BaseObject
# Same as:
# if ApplicationPolicy.can?(context[:current_user], :edit, object)
# return metrics
# else
# return []
# end
field :metrics, [MetricType], null: false, authorize: :edit, fallback: []
# Same as:
# if ApplicationPolicy.can?(context[:current_user], :moderate, object)
# return moderation_changes_count
# else
# return 0
# end
field :moderation_changes_count, Integer, null: false, authorize: :moderate, fallback: 0
end
end
module Types
class QueryType < BaseObject
# With fallback, same as:
# if ApplicationPolicy.can?(context[:current_user], :view, post)
# return post
# else
# return nil
# end
field :post, PostType, null: false, authorize_object: :view, fallback: nil do
argument :id, ID, required: true
end
# Without fallback, same as:
# if ApplicationPolicy.can?(context[:current_user], :view, post)
# return post
# else
# raise KittyPolicy::AccessDenied(context[:current_user], :view, post)
# end
field :post, PostType, null: false, authorize_object: :view do
argument :id, ID, required: true
end
end
end
Can resolver
Exposes if current user can perform certain action.
# Manually import graphql plugin
require 'kitty_policy/graphql/can_resolver'
module Resolvers
Can = KittyPolicy::GraphQL::CanResolver.new(
policy: ApplicationPolicy, # required
current_user_key: :current_user, # optional, default: :current_user
base_resolver: BaseResolver, # optional, default: ::GraphQL::Schema::Resolver,
)
end
module Types
class PostType < BaseObject
# ...
field :can_edit, resolver: Resolvers::Can.perform(:edit) # -> ApplicationPolicy.can?(edit, post)
field :can_moderate, resolver: Resolvers::Can.perform(:moderate) { :site } # -> ApplicationPolicy.can?(:moderate, :site)
end
end
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
- Fork it
- 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
) - Run the tests (
rake
) - Create new Pull Request
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the KittyPolicy project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.