cancan 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. data/CHANGELOG.rdoc +21 -0
  2. data/Gemfile +17 -0
  3. data/LICENSE +1 -1
  4. data/README.rdoc +20 -90
  5. data/Rakefile +8 -0
  6. data/lib/cancan/ability.rb +24 -26
  7. data/lib/cancan/controller_additions.rb +50 -0
  8. data/lib/cancan/controller_resource.rb +33 -15
  9. data/lib/cancan/exceptions.rb +3 -0
  10. data/lib/cancan/model_adapters/abstract_adapter.rb +40 -0
  11. data/lib/cancan/model_adapters/active_record_adapter.rb +119 -0
  12. data/lib/cancan/model_adapters/data_mapper_adapter.rb +33 -0
  13. data/lib/cancan/model_adapters/default_adapter.rb +7 -0
  14. data/lib/cancan/model_adapters/mongoid_adapter.rb +41 -0
  15. data/lib/cancan/{active_record_additions.rb → model_additions.rb} +5 -16
  16. data/lib/cancan/{can_definition.rb → rule.rb} +29 -25
  17. data/lib/cancan.rb +8 -3
  18. data/lib/generators/cancan/ability/USAGE +4 -0
  19. data/lib/generators/cancan/ability/ability_generator.rb +11 -0
  20. data/lib/generators/cancan/ability/templates/ability.rb +28 -0
  21. data/spec/README.rdoc +28 -0
  22. data/spec/cancan/ability_spec.rb +11 -3
  23. data/spec/cancan/controller_additions_spec.rb +30 -0
  24. data/spec/cancan/controller_resource_spec.rb +68 -2
  25. data/spec/cancan/inherited_resource_spec.rb +3 -1
  26. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +185 -0
  27. data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +115 -0
  28. data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
  29. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +168 -0
  30. data/spec/cancan/rule_spec.rb +39 -0
  31. data/spec/spec_helper.rb +2 -24
  32. metadata +24 -16
  33. data/lib/cancan/query.rb +0 -97
  34. data/spec/cancan/active_record_additions_spec.rb +0 -75
  35. data/spec/cancan/can_definition_spec.rb +0 -57
  36. data/spec/cancan/query_spec.rb +0 -107
@@ -0,0 +1,33 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class DataMapperAdapter < AbstractAdapter
4
+ def self.for_class?(model_class)
5
+ model_class <= DataMapper::Resource
6
+ end
7
+
8
+ def self.override_conditions_hash_matching?(subject, conditions)
9
+ conditions.any? { |k,v| !k.kind_of?(Symbol) }
10
+ end
11
+
12
+ def self.matches_conditions_hash?(subject, conditions)
13
+ subject.class.all(:conditions => conditions).include?(subject) # TODO don't use a database query here for performance and other instances
14
+ end
15
+
16
+ def database_records
17
+ scope = @model_class.all(:conditions => ["0=1"])
18
+ conditions.each do |condition|
19
+ scope += @model_class.all(:conditions => condition)
20
+ end
21
+ scope
22
+ end
23
+
24
+ def conditions
25
+ @rules.map(&:conditions)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ DataMapper::Model.class_eval do
32
+ include CanCan::ModelAdditions::ClassMethods
33
+ end
@@ -0,0 +1,7 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class DefaultAdapter < AbstractAdapter
4
+ # This adapter is used when no matching adapter is found
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class MongoidAdapter < AbstractAdapter
4
+ def self.for_class?(model_class)
5
+ model_class <= Mongoid::Document
6
+ end
7
+
8
+ def self.override_conditions_hash_matching?(subject, conditions)
9
+ conditions.any? { |k,v| !k.kind_of?(Symbol) }
10
+ end
11
+
12
+ def self.matches_conditions_hash?(subject, conditions)
13
+ # To avoid hitting the db, retrieve the raw Mongo selector from
14
+ # the Mongoid Criteria and use Mongoid::Matchers#matches?
15
+ subject.matches?( subject.class.where(conditions).selector )
16
+ end
17
+
18
+ def database_records
19
+ @model_class.where(conditions)
20
+ end
21
+
22
+ def conditions
23
+ if @rules.size == 0
24
+ false_query
25
+ else
26
+ @rules.first.conditions
27
+ end
28
+ end
29
+
30
+ def false_query
31
+ # this query is sure to return no results
32
+ {:_id => {'$exists' => false, '$type' => 7}} # type 7 is an ObjectID (default for _id)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ # simplest way to add `accessible_by` to all Mongoid Documents
39
+ module Mongoid::Document::ClassMethods
40
+ include CanCan::ModelAdditions::ClassMethods
41
+ end
@@ -1,6 +1,7 @@
1
1
  module CanCan
2
- # This module is automatically included into all Active Record models.
3
- module ActiveRecordAdditions
2
+
3
+ # This module adds the accessible_by class method to a model. It is included in the model adapters.
4
+ module ModelAdditions
4
5
  module ClassMethods
5
6
  # Returns a scope which fetches only the records that the passed ability
6
7
  # can perform a given action on. The action defaults to :read. This
@@ -17,15 +18,9 @@ module CanCan
17
18
  #
18
19
  # @articles = Article.accessible_by(current_ability, :update)
19
20
  #
20
- # Here only the articles which the user can update are returned. This
21
- # internally uses Ability#conditions method, see that for more information.
21
+ # Here only the articles which the user can update are returned.
22
22
  def accessible_by(ability, action = :read)
23
- query = ability.query(action, self)
24
- if respond_to?(:where) && respond_to?(:joins)
25
- where(query.conditions).joins(query.joins)
26
- else
27
- scoped(:conditions => query.conditions, :joins => query.joins)
28
- end
23
+ ability.model_adapter(self, action).database_records
29
24
  end
30
25
  end
31
26
 
@@ -34,9 +29,3 @@ module CanCan
34
29
  end
35
30
  end
36
31
  end
37
-
38
- if defined? ActiveRecord
39
- ActiveRecord::Base.class_eval do
40
- include CanCan::ActiveRecordAdditions
41
- end
42
- end
@@ -2,8 +2,8 @@ module CanCan
2
2
  # This class is used internally and should only be called through Ability.
3
3
  # it holds the information about a "can" call made on Ability and provides
4
4
  # helpful methods to determine permission checking and conditions hash generation.
5
- class CanDefinition # :nodoc:
6
- attr_reader :base_behavior, :actions
5
+ class Rule # :nodoc:
6
+ attr_reader :base_behavior, :actions, :conditions
7
7
  attr_writer :expanded_actions
8
8
 
9
9
  # The first argument when initializing is the base_behavior which is a true/false
@@ -41,18 +41,6 @@ module CanCan
41
41
  end
42
42
  end
43
43
 
44
- def tableized_conditions(conditions = @conditions)
45
- return conditions unless conditions.kind_of? Hash
46
- conditions.inject({}) do |result_hash, (name, value)|
47
- if value.kind_of? Hash
48
- name = name.to_s.tableize.to_sym
49
- value = tableized_conditions(value)
50
- end
51
- result_hash[name] = value
52
- result_hash
53
- end
54
- end
55
-
56
44
  def only_block?
57
45
  conditions_empty? && !@block.nil?
58
46
  end
@@ -100,19 +88,31 @@ module CanCan
100
88
  @subjects.any? { |sub| sub.kind_of?(Module) && (subject.kind_of?(sub) || subject.class.to_s == sub.to_s || subject.kind_of?(Module) && subject.ancestors.include?(sub)) }
101
89
  end
102
90
 
91
+ # Checks if the given subject matches the given conditions hash.
92
+ # This behavior can be overriden by a model adapter by defining two class methods:
93
+ # override_matching_for_conditions?(subject, conditions) and
94
+ # matches_conditions_hash?(subject, conditions)
103
95
  def matches_conditions_hash?(subject, conditions = @conditions)
