Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Last active June 4, 2022 00:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoshCheek/a1b4eab576e568c833a8abb3360ab66a to your computer and use it in GitHub Desktop.
Save JoshCheek/a1b4eab576e568c833a8abb3360ab66a to your computer and use it in GitHub Desktop.
What I feel like cancancan abilities should have been
# Library implementation of what it feels like cancan should have been
module NotCanCan
class Permission
attr_reader :verb, :direct_object, :default, :dynamic_override
def initialize(verb, direct_object, default, &dynamic_override)
@verb, @direct_object = verb, direct_object
@default, @dynamic_override = !!default, dynamic_override
end
def for?(verb, direct_object)
return false unless self.verb == verb
return true if self.direct_object == direct_object
direct_object.is_a? self.direct_object
end
def permitted?(context_object, verb, direct_object)
for?(verb, direct_object) or raise ArgumentError, "permission called on verb/direct object taht it is not for"
return default unless dynamic_override
return default unless direct_object.is_a?(self.direct_object)
return default if context_object.instance_exec direct_object, &dynamic_override
!default
end
def check_instances?
@check_instances
end
def specificity(direct_object)
specificity = 0
# more specific if we need to consider this specific direct object
specificity -= 1 if dynamic_override
# more specific if they are the same object (rather than an instance)
specificity -= 1 if self.direct_object == direct_object
specificity
end
end
module Ability
def self.extended(instance)
instance.singleton_class.include self
end
def self.included(base)
base.extend ClassMethods
base.include InstanceMethods
end
module InstanceMethods
def initialize(*args, fallback: nil, **keywords, &block)
@_fallback = !fallback ? nil :
fallback.respond_to?(:can?) ? fallback :
fallback.new(*args, **keywords, &block)
end
def can?(verb, direct_object)
permission_for!(verb, direct_object).permitted?(self, verb, direct_object)
end
protected
def permission_for!(verb, direct_object)
self.class.permission_for(verb, direct_object) ||
@_fallback&.permission_for!(verb, direct_object) ||
raise(ArgumentError, "No abilities defined for #{verb.inspect} on #{direct_object.inspect}")
end
end
module ClassMethods
def self.extended(base)
base.instance_variable_set(:@permissions, [])
end
def can(action, subject, &block)
@permissions << Permission.new(action, subject, true, &block)
end
def cannot(action, subject, &block)
@permissions << Permission.new(action, subject, false, &block)
end
def permission_for(verb, direct_object)
permissions_for(verb, direct_object).first
end
def permissions_for(verb, direct_object)
@permissions
.select { |perm| perm.for? verb, direct_object }
.sort_by { |perm| perm.specificity direct_object }
end
end
end
end
# Some models
Post = Struct.new(:author, :published, :body) { alias published? published }
User = Struct.new(:name, :admin) { alias admin? admin }
# Abilities
class CommonAbility
def initialize(user, **) # in Ruby 3, change `**` to `...`
super
@user = user
end
include NotCanCan::Ability
can(:read, Post) { |post| post.published? || post.author == @user }
can(:edit, Post) { |post| post.author == @user }
can(:favourite, Post)
cannot(:delete, Post)
end
class AdminAbility
def initialize(admin, **)
super
@admin = admin
end
include NotCanCan::Ability
can :edit, Post
cannot :favourite, Post
end
# Test data
reader_user = User.new 'reader', false
author_user = User.new 'author', false
admin_user = User.new 'admin', true
published_post = Post.new author_user, true, 'My finished thoughts'
private_post = Post.new author_user, false, 'My unfinished thoughts'
# The reader:
ability = CommonAbility.new(reader_user)
# can read other people's published posts
ability.can? :read, published_post # => true
ability.can? :read, private_post # => false
# cannot edit, b/c is not the author
ability.can? :edit, published_post # => false
ability.can? :edit, private_post # => false
# can favourite, b/c everyone can
ability.can? :favourite, published_post # => true
ability.can? :favourite, private_post # => true
# cannot delete, b/c no one can
ability.can? :delete, published_post # => false
ability.can? :delete, private_post # => false
# The author:
ability = CommonAbility.new(author_user)
# can read public post since everyone can
# can read private post since they are the author
ability.can? :read, published_post # => true
ability.can? :read, private_post # => true
# can edit b/c they are the author
ability.can? :edit, published_post # => true
ability.can? :edit, private_post # => true
# can favourite, b/c everyone can
ability.can? :favourite, published_post # => true
ability.can? :favourite, private_post # => true
# cannot delete, b/c no one can
ability.can? :delete, published_post # => false
ability.can? :delete, private_post # => false
# The admin:
common = CommonAbility.new(admin_user)
admin = AdminAbility.new(admin_user, fallback: common)
# reading falls back to the common ability
common.can? :read, published_post # => true
common.can? :read, private_post # => false
admin.can? :read, published_post # => true
admin.can? :read, private_post # => false
# admins are overridden to allow editing
common.can? :edit, published_post # => false
common.can? :edit, private_post # => false
admin.can? :edit, published_post # => true
admin.can? :edit, private_post # => true
# admins are overridden to disallow favouriting
common.can? :favourite, published_post # => true
common.can? :favourite, private_post # => true
admin.can? :favourite, published_post # => false
admin.can? :favourite, private_post # => false
# deleting falls back to the common ability
common.can? :delete, published_post # => false
common.can? :delete, private_post # => false
admin.can? :delete, published_post # => false
admin.can? :delete, private_post # => false
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment