cancancan 1.7.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 (42) hide show
  1. checksums.yaml +15 -0
  2. data/CHANGELOG.rdoc +427 -0
  3. data/CONTRIBUTING.md +11 -0
  4. data/Gemfile +23 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +161 -0
  7. data/Rakefile +18 -0
  8. data/init.rb +1 -0
  9. data/lib/cancan.rb +13 -0
  10. data/lib/cancan/ability.rb +324 -0
  11. data/lib/cancan/controller_additions.rb +397 -0
  12. data/lib/cancan/controller_resource.rb +286 -0
  13. data/lib/cancan/exceptions.rb +50 -0
  14. data/lib/cancan/inherited_resource.rb +20 -0
  15. data/lib/cancan/matchers.rb +14 -0
  16. data/lib/cancan/model_adapters/abstract_adapter.rb +56 -0
  17. data/lib/cancan/model_adapters/active_record_adapter.rb +180 -0
  18. data/lib/cancan/model_adapters/data_mapper_adapter.rb +34 -0
  19. data/lib/cancan/model_adapters/default_adapter.rb +7 -0
  20. data/lib/cancan/model_adapters/mongoid_adapter.rb +54 -0
  21. data/lib/cancan/model_additions.rb +31 -0
  22. data/lib/cancan/rule.rb +147 -0
  23. data/lib/cancancan.rb +1 -0
  24. data/lib/generators/cancan/ability/USAGE +4 -0
  25. data/lib/generators/cancan/ability/ability_generator.rb +11 -0
  26. data/lib/generators/cancan/ability/templates/ability.rb +32 -0
  27. data/spec/README.rdoc +28 -0
  28. data/spec/cancan/ability_spec.rb +455 -0
  29. data/spec/cancan/controller_additions_spec.rb +141 -0
  30. data/spec/cancan/controller_resource_spec.rb +553 -0
  31. data/spec/cancan/exceptions_spec.rb +58 -0
  32. data/spec/cancan/inherited_resource_spec.rb +60 -0
  33. data/spec/cancan/matchers_spec.rb +29 -0
  34. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +358 -0
  35. data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +118 -0
  36. data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
  37. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +226 -0
  38. data/spec/cancan/rule_spec.rb +52 -0
  39. data/spec/matchers.rb +13 -0
  40. data/spec/spec.opts +2 -0
  41. data/spec/spec_helper.rb +77 -0
  42. metadata +126 -0
