# Authorization::AuthorizationInModel require File.dirname(__FILE__) + '/authorization.rb' require File.dirname(__FILE__) + '/obligation_scope.rb' module Authorization module AuthorizationInModel # If the user meets the given privilege, permitted_to? returns true # and yields to the optional block. def permitted_to? (privilege, options = {}, &block) options = { :user => Authorization.current_user, :object => self }.merge(options) Authorization::Engine.instance.permit?(privilege, {:user => options[:user], :object => options[:object]}, &block) end # Works similar to the permitted_to? method, but doesn't accept a block # and throws the authorization exceptions, just like Engine#permit! def permitted_to! (privilege, options = {} ) options = { :user => Authorization.current_user, :object => self }.merge(options) Authorization::Engine.instance.permit!(privilege, {:user => options[:user], :object => options[:object]}) end def self.included(base) # :nodoc: #base.extend(ClassMethods) base.module_eval do scopes[:with_permissions_to] = lambda do |parent_scope, *args| options = args.last.is_a?(Hash) ? args.pop : {} privilege = (args[0] || :read).to_sym privileges = [privilege] context = if options[:context] options[:context] elsif parent_scope.respond_to?(:proxy_reflection) parent_scope.proxy_reflection.klass.name.tableize.to_sym elsif parent_scope.respond_to?(:decl_auth_context) parent_scope.decl_auth_context else parent_scope.name.tableize.to_sym end user = options[:user] || Authorization.current_user engine = options[:engine] || Authorization::Engine.instance engine.permit!(privileges, :user => user, :skip_attribute_test => true, :context => context) obligation_scope_for( privileges, :user => user, :context => context, :engine => engine, :model => parent_scope) end # Builds and returns a scope with joins and conditions satisfying all obligations. def self.obligation_scope_for( privileges, options = {} ) options = { :user => Authorization.current_user, :context => nil, :model => self, :engine => nil, }.merge(options) engine = options[:engine] || Authorization::Engine.instance obligation_scope = ObligationScope.new( options[:model], {} ) engine.obligations( privileges, :user => options[:user], :context => options[:context] ).each do |obligation| obligation_scope.parse!( obligation ) end obligation_scope.scope end # Named scope for limiting query results according to the authorization # of the current user. If no privilege is given, :+read+ is assumed. # # User.with_permissions_to # User.with_permissions_to(:update) # User.with_permissions_to(:update, :context => :users) # # As in the case of other named scopes, this one may be chained: # User.with_permission_to.find(:all, :conditions...) # # Options # [:+context+] # Context for the privilege to be evaluated in; defaults to the # model's table name. # [:+user+] # User to be used for gathering obligations; defaults to the # current user. # def self.with_permissions_to (*args) scopes[:with_permissions_to].call(self, *args) end # Activates model security for the current model. Then, CRUD operations # are checked against the authorization of the current user. The # privileges are :+create+, :+read+, :+update+ and :+delete+ in the # context of the model. By default, :+read+ is not checked because of # performance impacts, especially with large result sets. # # class User < ActiveRecord::Base # using_access_control # end # # If an operation is not permitted, a Authorization::AuthorizationError # is raised. # # To activate model security on all models, call using_access_control # on ActiveRecord::Base # ActiveRecord::Base.using_access_control # # Available options # [:+context+] Specify context different from the models table name. # [:+include_read+] Also check for :+read+ privilege after find. # def self.using_access_control (options = {}) options = { :context => nil, :include_read => false }.merge(options) class_eval do [:create, :update, [:destroy, :delete]].each do |action, privilege| send(:"before_#{action}") do |object| Authorization::Engine.instance.permit!(privilege || action, :object => object, :context => options[:context]) end end if options[:include_read] # after_find is only called if after_find is implemented after_find do |object| Authorization::Engine.instance.permit!(:read, :object => object, :context => options[:context]) end #Patch - Taking it down to attribute level #Protect ActiveRecord-attribute # By calling this method, the ActiveRecord-attribute "name" is protected # * reading requires :read_name or :read # * writing requires :write_name or :write # This method is called for all ActiveRecord attributes by default - in contrast to def protect_attribute(name) this methods # uses def read_attribute(:name) and write_attribute(:name,value) as fall-backs def self.protect_ar_attribute(name) #Alias old methods (if no alias is available), override methods class_eval <<-EOL alias_method :no_acl_#{name}, :#{name} if respond_to?(:#{name}) && !respond_to(:no_acl_#{name}) alias_method :no_acl_#{name}=, :#{name}= if respond_to?(:#{name}=) && !respond_to(:no_acl_#{name}=) def #{name}() permitted_to!(:read_#{name}) if !permitted_to?(:read) if(respond_to?(:no_acl_#{name})) return send(:no_acl_#{name}) else return read_attribute(:no_acl_#{name}) end end def #{name}=(v) permitted_to!(:read_#{write}) if !permitted_to?(:write) if(respond_to?(:no_acl_#{name}=)) return send(:no_acl_#{name}=,v) else return write_attribute(:no_acl_#{name}) end end EOL end #Protect attribute # By calling this method, the attribute "name" is protected # * reading requires :read_name or :read # * writing requires :write_name or :write def protect_attribute(name) #Protecting an attributes means protecting its setter and getters protect_method(name,:read) protect_method("#{name}=",:write) end #Protect method from beeing called without permission # * name: Name of method # * mode: :read or :write (or anything else in obscure scenarios) def protect_instance_method(name,mode) #Alias old method (if no alias is available), override method class_eval <<-EOL alias_method :no_acl_#{name}, :#{name} if !respond_to(:no_acl_#{name}) EOL instance_eval <<-EOL def #{name}(*args,&block) permitted_to!(:#{mode}_#{name}) if !permitted_to?(:#{mode}) return send(:#{name},*args,&block) end EOL end if(options[:include_attributes]) #If attribute / getter-setter-access ought to be checekd# #Try to parse input - if there's any #Provide defaults require_read_for = [] require_write_for = [] whitelist = [] #try reading parameters begin require_read_for = options[:include_attributes][0][:require_read_for] || [] require_write_for = options[:include_attributes][0][:require_write_for] || [] whitelist = options[:include_attributes][0][:whitelist] || [] rescue;end #convert arrays to sets require_read_for = require_read_for.to_set require_write_for = require_write_for.to_set whitelist = whitelist.to_set #Enable callback for instance-level meta programming def after_initialize; end #1 Generate guards for ar-attributes column_names.each do |name| protect_ar_attribute(name) unless name.to_s == self.primary_key.to_s || whitelist.include?(name) end #2 Evaluate :require_read_for, :require_write_for if protect_attributes after_initialize do |object| require_write_for.each {|attr| object.protect_instance_method(attr,:write) } require_read_for.each {|attr| object.protect_instance_method(attr,:read) } end end #3 Generate guards for ar-proxies after_initialize do |object| end #2nd Generate guards for ar-proxies after_initialize do |object| reflect_on_all_associations.each do |assoc| #Respect excludes #Ok, we've to intercept these calls (See: ActiveRecord::Associations::ClassMethods) # one-to-one: other_id, other_id=(id), other, other=(other), build_other(attributes={}), create_other(attributes={}) # one-to-many / many-to-many: others, others=(other,other,...), other_ids, other_ids=(id,id,...), others<< object.inject_acl_object_getter_setter(assoc.name.to_s) unless whitelist.include?(assoc.name) if(assoc.collection?) #Collection? if so, many-to-many case object.protect_instance_method("#{assoc.name.to_s.singularize}_ids",:read) unless whitelist.include?("#{assoc.name.to_s.singularize}_ids".to_sym) object.protect_instance_method("#{assoc.name.to_s.singularize}_ids",:write) unless whitelist.include?("#{assoc.name.to_s.singularize}_ids".to_sym) #inject_acl_write_check("#{assoc.name}<<") else object.protect_instance_method("#{assoc.name}_id",:read) unless assoc.macro != :has_one || whitelist.include?("#{assoc.name}_id".to_sym) object.protect_instance_method("#{assoc.name}_id",:write) unless assoc.macro != :has_one || whitelist.include?("#{assoc.name}_id".to_sym) object.protect_instance_method("build_#{assoc.name}",:write) unless whitelist.include?("build_#{assoc.name}".to_sym) object.protect_instance_method("create_#{assoc.name}",:read) unless whitelist.include?("create_#{assoc.name}".to_sym) end end end def readable_attributes #Define Attribute - Arrays as a replacement for model.attributes return attributes if permitted_to?(:read) attributes.reject do |k,v| !permitted_to?("read_#{k}".to_sym) && k.to_s != self.class.primary_key.to_s end end def writable_attributes return attributes if permitted_to?(:write) attributes.reject do |k,v| !permitted_to?("write_#{k}#".to_sym) && k.to_s != self.class.primary_key.to_s end end end # #End patch - Taking it down to attribute level if Rails.version < "3" def after_find; end end end def self.using_access_control? true end end end # Returns true if the model is using model security. def self.using_access_control? false end end end end end