104
- conditions.all? do |name, value|
105
- attribute = subject.send(name)
106
- if value.kind_of?(Hash)
107
- if attribute.kind_of? Array
108
- attribute.any? { |element| matches_conditions_hash? element, value }
109
- else
110
- matches_conditions_hash? attribute, value
111
- end
112
- elsif value.kind_of?(Array) || value.kind_of?(Range)
113
- value.include? attribute
96
+ if conditions.empty?
97
+ true
98
+ else
99
+ if model_adapter(subject).override_conditions_hash_matching? subject, conditions
100
+ model_adapter(subject).matches_conditions_hash? subject, conditions
114
101
  else
115
- attribute == value
102
+ conditions.all? do |name, value|
103
+ attribute = subject.send(name)
104
+ if value.kind_of?(Hash)
105
+ if attribute.kind_of? Array
106
+ attribute.any? { |element| matches_conditions_hash? element, value }
107
+ else
108
+ matches_conditions_hash? attribute, value
109
+ end
110
+ elsif value.kind_of?(Array) || value.kind_of?(Range)
111
+ value.include? attribute
112
+ else
113
+ attribute == value
114
+ end
115
+ end
116
116
  end
117
117
  end
118
118
  end
@@ -129,5 +129,9 @@ module CanCan
129
129
  @block.call(action, subject.class, subject, *extra_args)
130
130
  end
131
131
  end
132
+
133
+ def model_adapter(subject)
134
+ ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
135
+ end
132
136
  end
133
137
  end
data/lib/cancan.rb CHANGED
@@ -1,8 +1,13 @@
1
1
  require 'cancan/ability'
2
- require 'cancan/can_definition'
2
+ require 'cancan/rule'
3
3
  require 'cancan/controller_resource'
4
4
  require 'cancan/controller_additions'
5
- require 'cancan/active_record_additions'
5
+ require 'cancan/model_additions'
6
6
  require 'cancan/exceptions'
7
- require 'cancan/query'
8
7
  require 'cancan/inherited_resource'
8
+
9
+ require 'cancan/model_adapters/abstract_adapter'
10
+ require 'cancan/model_adapters/default_adapter'
11
+ require 'cancan/model_adapters/active_record_adapter' if defined? ActiveRecord
12
+ require 'cancan/model_adapters/data_mapper_adapter' if defined? DataMapper
13
+ require 'cancan/model_adapters/mongoid_adapter' if defined? Mongoid
@@ -0,0 +1,4 @@
1
+ Description:
2
+ The cancan:ability generator creates an Ability class in the models
3
+ directory. You can move this file anywhere you want as long as it
4
+ is in the load path.
@@ -0,0 +1,11 @@
1
+ module Cancan
2
+ module Generators
3
+ class AbilityGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ def generate_ability
7
+ copy_file "ability.rb", "app/models/ability.rb"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ class Ability
2
+ include CanCan::Ability
3
+
4
+ def initialize(user)
5
+ # Define abilities for the passed in user here. For example:
6
+ #
7
+ # user ||= User.new # guest user (not logged in)
8
+ # if user.admin?
9
+ # can :manage, :all
10
+ # else
11
+ # can :read, :all
12
+ # end
13
+ #
14
+ # The first argument to `can` is the action you are giving the user permission to do.
15
+ # If you pass :manage it will apply to every action. Other common actions here are
16
+ # :read, :create, :update and :destroy.
17
+ #
18
+ # The second argument is the resource the user can perform the action on. If you pass
19
+ # :all it will apply to every resource. Otherwise pass a Ruby class of the resource.
20
+ #
21
+ # The third argument is an optional hash of conditions to further filter the objects.
22
+ # For example, here the user can only update published articles.
23
+ #
24
+ # can :update, Article, :published => true
25
+ #
26
+ # See the wiki for details: https://github.com/ryanb/cancan/wiki/Defining-Abilities
27
+ end
28
+ end
data/spec/README.rdoc ADDED
@@ -0,0 +1,28 @@
1
+ = CanCan Specs
2
+
3
+ == Running the specs
4
+
5
+ To run the specs first run the +bundle+ command to install the necessary gems and the +rake+ command to run the specs.
6
+
7
+ bundle
8
+ rake
9
+
10
+ The specs currently require Ruby 1.8.7. Ruby 1.9.2 support will be coming soon.
11
+
12
+
13
+ == Model Adapters
14
+
15
+ CanCan offers separate specs for different model adapters (such as Mongoid and Data Mapper). By default it will use Active Record but you can change this by setting the +MODEL_ADAPTER+ environment variable before running. You can run the +bundle+ command with this as well to ensure you have the installed gems.
16
+
17
+ MODEL_ADAPTER=data_mapper bundle
18
+ MODEL_ADAPTER=data_mapper rake
19
+
20
+ The different model adapters you can specify are:
21
+
22
+ * active_record (default)
23
+ * data_mapper
24
+ * mongoid
25
+
26
+ You can also run the +spec_all+ rake task to run specs for each adapter.
27
+
28
+ rake spec_all
@@ -32,7 +32,7 @@ describe CanCan::Ability do
32
32
  @ability.can?(:read, Symbol).should be_true
