cancancan 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
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