@@ -0,0 +1,118 @@
1
+ if ENV["MODEL_ADAPTER"] == "data_mapper"
2
+ require "spec_helper"
3
+
4
+ DataMapper.setup(:default, 'sqlite::memory:')
5
+
6
+ class Article
7
+ include DataMapper::Resource
8
+ property :id, Serial
9
+ property :published, Boolean, :default => false
10
+ property :secret, Boolean, :default => false
11
+ property :priority, Integer
12
+ has n, :comments
13
+ end
14
+
15
+ class Comment
16
+ include DataMapper::Resource
17
+ property :id, Serial
18
+ property :spam, Boolean, :default => false
19
+ belongs_to :article
20
+ end
21
+
22
+ DataMapper.finalize
23
+ DataMapper.auto_migrate!
24
+
25
+ describe CanCan::ModelAdapters::DataMapperAdapter do
26
+ before(:each) do
27
+ Article.destroy
28
+ Comment.destroy
29
+ (@ability = double).extend(CanCan::Ability)
30
+ end
31
+
32
+ it "is for only data mapper classes" do
33
+ expect(CanCan::ModelAdapters::DataMapperAdapter).not_to be_for_class(Object)
34
+ expect(CanCan::ModelAdapters::DataMapperAdapter).to be_for_class(Article)
35
+ expect(CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article)).to eq(CanCan::ModelAdapters::DataMapperAdapter)
36
+ end
37
+
38
+ it "finds record" do
39
+ article = Article.create
40
+ expect(CanCan::ModelAdapters::DataMapperAdapter.find(Article, article.id)).to eq(article)
41
+ end
42
+
43
+ it "does not fetch any records when no abilities are defined" do
44
+ Article.create
45
+ expect(Article.accessible_by(@ability)).to be_empty
46
+ end
47
+
48
+ it "fetches all articles when one can read all" do
49
+ @ability.can :read, Article
50
+ article = Article.create
51
+ expect(Article.accessible_by(@ability)).to eq([article])
52
+ end
53
+
54
+ it "fetches only the articles that are published" do
55
+ @ability.can :read, Article, :published => true
56
+ article1 = Article.create(:published => true)
57
+ article2 = Article.create(:published => false)
58
+ expect(Article.accessible_by(@ability)).to eq([article1])
59
+ end
60
+
61
+ it "fetches any articles which are published or secret" do
62
+ @ability.can :read, Article, :published => true
63
+ @ability.can :read, Article, :secret => true
64
+ article1 = Article.create(:published => true, :secret => false)
65
+ article2 = Article.create(:published => true, :secret => true)
66
+ article3 = Article.create(:published => false, :secret => true)
67
+ article4 = Article.create(:published => false, :secret => false)
68
+ expect(Article.accessible_by(@ability)).to eq([article1, article2, article3])
69
+ end
70
+
71
+ it "fetches only the articles that are published and not secret" do
72
+ @ability.can :read, Article, :published => true
73
+ @ability.cannot :read, Article, :secret => true
74
+ article1 = Article.create(:published => true, :secret => false)
75
+ article2 = Article.create(:published => true, :secret => true)
76
+ article3 = Article.create(:published => false, :secret => true)
77
+ article4 = Article.create(:published => false, :secret => false)
78
+ expect(Article.accessible_by(@ability)).to eq([article1])
79
+ end
80
+
81
+ it "only reads comments for articles which are published" do
82
+ @ability.can :read, Comment, :article => { :published => true }
83
+ comment1 = Comment.create(:article => Article.create!(:published => true))
84
+ comment2 = Comment.create(:article => Article.create!(:published => false))
85
+ expect(Comment.accessible_by(@ability)).to eq([comment1])
86
+ end
87
+
88
+ it "allows conditions in SQL and merge with hash conditions" do
89
+ @ability.can :read, Article, :published => true
90
+ @ability.can :read, Article, ["secret=?", true]
91
+ article1 = Article.create(:published => true, :secret => false)
92
+ article4 = Article.create(:published => false, :secret => false)
93
+ expect(Article.accessible_by(@ability)).to eq([article1])
94
+ end
95
+
96
+ it "matches gt comparison" do
97
+ @ability.can :read, Article, :priority.gt => 3
98
+ article1 = Article.create(:priority => 4)
99
+ article2 = Article.create(:priority => 3)
100
+ expect(Article.accessible_by(@ability)).to eq([article1])
101
+ expect(@ability).to be_able_to(:read, article1)
102
+ expect(@ability).not_to be_able_to(:read, article2)
103
+ end
104
+
105
+ it "matches gte comparison" do
106
+ @ability.can :read, Article, :priority.gte => 3
107
+ article1 = Article.create(:priority => 4)
108
+ article2 = Article.create(:priority => 3)
109
+ article3 = Article.create(:priority => 2)
110
+ expect(Article.accessible_by(@ability)).to eq([article1, article2])
111
+ expect(@ability).to be_able_to(:read, article1)
112
+ expect(@ability).to be_able_to(:read, article2)
113
+ expect(@ability).not_to be_able_to(:read, article3)
114
+ end
115
+
116
+ # TODO: add more comparison specs
117
+ end
118
+ end
@@ -0,0 +1,7 @@
1
+ require "spec_helper"
2
+
3
+ describe CanCan::ModelAdapters::DefaultAdapter do
4
+ it "is default for generic classes" do
5
+ expect(CanCan::ModelAdapters::AbstractAdapter.adapter_class(Object)).to eq(CanCan::ModelAdapters::DefaultAdapter)
6
+ end
7
+ end
@@ -0,0 +1,226 @@
1
+ if ENV["MODEL_ADAPTER"] == "mongoid"
2
+ require "spec_helper"
3
+
4
+ class MongoidCategory
5
+ include Mongoid::Document
6
+
7
+ references_many :mongoid_projects
8
+ end
9
+
10
+ class MongoidProject
11
+ include Mongoid::Document
12
+
13
+ referenced_in :mongoid_category
14
+ end
15
+
16
+ Mongoid.configure do |config|
17
+ config.master = Mongo::Connection.new('127.0.0.1', 27017).db("cancan_mongoid_spec")
18
+ end
19
+
20
+ describe CanCan::ModelAdapters::MongoidAdapter do
21
+ context "Mongoid defined" do
22
+ before(:each) do
23
+ (@ability = double).extend(CanCan::Ability)
24
+ end
25
+
26
+ after(:each) do
27
+ Mongoid.master.collections.select do |collection|
28
+ collection.name !~ /system/
29
+ end.each(&:drop)
30
+ end
31
+
32
+ it "is for only Mongoid classes" do
33
+ expect(CanCan::ModelAdapters::MongoidAdapter).not_to be_for_class(Object)
34
+ expect(CanCan::ModelAdapters::MongoidAdapter).to be_for_class(MongoidProject)
35
+ expect(CanCan::ModelAdapters::AbstractAdapter.adapter_class(MongoidProject)).to eq(CanCan::ModelAdapters::MongoidAdapter)
36
+ end
37
+
38
+ it "finds record" do
39
+ project = MongoidProject.create
40
+ expect(CanCan::ModelAdapters::MongoidAdapter.find(MongoidProject, project.id)).to eq(project)
41
+ end
42
+
43
+ it "compares properties on mongoid documents with the conditions hash" do
44
+ model = MongoidProject.new
45
+ @ability.can :read, MongoidProject, :id => model.id
46
+ expect(@ability).to be_able_to(:read, model)
47
+ end
48
+
49
+ it "is able to read hashes when field is array" do
50
+ one_to_three = MongoidProject.create(:numbers => ['one', 'two', 'three'])
51
+ two_to_five = MongoidProject.create(:numbers => ['two', 'three', 'four', 'five'])
52
+
53
+ @ability.can :foo, MongoidProject, :numbers => 'one'
54
+ expect(@ability).to be_able_to(:foo, one_to_three)
55
+ expect(@ability).not_to be_able_to(:foo, two_to_five)
56
+ end
57
+
58
+ it "returns [] when no ability is defined so no records are found" do
59
+ MongoidProject.create(:title => 'Sir')
60
+ MongoidProject.create(:title => 'Lord')
61
+ MongoidProject.create(:title => 'Dude')
62
+
63
+ expect(MongoidProject.accessible_by(@ability, :read).entries).to eq([])
64
+ end
65
+
66
+ it "returns the correct records based on the defined ability" do
67
+ @ability.can :read, MongoidProject, :title => "Sir"
68
+ sir = MongoidProject.create(:title => 'Sir')
69
+ lord = MongoidProject.create(:title => 'Lord')
70
+ dude = MongoidProject.create(:title => 'Dude')
71
+
72
+ expect(MongoidProject.accessible_by(@ability, :read).entries).to eq([sir])
73
+ end
74
+
75
+ it "returns the correct records when a mix of can and cannot rules in defined ability" do
76
+ @ability.can :manage, MongoidProject, :title => 'Sir'
77
+ @ability.cannot :destroy, MongoidProject
78
+
79
+ sir = MongoidProject.create(:title => 'Sir')
80
+ lord = MongoidProject.create(:title => 'Lord')
81
+ dude = MongoidProject.create(:title => 'Dude')
82
+
83
+ expect(MongoidProject.accessible_by(@ability, :destroy).entries).to eq([sir])
84
+ end
85
+
86
+ it "is able to mix empty conditions and hashes" do
87
+ @ability.can :read, MongoidProject
88
+ @ability.can :read, MongoidProject, :title => 'Sir'
89
+ sir = MongoidProject.create(:title => 'Sir')
90
+ lord = MongoidProject.create(:title => 'Lord')
91
+
92
+ expect(MongoidProject.accessible_by(@ability, :read).count).to eq(2)
93
+ end
94
+
95
+ it "returns everything when the defined ability is access all" do
96
+ @ability.can :manage, :all
97
+ sir = MongoidProject.create(:title => 'Sir')
98
+ lord = MongoidProject.create(:title => 'Lord')
99
+ dude = MongoidProject.create(:title => 'Dude')
100
+
101
+ expect(MongoidProject.accessible_by(@ability, :read).entries).to eq([sir, lord, dude])
102
+ end
103
+
104
+ it "allows a scope for conditions" do
105
+ @ability.can :read, MongoidProject, MongoidProject.where(:title => 'Sir')
106
+ sir = MongoidProject.create(:title => 'Sir')
107
+ lord = MongoidProject.create(:title => 'Lord')
108
+ dude = MongoidProject.create(:title => 'Dude')
109
+
110
+ expect(MongoidProject.accessible_by(@ability, :read).entries).to eq([sir])
111
+ end
112
+
113
+ describe "Mongoid::Criteria where clause Symbol extensions using MongoDB expressions" do
114
+ it "handles :field.in" do
115
+ obj = MongoidProject.create(:title => 'Sir')
116
+ @ability.can :read, MongoidProject, :title.in => ["Sir", "Madam"]
117
+ expect(@ability.can?(:read, obj)).to eq(true)
118
+ expect(MongoidProject.accessible_by(@ability, :read)).to eq([obj])
119
+
120
+ obj2 = MongoidProject.create(:title => 'Lord')
121
+ expect(@ability.can?(:read, obj2)).to be_false
122
+ end
123
+
124
+ describe "activates only when there are Criteria in the hash" do
125
+ it "Calls where on the model class when there are criteria" do
126
+ obj = MongoidProject.create(:title => 'Bird')
127
+ @conditions = {:title.nin => ["Fork", "Spoon"]}
128
+
129
+ @ability.can :read, MongoidProject, @conditions
130
+ expect(@ability).to be_able_to(:read, obj)
131
+ end
132
+ it "Calls the base version if there are no mongoid criteria" do
133
+ obj = MongoidProject.new(:title => 'Bird')
134
+ @conditions = {:id => obj.id}
135
+ @ability.can :read, MongoidProject, @conditions
136
+ expect(@ability).to be_able_to(:read, obj)
137
+ end
138
+ end
139
+
140
+ it "handles :field.nin" do
141
+ obj = MongoidProject.create(:title => 'Sir')
142
+ @ability.can :read, MongoidProject, :title.nin => ["Lord", "Madam"]
143
+ expect(@ability.can?(:read, obj)).to eq(true)
144
+ expect(MongoidProject.accessible_by(@ability, :read)).to eq([obj])
145
+
146
+ obj2 = MongoidProject.create(:title => 'Lord')
147
+ expect(@ability.can?(:read, obj2)).to be_false
148
+ end
149
+
150
+ it "handles :field.size" do
151
+ obj = MongoidProject.create(:titles => ['Palatin', 'Margrave'])
152
+ @ability.can :read, MongoidProject, :titles.size => 2
153
+ expect(@ability.can?(:read, obj)).to eq(true)
154
+ expect(MongoidProject.accessible_by(@ability, :read)).to eq([obj])
155
+
156
+ obj2 = MongoidProject.create(:titles => ['Palatin', 'Margrave', 'Marquis'])
157
+ expect(@ability.can?(:read, obj2)).to be_false
158
+ end
159
+
160
+ it "handles :field.exists" do
161
+ obj = MongoidProject.create(:titles => ['Palatin', 'Margrave'])
162
+ @ability.can :read, MongoidProject, :titles.exists => true
163
+ expect(@ability.can?(:read, obj)).to eq(true)
164
+ expect(MongoidProject.accessible_by(@ability, :read)).to eq([obj])
165
+
166
+ obj2 = MongoidProject.create
167
+ expect(@ability.can?(:read, obj2)).to be_false
168
+ end
169
+
170
+ it "handles :field.gt" do
171
+ obj = MongoidProject.create(:age => 50)
172
+ @ability.can :read, MongoidProject, :age.gt => 45
173
+ expect(@ability.can?(:read, obj)).to eq(true)
174
+ expect(MongoidProject.accessible_by(@ability, :read)).to eq([obj])
175
+
176
+ obj2 = MongoidProject.create(:age => 40)
177
+ expect(@ability.can?(:read, obj2)).to be_false
178
+ end
179
+
180
+ it "handles instance not saved to database" do
181
+ obj = MongoidProject.new(:title => 'Sir')
182
+ @ability.can :read, MongoidProject, :title.in => ["Sir", "Madam"]
183
+ expect(@ability.can?(:read, obj)).to eq(true)
184
+
185
+ # accessible_by only returns saved records
186
+ expect(MongoidProject.accessible_by(@ability, :read).entries).to eq([])
187
+
188
+ obj2 = MongoidProject.new(:title => 'Lord')
189
+ expect(@ability.can?(:read, obj2)).to be_false
190
+ end
191
+ end
192
+
193
+ it "calls where with matching ability conditions" do
194
+ obj = MongoidProject.create(:foo => {:bar => 1})
195
+ @ability.can :read, MongoidProject, :foo => {:bar => 1}
196
+ expect(MongoidProject.accessible_by(@ability, :read).entries.first).to eq(obj)
197
+ end
198
+
199
+ it "excludes from the result if set to cannot" do
200
+ obj = MongoidProject.create(:bar => 1)
201
+ obj2 = MongoidProject.create(:bar => 2)
202
+ @ability.can :read, MongoidProject
203
+ @ability.cannot :read, MongoidProject, :bar => 2
204
+ expect(MongoidProject.accessible_by(@ability, :read).entries).to eq([obj])
205
+ end
206
+
207
+ it "combines the rules" do
208
+ obj = MongoidProject.create(:bar => 1)
209
+ obj2 = MongoidProject.create(:bar => 2)
210
+ obj3 = MongoidProject.create(:bar => 3)
211
+ @ability.can :read, MongoidProject, :bar => 1
212
+ @ability.can :read, MongoidProject, :bar => 2
213
+ expect(MongoidProject.accessible_by(@ability, :read).entries).to match_array([obj, obj2])
214
+ end
215
+
216
+ it "does not allow to fetch records when ability with just block present" do
217
+ @ability.can :read, MongoidProject do
218
+ false
219
+ end
220
+ expect {
221
+ MongoidProject.accessible_by(@ability)
222
+ }.to raise_error(CanCan::Error)
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,52 @@
1
+ require "spec_helper"
2
+ require "ostruct" # for OpenStruct below
3
+
4
+ # Most of Rule functionality is tested in Ability specs
5
+ describe CanCan::Rule do
6
+ before(:each) do
7
+ @conditions = {}
8
+ @rule = CanCan::Rule.new(true, :read, Integer, @conditions, nil)
9
+ end
10
+
11
+ it "returns no association joins if none exist" do
12
+ expect(@rule.associations_hash).to eq({})
13
+ end
14
+
15
+ it "returns no association for joins if just attributes" do
16
+ @conditions[:foo] = :bar
17
+ expect(@rule.associations_hash).to eq({})
18
+ end
19
+
20
+ it "returns single association for joins" do
21
+ @conditions[:foo] = {:bar => 1}
22
+ expect(@rule.associations_hash).to eq({:foo => {}})
23
+ end
24
+
25
+ it "returns multiple associations for joins" do
26
+ @conditions[:foo] = {:bar => 1}
27
+ @conditions[:test] = {1 => 2}
28
+ expect(@rule.associations_hash).to eq({:foo => {}, :test => {}})
29
+ end
30
+
31
+ it "returns nested associations for joins" do
32
+ @conditions[:foo] = {:bar => {1 => 2}}
33
+ expect(@rule.associations_hash).to eq({:foo => {:bar => {}}})
34
+ end
35
+
36
+ it "returns no association joins if conditions is nil" do
37
+ rule = CanCan::Rule.new(true, :read, Integer, nil, nil)
38
+ expect(rule.associations_hash).to eq({})
39
+ end
40
+
41
+ it "is not mergeable if conditions are not simple hashes" do
42
+ meta_where = OpenStruct.new(:name => 'metawhere', :column => 'test')
43
+ @conditions[meta_where] = :bar
44
+
45
+ expect(@rule).to be_unmergeable
46
+ end
47
+
48
+ it "is not mergeable if conditions is an empty hash" do
49
+ @conditions = {}
50
+ expect(@rule).to_not be_unmergeable
51
+ end
52
+ end
@@ -0,0 +1,13 @@
1
+ RSpec::Matchers.define :orderlessly_match do |original_string|
2
+ match do |given_string|
3
+ original_string.split('').sort == given_string.split('').sort
4
+ end
5
+
6
+ failure_message_for_should do |given_string|
7
+ "expected \"#{given_string}\" to have the same characters as \"#{original_string}\""
8
+ end
9
+
10
+ failure_message_for_should_not do |given_string|
11
+ "expected \"#{given_string}\" not to have the same characters as \"#{original_string}\""
12
+ end
13
+ end
@@ -0,0 +1,2 @@
1
+ --color
2
+ --backtrace
@@ -0,0 +1,77 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ Bundler.require(:default)
5
+
6
+ require 'supermodel' # shouldn't Bundler do this already?
7
+ require 'active_support/all'
8
+ require 'matchers'
9
+ require 'cancan/matchers'
10
+
11
+ RSpec.configure do |config|
12
+ config.treat_symbols_as_metadata_keys_with_true_values = true
13
+ config.filter_run :focus => true
14
+ config.run_all_when_everything_filtered = true
15
+ config.mock_with :rspec
16
+
17
+ config.expect_with :rspec do |c|
18
+ c.syntax = :expect
19
+ end
20
+
21
+ config.before(:each) do
22
+ Project.delete_all
23
+ Category.delete_all
24
+ end
25
+ config.extend WithModel if ENV["MODEL_ADAPTER"].nil? || ENV["MODEL_ADAPTER"] == "active_record"
26
+ end
27
+
28
+ # Working around CVE-2012-5664 requires us to convert all ID params
29
+ # to strings. Let's switch to using string IDs in tests, otherwise
30
+ # SuperModel and/or RR will fail (as strings are not fixnums).
31
+
32
+ module SuperModel
33
+ class Base
34
+ def generate_id
35
+ object_id.to_s
36
+ end
37
+ end
38
+ end
39
+
40
+ class Ability
41
+ include CanCan::Ability
42
+
43
+ def initialize(user)
44
+ end
45
+ end
46
+
47
+ class Category < SuperModel::Base
48
+ has_many :projects
49
+ end
50
+
51
+ module Sub
52
+ class Project < SuperModel::Base
53
+ belongs_to :category
54
+ attr_accessor :category # why doesn't SuperModel do this automatically?
55
+
56
+ def self.respond_to?(method, include_private = false)
57
+ if method.to_s == "find_by_name!" # hack to simulate ActiveRecord
58
+ true
59
+ else
60
+ super
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ class Project < SuperModel::Base
67
+ belongs_to :category
68
+ attr_accessor :category # why doesn't SuperModel do this automatically?
69
+
70
+ def self.respond_to?(method, include_private = false)
71
+ if method.to_s == "find_by_name!" # hack to simulate ActiveRecord
72
+ true
73
+ else
74
+ super
75
+ end
76
+ end
77
+ end