33
33
  end
34
34
 
35
- it "should pass to previous can definition, if block returns false or nil" do
35
+ it "should pass to previous rule, if block returns false or nil" do
36
36
  @ability.can :read, Symbol
37
37
  @ability.can :read, Integer do |i|
38
38
  i < 5
@@ -144,7 +144,7 @@ describe CanCan::Ability do
144
144
  @ability.can?(:update, 123).should be_false
145
145
  end
146
146
 
147
- it "should support custom objects in the can definition" do
147
+ it "should support custom objects in the rule" do
148
148
  @ability.can :read, :stats
149
149
  @ability.can?(:read, :stats).should be_true
150
150
  @ability.can?(:update, :stats).should be_false
@@ -165,7 +165,7 @@ describe CanCan::Ability do
165
165
  @ability.can?(:read, 123).should be_false
166
166
  end
167
167
 
168
- it "should pass to previous can definition, if block returns false or nil" do
168
+ it "should pass to previous rule, if block returns false or nil" do
169
169
  @ability.can :read, :all
170
170
  @ability.cannot :read, Integer do |int|
171
171
  int > 10 ? nil : ( int > 5 )
@@ -343,6 +343,14 @@ describe CanCan::Ability do
343
343
  end
344
344
  end
345
345
 
346
+ it "should determine model adapter class by asking AbstractAdapter" do
347
+ model_class = Object.new
348
+ adapter_class = Object.new
349
+ stub(CanCan::ModelAdapters::AbstractAdapter).adapter_class(model_class) { adapter_class }
350
+ stub(adapter_class).new(model_class, []) { :adapter_instance }
351
+ @ability.model_adapter(model_class, :read).should == :adapter_instance
352
+ end
353
+
346
354
  describe "unauthorized message" do
347
355
  after(:each) do
348
356
  I18n.backend = nil
@@ -83,4 +83,34 @@ describe CanCan::ControllerAdditions do
83
83
  stub(@controller.class).ancestors { ["InheritedResources::Actions"] }
84
84
  @controller.class.cancan_resource_class.should == CanCan::InheritedResource
85
85
  end
86
+
87
+ it "cancan_skipper should be an empty hash with :authorize and :load options and remember changes" do
88
+ @controller_class.cancan_skipper.should == {:authorize => {}, :load => {}}
89
+ @controller_class.cancan_skipper[:load] = true
90
+ @controller_class.cancan_skipper[:load].should == true
91
+ end
92
+
93
+ it "skip_authorize_resource should add itself to the cancan skipper with given model name and options" do
94
+ @controller_class.skip_authorize_resource(:project, :only => [:index, :show])
95
+ @controller_class.cancan_skipper[:authorize][:project].should == {:only => [:index, :show]}
96
+ @controller_class.skip_authorize_resource(:only => [:index, :show])
97
+ @controller_class.cancan_skipper[:authorize][nil].should == {:only => [:index, :show]}
98
+ @controller_class.skip_authorize_resource(:article)
99
+ @controller_class.cancan_skipper[:authorize][:article].should == {}
100
+ end
101
+
102
+ it "skip_load_resource should add itself to the cancan skipper with given model name and options" do
103
+ @controller_class.skip_load_resource(:project, :only => [:index, :show])
104
+ @controller_class.cancan_skipper[:load][:project].should == {:only => [:index, :show]}
105
+ @controller_class.skip_load_resource(:only => [:index, :show])
106
+ @controller_class.cancan_skipper[:load][nil].should == {:only => [:index, :show]}
107
+ @controller_class.skip_load_resource(:article)
108
+ @controller_class.cancan_skipper[:load][:article].should == {}
109
+ end
110
+
111
+ it "skip_load_and_authore_resource should add itself to the cancan skipper with given model name and options" do
112
+ @controller_class.skip_load_and_authorize_resource(:project, :only => [:index, :show])
113
+ @controller_class.cancan_skipper[:load][:project].should == {:only => [:index, :show]}
114
+ @controller_class.cancan_skipper[:authorize][:project].should == {:only => [:index, :show]}
115
+ end
86
116
  end
@@ -3,10 +3,12 @@ require "spec_helper"
3
3
  describe CanCan::ControllerResource do
4
4
  before(:each) do
5
5
  @params = HashWithIndifferentAccess.new(:controller => "projects")
6
- @controller = Object.new # simple stub for now
6
+ @controller_class = Class.new
7
+ @controller = @controller_class.new
7
8
  @ability = Ability.new(nil)
8
9
  stub(@controller).params { @params }
9
10
  stub(@controller).current_ability { @ability }
11
+ stub(@controller_class).cancan_skipper { {:authorize => {}, :load => {}} }
10
12
  end
11
13
 
12
14
  it "should load the resource into an instance variable if params[:id] is specified" do
@@ -91,6 +93,22 @@ describe CanCan::ControllerResource do
91
93
  @controller.instance_variable_defined?(:@projects).should be_false
92
94
  end
93
95
 
96
+ it "should not authorize single resource in collection action" do
97
+ @params[:action] = "index"
98
+ @controller.instance_variable_set(:@project, :some_project)
99
+ stub(@controller).authorize!(:index, Project) { raise CanCan::AccessDenied }
100
+ resource = CanCan::ControllerResource.new(@controller)
101
+ lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
102
+ end
103
+
104
+ it "should authorize parent resource in collection action" do
105
+ @params[:action] = "index"
106
+ @controller.instance_variable_set(:@category, :some_category)
107
+ stub(@controller).authorize!(:read, :some_category) { raise CanCan::AccessDenied }
108
+ resource = CanCan::ControllerResource.new(@controller, :category, :parent => true)
109
+ lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
110
+ end
111
+
94
112
  it "should perform authorization using controller action and loaded model" do
95
113
  @params[:action] = "show"
96
114
  @controller.instance_variable_set(:@project, :some_project)
@@ -245,7 +263,7 @@ describe CanCan::ControllerResource do
245
263
  @params.merge!(:action => "create", :project => {:name => "foobar"})
246
264
  category = Object.new
247
265
  @controller.instance_variable_set(:@category, category)
248
- stub(category).build_project { Project.new }
266
+ stub(category).build_project { |attributes| Project.new(attributes) }
249
267
  resource = CanCan::ControllerResource.new(@controller, :through => :category, :singleton => true)
250
268
  resource.load_resource
251
269
  @controller.instance_variable_get(:@project).name.should == "foobar"
@@ -323,4 +341,52 @@ describe CanCan::ControllerResource do
323
341
  CanCan::ControllerResource.new(@controller, :nested => :project)
324
342
  }.should raise_error(CanCan::ImplementationRemoved)
325
343
  end
344
+
345
+ it "should skip resource behavior for :only actions in array" do
346
+ stub(@controller_class).cancan_skipper { {:load => {nil => {:only => [:index, :show]}}} }
347
+ @params.merge!(:action => "index")
348
+ CanCan::ControllerResource.new(@controller).skip?(:load).should be_true
349
+ CanCan::ControllerResource.new(@controller, :some_resource).skip?(:load).should be_false
350
+ @params.merge!(:action => "show")
351
+ CanCan::ControllerResource.new(@controller).skip?(:load).should be_true
352
+ @params.merge!(:action => "other_action")
353
+ CanCan::ControllerResource.new(@controller).skip?(:load).should be_false
354
+ end
355
+
356
+ it "should skip resource behavior for :only one action on resource" do
357
+ stub(@controller_class).cancan_skipper { {:authorize => {:project => {:only => :index}}} }
358
+ @params.merge!(:action => "index")
359
+ CanCan::ControllerResource.new(@controller).skip?(:authorize).should be_false
360
+ CanCan::ControllerResource.new(@controller, :project).skip?(:authorize).should be_true
361
+ @params.merge!(:action => "other_action")
362
+ CanCan::ControllerResource.new(@controller, :project).skip?(:authorize).should be_false
363
+ end
364
+
365
+ it "should skip resource behavior :except actions in array" do
366
+ stub(@controller_class).cancan_skipper { {:load => {nil => {:except => [:index, :show]}}} }
367
+ @params.merge!(:action => "index")
368
+ CanCan::ControllerResource.new(@controller).skip?(:load).should be_false
369
+ @params.merge!(:action => "show")
370
+ CanCan::ControllerResource.new(@controller).skip?(:load).should be_false
371
+ @params.merge!(:action => "other_action")
372
+ CanCan::ControllerResource.new(@controller).skip?(:load).should be_true
373
+ CanCan::ControllerResource.new(@controller, :some_resource).skip?(:load).should be_false
374
+ end
375
+
376
+ it "should skip resource behavior :except one action on resource" do
377
+ stub(@controller_class).cancan_skipper { {:authorize => {:project => {:except => :index}}} }
378
+ @params.merge!(:action => "index")
379
+ CanCan::ControllerResource.new(@controller, :project).skip?(:authorize).should be_false
380
+ @params.merge!(:action => "other_action")
381
+ CanCan::ControllerResource.new(@controller).skip?(:authorize).should be_false
382
+ CanCan::ControllerResource.new(@controller, :project).skip?(:authorize).should be_true
383
+ end
384
+
385
+ it "should skip loading and authorization" do
386
+ stub(@controller_class).cancan_skipper { {:authorize => {nil => {}}, :load => {nil => {}}} }
387
+ @params.merge!(:action => "new")
388
+ resource = CanCan::ControllerResource.new(@controller)
389
+ lambda { resource.load_and_authorize_resource }.should_not raise_error
390
+ @controller.instance_variable_get(:@project).should be_nil
391
+ end
326
392
  end
@@ -3,10 +3,12 @@ require "spec_helper"
3
3
  describe CanCan::InheritedResource do
4
4
  before(:each) do
5
5
  @params = HashWithIndifferentAccess.new(:controller => "projects")
6
- @controller = Object.new # simple stub for now
6
+ @controller_class = Class.new
7
+ @controller = @controller_class.new
7
8
  @ability = Ability.new(nil)
8
9
  stub(@controller).params { @params }
9
10
  stub(@controller).current_ability { @ability }
11
+ stub(@controller_class).cancan_skipper { {:authorize => {}, :load => {}} }
10
12
  end
11
13
 
12
14
  it "show should load resource through @controller.resource" do
@@ -0,0 +1,185 @@
1
+ if ENV["MODEL_ADAPTER"].nil? || ENV["MODEL_ADAPTER"] == "active_record"
2
+ require "spec_helper"
3
+
4
+ RSpec.configure do |config|
5
+ config.extend WithModel
6
+ end
7
+
8
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
9
+
10
+ describe CanCan::ModelAdapters::ActiveRecordAdapter do
11
+ with_model :article do
12
+ table do |t|
13
+ t.boolean "published"
14
+ t.boolean "secret"
15
+ end
16
+ model do
17
+ has_many :comments
18
+ end
19
+ end
20
+
21
+ with_model :comment do
22
+ table do |t|
23
+ t.boolean "spam"
24
+ t.integer "article_id"
25
+ end
26
+ model do
27
+ belongs_to :article
28
+ end
29
+ end
30
+
31
+ before(:each) do
32
+ Article.delete_all
33
+ Comment.delete_all
34
+ @ability = Object.new
35
+ @ability.extend(CanCan::Ability)
36
+ @article_table = Article.table_name
37
+ @comment_table = Comment.table_name
38
+ end
39
+
40
+ it "should be for only active record classes" do
41
+ CanCan::ModelAdapters::ActiveRecordAdapter.should_not be_for_class(Object)
42
+ CanCan::ModelAdapters::ActiveRecordAdapter.should be_for_class(Article)
43
+ CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article).should == CanCan::ModelAdapters::ActiveRecordAdapter
44
+ end
45
+
46
+ it "should not fetch any records when no abilities are defined" do
47
+ Article.create!
48
+ Article.accessible_by(@ability).should be_empty
49
+ end
50
+
51
+ it "should fetch all articles when one can read all" do
52
+ @ability.can :read, Article
53
+ article = Article.create!
54
+ Article.accessible_by(@ability).should == [article]
55
+ end
56
+
57
+ it "should fetch only the articles that are published" do
58
+ @ability.can :read, Article, :published => true
59
+ article1 = Article.create!(:published => true)
60
+ article2 = Article.create!(:published => false)
61
+ Article.accessible_by(@ability).should == [article1]
62
+ end
63
+
64
+ it "should fetch any articles which are published or secret" do
65
+ @ability.can :read, Article, :published => true
66
+ @ability.can :read, Article, :secret => true
67
+ article1 = Article.create!(:published => true, :secret => false)
68
+ article2 = Article.create!(:published => true, :secret => true)
69
+ article3 = Article.create!(:published => false, :secret => true)
70
+ article4 = Article.create!(:published => false, :secret => false)
71
+ Article.accessible_by(@ability).should == [article1, article2, article3]
72
+ end
73
+
74
+ it "should fetch only the articles that are published and not secret" do
75
+ @ability.can :read, Article, :published => true
76
+ @ability.cannot :read, Article, :secret => true
77
+ article1 = Article.create!(:published => true, :secret => false)
78
+ article2 = Article.create!(:published => true, :secret => true)
79
+ article3 = Article.create!(:published => false, :secret => true)
80
+ article4 = Article.create!(:published => false, :secret => false)
81
+ Article.accessible_by(@ability).should == [article1]
82
+ end
83
+
84
+ it "should only read comments for articles which are published" do
85
+ @ability.can :read, Comment, :article => { :published => true }
86
+ comment1 = Comment.create!(:article => Article.create!(:published => true))
87
+ comment2 = Comment.create!(:article => Article.create!(:published => false))
88
+ Comment.accessible_by(@ability).should == [comment1]
89
+ end
90
+
91
+ it "should allow conditions in SQL and merge with hash conditions" do
92
+ @ability.can :read, Article, :published => true
93
+ @ability.can :read, Article, ["secret=?", true]
94
+ article1 = Article.create!(:published => true, :secret => false)
95
+ article4 = Article.create!(:published => false, :secret => false)
96
+ Article.accessible_by(@ability).should == [article1]
97
+ end
98
+
99
+ it "should not allow to fetch records when ability with just block present" do
100
+ @ability.can :read, Article do
101
+ false
102
+ end
103
+ lambda { Article.accessible_by(@ability) }.should raise_error(CanCan::Error)
104
+ end
105
+
106
+ it "should not allow to check ability on object against SQL conditions without block" do
107
+ @ability.can :read, Article, ["secret=?", true]
108
+ lambda { @ability.can? :read, Article.new }.should raise_error(CanCan::Error)
109
+ end
110
+
111
+ it "should have false conditions if no abilities match" do
112
+ @ability.model_adapter(Article, :read).conditions.should == "'t'='f'"
113
+ end
114
+
115
+ it "should return false conditions for cannot clause" do
116
+ @ability.cannot :read, Article
117
+ @ability.model_adapter(Article, :read).conditions.should == "'t'='f'"
118
+ end
119
+
120
+ it "should return SQL for single `can` definition in front of default `cannot` condition" do
121
+ @ability.cannot :read, Article
122
+ @ability.can :read, Article, :published => false, :secret => true
123
+ @ability.model_adapter(Article, :read).conditions.should orderlessly_match(%Q["#{@article_table}"."published" = 'f' AND "#{@article_table}"."secret" = 't'])
124
+ end
125
+
126
+ it "should return true condition for single `can` definition in front of default `can` condition" do
127
+ @ability.can :read, Article
128
+ @ability.can :read, Article, :published => false, :secret => true
129
+ @ability.model_adapter(Article, :read).conditions.should == "'t'='t'"
130
+ end
131
+
132
+ it "should return `false condition` for single `cannot` definition in front of default `cannot` condition" do
133
+ @ability.cannot :read, Article
134
+ @ability.cannot :read, Article, :published => false, :secret => true
135
+ @ability.model_adapter(Article, :read).conditions.should == "'t'='f'"
136
+ end
137
+
138
+ it "should return `not (sql)` for single `cannot` definition in front of default `can` condition" do
139
+ @ability.can :read, Article
140
+ @ability.cannot :read, Article, :published => false, :secret => true
141
+ @ability.model_adapter(Article, :read).conditions.should orderlessly_match(%Q["not (#{@article_table}"."published" = 'f' AND "#{@article_table}"."secret" = 't')])
142
+ end
143
+
144
+ it "should return appropriate sql conditions in complex case" do
145
+ @ability.can :read, Article
146
+ @ability.can :manage, Article, :id => 1
147
+ @ability.can :update, Article, :published => true
148
+ @ability.cannot :update, Article, :secret => true
149
+ @ability.model_adapter(Article, :update).conditions.should == %Q[not ("#{@article_table}"."secret" = 't') AND (("#{@article_table}"."published" = 't') OR ("#{@article_table}"."id" = 1))]
150
+ @ability.model_adapter(Article, :manage).conditions.should == {:id => 1}
151
+ @ability.model_adapter(Article, :read).conditions.should == "'t'='t'"
152
+ end
153
+
154
+ it "should not forget conditions when calling with SQL string" do
155
+ @ability.can :read, Article, :published => true
156
+ @ability.can :read, Article, ['secret=?', false]
157
+ adapter = @ability.model_adapter(Article, :read)
158
+ 2.times do
159
+ adapter.conditions.should == %Q[(secret='f') OR ("#{@article_table}"."published" = 't')]
160
+ end
161
+ end
162
+
163
+ it "should have nil joins if no rules" do
164
+ @ability.model_adapter(Article, :read).joins.should be_nil
165
+ end
166
+
167
+ it "should have nil joins if no nested hashes specified in conditions" do
168
+ @ability.can :read, Article, :published => false
169
+ @ability.can :read, Article, :secret => true
170
+ @ability.model_adapter(Article, :read).joins.should be_nil
171
+ end
172
+
173
+ it "should merge separate joins into a single array" do
174
+ @ability.can :read, Article, :project => { :blocked => false }
175
+ @ability.can :read, Article, :company => { :admin => true }
176
+ @ability.model_adapter(Article, :read).joins.inspect.should orderlessly_match([:company, :project].inspect)
177
+ end
178
+
179
+ it "should merge same joins into a single array" do
180
+ @ability.can :read, Article, :project => { :blocked => false }
181
+ @ability.can :read, Article, :project => { :admin => true }
182
+ @ability.model_adapter(Article, :read).joins.should == [:project]
183
+ end
184
+ end
185
+ end