diff --git a/cancan.gemspec b/cancan.gemspec index fcf362c..4cabd66 100644 --- a/cancan.gemspec +++ b/cancan.gemspec @@ -15,6 +15,9 @@ Gem::Specification.new do |s| s.add_development_dependency 'rr', '~> 0.10.11' # 1.0.0 has respond_to? issues: http://github.com/btakita/rr/issues/issue/43 s.add_development_dependency 'supermodel', '~> 0.1.4' + s.add_development_dependency 'mongoid', '~> 2.0.0.beta.19' + s.add_development_dependency 'bson_ext', '~> 1.1' + s.rubyforge_project = s.name s.required_rubygems_version = ">= 1.3.4" end diff --git a/lib/cancan.rb b/lib/cancan.rb index 02cd75e..8df4caa 100644 --- a/lib/cancan.rb +++ b/lib/cancan.rb @@ -3,6 +3,7 @@ require 'cancan/can_definition' require 'cancan/controller_resource' require 'cancan/controller_additions' require 'cancan/active_record_additions' +require 'cancan/mongoid_additions' require 'cancan/exceptions' require 'cancan/query' require 'cancan/inherited_resource' diff --git a/lib/cancan/mongoid_additions.rb b/lib/cancan/mongoid_additions.rb new file mode 100644 index 0000000..c08c790 --- /dev/null +++ b/lib/cancan/mongoid_additions.rb @@ -0,0 +1,78 @@ +module CanCan + + module Ability + alias_method :query_without_mongoid_support, :query + def query(action, subject) + if Object.const_defined?(:Mongoid) && subject <= CanCan::MongoidAdditions + query_with_mongoid_support(action, subject) + else + query_without_mongoid_support(action, subject) + end + end + + def query_with_mongoid_support(action, subject) + MongoidQuery.new(subject, relevant_can_definitions_for_query(action, subject)) + end + end + + class MongoidQuery + def initialize(sanitizer, can_definitions) + @sanitizer = sanitizer + @can_definitions = can_definitions + end + + def conditions + @can_definitions.first.try(:tableized_conditions) + end + end + + module MongoidAdditions + module ClassMethods + # Returns a scope which fetches only the records that the passed ability + # can perform a given action on. The action defaults to :read. This + # is usually called from a controller and passed the +current_ability+. + # + # @articles = Article.accessible_by(current_ability) + # + # Here only the articles which the user is able to read will be returned. + # If the user does not have permission to read any articles then an empty + # result is returned. Since this is a scope it can be combined with any + # other scopes or pagination. + # + # An alternative action can optionally be passed as a second argument. + # + # @articles = Article.accessible_by(current_ability, :update) + # + # Here only the articles which the user can update are returned. This + # internally uses Ability#conditions method, see that for more information. + def accessible_by(ability, action = :read) + query = ability.query(action, self) + if query.conditions.blank? + # this query is sure to return no results + # we need this so there is a Mongoid::Criteria object to return, since an empty array would cause problems + where({:_id => {'$exists' => true, '$type' => 2}}) + else + where(query.conditions) + end + end + end + + def self.included(base) + base.extend ClassMethods + end + end +end + +# Info on monkeypatching Mongoid : +# http://log.mazniak.org/post/719062325/monkey-patching-activesupport-concern-and-you#footer +if defined? Mongoid + module Mongoid + module Components + old_block = @_included_block + @_included_block = Proc.new do + class_eval(&old_block) if old_block + include CanCan::MongoidAdditions + end + end + end +end \ No newline at end of file diff --git a/spec/cancan/mongoid_additions_spec.rb b/spec/cancan/mongoid_additions_spec.rb new file mode 100644 index 0000000..d25785d --- /dev/null +++ b/spec/cancan/mongoid_additions_spec.rb @@ -0,0 +1,67 @@ +require "spec_helper" +require 'mongoid' + +class MongoidCategory + include Mongoid::Document + references_many :mongoid_projects +end + +class MongoidProject + include Mongoid::Document + + referenced_in :mongoid_category + + class << self + protected + + def sanitize_sql(hash_cond) + hash_cond + end + + def sanitize_hash(hash) + hash.map do |name, value| + if Hash === value + sanitize_hash(value).map{|cond| "#{name}.#{cond}"} + else + "#{name}=#{value}" + end + end.flatten + end + end +end + +Mongoid.configure do |config| + config.master = Mongo::Connection.new('127.0.0.1', 27017).db("workflow_on_mongoid") +end + +describe CanCan::MongoidAdditions do + before(:each) do + @model_class = Class.new(MongoidProject) + stub(@model_class).scoped { :scoped_stub } + @model_class.send(:include, CanCan::MongoidAdditions) + @ability = Object.new + @ability.extend(CanCan::Ability) + end + + after(:each) do + Mongoid.master.collections.select do |collection| + collection.name !~ /system/ + end.each(&:drop) + end + + it "should return [] when no ability is defined so no records are found" do + @model_class.accessible_by(@ability, :read).should == [] + end + + it "should call where with matching ability conditions" do + @ability.can :read, @model_class, :foo => {:bar => 1} + @model_class.accessible_by(@ability, :read).should == @model_class.where(:foos => { :bar => 1 }) + end + + it "should not allow to fetch records when ability with just block present" do + @ability.can :read, @model_class do false end + lambda { + @model_class.accessible_by(@ability) + }.should raise_error(CanCan::Error) + end +end \ No newline at end of file