Skip to content

Instantly share code, notes, and snippets.

@harmon
Created October 30, 2009 18:17
Show Gist options
  • Save harmon/222602 to your computer and use it in GitHub Desktop.
Save harmon/222602 to your computer and use it in GitHub Desktop.
class SomeResource < ActiveRecord::Base
include Hobo::Permissions
...
end
module Hobo
module Permissions
def self.included(klass)
klass.class_eval do
Hobo::Permissions::Associations.enable
extend ClassMethods
alias_method_chain :create, :permission_check
alias_method_chain :update, :permission_check
alias_method_chain :destroy, :permission_check
attr_accessor :acting_user, :origin, :origin_attribute, :exempt_from_edit_checks
define_callbacks :after_user_new
end
end
module ClassMethods
def user_find(user, *args)
record = find(*args)
yield(record) if block_given?
record.user_view user
record
end
def user_new(user, attributes={})
new(attributes) do |r|
r.set_creator user
yield r if block_given?
r.user_view(user)
r.send :callback, :after_user_new
end
end
def user_create(user, attributes={}, &block)
if attributes.is_a?(Array)
attributes.map { |attrs| user_create(user, attrs) }
else
record = user_new(user, attributes, &block)
record.user_save(user)
record
end
end
def user_create!(user, attributes={}, &block)
if attributes.is_a?(Array)
attributes.map { |attrs| user_create(user, attrs) }
else
record = user_new(user, attributes, &block)
record.user_save!(user)
record
end
end
def viewable_by?(user, attribute=nil)
new.viewable_by?(user, attribute)
end
end
# --- Hook ActiveRecord CRUD actions --- #
def permission_check_required?
acting_user
end
def create_with_permission_check(*args, &b)
if permission_check_required?
create_permitted? or raise PermissionDeniedError, "#{self.class.name}#create"
end
create_without_permission_check(*args, &b)
end
def update_with_permission_check(*args)
if permission_check_required?
update_permitted? or raise PermissionDeniedError, "#{self.class.name}#update"
end
update_without_permission_check(*args)
end
def destroy_with_permission_check
if permission_check_required?
destroy_permitted? or raise PermissionDeniedError, "#{self.class.name}#.destroy"
end
destroy_without_permission_check
end
# -------------------------------------- #
# --- Permissions API --- #
def with_acting_user(user)
old = acting_user
self.acting_user = user
result = yield
self.acting_user = old
result
end
def user_save(user)
with_acting_user(user) { save }
end
def user_save!(user)
with_acting_user(user) { save! }
end
def user_destroy(user)
with_acting_user(user) { destroy }
end
def user_view(user, attribute=nil)
raise PermissionDeniedError unless viewable_by?(user, attribute)
end
def user_update_attributes(user, attributes)
with_acting_user(user) do
self.attributes = attributes
save
end
end
def user_update_attributes!(user, attributes)
with_acting_user(user) do
self.attributes = attributes
save!
end
end
def creatable_by?(user)
with_acting_user(user) { create_permitted? }
end
def updatable_by?(user)
with_acting_user(user) { update_permitted? }
end
def destroyable_by?(user)
with_acting_user(user) { destroy_permitted? }
end
def method_callable_by?(user, method)
permission_method = "#{method}_permitted?"
respond_to?(permission_method) && with_acting_user(user) { send(permission_method) }
end
def viewable_by?(user, attribute=nil)
if attribute
attribute = attribute.to_s.sub(/\?$/, '').to_sym
return false if attribute && self.class.never_show?(attribute)
end
with_acting_user(user) { view_permitted?(attribute) }
end
def editable_by?(user, attribute=nil)
return false if attribute_protected?(attribute)
return true if exempt_from_edit_checks?
# Can't view implies can't edit
return false unless viewable_by?(user, attribute)
if attribute
attribute = attribute.to_s.sub(/\?$/, '').to_sym
# Try the attribute-specic edit-permission method if there is one
if respond_to?(meth = "#{attribute}_edit_permitted?")
with_acting_user(user) { send(meth) }
end
# No setter = no edit permission
return false if !respond_to?("#{attribute}=")
refl = self.class.reflections[attribute.to_sym]
if refl && refl.macro != :belongs_to # a belongs_to is handled the same as a regular attribute
return association_editable_by?(user, refl)
end
end
with_acting_user(user) { edit_permitted?(attribute) }
end
def attribute_protected?(attribute)
attribute = attribute.to_s
return true if attributes_protected_by_default.include? attribute
if self.class.accessible_attributes
return true if !self.class.accessible_attributes.include?(attribute)
elsif self.class.protected_attributes
return true if self.class.protected_attributes.include?(attribute)
end
# Readonly attributes can be set on creation but not thereafter
return self.class.readonly_attributes.include?(attribute) if !new_record? && self.class.readonly_attributes
false
end
def association_editable_by?(user, reflection)
# has_one and polymorphic associations are not editable (for now)
return false if reflection.macro == :has_one || reflection.options[:polymorphic]
return false unless reflection.options[:accessible]
record = if (through = reflection.through_reflection)
# For edit permission on a has_many :through,
# the user needs create+destroy permission on the join model
send(through.name).new_candidate
else
# For edit permission on a regular has_many,
# the user needs create/destroy permission on the member model
send(reflection.name).new_candidate
end
record.creatable_by?(user) && record.destroyable_by?(user)
end
# ----------------------- #
# --- Permission Declaration Helpers --- #
def only_changed?(*attributes)
attributes = attributes.map do |attr|
with_attribute_or_belongs_to_keys(attr) { |a, ftype| ftype ? [a, ftype] : a }
end.flatten
changed.all? { |attr| attributes.include?(attr) }
end
def none_changed?(*attributes)
attributes = attributes.map do |attr|
with_attribute_or_belongs_to_keys(attr) { |a, ftype| ftype ? [a, ftype] : a }
end.flatten
attributes.all? { |attr| !changed.include?(attr) }
end
def any_changed?(*attributes)
attributes.any? do |attr|
with_attribute_or_belongs_to_keys(attr) do |a, ftype|
if ftype
changed.include?(a) || changed.include?(ftype)
else
changed.include?(a)
end
end
end
end
def all_changed?(*attributes)
attributes = prepare_attributes_for_change_helpers(attributes)
attributes.all? do |attr|
with_attribute_or_belongs_to_keys(attr) do |a, ftype|
if ftype
changed.include?(a) || changed.include?(ftype)
else
changed.include?(a)
end
end
end
end
def with_attribute_or_belongs_to_keys(attribute)
if (refl = self.class.reflections[attribute.to_sym]) && refl.macro == :belongs_to
if refl.options[:polymorphic]
yield refl.primary_key_name, refl.options[:foreign_type]
else
yield refl.primary_key_name, nil
end
else
yield attribute.to_s, nil
end
end
# -------------------------------------- #
# --- Default *_permitted? methods --- #
# Conservative default permissions #
def create_permitted?; false end
def update_permitted?; false end
def destroy_permitted?; false end
# Allow viewing by default
def view_permitted?(attribute) true end
# By default, attempt to derive edit permission from create/update permission
def edit_permitted?(attribute)
unknownify_attribute(attribute) if attribute
new_record? ? create_permitted? : update_permitted?
rescue Hobo::UndefinedAccessError
# The permission is dependent on the unknown value
# so this attribute is not editable
false
ensure
deunknownify_attribute(attribute) if attribute
end
# Add some singleton methods to +record+ to give the effect that +attribute+ is unknown. That is,
# attempts to access the attribute will result in a Hobo::UndefinedAccessError
def unknownify_attribute(attr)
metaclass.class_eval do
define_method attr do
raise Hobo::UndefinedAccessError
end
end
if (refl = self.class.reflections[attr.to_sym]) && refl.macro == :belongs_to
# A belongs_to -- also unknownify the underlying fields
unknownify_attribute refl.primary_key_name
unknownify_attribute refl.options[:foreign_type] if refl.options[:polymorphic]
else
# A regular field -- hack the dirty tracking methods
metaclass.class_eval do
define_method "#{attr}_change" do
raise Hobo::UndefinedAccessError
end
define_method "#{attr}_was" do
read_attribute attr
end
define_method "#{attr}_changed?" do
true
end
def changed?
true
end
define_method :changed do
changed_attributes.keys | [attr.to_s]
end
def changes
raise Hobo::UndefinedAccessError
end
end
end
end
# Best. Name. Ever
def deunknownify_attribute(attr)
attr = attr.to_sym
metaclass.send :remove_method, attr
if (refl = self.class.reflections[attr]) && refl.macro == :belongs_to
# A belongs_to -- restore the underlying fields
deunknownify_attribute refl.primary_key_name
deunknownify_attribute refl.options[:foreign_type] if refl.options[:polymorphic]
else
# A regular field -- restore the dirty tracking methods
["#{attr}_change", "#{attr}_was", "#{attr}_changed?", :changed?, :changed, :changes].each do |m|
metaclass.send :remove_method, m.to_sym
end
end
end
end
end
module Hobo
module Permissions
module Associations
def self.enable
# Re-open AR classes...
ActiveRecord::Associations::HasManyAssociation.class_eval do
# Helper - the user acting on the owner (if there is one)
def acting_user
@owner.acting_user if @owner.is_a?(Hobo::Permissions)
end
def delete_records(records)
case @reflection.options[:dependent]
when :destroy
records.each { |r| r.is_a?(Hobo::Permissions) ? r.user_destroy(acting_user) : r.destroy }
when :delete_all
# No destroy permission check if the :delete_all option has been chosen
@reflection.klass.delete(records.map(&:id))
else
nullify_keys(records)
end
end
# Set the fkey used by this has_many to null on the passed records, checking for permission first if both the owner
# and record in question are Hobo models
def nullify_keys(records)
if (user = acting_user)
records.each { |r| r.user_update_attributes!(user, @reflection.primary_key_name => nil) if r.is_a?(Hobo::Permissions) }
end
# Normal ActiveRecord implementatin
ids = quoted_record_ids(records)
@reflection.klass.update_all(
"#{@reflection.primary_key_name} = NULL",
"#{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
)
end
def insert_record(record)
set_belongs_to_association_for(record)
if (user = acting_user) && record.is_a?(Hobo::Permissions)
record.user_save(user)
else
record.save
end
end
def viewable_by?(user, field=nil)
# view check on an example member record is not supported on associations with conditions
return true if @reflection.options[:conditions]
new_candidate.viewable_by?(user, field)
end
end
ActiveRecord::Associations::HasManyThroughAssociation.class_eval do
def acting_user
@owner.acting_user if @owner.is_a?(Hobo::Permissions)
end
def create!(attrs = nil)
klass = @reflection.klass
user = acting_user if klass < Hobo::Permissions
klass.transaction do
object = if attrs
klass.send(:with_scope, :create => attrs) { user ? klass.user_create!(user) : klass.create! }
else
user ? klass.user_create!(user) : klass.create!
end
self << object
object
end
end
def create!(attrs = nil)
klass = @reflection.klass
user = acting_user if klass < Hobo::Permissions
klass.transaction do
object = if attrs
klass.send(:with_scope, :create => attrs) { user ? klass.user_create(user) : klass.create }
else
user ? klass.user_create(user) : klass.create
end
self << object
object
end
end
def insert_record(record, force=true)
user = acting_user if record.is_a?(Hobo::Permissions)
if record.new_record?
if force
user ? record.user_save!(user) : record.save!
else
return false unless (user ? record.user_save(user) : record.save)
end
end
klass = @reflection.through_reflection.klass
@owner.send(@reflection.through_reflection.name).proxy_target <<
klass.send(:with_scope, :create => construct_join_attributes(record)) { user ? klass.user_create!(user) : klass.create! }
end
# TODO - add dependent option support
def delete_records_with_hobo_permission_check(records)
klass = @reflection.through_reflection.klass
user = acting_user
if user && records.any? { |r|
joiner = klass.find(:first, :conditions => construct_join_attributes(r))
joiner.is_a?(Hobo::Permissions) && !joiner.destroyable_by?(user)
}
raise Hobo::PermissionDeniedError, "#{@owner.class}##{proxy_reflection.name}.destroy"
end
delete_records_without_hobo_permission_check(records)
end
alias_method_chain :delete_records, :hobo_permission_check
end
ActiveRecord::Associations::AssociationCollection.class_eval do
# Helper - the user acting on the owner (if there is one)
def acting_user
@owner.acting_user if @owner.is_a?(Hobo::Permissions)
end
def create(attrs = {})
if attrs.is_a?(Array)
attrs.collect { |attr| create(attr) }
else
create_record(attrs) do |record|
yield(record) if block_given?
user = acting_user if record.is_a?(Hobo::Permissions)
user ? record.user_save(user) : record.save
end
end
end
def create!(attrs = {})
create_record(attrs) do |record|
yield(record) if block_given?
user = acting_user if record.is_a?(Hobo::Permissions)
user ? record.user_save!(user) : record.save!
end
end
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment