├── .gitignore
├── .rbenv-version
├── .rspec
├── .rvmrc
├── .travis.yml
├── CHANGELOG.rdoc
├── CONTRIBUTING.md
├── Gemfile
├── LICENSE
├── README.rdoc
├── Rakefile
├── cancan.gemspec
├── init.rb
├── lib
├── cancan.rb
├── cancan
│ ├── ability.rb
│ ├── controller_additions.rb
│ ├── controller_resource.rb
│ ├── exceptions.rb
│ ├── inherited_resource.rb
│ ├── matchers.rb
│ ├── model_adapters
│ │ ├── abstract_adapter.rb
│ │ ├── active_record_adapter.rb
│ │ ├── data_mapper_adapter.rb
│ │ ├── default_adapter.rb
│ │ └── mongoid_adapter.rb
│ ├── model_additions.rb
│ └── rule.rb
└── generators
│ └── cancan
│ └── ability
│ ├── USAGE
│ ├── ability_generator.rb
│ └── templates
│ └── ability.rb
└── spec
├── README.rdoc
├── cancan
├── ability_spec.rb
├── controller_additions_spec.rb
├── controller_resource_spec.rb
├── exceptions_spec.rb
├── inherited_resource_spec.rb
├── matchers_spec.rb
├── model_adapters
│ ├── active_record_adapter_spec.rb
│ ├── data_mapper_adapter_spec.rb
│ ├── default_adapter_spec.rb
│ └── mongoid_adapter_spec.rb
└── rule_spec.rb
├── matchers.rb
├── spec.opts
└── spec_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | **/*.swp
3 | *.gem
4 | Gemfile.lock
5 | .bundle
6 |
--------------------------------------------------------------------------------
/.rbenv-version:
--------------------------------------------------------------------------------
1 | 1.8.7-p357
2 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 |
--------------------------------------------------------------------------------
/.rvmrc:
--------------------------------------------------------------------------------
1 | rvm use 1.8.7@cancan --create
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | rvm:
2 | - 1.8.7
3 | - ree
4 | notifications:
5 | recipients:
6 | - graf.otodrakula@gmail.com
7 | - ryan@railscasts.com
8 |
--------------------------------------------------------------------------------
/CHANGELOG.rdoc:
--------------------------------------------------------------------------------
1 | 1.6.10 (May 7, 2013)
2 |
3 | * fix matches_conditons_hash for string values on 1.8 (thanks rrosen)
4 |
5 | * work around SQL injection vulnerability in older Rails versions (thanks steerio) - issue #800
6 |
7 | * add support for nested join conditions (thanks yuszuv) - issue #806
8 |
9 | * fix load_resource "find_by" in mongoid resources (thanks albertobajo) - issue #705
10 |
11 | * fix namespace split behavior (thanks xinuc) - issue #668
12 |
13 | 1.6.9 (February 4, 2013)
14 |
15 | * fix inserting AND (NULL) to end of SQL queries (thanks jonsgreen) - issue #687
16 |
17 | * fix merge_joins for nested association hashes (thanks DavidMikeSimon) - issues #655, #560
18 |
19 | * raise error on recursive alias_action (thanks fl00r) - issue #660
20 |
21 | * fix namespace controllers not loading params (thanks andhapp) - issues #670, #664
22 |
23 |
24 | 1.6.8 (June 25, 2012)
25 |
26 | * improved support for namespaced controllers and models
27 |
28 | * pass :if and :unless options for load and authorize resource (thanks mauriciozaffari)
29 |
30 | * Travis CI badge (thanks plentz)
31 |
32 | * adding Ability#merge for combining multiple abilities (thanks rogercampos)
33 |
34 | * support for multiple MetaWhere rules (thanks andhapp)
35 |
36 | * various fixes for DataMapper, Mongoid, and Inherited Resource integration
37 |
38 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.6.7...1.6.8]
39 |
40 |
41 | 1.6.7 (October 4, 2011)
42 |
43 | * fixing nested resource problem caused by namespace addition - issue #482
44 |
45 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.6.6...1.6.7]
46 |
47 |
48 | 1.6.6 (September 28, 2011)
49 |
50 | * correct "return cant jump across threads" error when using check_authorization (thanks codeprimate) - issues #463, #469
51 |
52 | * fixing tests in development by specifying with_model version (thanks kirkconnell) - issue #476
53 |
54 | * added travis.yml file for TravisCI support (thanks bai) - issue #427
55 |
56 | * better support for namespaced models (thanks whilefalse) - issues #424
57 |
58 | * adding :id_param option to load_and_authorize_resource (thanks skhisma) - issue #425
59 |
60 | * make default unauthorized message translatable text (thanks nhocki) - issue #409
61 |
62 | * improving DataMapper behavior (thanks psanford, maxsum-corin) - issue #410, #373
63 |
64 | * allow :find_by option to be full find method name - issue #335
65 |
66 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.6.5...1.6.6]
67 |
68 |
69 | 1.6.5 (May 18, 2011)
70 |
71 | * pass action and subject through AccessDenied exception when :through isn't found - issue #366
72 |
73 | * many Mongoid adapter improvements (thanks rahearn, cardagin) - issues #363, #352, #343
74 |
75 | * allow :through option to work with private controller methods - issue #360
76 |
77 | * ensure Mongoid::Document is defined before loading Mongoid adapter - issue #359
78 |
79 | * many DataMapper adapter improvements (thanks emmanuel) - issue #355
80 |
81 | * handle checking nil attributes through associations (thanks thatothermitch) - issue #330
82 |
83 | * improve scope merging - issue #328
84 |
85 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.6.4...1.6.5]
86 |
87 |
88 | 1.6.4 (March 29, 2011)
89 |
90 | * Fixed mongoid 'or' error - see issue #322
91 |
92 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.6.3...1.6.4]
93 |
94 |
95 | 1.6.3 (March 25, 2011)
96 |
97 | * Make sure ActiveRecord::Relation is defined before checking conditions against it so Rails 2 is supported again - see issue #312
98 |
99 | * Return subject passed to authorize! - see issue #314
100 |
101 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.6.2...1.6.3]
102 |
103 |
104 | 1.6.2 (March 18, 2011)
105 |
106 | * Fixed instance loading when :singleton option is used - see issue #310
107 |
108 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.6.1...1.6.2]
109 |
110 |
111 | 1.6.1 (March 15, 2011)
112 |
113 | * Use Item.new instead of build_item for singleton resource so it doesn't effect database - see issue #304
114 |
115 | * Made accessible_by action default to :index and parent action default to :show instead of :read - see issue #302
116 |
117 | * Reverted Inherited Resources "collection" override since it doesn't seem to be working - see issue #305
118 |
119 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.6.0...1.6.1]
120 |
121 |
122 | 1.6.0 (March 11, 2011)
123 |
124 | * Added MetaWhere support - see issue #194 and #261
125 |
126 | * Allow Active Record scopes in Ability conditions - see issue #257
127 |
128 | * Added :if and :unless options to check_authorization - see issue #284
129 |
130 | * Several Inherited Resources fixes (thanks aq1018, tanordheim and stefanoverna)
131 |
132 | * Pass action name to accessible_by call when loading a collection (thanks amw)
133 |
134 | * Added :prepend option to load_and_authorize_resource to load before other filters - see issue #290
135 |
136 | * Fixed spacing issue in I18n message for multi-word model names - see issue #292
137 |
138 | * Load resource collection for any action which doesn't have an "id" parameter - see issue #296
139 |
140 | * Raise an exception when trying to make a Ability condition with both a hash of conditions and a block - see issue #269
141 |
142 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.5.1...1.6.0]
143 |
144 |
145 | 1.5.1 (January 20, 2011)
146 |
147 | * Fixing deeply nested conditions in Active Record adapter - see issue #246
148 |
149 | * Improving Mongoid support for multiple can and cannot definitions (thanks stellard) - see issue #239
150 |
151 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.5.0...1.5.1]
152 |
153 |
154 | 1.5.0 (January 11, 2011)
155 |
156 | * Added an Ability generator - see issue #170
157 |
158 | * Added DataMapper support (thanks natemueller)
159 |
160 | * Added Mongoid support (thanks bowsersenior)
161 |
162 | * Added skip_load_and_authorize_resource methods to controller class - see issue #164
163 |
164 | * Added support for uncountable resources in index action - see issue #193
165 |
166 | * Cleaned up README and added spec/README
167 |
168 | * Internal: renamed CanDefinition to Rule
169 |
170 | * Internal: added a model adapter layer for easily supporting more ORMs
171 |
172 | * Internal: added .rvmrc to auto-switch to 1.8.7 with gemset - see issue #231
173 |
174 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.4.1...1.5.0]
175 |
176 |
177 | 1.4.1 (November 12, 2010)
178 |
179 | * Renaming skip_authorization to skip_authorization_check - see issue #169
180 |
181 | * Adding :through_association option to load_resource (thanks hunterae) - see issue #171
182 |
183 | * The :shallow option now works with the :singleton option (thanks nandalopes) - see issue #187
184 |
185 | * Play nicely with quick_scopes gem (thanks ramontayag) - see issue #183
186 |
187 | * Fix odd behavior when "cache_classes = false" (thanks mphalliday) - see issue #174
188 |
189 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.4.0...1.4.1]
190 |
191 |
192 | 1.4.0 (October 5, 2010)
193 |
194 | * Adding Gemfile; to get specs running just +bundle+ and +rake+ - see issue #163
195 |
196 | * Stop at 'cannot' definition when there are no conditions - see issue #161
197 |
198 | * The :through option will now call a method with that name if instance variable doesn't exist - see issue #146
199 |
200 | * Adding :shallow option to load_resource to bring back old behavior of fetching a child without a parent
201 |
202 | * Raise AccessDenied error when loading a child and parent resource isn't found
203 |
204 | * Abilities defined on a module will apply to anything that includes that module - see issue #150 and #152
205 |
206 | * Abilities can be defined with a string of SQL in addition to a block so accessible_by works with a block - see issue #150
207 |
208 | * Adding better support for InheritedResource - see issue #23
209 |
210 | * Loading the collection instance variable (for index action) using accessible_by - see issue #137
211 |
212 | * Adding action and subject variables to I18n unauthorized message - closes #142
213 |
214 | * Adding check_authorization and skip_authorization controller class methods to ensure authorization is performed (thanks justinko) - see issue #135
215 |
216 | * Setting initial attributes based on ability conditions in new/create actions - see issue #114
217 |
218 | * Check parent attributes for nested association in index action - see issue #121
219 |
220 | * Supporting nesting in can? method using hash - see issue #121
221 |
222 | * Adding I18n support for Access Denied messages (thanks EppO) - see issue #103
223 |
224 | * Passing no arguments to +can+ definition will pass action, class, and object to block - see issue #129
225 |
226 | * Don't pass action to block in +can+ definition when using :+manage+ option - see issue #129
227 |
228 | * No longer calling block in +can+ definition when checking on class - see issue #116
229 |
230 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.3.4...1.4.0]
231 |
232 |
233 | 1.3.4 (August 31, 2010)
234 |
235 | * Don't stop at +cannot+ with hash conditions when checking class (thanks tamoya) - see issue #131
236 |
237 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.3.3...1.3.4]
238 |
239 |
240 | 1.3.3 (August 20, 2010)
241 |
242 | * Switching to Rspec namespace to remove deprecation warning in Rspec 2 - see issue #119
243 |
244 | * Pluralize nested associations for conditions in accessible_by (thanks mlooney) - see issue #123
245 |
246 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.3.2...1.3.3]
247 |
248 |
249 | 1.3.2 (August 7, 2010)
250 |
251 | * Fixing slice error when passing in custom resource name - see issue #112
252 |
253 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.3.1...1.3.2]
254 |
255 |
256 | 1.3.1 (August 6, 2010)
257 |
258 | * Fixing protected sanitize_sql error - see issue #111
259 |
260 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.3.0...1.3.1]
261 |
262 |
263 | 1.3.0 (August 6, 2010)
264 |
265 | * Adding :find_by option to load_resource - see issue #19
266 |
267 | * Adding :singleton option to load_resource - see issue #93
268 |
269 | * Supporting multiple resources in :through option for polymorphic associations - see issue #73
270 |
271 | * Supporting Single Table Inheritance for "can" comparisons - see issue #55
272 |
273 | * Adding :instance_name option to load/authorize_resource - see issue #44
274 |
275 | * Don't pass nil to "new" to keep MongoMapper happy - see issue #63
276 |
277 | * Parent resources are now authorized with :read action.
278 |
279 | * Changing :resource option in load/authorize_resource back to :class with ability to pass false
280 |
281 | * Removing :nested option in favor of :through option with separate load/authorize call
282 |
283 | * Moving internal logic from ResourceAuthorization to ControllerResource class
284 |
285 | * Supporting multiple "can" and "cannot" calls with accessible_by (thanks funny-falcon) - see issue #71
286 |
287 | * Supporting deeply nested aliases - see issue #98
288 |
289 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.2.0...1.3.0]
290 |
291 |
292 | 1.2.0 (July 16, 2010)
293 |
294 | * Load nested parent resources on collection actions such as "index" (thanks dohzya)
295 |
296 | * Adding :name option to load_and_authorize_resource if it does not match controller - see issue #65
297 |
298 | * Fixing issue when using accessible_by with nil can conditions (thanks jrallison) - see issue #66
299 |
300 | * Pluralize table name for belongs_to associations in can conditions hash (thanks logandk) - see issue #62
301 |
302 | * Support has_many association or arrays in can conditions hash
303 |
304 | * Adding joins clause to accessible_by when conditions are across associations
305 |
306 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.1.1...1.2.0]
307 |
308 |
309 | 1.1.1 (April 17, 2010)
310 |
311 | * Fixing behavior in Rails 3 by properly initializing ResourceAuthorization
312 |
313 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.1...1.1.1]
314 |
315 |
316 | 1.1.0 (April 17, 2010)
317 |
318 | * Supporting arrays, ranges, and nested hashes in ability conditions
319 |
320 | * Removing "unauthorized!" method in favor of "authorize!" in controllers
321 |
322 | * Adding action, subject and default_message abilities to AccessDenied exception - see issue #40
323 |
324 | * Adding caching to current_ability controller method, if you're overriding this be sure to add caching too.
325 |
326 | * Adding "accessible_by" method to Active Record for fetching records matching a specific ability
327 |
328 | * Adding conditions behavior to Ability#can and fetch with Ability#conditions - see issue #53
329 |
330 | * Renaming :class option to :resource for load_and_authorize_resource which now supports a symbol for non models - see issue #45
331 |
332 | * Properly handle Admin::AbilitiesController in params[:controller] - see issue #46
333 |
334 | * Adding be_able_to RSpec matcher (thanks dchelimsky), requires Ruby 1.8.7 or higher - see issue #54
335 |
336 | * Support additional arguments to can? which get passed to the block - see issue #48
337 |
338 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.0.2...1.1]
339 |
340 |
341 | 1.0.2 (Dec 30, 2009)
342 |
343 | * Adding clear_aliased_actions to Ability which removes previously defined actions including defaults - see issue #20
344 |
345 | * Append aliased actions (don't overwrite them) - see issue #20
346 |
347 | * Adding custom message argument to unauthorized! method (thanks tjwallace) - see issue #18
348 |
349 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.0.1...1.0.2]
350 |
351 |
352 | 1.0.1 (Dec 14, 2009)
353 |
354 | * Adding :class option to load_resource so one can customize which class to use for the model - see issue #17
355 |
356 | * Don't fetch parent of nested resource if *_id parameter is missing so it works with shallow nested routes - see issue #14
357 |
358 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/1.0.0...1.0.1]
359 |
360 |
361 | 1.0.0 (Dec 13, 2009)
362 |
363 | * Don't set resource instance variable if it has been set already - see issue #13
364 |
365 | * Allowing :nested option to accept an array for deep nesting
366 |
367 | * Adding :nested option to load resource method - see issue #10
368 |
369 | * Pass :only and :except options to before filters for load/authorize resource methods.
370 |
371 | * Adding :collection and :new options to load_resource method so we can specify behavior of additional actions if needed.
372 |
373 | * BACKWARDS INCOMPATIBLE: turning load and authorize resource methods into class methods which set up the before filter so they can accept additional arguments.
374 |
375 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/0.2.1...1.0.0]
376 |
377 |
378 | 0.2.1 (Nov 26, 2009)
379 |
380 | * many internal refactorings - see issues #11 and #12
381 |
382 | * adding "cannot" method to define which abilities cannot be done - see issue #7
383 |
384 | * support custom objects (usually symbols) in can definition - see issue #8
385 |
386 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/0.2.0...0.2.1]
387 |
388 |
389 | 0.2.0 (Nov 17, 2009)
390 |
391 | * fix behavior of load_and_authorize_resource for namespaced controllers - see issue #3
392 |
393 | * support arrays being passed to "can" to specify multiple actions or classes - see issue #2
394 |
395 | * adding "cannot?" method to ability, controller, and view which is inverse of "can?" - see issue #1
396 |
397 | * BACKWARDS INCOMPATIBLE: use Ability#initialize instead of 'prepare' to set up abilities - see issue #4
398 |
399 | * {see the full list of changes}[https://github.com/ryanb/cancan/compare/0.1.0...0.2.0]
400 |
401 |
402 | 0.1.0 (Nov 16, 2009)
403 |
404 | * initial release
405 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ### Please read before contributing
2 |
3 | 1) If you have any questions about CanCan, search the [Wiki](https://github.com/ryanb/cancan/wiki) or use [Stack Overflow](http://stackoverflow.com/questions/tagged/cancan). Do not post questions here.
4 |
5 | 2) If you find a security bug, **DO NOT** submit an issue here. Please send an e-mail to [ryan@railscasts.com](mailto:ryan@railscasts.com) instead.
6 |
7 | 3) Do a small search on the issues tracker before submitting your issue to see if it was already reported / fixed. In case it was not, create your report including Rails and CanCan versions. If you are getting exceptions, please include the full backtrace.
8 |
9 | That's it! The more information you give, the more easy it becomes for us to track it down and fix it. Ideal scenario would be adding the issue to CanCan test suite or to a sample application.
10 |
11 | Thanks!
12 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | case ENV["MODEL_ADAPTER"]
4 | when nil, "active_record"
5 | gem "sqlite3"
6 | gem "activerecord", '~> 3.0.9', :require => "active_record"
7 | gem "with_model", "~> 0.2.5"
8 | gem "meta_where"
9 | when "data_mapper"
10 | gem "dm-core", "~> 1.0.2"
11 | gem "dm-sqlite-adapter", "~> 1.0.2"
12 | gem "dm-migrations", "~> 1.0.2"
13 | when "mongoid"
14 | gem "bson_ext", "~> 1.1"
15 | gem "mongoid", "~> 2.0.0.beta.20"
16 | else
17 | raise "Unknown model adapter: #{ENV["MODEL_ADAPTER"]}"
18 | end
19 |
20 | gemspec
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011 Ryan Bates
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = Unmaintained
2 |
3 | The CanCan gem is no longer maintained. Please use another authorization library such as CanCanCan[https://github.com/CanCanCommunity/cancancan] or Pundit[https://github.com/varvet/pundit].
4 |
5 | = CanCan
6 | {
}[http://badge.fury.io/rb/cancan]
7 | {
}[http://travis-ci.org/ryanb/cancan]
8 | {
}[https://codeclimate.com/github/ryanb/cancan]
9 |
10 | Wiki[https://github.com/ryanb/cancan/wiki] | RDocs[http://rdoc.info/projects/ryanb/cancan] | Screencast[http://railscasts.com/episodes/192-authorization-with-cancan]
11 |
12 | CanCan is an authorization library for Ruby on Rails which restricts what resources a given user is allowed to access. All permissions are defined in a single location (the +Ability+ class) and not duplicated across controllers, views, and database queries.
13 |
14 |
15 | == Installation
16 |
17 | In Rails 3, add this to your Gemfile and run the +bundle+ command.
18 |
19 | gem "cancan"
20 |
21 | In Rails 2, add this to your environment.rb file.
22 |
23 | config.gem "cancan"
24 |
25 | Alternatively, you can install it as a plugin.
26 |
27 | rails plugin install git://github.com/ryanb/cancan.git
28 |
29 |
30 | == Getting Started
31 |
32 | CanCan expects a +current_user+ method to exist in the controller. First, set up some authentication (such as Authlogic[https://github.com/binarylogic/authlogic] or Devise[https://github.com/plataformatec/devise]). See {Changing Defaults}[https://github.com/ryanb/cancan/wiki/changing-defaults] if you need different behavior.
33 |
34 |
35 | === 1. Define Abilities
36 |
37 | User permissions are defined in an +Ability+ class. CanCan 1.5 includes a Rails 3 generator for creating this class.
38 |
39 | rails g cancan:ability
40 |
41 | In Rails 2.3, just add a new class in `app/models/ability.rb` with the following contents:
42 |
43 | class Ability
44 | include CanCan::Ability
45 |
46 | def initialize(user)
47 | end
48 | end
49 |
50 | See {Defining Abilities}[https://github.com/ryanb/cancan/wiki/defining-abilities] for details.
51 |
52 |
53 | === 2. Check Abilities & Authorization
54 |
55 | The current user's permissions can then be checked using the can? and cannot? methods in the view and controller.
56 |
57 | <% if can? :update, @article %>
58 | <%= link_to "Edit", edit_article_path(@article) %>
59 | <% end %>
60 |
61 | See {Checking Abilities}[https://github.com/ryanb/cancan/wiki/checking-abilities] for more information
62 |
63 | The authorize! method in the controller will raise an exception if the user is not able to perform the given action.
64 |
65 | def show
66 | @article = Article.find(params[:id])
67 | authorize! :read, @article
68 | end
69 |
70 | Setting this for every action can be tedious, therefore the +load_and_authorize_resource+ method is provided to automatically authorize all actions in a RESTful style resource controller. It will use a before filter to load the resource into an instance variable and authorize it for every action.
71 |
72 | class ArticlesController < ApplicationController
73 | load_and_authorize_resource
74 |
75 | def show
76 | # @article is already loaded and authorized
77 | end
78 | end
79 |
80 | See {Authorizing Controller Actions}[https://github.com/ryanb/cancan/wiki/authorizing-controller-actions] for more information.
81 |
82 |
83 | === 3. Handle Unauthorized Access
84 |
85 | If the user authorization fails, a CanCan::AccessDenied exception will be raised. You can catch this and modify its behavior in the +ApplicationController+.
86 |
87 | class ApplicationController < ActionController::Base
88 | rescue_from CanCan::AccessDenied do |exception|
89 | redirect_to root_url, :alert => exception.message
90 | end
91 | end
92 |
93 | See {Exception Handling}[https://github.com/ryanb/cancan/wiki/exception-handling] for more information.
94 |
95 |
96 | === 4. Lock It Down
97 |
98 | If you want to ensure authorization happens on every action in your application, add +check_authorization+ to your ApplicationController.
99 |
100 | class ApplicationController < ActionController::Base
101 | check_authorization
102 | end
103 |
104 | This will raise an exception if authorization is not performed in an action. If you want to skip this add +skip_authorization_check+ to a controller subclass. See {Ensure Authorization}[https://github.com/ryanb/cancan/wiki/Ensure-Authorization] for more information.
105 |
106 |
107 | == Wiki Docs
108 |
109 | * {Upgrading to 1.6}[https://github.com/ryanb/cancan/wiki/Upgrading-to-1.6]
110 | * {Defining Abilities}[https://github.com/ryanb/cancan/wiki/Defining-Abilities]
111 | * {Checking Abilities}[https://github.com/ryanb/cancan/wiki/Checking-Abilities]
112 | * {Authorizing Controller Actions}[https://github.com/ryanb/cancan/wiki/Authorizing-Controller-Actions]
113 | * {Exception Handling}[https://github.com/ryanb/cancan/wiki/Exception-Handling]
114 | * {Changing Defaults}[https://github.com/ryanb/cancan/wiki/Changing-Defaults]
115 | * {See more}[https://github.com/ryanb/cancan/wiki]
116 |
117 |
118 | == Questions or Problems?
119 |
120 | If you have any issues with CanCan which you cannot find the solution to in the documentation[https://github.com/ryanb/cancan/wiki], please add an {issue on GitHub}[https://github.com/ryanb/cancan/issues] or fork the project and send a pull request.
121 |
122 | To get the specs running you should call +bundle+ and then +rake+. See the {spec/README}[https://github.com/ryanb/cancan/blob/master/spec/README.rdoc] for more information.
123 |
124 |
125 | == Special Thanks
126 |
127 | CanCan was inspired by declarative_authorization[https://github.com/stffn/declarative_authorization/] and aegis[https://github.com/makandra/aegis]. Also many thanks to the CanCan contributors[https://github.com/ryanb/cancan/contributors]. See the CHANGELOG[https://github.com/ryanb/cancan/blob/master/CHANGELOG.rdoc] for the full list.
128 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'rake'
3 | require 'rspec/core/rake_task'
4 |
5 | desc "Run RSpec"
6 | RSpec::Core::RakeTask.new do |t|
7 | t.verbose = false
8 | end
9 |
10 | desc "Run specs for all adapters"
11 | task :spec_all do
12 | %w[active_record data_mapper mongoid].each do |model_adapter|
13 | puts "MODEL_ADAPTER = #{model_adapter}"
14 | system "rake spec MODEL_ADAPTER=#{model_adapter}"
15 | end
16 | end
17 |
18 | task :default => :spec
19 |
--------------------------------------------------------------------------------
/cancan.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |s|
2 | s.name = "cancan"
3 | s.version = "1.6.10"
4 | s.author = "Ryan Bates"
5 | s.email = "ryan@railscasts.com"
6 | s.homepage = "http://github.com/ryanb/cancan"
7 | s.summary = "Simple authorization solution for Rails."
8 | s.description = "Simple authorization solution for Rails which is decoupled from user roles. All permissions are stored in a single location."
9 |
10 | s.files = Dir["{lib,spec}/**/*", "[A-Z]*", "init.rb"] - ["Gemfile.lock"]
11 | s.require_path = "lib"
12 |
13 | s.add_development_dependency 'rspec', '~> 2.6.0'
14 | s.add_development_dependency 'rails', '~> 3.0.9'
15 | s.add_development_dependency 'rr', '~> 0.10.11' # 1.0.0 has respond_to? issues: http://github.com/btakita/rr/issues/issue/43
16 | s.add_development_dependency 'supermodel', '~> 0.1.4'
17 |
18 | s.rubyforge_project = s.name
19 | s.required_rubygems_version = ">= 1.3.4"
20 | end
21 |
--------------------------------------------------------------------------------
/init.rb:
--------------------------------------------------------------------------------
1 | require 'cancan'
2 |
--------------------------------------------------------------------------------
/lib/cancan.rb:
--------------------------------------------------------------------------------
1 | require 'cancan/ability'
2 | require 'cancan/rule'
3 | require 'cancan/controller_resource'
4 | require 'cancan/controller_additions'
5 | require 'cancan/model_additions'
6 | require 'cancan/exceptions'
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) && defined?(Mongoid::Document)
14 |
--------------------------------------------------------------------------------
/lib/cancan/ability.rb:
--------------------------------------------------------------------------------
1 | module CanCan
2 |
3 | # This module is designed to be included into an Ability class. This will
4 | # provide the "can" methods for defining and checking abilities.
5 | #
6 | # class Ability
7 | # include CanCan::Ability
8 | #
9 | # def initialize(user)
10 | # if user.admin?
11 | # can :manage, :all
12 | # else
13 | # can :read, :all
14 | # end
15 | # end
16 | # end
17 | #
18 | module Ability
19 | # Check if the user has permission to perform a given action on an object.
20 | #
21 | # can? :destroy, @project
22 | #
23 | # You can also pass the class instead of an instance (if you don't have one handy).
24 | #
25 | # can? :create, Project
26 | #
27 | # Nested resources can be passed through a hash, this way conditions which are
28 | # dependent upon the association will work when using a class.
29 | #
30 | # can? :create, @category => Project
31 | #
32 | # Any additional arguments will be passed into the "can" block definition. This
33 | # can be used to pass more information about the user's request for example.
34 | #
35 | # can? :create, Project, request.remote_ip
36 | #
37 | # can :create, Project do |project, remote_ip|
38 | # # ...
39 | # end
40 | #
41 | # Not only can you use the can? method in the controller and view (see ControllerAdditions),
42 | # but you can also call it directly on an ability instance.
43 | #
44 | # ability.can? :destroy, @project
45 | #
46 | # This makes testing a user's abilities very easy.
47 | #
48 | # def test "user can only destroy projects which he owns"
49 | # user = User.new
50 | # ability = Ability.new(user)
51 | # assert ability.can?(:destroy, Project.new(:user => user))
52 | # assert ability.cannot?(:destroy, Project.new)
53 | # end
54 | #
55 | # Also see the RSpec Matchers to aid in testing.
56 | def can?(action, subject, *extra_args)
57 | match = relevant_rules_for_match(action, subject).detect do |rule|
58 | rule.matches_conditions?(action, subject, extra_args)
59 | end
60 | match ? match.base_behavior : false
61 | end
62 |
63 | # Convenience method which works the same as "can?" but returns the opposite value.
64 | #
65 | # cannot? :destroy, @project
66 | #
67 | def cannot?(*args)
68 | !can?(*args)
69 | end
70 |
71 | # Defines which abilities are allowed using two arguments. The first one is the action
72 | # you're setting the permission for, the second one is the class of object you're setting it on.
73 | #
74 | # can :update, Article
75 | #
76 | # You can pass an array for either of these parameters to match any one.
77 | # Here the user has the ability to update or destroy both articles and comments.
78 | #
79 | # can [:update, :destroy], [Article, Comment]
80 | #
81 | # You can pass :all to match any object and :manage to match any action. Here are some examples.
82 | #
83 | # can :manage, :all
84 | # can :update, :all
85 | # can :manage, Project
86 | #
87 | # You can pass a hash of conditions as the third argument. Here the user can only see active projects which he owns.
88 | #
89 | # can :read, Project, :active => true, :user_id => user.id
90 | #
91 | # See ActiveRecordAdditions#accessible_by for how to use this in database queries. These conditions
92 | # are also used for initial attributes when building a record in ControllerAdditions#load_resource.
93 | #
94 | # If the conditions hash does not give you enough control over defining abilities, you can use a block
95 | # along with any Ruby code you want.
96 | #
97 | # can :update, Project do |project|
98 | # project.groups.include?(user.group)
99 | # end
100 | #
101 | # If the block returns true then the user has that :update ability for that project, otherwise he
102 | # will be denied access. The downside to using a block is that it cannot be used to generate
103 | # conditions for database queries.
104 | #
105 | # You can pass custom objects into this "can" method, this is usually done with a symbol
106 | # and is useful if a class isn't available to define permissions on.
107 | #
108 | # can :read, :stats
109 | # can? :read, :stats # => true
110 | #
111 | # IMPORTANT: Neither a hash of conditions or a block will be used when checking permission on a class.
112 | #
113 | # can :update, Project, :priority => 3
114 | # can? :update, Project # => true
115 | #
116 | # If you pass no arguments to +can+, the action, class, and object will be passed to the block and the
117 | # block will always be executed. This allows you to override the full behavior if the permissions are
118 | # defined in an external source such as the database.
119 | #
120 | # can do |action, object_class, object|
121 | # # check the database and return true/false
122 | # end
123 | #
124 | def can(action = nil, subject = nil, conditions = nil, &block)
125 | rules << Rule.new(true, action, subject, conditions, block)
126 | end
127 |
128 | # Defines an ability which cannot be done. Accepts the same arguments as "can".
129 | #
130 | # can :read, :all
131 | # cannot :read, Comment
132 | #
133 | # A block can be passed just like "can", however if the logic is complex it is recommended
134 | # to use the "can" method.
135 | #
136 | # cannot :read, Product do |product|
137 | # product.invisible?
138 | # end
139 | #
140 | def cannot(action = nil, subject = nil, conditions = nil, &block)
141 | rules << Rule.new(false, action, subject, conditions, block)
142 | end
143 |
144 | # Alias one or more actions into another one.
145 | #
146 | # alias_action :update, :destroy, :to => :modify
147 | # can :modify, Comment
148 | #
149 | # Then :modify permission will apply to both :update and :destroy requests.
150 | #
151 | # can? :update, Comment # => true
152 | # can? :destroy, Comment # => true
153 | #
154 | # This only works in one direction. Passing the aliased action into the "can?" call
155 | # will not work because aliases are meant to generate more generic actions.
156 | #
157 | # alias_action :update, :destroy, :to => :modify
158 | # can :update, Comment
159 | # can? :modify, Comment # => false
160 | #
161 | # Unless that exact alias is used.
162 | #
163 | # can :modify, Comment
164 | # can? :modify, Comment # => true
165 | #
166 | # The following aliases are added by default for conveniently mapping common controller actions.
167 | #
168 | # alias_action :index, :show, :to => :read
169 | # alias_action :new, :to => :create
170 | # alias_action :edit, :to => :update
171 | #
172 | # This way one can use params[:action] in the controller to determine the permission.
173 | def alias_action(*args)
174 | target = args.pop[:to]
175 | validate_target(target)
176 | aliased_actions[target] ||= []
177 | aliased_actions[target] += args
178 | end
179 |
180 | # User shouldn't specify targets with names of real actions or it will cause Seg fault
181 | def validate_target(target)
182 | raise Error, "You can't specify target (#{target}) as alias because it is real action name" if aliased_actions.values.flatten.include? target
183 | end
184 |
185 | # Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key.
186 | def aliased_actions
187 | @aliased_actions ||= default_alias_actions
188 | end
189 |
190 | # Removes previously aliased actions including the defaults.
191 | def clear_aliased_actions
192 | @aliased_actions = {}
193 | end
194 |
195 | def model_adapter(model_class, action)
196 | adapter_class = ModelAdapters::AbstractAdapter.adapter_class(model_class)
197 | adapter_class.new(model_class, relevant_rules_for_query(action, model_class))
198 | end
199 |
200 | # See ControllerAdditions#authorize! for documentation.
201 | def authorize!(action, subject, *args)
202 | message = nil
203 | if args.last.kind_of?(Hash) && args.last.has_key?(:message)
204 | message = args.pop[:message]
205 | end
206 | if cannot?(action, subject, *args)
207 | message ||= unauthorized_message(action, subject)
208 | raise AccessDenied.new(message, action, subject)
209 | end
210 | subject
211 | end
212 |
213 | def unauthorized_message(action, subject)
214 | keys = unauthorized_message_keys(action, subject)
215 | variables = {:action => action.to_s}
216 | variables[:subject] = (subject.class == Class ? subject : subject.class).to_s.underscore.humanize.downcase
217 | message = I18n.translate(nil, variables.merge(:scope => :unauthorized, :default => keys + [""]))
218 | message.blank? ? nil : message
219 | end
220 |
221 | def attributes_for(action, subject)
222 | attributes = {}
223 | relevant_rules(action, subject).map do |rule|
224 | attributes.merge!(rule.attributes_from_conditions) if rule.base_behavior
225 | end
226 | attributes
227 | end
228 |
229 | def has_block?(action, subject)
230 | relevant_rules(action, subject).any?(&:only_block?)
231 | end
232 |
233 | def has_raw_sql?(action, subject)
234 | relevant_rules(action, subject).any?(&:only_raw_sql?)
235 | end
236 |
237 | def merge(ability)
238 | ability.send(:rules).each do |rule|
239 | rules << rule.dup
240 | end
241 | self
242 | end
243 |
244 | private
245 |
246 | def unauthorized_message_keys(action, subject)
247 | subject = (subject.class == Class ? subject : subject.class).name.underscore unless subject.kind_of? Symbol
248 | [subject, :all].map do |try_subject|
249 | [aliases_for_action(action), :manage].flatten.map do |try_action|
250 | :"#{try_action}.#{try_subject}"
251 | end
252 | end.flatten
253 | end
254 |
255 | # Accepts an array of actions and returns an array of actions which match.
256 | # This should be called before "matches?" and other checking methods since they
257 | # rely on the actions to be expanded.
258 | def expand_actions(actions)
259 | actions.map do |action|
260 | aliased_actions[action] ? [action, *expand_actions(aliased_actions[action])] : action
261 | end.flatten
262 | end
263 |
264 | # Given an action, it will try to find all of the actions which are aliased to it.
265 | # This does the opposite kind of lookup as expand_actions.
266 | def aliases_for_action(action)
267 | results = [action]
268 | aliased_actions.each do |aliased_action, actions|
269 | results += aliases_for_action(aliased_action) if actions.include? action
270 | end
271 | results
272 | end
273 |
274 | def rules
275 | @rules ||= []
276 | end
277 |
278 | # Returns an array of Rule instances which match the action and subject
279 | # This does not take into consideration any hash conditions or block statements
280 | def relevant_rules(action, subject)
281 | rules.reverse.select do |rule|
282 | rule.expanded_actions = expand_actions(rule.actions)
283 | rule.relevant? action, subject
284 | end
285 | end
286 |
287 | def relevant_rules_for_match(action, subject)
288 | relevant_rules(action, subject).each do |rule|
289 | if rule.only_raw_sql?
290 | raise Error, "The can? and cannot? call cannot be used with a raw sql 'can' definition. The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
291 | end
292 | end
293 | end
294 |
295 | def relevant_rules_for_query(action, subject)
296 | relevant_rules(action, subject).each do |rule|
297 | if rule.only_block?
298 | raise Error, "The accessible_by call cannot be used with a block 'can' definition. The SQL cannot be determined for #{action.inspect} #{subject.inspect}"
299 | end
300 | end
301 | end
302 |
303 | def default_alias_actions
304 | {
305 | :read => [:index, :show],
306 | :create => [:new],
307 | :update => [:edit],
308 | }
309 | end
310 | end
311 | end
312 |
--------------------------------------------------------------------------------
/lib/cancan/controller_additions.rb:
--------------------------------------------------------------------------------
1 | module CanCan
2 |
3 | # This module is automatically included into all controllers.
4 | # It also makes the "can?" and "cannot?" methods available to all views.
5 | module ControllerAdditions
6 | module ClassMethods
7 | # Sets up a before filter which loads and authorizes the current resource. This performs both
8 | # load_resource and authorize_resource and accepts the same arguments. See those methods for details.
9 | #
10 | # class BooksController < ApplicationController
11 | # load_and_authorize_resource
12 | # end
13 | #
14 | def load_and_authorize_resource(*args)
15 | cancan_resource_class.add_before_filter(self, :load_and_authorize_resource, *args)
16 | end
17 |
18 | # Sets up a before filter which loads the model resource into an instance variable.
19 | # For example, given an ArticlesController it will load the current article into the @article
20 | # instance variable. It does this by either calling Article.find(params[:id]) or
21 | # Article.new(params[:article]) depending upon the action. The index action will
22 | # automatically set @articles to Article.accessible_by(current_ability).
23 | #
24 | # If a conditions hash is used in the Ability, the +new+ and +create+ actions will set
25 | # the initial attributes based on these conditions. This way these actions will satisfy
26 | # the ability restrictions.
27 | #
28 | # Call this method directly on the controller class.
29 | #
30 | # class BooksController < ApplicationController
31 | # load_resource
32 | # end
33 | #
34 | # A resource is not loaded if the instance variable is already set. This makes it easy to override
35 | # the behavior through a before_filter on certain actions.
36 | #
37 | # class BooksController < ApplicationController
38 | # before_filter :find_book_by_permalink, :only => :show
39 | # load_resource
40 | #
41 | # private
42 | #
43 | # def find_book_by_permalink
44 | # @book = Book.find_by_permalink!(params[:id)
45 | # end
46 | # end
47 | #
48 | # If a name is provided which does not match the controller it assumes it is a parent resource. Child
49 | # resources can then be loaded through it.
50 | #
51 | # class BooksController < ApplicationController
52 | # load_resource :author
53 | # load_resource :book, :through => :author
54 | # end
55 | #
56 | # Here the author resource will be loaded before each action using params[:author_id]. The book resource
57 | # will then be loaded through the @author instance variable.
58 | #
59 | # That first argument is optional and will default to the singular name of the controller.
60 | # A hash of options (see below) can also be passed to this method to further customize it.
61 | #
62 | # See load_and_authorize_resource to automatically authorize the resource too.
63 | #
64 | # Options:
65 | # [:+only+]
66 | # Only applies before filter to given actions.
67 | #
68 | # [:+except+]
69 | # Does not apply before filter to given actions.
70 | #
71 | # [:+through+]
72 | # Load this resource through another one. This should match the name of the parent instance variable or method.
73 | #
74 | # [:+through_association+]
75 | # The name of the association to fetch the child records through the parent resource. This is normally not needed
76 | # because it defaults to the pluralized resource name.
77 | #
78 | # [:+shallow+]
79 | # Pass +true+ to allow this resource to be loaded directly when parent is +nil+. Defaults to +false+.
80 | #
81 | # [:+singleton+]
82 | # Pass +true+ if this is a singleton resource through a +has_one+ association.
83 | #
84 | # [:+parent+]
85 | # True or false depending on if the resource is considered a parent resource. This defaults to +true+ if a resource
86 | # name is given which does not match the controller.
87 | #
88 | # [:+class+]
89 | # The class to use for the model (string or constant).
90 | #
91 | # [:+instance_name+]
92 | # The name of the instance variable to load the resource into.
93 | #
94 | # [:+find_by+]
95 | # Find using a different attribute other than id. For example.
96 | #
97 | # load_resource :find_by => :permalink # will use find_by_permalink!(params[:id])
98 | #
99 | # [:+id_param+]
100 | # Find using a param key other than :id. For example:
101 | #
102 | # load_resource :id_param => :url # will use find(params[:url])
103 | #
104 | # [:+collection+]
105 | # Specify which actions are resource collection actions in addition to :+index+. This
106 | # is usually not necessary because it will try to guess depending on if the id param is present.
107 | #
108 | # load_resource :collection => [:sort, :list]
109 | #
110 | # [:+new+]
111 | # Specify which actions are new resource actions in addition to :+new+ and :+create+.
112 | # Pass an action name into here if you would like to build a new resource instead of
113 | # fetch one.
114 | #
115 | # load_resource :new => :build
116 | #
117 | # [:+prepend+]
118 | # Passing +true+ will use prepend_before_filter instead of a normal before_filter.
119 | #
120 | def load_resource(*args)
121 | cancan_resource_class.add_before_filter(self, :load_resource, *args)
122 | end
123 |
124 | # Sets up a before filter which authorizes the resource using the instance variable.
125 | # For example, if you have an ArticlesController it will check the @article instance variable
126 | # and ensure the user can perform the current action on it. Under the hood it is doing
127 | # something like the following.
128 | #
129 | # authorize!(params[:action].to_sym, @article || Article)
130 | #
131 | # Call this method directly on the controller class.
132 | #
133 | # class BooksController < ApplicationController
134 | # authorize_resource
135 | # end
136 | #
137 | # If you pass in the name of a resource which does not match the controller it will assume
138 | # it is a parent resource.
139 | #
140 | # class BooksController < ApplicationController
141 | # authorize_resource :author
142 | # authorize_resource :book
143 | # end
144 | #
145 | # Here it will authorize :+show+, @+author+ on every action before authorizing the book.
146 | #
147 | # That first argument is optional and will default to the singular name of the controller.
148 | # A hash of options (see below) can also be passed to this method to further customize it.
149 | #
150 | # See load_and_authorize_resource to automatically load the resource too.
151 | #
152 | # Options:
153 | # [:+only+]
154 | # Only applies before filter to given actions.
155 | #
156 | # [:+except+]
157 | # Does not apply before filter to given actions.
158 | #
159 | # [:+singleton+]
160 | # Pass +true+ if this is a singleton resource through a +has_one+ association.
161 | #
162 | # [:+parent+]
163 | # True or false depending on if the resource is considered a parent resource. This defaults to +true+ if a resource
164 | # name is given which does not match the controller.
165 | #
166 | # [:+class+]
167 | # The class to use for the model (string or constant). This passed in when the instance variable is not set.
168 | # Pass +false+ if there is no associated class for this resource and it will use a symbol of the resource name.
169 | #
170 | # [:+instance_name+]
171 | # The name of the instance variable for this resource.
172 | #
173 | # [:+through+]
174 | # Authorize conditions on this parent resource when instance isn't available.
175 | #
176 | # [:+prepend+]
177 | # Passing +true+ will use prepend_before_filter instead of a normal before_filter.
178 | #
179 | def authorize_resource(*args)
180 | cancan_resource_class.add_before_filter(self, :authorize_resource, *args)
181 | end
182 |
183 | # Skip both the loading and authorization behavior of CanCan for this given controller. This is primarily
184 | # useful to skip the behavior of a superclass. You can pass :only and :except options to specify which actions
185 | # to skip the effects on. It will apply to all actions by default.
186 | #
187 | # class ProjectsController < SomeOtherController
188 | # skip_load_and_authorize_resource :only => :index
189 | # end
190 | #
191 | # You can also pass the resource name as the first argument to skip that resource.
192 | def skip_load_and_authorize_resource(*args)
193 | skip_load_resource(*args)
194 | skip_authorize_resource(*args)
195 | end
196 |
197 | # Skip the loading behavior of CanCan. This is useful when using +load_and_authorize_resource+ but want to
198 | # only do authorization on certain actions. You can pass :only and :except options to specify which actions to
199 | # skip the effects on. It will apply to all actions by default.
200 | #
201 | # class ProjectsController < ApplicationController
202 | # load_and_authorize_resource
203 | # skip_load_resource :only => :index
204 | # end
205 | #
206 | # You can also pass the resource name as the first argument to skip that resource.
207 | def skip_load_resource(*args)
208 | options = args.extract_options!
209 | name = args.first
210 | cancan_skipper[:load][name] = options
211 | end
212 |
213 | # Skip the authorization behavior of CanCan. This is useful when using +load_and_authorize_resource+ but want to
214 | # only do loading on certain actions. You can pass :only and :except options to specify which actions to
215 | # skip the effects on. It will apply to all actions by default.
216 | #
217 | # class ProjectsController < ApplicationController
218 | # load_and_authorize_resource
219 | # skip_authorize_resource :only => :index
220 | # end
221 | #
222 | # You can also pass the resource name as the first argument to skip that resource.
223 | def skip_authorize_resource(*args)
224 | options = args.extract_options!
225 | name = args.first
226 | cancan_skipper[:authorize][name] = options
227 | end
228 |
229 | # Add this to a controller to ensure it performs authorization through +authorized+! or +authorize_resource+ call.
230 | # If neither of these authorization methods are called, a CanCan::AuthorizationNotPerformed exception will be raised.
231 | # This is normally added to the ApplicationController to ensure all controller actions do authorization.
232 | #
233 | # class ApplicationController < ActionController::Base
234 | # check_authorization
235 | # end
236 | #
237 | # See skip_authorization_check to bypass this check on specific controller actions.
238 | #
239 | # Options:
240 | # [:+only+]
241 | # Only applies to given actions.
242 | #
243 | # [:+except+]
244 | # Does not apply to given actions.
245 | #
246 | # [:+if+]
247 | # Supply the name of a controller method to be called. The authorization check only takes place if this returns true.
248 | #
249 | # check_authorization :if => :admin_controller?
250 | #
251 | # [:+unless+]
252 | # Supply the name of a controller method to be called. The authorization check only takes place if this returns false.
253 | #
254 | # check_authorization :unless => :devise_controller?
255 | #
256 | def check_authorization(options = {})
257 | self.after_filter(options.slice(:only, :except)) do |controller|
258 | next if controller.instance_variable_defined?(:@_authorized)
259 | next if options[:if] && !controller.send(options[:if])
260 | next if options[:unless] && controller.send(options[:unless])
261 | raise AuthorizationNotPerformed, "This action failed the check_authorization because it does not authorize_resource. Add skip_authorization_check to bypass this check."
262 | end
263 | end
264 |
265 | # Call this in the class of a controller to skip the check_authorization behavior on the actions.
266 | #
267 | # class HomeController < ApplicationController
268 | # skip_authorization_check :only => :index
269 | # end
270 | #
271 | # Any arguments are passed to the +before_filter+ it triggers.
272 | def skip_authorization_check(*args)
273 | self.before_filter(*args) do |controller|
274 | controller.instance_variable_set(:@_authorized, true)
275 | end
276 | end
277 |
278 | def skip_authorization(*args)
279 | raise ImplementationRemoved, "The CanCan skip_authorization method has been renamed to skip_authorization_check. Please update your code."
280 | end
281 |
282 | def cancan_resource_class
283 | if ancestors.map(&:to_s).include? "InheritedResources::Actions"
284 | InheritedResource
285 | else
286 | ControllerResource
287 | end
288 | end
289 |
290 | def cancan_skipper
291 | @_cancan_skipper ||= {:authorize => {}, :load => {}}
292 | end
293 | end
294 |
295 | def self.included(base)
296 | base.extend ClassMethods
297 | base.helper_method :can?, :cannot?, :current_ability
298 | end
299 |
300 | # Raises a CanCan::AccessDenied exception if the current_ability cannot
301 | # perform the given action. This is usually called in a controller action or
302 | # before filter to perform the authorization.
303 | #
304 | # def show
305 | # @article = Article.find(params[:id])
306 | # authorize! :read, @article
307 | # end
308 | #
309 | # A :message option can be passed to specify a different message.
310 | #
311 | # authorize! :read, @article, :message => "Not authorized to read #{@article.name}"
312 | #
313 | # You can also use I18n to customize the message. Action aliases defined in Ability work here.
314 | #
315 | # en:
316 | # unauthorized:
317 | # manage:
318 | # all: "Not authorized to %{action} %{subject}."
319 | # user: "Not allowed to manage other user accounts."
320 | # update:
321 | # project: "Not allowed to update this project."
322 | #
323 | # You can rescue from the exception in the controller to customize how unauthorized
324 | # access is displayed to the user.
325 | #
326 | # class ApplicationController < ActionController::Base
327 | # rescue_from CanCan::AccessDenied do |exception|
328 | # redirect_to root_url, :alert => exception.message
329 | # end
330 | # end
331 | #
332 | # See the CanCan::AccessDenied exception for more details on working with the exception.
333 | #
334 | # See the load_and_authorize_resource method to automatically add the authorize! behavior
335 | # to the default RESTful actions.
336 | def authorize!(*args)
337 | @_authorized = true
338 | current_ability.authorize!(*args)
339 | end
340 |
341 | def unauthorized!(message = nil)
342 | raise ImplementationRemoved, "The unauthorized! method has been removed from CanCan, use authorize! instead."
343 | end
344 |
345 | # Creates and returns the current user's ability and caches it. If you
346 | # want to override how the Ability is defined then this is the place.
347 | # Just define the method in the controller to change behavior.
348 | #
349 | # def current_ability
350 | # # instead of Ability.new(current_user)
351 | # @current_ability ||= UserAbility.new(current_account)
352 | # end
353 | #
354 | # Notice it is important to cache the ability object so it is not
355 | # recreated every time.
356 | def current_ability
357 | @current_ability ||= ::Ability.new(current_user)
358 | end
359 |
360 | # Use in the controller or view to check the user's permission for a given action
361 | # and object.
362 | #
363 | # can? :destroy, @project
364 | #
365 | # You can also pass the class instead of an instance (if you don't have one handy).
366 | #
367 | # <% if can? :create, Project %>
368 | # <%= link_to "New Project", new_project_path %>
369 | # <% end %>
370 | #
371 | # If it's a nested resource, you can pass the parent instance in a hash. This way it will
372 | # check conditions which reach through that association.
373 | #
374 | # <% if can? :create, @category => Project %>
375 | # <%= link_to "New Project", new_project_path %>
376 | # <% end %>
377 | #
378 | # This simply calls "can?" on the current_ability. See Ability#can?.
379 | def can?(*args)
380 | current_ability.can?(*args)
381 | end
382 |
383 | # Convenience method which works the same as "can?" but returns the opposite value.
384 | #
385 | # cannot? :destroy, @project
386 | #
387 | def cannot?(*args)
388 | current_ability.cannot?(*args)
389 | end
390 | end
391 | end
392 |
393 | if defined? ActionController::Base
394 | ActionController::Base.class_eval do
395 | include CanCan::ControllerAdditions
396 | end
397 | end
398 |
--------------------------------------------------------------------------------
/lib/cancan/controller_resource.rb:
--------------------------------------------------------------------------------
1 | module CanCan
2 | # Handle the load and authorization controller logic so we don't clutter up all controllers with non-interface methods.
3 | # This class is used internally, so you do not need to call methods directly on it.
4 | class ControllerResource # :nodoc:
5 | def self.add_before_filter(controller_class, method, *args)
6 | options = args.extract_options!
7 | resource_name = args.first
8 | before_filter_method = options.delete(:prepend) ? :prepend_before_filter : :before_filter
9 | controller_class.send(before_filter_method, options.slice(:only, :except, :if, :unless)) do |controller|
10 | controller.class.cancan_resource_class.new(controller, resource_name, options.except(:only, :except, :if, :unless)).send(method)
11 | end
12 | end
13 |
14 | def initialize(controller, *args)
15 | @controller = controller
16 | @params = controller.params
17 | @options = args.extract_options!
18 | @name = args.first
19 | raise CanCan::ImplementationRemoved, "The :nested option is no longer supported, instead use :through with separate load/authorize call." if @options[:nested]
20 | raise CanCan::ImplementationRemoved, "The :name option is no longer supported, instead pass the name as the first argument." if @options[:name]
21 | raise CanCan::ImplementationRemoved, "The :resource option has been renamed back to :class, use false if no class." if @options[:resource]
22 | end
23 |
24 | def load_and_authorize_resource
25 | load_resource
26 | authorize_resource
27 | end
28 |
29 | def load_resource
30 | unless skip?(:load)
31 | if load_instance?
32 | self.resource_instance ||= load_resource_instance
33 | elsif load_collection?
34 | self.collection_instance ||= load_collection
35 | end
36 | end
37 | end
38 |
39 | def authorize_resource
40 | unless skip?(:authorize)
41 | @controller.authorize!(authorization_action, resource_instance || resource_class_with_parent)
42 | end
43 | end
44 |
45 | def parent?
46 | @options.has_key?(:parent) ? @options[:parent] : @name && @name != name_from_controller.to_sym
47 | end
48 |
49 | def skip?(behavior) # This could probably use some refactoring
50 | options = @controller.class.cancan_skipper[behavior][@name]
51 | if options.nil?
52 | false
53 | elsif options == {}
54 | true
55 | elsif options[:except] && ![options[:except]].flatten.include?(@params[:action].to_sym)
56 | true
57 | elsif [options[:only]].flatten.include?(@params[:action].to_sym)
58 | true
59 | end
60 | end
61 |
62 | protected
63 |
64 | def load_resource_instance
65 | if !parent? && new_actions.include?(@params[:action].to_sym)
66 | build_resource
67 | elsif id_param || @options[:singleton]
68 | find_resource
69 | end
70 | end
71 |
72 | def load_instance?
73 | parent? || member_action?
74 | end
75 |
76 | def load_collection?
77 | resource_base.respond_to?(:accessible_by) && !current_ability.has_block?(authorization_action, resource_class)
78 | end
79 |
80 | def load_collection
81 | resource_base.accessible_by(current_ability, authorization_action)
82 | end
83 |
84 | def build_resource
85 | resource = resource_base.new(resource_params || {})
86 | assign_attributes(resource)
87 | end
88 |
89 | def assign_attributes(resource)
90 | resource.send("#{parent_name}=", parent_resource) if @options[:singleton] && parent_resource
91 | initial_attributes.each do |attr_name, value|
92 | resource.send("#{attr_name}=", value)
93 | end
94 | resource
95 | end
96 |
97 | def initial_attributes
98 | current_ability.attributes_for(@params[:action].to_sym, resource_class).delete_if do |key, value|
99 | resource_params && resource_params.include?(key)
100 | end
101 | end
102 |
103 | def find_resource
104 | if @options[:singleton] && parent_resource.respond_to?(name)
105 | parent_resource.send(name)
106 | else
107 | if @options[:find_by]
108 | if resource_base.respond_to? "find_by_#{@options[:find_by]}!"
109 | resource_base.send("find_by_#{@options[:find_by]}!", id_param)
110 | elsif resource_base.respond_to? "find_by"
111 | resource_base.send("find_by", { @options[:find_by].to_sym => id_param })
112 | else
113 | resource_base.send(@options[:find_by], id_param)
114 | end
115 | else
116 | adapter.find(resource_base, id_param)
117 | end
118 | end
119 | end
120 |
121 | def adapter
122 | ModelAdapters::AbstractAdapter.adapter_class(resource_class)
123 | end
124 |
125 | def authorization_action
126 | parent? ? :show : @params[:action].to_sym
127 | end
128 |
129 | def id_param
130 | if @options[:id_param]
131 | @params[@options[:id_param]]
132 | else
133 | @params[parent? ? :"#{name}_id" : :id]
134 | end.to_s
135 | end
136 |
137 | def member_action?
138 | new_actions.include?(@params[:action].to_sym) || @options[:singleton] || ( (@params[:id] || @params[@options[:id_param]]) && !collection_actions.include?(@params[:action].to_sym))
139 | end
140 |
141 | # Returns the class used for this resource. This can be overriden by the :class option.
142 | # If +false+ is passed in it will use the resource name as a symbol in which case it should
143 | # only be used for authorization, not loading since there's no class to load through.
144 | def resource_class
145 | case @options[:class]
146 | when false then name.to_sym
147 | when nil then namespaced_name.to_s.camelize.constantize
148 | when String then @options[:class].constantize
149 | else @options[:class]
150 | end
151 | end
152 |
153 | def resource_class_with_parent
154 | parent_resource ? {parent_resource => resource_class} : resource_class
155 | end
156 |
157 | def resource_instance=(instance)
158 | @controller.instance_variable_set("@#{instance_name}", instance)
159 | end
160 |
161 | def resource_instance
162 | @controller.instance_variable_get("@#{instance_name}") if load_instance?
163 | end
164 |
165 | def collection_instance=(instance)
166 | @controller.instance_variable_set("@#{instance_name.to_s.pluralize}", instance)
167 | end
168 |
169 | def collection_instance
170 | @controller.instance_variable_get("@#{instance_name.to_s.pluralize}")
171 | end
172 |
173 | # The object that methods (such as "find", "new" or "build") are called on.
174 | # If the :through option is passed it will go through an association on that instance.
175 | # If the :shallow option is passed it will use the resource_class if there's no parent
176 | # If the :singleton option is passed it won't use the association because it needs to be handled later.
177 | def resource_base
178 | if @options[:through]
179 | if parent_resource
180 | @options[:singleton] ? resource_class : parent_resource.send(@options[:through_association] || name.to_s.pluralize)
181 | elsif @options[:shallow]
182 | resource_class
183 | else
184 | raise AccessDenied.new(nil, authorization_action, resource_class) # maybe this should be a record not found error instead?
185 | end
186 | else
187 | resource_class
188 | end
189 | end
190 |
191 | def parent_name
192 | @options[:through] && [@options[:through]].flatten.detect { |i| fetch_parent(i) }
193 | end
194 |
195 | # The object to load this resource through.
196 | def parent_resource
197 | parent_name && fetch_parent(parent_name)
198 | end
199 |
200 | def fetch_parent(name)
201 | if @controller.instance_variable_defined? "@#{name}"
202 | @controller.instance_variable_get("@#{name}")
203 | elsif @controller.respond_to?(name, true)
204 | @controller.send(name)
205 | end
206 | end
207 |
208 | def current_ability
209 | @controller.send(:current_ability)
210 | end
211 |
212 | def name
213 | @name || name_from_controller
214 | end
215 |
216 | def resource_params
217 | if @options[:class]
218 | params_key = extract_key(@options[:class])
219 | return @params[params_key] if @params[params_key]
220 | end
221 |
222 | resource_params_by_namespaced_name
223 | end
224 |
225 | def resource_params_by_namespaced_name
226 | @params[extract_key(namespaced_name)]
227 | end
228 |
229 | def namespace
230 | @params[:controller].split(/::|\//)[0..-2]
231 | end
232 |
233 | def namespaced_name
234 | [namespace, name.camelize].join('::').singularize.camelize.constantize
235 | rescue NameError
236 | name
237 | end
238 |
239 | def name_from_controller
240 | @params[:controller].sub("Controller", "").underscore.split('/').last.singularize
241 | end
242 |
243 | def instance_name
244 | @options[:instance_name] || name
245 | end
246 |
247 | def collection_actions
248 | [:index] + [@options[:collection]].flatten
249 | end
250 |
251 | def new_actions
252 | [:new, :create] + [@options[:new]].flatten
253 | end
254 |
255 | private
256 |
257 | def extract_key(value)
258 | value.to_s.underscore.gsub('/', '_')
259 | end
260 | end
261 | end
262 |
--------------------------------------------------------------------------------
/lib/cancan/exceptions.rb:
--------------------------------------------------------------------------------
1 | module CanCan
2 | # A general CanCan exception
3 | class Error < StandardError; end
4 |
5 | # Raised when behavior is not implemented, usually used in an abstract class.
6 | class NotImplemented < Error; end
7 |
8 | # Raised when removed code is called, an alternative solution is provided in message.
9 | class ImplementationRemoved < Error; end
10 |
11 | # Raised when using check_authorization without calling authorized!
12 | class AuthorizationNotPerformed < Error; end
13 |
14 | # This error is raised when a user isn't allowed to access a given controller action.
15 | # This usually happens within a call to ControllerAdditions#authorize! but can be
16 | # raised manually.
17 | #
18 | # raise CanCan::AccessDenied.new("Not authorized!", :read, Article)
19 | #
20 | # The passed message, action, and subject are optional and can later be retrieved when
21 | # rescuing from the exception.
22 | #
23 | # exception.message # => "Not authorized!"
24 | # exception.action # => :read
25 | # exception.subject # => Article
26 | #
27 | # If the message is not specified (or is nil) it will default to "You are not authorized
28 | # to access this page." This default can be overridden by setting default_message.
29 | #
30 | # exception.default_message = "Default error message"
31 | # exception.message # => "Default error message"
32 | #
33 | # See ControllerAdditions#authorized! for more information on rescuing from this exception
34 | # and customizing the message using I18n.
35 | class AccessDenied < Error
36 | attr_reader :action, :subject
37 | attr_writer :default_message
38 |
39 | def initialize(message = nil, action = nil, subject = nil)
40 | @message = message
41 | @action = action
42 | @subject = subject
43 | @default_message = I18n.t(:"unauthorized.default", :default => "You are not authorized to access this page.")
44 | end
45 |
46 | def to_s
47 | @message || @default_message
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/cancan/inherited_resource.rb:
--------------------------------------------------------------------------------
1 | module CanCan
2 | # For use with Inherited Resources
3 | class InheritedResource < ControllerResource # :nodoc:
4 | def load_resource_instance
5 | if parent?
6 | @controller.send :association_chain
7 | @controller.instance_variable_get("@#{instance_name}")
8 | elsif new_actions.include? @params[:action].to_sym
9 | resource = @controller.send :build_resource
10 | assign_attributes(resource)
11 | else
12 | @controller.send :resource
13 | end
14 | end
15 |
16 | def resource_base
17 | @controller.send :end_of_association_chain
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/cancan/matchers.rb:
--------------------------------------------------------------------------------
1 | rspec_module = defined?(RSpec::Core) ? 'RSpec' : 'Spec' # for RSpec 1 compatability
2 | Kernel.const_get(rspec_module)::Matchers.define :be_able_to do |*args|
3 | match do |ability|
4 | ability.can?(*args)
5 | end
6 |
7 | failure_message_for_should do |ability|
8 | "expected to be able to #{args.map(&:inspect).join(" ")}"
9 | end
10 |
11 | failure_message_for_should_not do |ability|
12 | "expected not to be able to #{args.map(&:inspect).join(" ")}"
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/cancan/model_adapters/abstract_adapter.rb:
--------------------------------------------------------------------------------
1 | module CanCan
2 | module ModelAdapters
3 | class AbstractAdapter
4 | def self.inherited(subclass)
5 | @subclasses ||= []
6 | @subclasses << subclass
7 | end
8 |
9 | def self.adapter_class(model_class)
10 | @subclasses.detect { |subclass| subclass.for_class?(model_class) } || DefaultAdapter
11 | end
12 |
13 | # Used to determine if the given adapter should be used for the passed in class.
14 | def self.for_class?(member_class)
15 | false # override in subclass
16 | end
17 |
18 | # Override if you need custom find behavior
19 | def self.find(model_class, id)
20 | model_class.find(id)
21 | end
22 |
23 | # Used to determine if this model adapter will override the matching behavior for a hash of conditions.
24 | # If this returns true then matches_conditions_hash? will be called. See Rule#matches_conditions_hash
25 | def self.override_conditions_hash_matching?(subject, conditions)
26 | false
27 | end
28 |
29 | # Override if override_conditions_hash_matching? returns true
30 | def self.matches_conditions_hash?(subject, conditions)
31 | raise NotImplemented, "This model adapter does not support matching on a conditions hash."
32 | end
33 |
34 | # Used to determine if this model adapter will override the matching behavior for a specific condition.
35 | # If this returns true then matches_condition? will be called. See Rule#matches_conditions_hash
36 | def self.override_condition_matching?(subject, name, value)
37 | false
38 | end
39 |
40 | # Override if override_condition_matching? returns true
41 | def self.matches_condition?(subject, name, value)
42 | raise NotImplemented, "This model adapter does not support matching on a specific condition."
43 | end
44 |
45 | def initialize(model_class, rules)
46 | @model_class = model_class
47 | @rules = rules
48 | end
49 |
50 | def database_records
51 | # This should be overridden in a subclass to return records which match @rules
52 | raise NotImplemented, "This model adapter does not support fetching records from the database."
53 | end
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/cancan/model_adapters/active_record_adapter.rb:
--------------------------------------------------------------------------------
1 | module CanCan
2 | module ModelAdapters
3 | class ActiveRecordAdapter < AbstractAdapter
4 | def self.for_class?(model_class)
5 | model_class <= ActiveRecord::Base
6 | end
7 |
8 | def self.override_condition_matching?(subject, name, value)
9 | name.kind_of?(MetaWhere::Column) if defined? MetaWhere
10 | end
11 |
12 | def self.matches_condition?(subject, name, value)
13 | subject_value = subject.send(name.column)
14 | if name.method.to_s.ends_with? "_any"
15 | value.any? { |v| meta_where_match? subject_value, name.method.to_s.sub("_any", ""), v }
16 | elsif name.method.to_s.ends_with? "_all"
17 | value.all? { |v| meta_where_match? subject_value, name.method.to_s.sub("_all", ""), v }
18 | else
19 | meta_where_match? subject_value, name.method, value
20 | end
21 | end
22 |
23 | def self.meta_where_match?(subject_value, method, value)
24 | case method.to_sym
25 | when :eq then subject_value == value
26 | when :not_eq then subject_value != value
27 | when :in then value.include?(subject_value)
28 | when :not_in then !value.include?(subject_value)
29 | when :lt then subject_value < value
30 | when :lteq then subject_value <= value
31 | when :gt then subject_value > value
32 | when :gteq then subject_value >= value
33 | when :matches then subject_value =~ Regexp.new("^" + Regexp.escape(value).gsub("%", ".*") + "$", true)
34 | when :does_not_match then !meta_where_match?(subject_value, :matches, value)
35 | else raise NotImplemented, "The #{method} MetaWhere condition is not supported."
36 | end
37 | end
38 |
39 | # Returns conditions intended to be used inside a database query. Normally you will not call this
40 | # method directly, but instead go through ModelAdditions#accessible_by.
41 | #
42 | # If there is only one "can" definition, a hash of conditions will be returned matching the one defined.
43 | #
44 | # can :manage, User, :id => 1
45 | # query(:manage, User).conditions # => { :id => 1 }
46 | #
47 | # If there are multiple "can" definitions, a SQL string will be returned to handle complex cases.
48 | #
49 | # can :manage, User, :id => 1
50 | # can :manage, User, :manager_id => 1
51 | # cannot :manage, User, :self_managed => true
52 | # query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
53 | #
54 | def conditions
55 | if @rules.size == 1 && @rules.first.base_behavior
56 | # Return the conditions directly if there's just one definition
57 | tableized_conditions(@rules.first.conditions).dup
58 | else
59 | @rules.reverse.inject(false_sql) do |sql, rule|
60 | merge_conditions(sql, tableized_conditions(rule.conditions).dup, rule.base_behavior)
61 | end
62 | end
63 | end
64 |
65 | def tableized_conditions(conditions, model_class = @model_class)
66 | return conditions unless conditions.kind_of? Hash
67 | conditions.inject({}) do |result_hash, (name, value)|
68 | if value.kind_of? Hash
69 | value = value.dup
70 | association_class = model_class.reflect_on_association(name).class_name.constantize
71 | nested = value.inject({}) do |nested,(k,v)|
72 | if v.kind_of? Hash
73 | value.delete(k)
74 | nested[k] = v
75 | else
76 | name = model_class.reflect_on_association(name).table_name.to_sym
77 | result_hash[name] = value
78 | end
79 | nested
80 | end
81 | result_hash.merge!(tableized_conditions(nested,association_class))
82 | else
83 | result_hash[name] = value
84 | end
85 | result_hash
86 | end
87 | end
88 |
89 | # Returns the associations used in conditions for the :joins option of a search.
90 | # See ModelAdditions#accessible_by
91 | def joins
92 | joins_hash = {}
93 | @rules.each do |rule|
94 | merge_joins(joins_hash, rule.associations_hash)
95 | end
96 | clean_joins(joins_hash) unless joins_hash.empty?
97 | end
98 |
99 | def database_records
100 | if override_scope
101 | @model_class.scoped.merge(override_scope)
102 | elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
103 | mergeable_conditions = @rules.select {|rule| rule.unmergeable? }.blank?
104 | if mergeable_conditions
105 | @model_class.where(conditions).joins(joins)
106 | else
107 | @model_class.where(*(@rules.map(&:conditions))).joins(joins)
108 | end
109 | else
110 | @model_class.scoped(:conditions => conditions, :joins => joins)
111 | end
112 | end
113 |
114 | private
115 |
116 | def override_scope
117 | conditions = @rules.map(&:conditions).compact
118 | if defined?(ActiveRecord::Relation) && conditions.any? { |c| c.kind_of?(ActiveRecord::Relation) }
119 | if conditions.size == 1
120 | conditions.first
121 | else
122 | rule = @rules.detect { |rule| rule.conditions.kind_of?(ActiveRecord::Relation) }
123 | raise Error, "Unable to merge an Active Record scope with other conditions. Instead use a hash or SQL for #{rule.actions.first} #{rule.subjects.first} ability."
124 | end
125 | end
126 | end
127 |
128 | def merge_conditions(sql, conditions_hash, behavior)
129 | if conditions_hash.blank?
130 | behavior ? true_sql : false_sql
131 | else
132 | conditions = sanitize_sql(conditions_hash)
133 | case sql
134 | when true_sql
135 | behavior ? true_sql : "not (#{conditions})"
136 | when false_sql
137 | behavior ? conditions : false_sql
138 | else
139 | behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
140 | end
141 | end
142 | end
143 |
144 | def false_sql
145 | sanitize_sql(['?=?', true, false])
146 | end
147 |
148 | def true_sql
149 | sanitize_sql(['?=?', true, true])
150 | end
151 |
152 | def sanitize_sql(conditions)
153 | @model_class.send(:sanitize_sql, conditions)
154 | end
155 |
156 | # Takes two hashes and does a deep merge.
157 | def merge_joins(base, add)
158 | add.each do |name, nested|
159 | if base[name].is_a?(Hash)
160 | merge_joins(base[name], nested) unless nested.empty?
161 | else
162 | base[name] = nested
163 | end
164 | end
165 | end
166 |
167 | # Removes empty hashes and moves everything into arrays.
168 | def clean_joins(joins_hash)
169 | joins = []
170 | joins_hash.each do |name, nested|
171 | joins << (nested.empty? ? name : {name => clean_joins(nested)})
172 | end
173 | joins
174 | end
175 | end
176 | end
177 | end
178 |
179 | ActiveRecord::Base.class_eval do
180 | include CanCan::ModelAdditions
181 | end
182 |
--------------------------------------------------------------------------------
/lib/cancan/model_adapters/data_mapper_adapter.rb:
--------------------------------------------------------------------------------
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.find(model_class, id)
9 | model_class.get(id)
10 | end
11 |
12 | def self.override_conditions_hash_matching?(subject, conditions)
13 | conditions.any? { |k,v| !k.kind_of?(Symbol) }
14 | end
15 |
16 | def self.matches_conditions_hash?(subject, conditions)
17 | collection = DataMapper::Collection.new(subject.query, [ subject ])
18 | !!collection.first(conditions)
19 | end
20 |
21 | def database_records
22 | scope = @model_class.all(:conditions => ["0 = 1"])
23 | cans, cannots = @rules.partition { |r| r.base_behavior }
24 | return scope if cans.empty?
25 | # apply unions first, then differences. this mean cannot overrides can
26 | cans.each { |r| scope += @model_class.all(:conditions => r.conditions) }
27 | cannots.each { |r| scope -= @model_class.all(:conditions => r.conditions) }
28 | scope
29 | end
30 | end # class DataMapper
31 | end # module ModelAdapters
32 | end # module CanCan
33 |
34 | DataMapper::Model.append_extensions(CanCan::ModelAdditions::ClassMethods)
35 |
--------------------------------------------------------------------------------
/lib/cancan/model_adapters/default_adapter.rb:
--------------------------------------------------------------------------------
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
8 |
--------------------------------------------------------------------------------
/lib/cancan/model_adapters/mongoid_adapter.rb:
--------------------------------------------------------------------------------
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? do |k,v|
10 | key_is_not_symbol = lambda { !k.kind_of?(Symbol) }
11 | subject_value_is_array = lambda do
12 | subject.respond_to?(k) && subject.send(k).is_a?(Array)
13 | end
14 |
15 | key_is_not_symbol.call || subject_value_is_array.call
16 | end
17 | end
18 |
19 | def self.matches_conditions_hash?(subject, conditions)
20 | # To avoid hitting the db, retrieve the raw Mongo selector from
21 | # the Mongoid Criteria and use Mongoid::Matchers#matches?
22 | subject.matches?( subject.class.where(conditions).selector )
23 | end
24 |
25 | def database_records
26 | if @rules.size == 0
27 | @model_class.where(:_id => {'$exists' => false, '$type' => 7}) # return no records in Mongoid
28 | elsif @rules.size == 1 && @rules[0].conditions.is_a?(Mongoid::Criteria)
29 | @rules[0].conditions
30 | else
31 | # we only need to process can rules if
32 | # there are no rules with empty conditions
33 | rules = @rules.reject { |rule| rule.conditions.empty? && rule.base_behavior }
34 | process_can_rules = @rules.count == rules.count
35 |
36 | rules.inject(@model_class.all) do |records, rule|
37 | if process_can_rules && rule.base_behavior
38 | records.or rule.conditions
39 | elsif !rule.base_behavior
40 | records.excludes rule.conditions
41 | else
42 | records
43 | end
44 | end
45 | end
46 | end
47 | end
48 | end
49 | end
50 |
51 | # simplest way to add `accessible_by` to all Mongoid Documents
52 | module Mongoid::Document::ClassMethods
53 | include CanCan::ModelAdditions::ClassMethods
54 | end
55 |
--------------------------------------------------------------------------------
/lib/cancan/model_additions.rb:
--------------------------------------------------------------------------------
1 | module CanCan
2 |
3 | # This module adds the accessible_by class method to a model. It is included in the model adapters.
4 | module ModelAdditions
5 | module ClassMethods
6 | # Returns a scope which fetches only the records that the passed ability
7 | # can perform a given action on. The action defaults to :index. This
8 | # is usually called from a controller and passed the +current_ability+.
9 | #
10 | # @articles = Article.accessible_by(current_ability)
11 | #
12 | # Here only the articles which the user is able to read will be returned.
13 | # If the user does not have permission to read any articles then an empty
14 | # result is returned. Since this is a scope it can be combined with any
15 | # other scopes or pagination.
16 | #
17 | # An alternative action can optionally be passed as a second argument.
18 | #
19 | # @articles = Article.accessible_by(current_ability, :update)
20 | #
21 | # Here only the articles which the user can update are returned.
22 | def accessible_by(ability, action = :index)
23 | ability.model_adapter(self, action).database_records
24 | end
25 | end
26 |
27 | def self.included(base)
28 | base.extend ClassMethods
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/cancan/rule.rb:
--------------------------------------------------------------------------------
1 | module CanCan
2 | # This class is used internally and should only be called through Ability.
3 | # it holds the information about a "can" call made on Ability and provides
4 | # helpful methods to determine permission checking and conditions hash generation.
5 | class Rule # :nodoc:
6 | attr_reader :base_behavior, :subjects, :actions, :conditions
7 | attr_writer :expanded_actions
8 |
9 | # The first argument when initializing is the base_behavior which is a true/false
10 | # value. True for "can" and false for "cannot". The next two arguments are the action
11 | # and subject respectively (such as :read, @project). The third argument is a hash
12 | # of conditions and the last one is the block passed to the "can" call.
13 | def initialize(base_behavior, action, subject, conditions, block)
14 | raise Error, "You are not able to supply a block with a hash of conditions in #{action} #{subject} ability. Use either one." if conditions.kind_of?(Hash) && !block.nil?
15 | @match_all = action.nil? && subject.nil?
16 | @base_behavior = base_behavior
17 | @actions = [action].flatten
18 | @subjects = [subject].flatten
19 | @conditions = conditions || {}
20 | @block = block
21 | end
22 |
23 | # Matches both the subject and action, not necessarily the conditions
24 | def relevant?(action, subject)
25 | subject = subject.values.first if subject.class == Hash
26 | @match_all || (matches_action?(action) && matches_subject?(subject))
27 | end
28 |
29 | # Matches the block or conditions hash
30 | def matches_conditions?(action, subject, extra_args)
31 | if @match_all
32 | call_block_with_all(action, subject, extra_args)
33 | elsif @block && !subject_class?(subject)
34 | @block.call(subject, *extra_args)
35 | elsif @conditions.kind_of?(Hash) && subject.class == Hash
36 | nested_subject_matches_conditions?(subject)
37 | elsif @conditions.kind_of?(Hash) && !subject_class?(subject)
38 | matches_conditions_hash?(subject)
39 | else
40 | # Don't stop at "cannot" definitions when there are conditions.
41 | @conditions.empty? ? true : @base_behavior
42 | end
43 | end
44 |
45 | def only_block?
46 | conditions_empty? && !@block.nil?
47 | end
48 |
49 | def only_raw_sql?
50 | @block.nil? && !conditions_empty? && !@conditions.kind_of?(Hash)
51 | end
52 |
53 | def conditions_empty?
54 | @conditions == {} || @conditions.nil?
55 | end
56 |
57 | def unmergeable?
58 | @conditions.respond_to?(:keys) && @conditions.present? &&
59 | (!@conditions.keys.first.kind_of? Symbol)
60 | end
61 |
62 | def associations_hash(conditions = @conditions)
63 | hash = {}
64 | conditions.map do |name, value|
65 | hash[name] = associations_hash(value) if value.kind_of? Hash
66 | end if conditions.kind_of? Hash
67 | hash
68 | end
69 |
70 | def attributes_from_conditions
71 | attributes = {}
72 | @conditions.each do |key, value|
73 | attributes[key] = value unless [Array, Range, Hash].include? value.class
74 | end if @conditions.kind_of? Hash
75 | attributes
76 | end
77 |
78 | private
79 |
80 | def subject_class?(subject)
81 | klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class
82 | klass == Class || klass == Module
83 | end
84 |
85 | def matches_action?(action)
86 | @expanded_actions.include?(:manage) || @expanded_actions.include?(action)
87 | end
88 |
89 | def matches_subject?(subject)
90 | @subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject)
91 | end
92 |
93 | def matches_subject_class?(subject)
94 | @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)) }
95 | end
96 |
97 | # Checks if the given subject matches the given conditions hash.
98 | # This behavior can be overriden by a model adapter by defining two class methods:
99 | # override_matching_for_conditions?(subject, conditions) and
100 | # matches_conditions_hash?(subject, conditions)
101 | def matches_conditions_hash?(subject, conditions = @conditions)
102 | if conditions.empty?
103 | true
104 | else
105 | if model_adapter(subject).override_conditions_hash_matching? subject, conditions
106 | model_adapter(subject).matches_conditions_hash? subject, conditions
107 | else
108 | conditions.all? do |name, value|
109 | if model_adapter(subject).override_condition_matching? subject, name, value
110 | model_adapter(subject).matches_condition? subject, name, value
111 | else
112 | attribute = subject.send(name)
113 | if value.kind_of?(Hash)
114 | if attribute.kind_of? Array
115 | attribute.any? { |element| matches_conditions_hash? element, value }
116 | else
117 | !attribute.nil? && matches_conditions_hash?(attribute, value)
118 | end
119 | elsif !value.is_a?(String) && value.kind_of?(Enumerable)
120 | value.include? attribute
121 | else
122 | attribute == value
123 | end
124 | end
125 | end
126 | end
127 | end
128 | end
129 |
130 | def nested_subject_matches_conditions?(subject_hash)
131 | parent, child = subject_hash.first
132 | matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
133 | end
134 |
135 | def call_block_with_all(action, subject, extra_args)
136 | if subject.class == Class
137 | @block.call(action, subject, nil, *extra_args)
138 | else
139 | @block.call(action, subject.class, subject, *extra_args)
140 | end
141 | end
142 |
143 | def model_adapter(subject)
144 | CanCan::ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
145 | end
146 | end
147 | end
148 |
--------------------------------------------------------------------------------
/lib/generators/cancan/ability/USAGE:
--------------------------------------------------------------------------------
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.
5 |
--------------------------------------------------------------------------------
/lib/generators/cancan/ability/ability_generator.rb:
--------------------------------------------------------------------------------
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
12 |
--------------------------------------------------------------------------------
/lib/generators/cancan/ability/templates/ability.rb:
--------------------------------------------------------------------------------
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
15 | # permission to do.
16 | # If you pass :manage it will apply to every action. Other common actions
17 | # here are :read, :create, :update and :destroy.
18 | #
19 | # The second argument is the resource the user can perform the action on.
20 | # If you pass :all it will apply to every resource. Otherwise pass a Ruby
21 | # class of the resource.
22 | #
23 | # The third argument is an optional hash of conditions to further filter the
24 | # objects.
25 | # For example, here the user can only update published articles.
26 | #
27 | # can :update, Article, :published => true
28 | #
29 | # See the wiki for details:
30 | # https://github.com/ryanb/cancan/wiki/Defining-Abilities
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/README.rdoc:
--------------------------------------------------------------------------------
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
29 |
--------------------------------------------------------------------------------
/spec/cancan/ability_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe CanCan::Ability do
4 | before(:each) do
5 | @ability = Object.new
6 | @ability.extend(CanCan::Ability)
7 | end
8 |
9 | it "should be able to :read anything" do
10 | @ability.can :read, :all
11 | @ability.can?(:read, String).should be_true
12 | @ability.can?(:read, 123).should be_true
13 | end
14 |
15 | it "should not have permission to do something it doesn't know about" do
16 | @ability.can?(:foodfight, String).should be_false
17 | end
18 |
19 | it "should pass true to `can?` when non false/nil is returned in block" do
20 | @ability.can :read, :all
21 | @ability.can :read, Symbol do |sym|
22 | "foo" # TODO test that sym is nil when no instance is passed
23 | end
24 | @ability.can?(:read, :some_symbol).should == true
25 | end
26 |
27 | it "should pass nil to a block when no instance is passed" do
28 | @ability.can :read, Symbol do |sym|
29 | sym.should be_nil
30 | true
31 | end
32 | @ability.can?(:read, Symbol).should be_true
33 | end
34 |
35 | it "should pass to previous rule, if block returns false or nil" do
36 | @ability.can :read, Symbol
37 | @ability.can :read, Integer do |i|
38 | i < 5
39 | end
40 | @ability.can :read, Integer do |i|
41 | i > 10
42 | end
43 | @ability.can?(:read, Symbol).should be_true
44 | @ability.can?(:read, 11).should be_true
45 | @ability.can?(:read, 1).should be_true
46 | @ability.can?(:read, 6).should be_false
47 | end
48 |
49 | it "should not pass class with object if :all objects are accepted" do
50 | @ability.can :preview, :all do |object|
51 | object.should == 123
52 | @block_called = true
53 | end
54 | @ability.can?(:preview, 123)
55 | @block_called.should be_true
56 | end
57 |
58 | it "should not call block when only class is passed, only return true" do
59 | @block_called = false
60 | @ability.can :preview, :all do |object|
61 | @block_called = true
62 | end
63 | @ability.can?(:preview, Hash).should be_true
64 | @block_called.should be_false
65 | end
66 |
67 | it "should pass only object for global manage actions" do
68 | @ability.can :manage, String do |object|
69 | object.should == "foo"
70 | @block_called = true
71 | end
72 | @ability.can?(:stuff, "foo").should
73 | @block_called.should be_true
74 | end
75 |
76 | it "should alias update or destroy actions to modify action" do
77 | @ability.alias_action :update, :destroy, :to => :modify
78 | @ability.can :modify, :all
79 | @ability.can?(:update, 123).should be_true
80 | @ability.can?(:destroy, 123).should be_true
81 | end
82 |
83 | it "should allow deeply nested aliased actions" do
84 | @ability.alias_action :increment, :to => :sort
85 | @ability.alias_action :sort, :to => :modify
86 | @ability.can :modify, :all
87 | @ability.can?(:increment, 123).should be_true
88 | end
89 |
90 | it "should raise an Error if alias target is an exist action" do
91 | lambda{ @ability.alias_action :show, :to => :show }.should raise_error(CanCan::Error, "You can't specify target (show) as alias because it is real action name")
92 | end
93 |
94 | it "should always call block with arguments when passing no arguments to can" do
95 | @ability.can do |action, object_class, object|
96 | action.should == :foo
97 | object_class.should == 123.class
98 | object.should == 123
99 | @block_called = true
100 | end
101 | @ability.can?(:foo, 123)
102 | @block_called.should be_true
103 | end
104 |
105 | it "should pass nil to object when comparing class with can check" do
106 | @ability.can do |action, object_class, object|
107 | action.should == :foo
108 | object_class.should == Hash
109 | object.should be_nil
110 | @block_called = true
111 | end
112 | @ability.can?(:foo, Hash)
113 | @block_called.should be_true
114 | end
115 |
116 | it "should automatically alias index and show into read calls" do
117 | @ability.can :read, :all
118 | @ability.can?(:index, 123).should be_true
119 | @ability.can?(:show, 123).should be_true
120 | end
121 |
122 | it "should automatically alias new and edit into create and update respectively" do
123 | @ability.can :create, :all
124 | @ability.can :update, :all
125 | @ability.can?(:new, 123).should be_true
126 | @ability.can?(:edit, 123).should be_true
127 | end
128 |
129 | it "should not respond to prepare (now using initialize)" do
130 | @ability.should_not respond_to(:prepare)
131 | end
132 |
133 | it "should offer cannot? method which is simply invert of can?" do
134 | @ability.cannot?(:tie, String).should be_true
135 | end
136 |
137 | it "should be able to specify multiple actions and match any" do
138 | @ability.can [:read, :update], :all
139 | @ability.can?(:read, 123).should be_true
140 | @ability.can?(:update, 123).should be_true
141 | @ability.can?(:count, 123).should be_false
142 | end
143 |
144 | it "should be able to specify multiple classes and match any" do
145 | @ability.can :update, [String, Range]
146 | @ability.can?(:update, "foo").should be_true
147 | @ability.can?(:update, 1..3).should be_true
148 | @ability.can?(:update, 123).should be_false
149 | end
150 |
151 | it "should support custom objects in the rule" do
152 | @ability.can :read, :stats
153 | @ability.can?(:read, :stats).should be_true
154 | @ability.can?(:update, :stats).should be_false
155 | @ability.can?(:read, :nonstats).should be_false
156 | end
157 |
158 | it "should check ancestors of class" do
159 | @ability.can :read, Numeric
160 | @ability.can?(:read, Integer).should be_true
161 | @ability.can?(:read, 1.23).should be_true
162 | @ability.can?(:read, "foo").should be_false
163 | end
164 |
165 | it "should support 'cannot' method to define what user cannot do" do
166 | @ability.can :read, :all
167 | @ability.cannot :read, Integer
168 | @ability.can?(:read, "foo").should be_true
169 | @ability.can?(:read, 123).should be_false
170 | end
171 |
172 | it "should pass to previous rule, if block returns false or nil" do
173 | @ability.can :read, :all
174 | @ability.cannot :read, Integer do |int|
175 | int > 10 ? nil : ( int > 5 )
176 | end
177 | @ability.can?(:read, "foo").should be_true
178 | @ability.can?(:read, 3).should be_true
179 | @ability.can?(:read, 8).should be_false
180 | @ability.can?(:read, 123).should be_true
181 | end
182 |
183 | it "should always return `false` for single cannot definition" do
184 | @ability.cannot :read, Integer do |int|
185 | int > 10 ? nil : ( int > 5 )
186 | end
187 | @ability.can?(:read, "foo").should be_false
188 | @ability.can?(:read, 3).should be_false
189 | @ability.can?(:read, 8).should be_false
190 | @ability.can?(:read, 123).should be_false
191 | end
192 |
193 | it "should pass to previous cannot definition, if block returns false or nil" do
194 | @ability.cannot :read, :all
195 | @ability.can :read, Integer do |int|
196 | int > 10 ? nil : ( int > 5 )
197 | end
198 | @ability.can?(:read, "foo").should be_false
199 | @ability.can?(:read, 3).should be_false
200 | @ability.can?(:read, 10).should be_true
201 | @ability.can?(:read, 123).should be_false
202 | end
203 |
204 | it "should append aliased actions" do
205 | @ability.alias_action :update, :to => :modify
206 | @ability.alias_action :destroy, :to => :modify
207 | @ability.aliased_actions[:modify].should == [:update, :destroy]
208 | end
209 |
210 | it "should clear aliased actions" do
211 | @ability.alias_action :update, :to => :modify
212 | @ability.clear_aliased_actions
213 | @ability.aliased_actions[:modify].should be_nil
214 | end
215 |
216 | it "should pass additional arguments to block from can?" do
217 | @ability.can :read, Integer do |int, x|
218 | int > x
219 | end
220 | @ability.can?(:read, 2, 1).should be_true
221 | @ability.can?(:read, 2, 3).should be_false
222 | end
223 |
224 | it "should use conditions as third parameter and determine abilities from it" do
225 | @ability.can :read, Range, :begin => 1, :end => 3
226 | @ability.can?(:read, 1..3).should be_true
227 | @ability.can?(:read, 1..4).should be_false
228 | @ability.can?(:read, Range).should be_true
229 | end
230 |
231 | it "should allow an array of options in conditions hash" do
232 | @ability.can :read, Range, :begin => [1, 3, 5]
233 | @ability.can?(:read, 1..3).should be_true
234 | @ability.can?(:read, 2..4).should be_false
235 | @ability.can?(:read, 3..5).should be_true
236 | end
237 |
238 | it "should allow a range of options in conditions hash" do
239 | @ability.can :read, Range, :begin => 1..3
240 | @ability.can?(:read, 1..10).should be_true
241 | @ability.can?(:read, 3..30).should be_true
242 | @ability.can?(:read, 4..40).should be_false
243 | end
244 |
245 | it "should allow nested hashes in conditions hash" do
246 | @ability.can :read, Range, :begin => { :to_i => 5 }
247 | @ability.can?(:read, 5..7).should be_true
248 | @ability.can?(:read, 6..8).should be_false
249 | end
250 |
251 | it "should match any element passed in to nesting if it's an array (for has_many associations)" do
252 | @ability.can :read, Range, :to_a => { :to_i => 3 }
253 | @ability.can?(:read, 1..5).should be_true
254 | @ability.can?(:read, 4..6).should be_false
255 | end
256 |
257 | it "should accept a set as a condition value" do
258 | mock(object_with_foo_2 = Object.new).foo { 2 }
259 | mock(object_with_foo_3 = Object.new).foo { 3 }
260 | @ability.can :read, Object, :foo => [1, 2, 5].to_set
261 | @ability.can?(:read, object_with_foo_2).should be_true
262 | @ability.can?(:read, object_with_foo_3).should be_false
263 | end
264 |
265 | it "should not match subjects return nil for methods that must match nested a nested conditions hash" do
266 | mock(object_with_foo = Object.new).foo { :bar }
267 | @ability.can :read, Array, :first => { :foo => :bar }
268 | @ability.can?(:read, [object_with_foo]).should be_true
269 | @ability.can?(:read, []).should be_false
270 | end
271 |
272 | it "should match strings but not substrings specified in a conditions hash" do
273 | @ability.can :read, String, :presence => "declassified"
274 | @ability.can?(:read, "declassified").should be_true
275 | @ability.can?(:read, "classified").should be_false
276 | end
277 |
278 | it "should not stop at cannot definition when comparing class" do
279 | @ability.can :read, Range
280 | @ability.cannot :read, Range, :begin => 1
281 | @ability.can?(:read, 2..5).should be_true
282 | @ability.can?(:read, 1..5).should be_false
283 | @ability.can?(:read, Range).should be_true
284 | end
285 |
286 | it "should stop at cannot definition when no hash is present" do
287 | @ability.can :read, :all
288 | @ability.cannot :read, Range
289 | @ability.can?(:read, 1..5).should be_false
290 | @ability.can?(:read, Range).should be_false
291 | end
292 |
293 | it "should allow to check ability for Module" do
294 | module B; end
295 | class A; include B; end
296 | @ability.can :read, B
297 | @ability.can?(:read, A).should be_true
298 | @ability.can?(:read, A.new).should be_true
299 | end
300 |
301 | it "should pass nil to a block for ability on Module when no instance is passed" do
302 | module B; end
303 | class A; include B; end
304 | @ability.can :read, B do |sym|
305 | sym.should be_nil
306 | true
307 | end
308 | @ability.can?(:read, B).should be_true
309 | @ability.can?(:read, A).should be_true
310 | end
311 |
312 | it "passing a hash of subjects should check permissions through association" do
313 | @ability.can :read, Range, :string => {:length => 3}
314 | @ability.can?(:read, "foo" => Range).should be_true
315 | @ability.can?(:read, "foobar" => Range).should be_false
316 | @ability.can?(:read, 123 => Range).should be_true
317 | end
318 |
319 | it "passing a hash of subjects with multiple definitions should check permissions correctly" do
320 | @ability.can :read, Range, :string => {:length => 4}
321 | @ability.can [:create, :read], Range, :string => {:upcase => 'FOO'}
322 | @ability.can?(:read, "foo" => Range).should be_true
323 | @ability.can?(:read, "foobar" => Range).should be_false
324 | @ability.can?(:read, 1234 => Range).should be_true
325 | end
326 |
327 | it "should allow to check ability on Hash-like object" do
328 | class Container < Hash; end
329 | @ability.can :read, Container
330 | @ability.can?(:read, Container.new).should be_true
331 | end
332 |
333 | it "should have initial attributes based on hash conditions of 'new' action" do
334 | @ability.can :manage, Range, :foo => "foo", :hash => {:skip => "hashes"}
335 | @ability.can :create, Range, :bar => 123, :array => %w[skip arrays]
336 | @ability.can :new, Range, :baz => "baz", :range => 1..3
337 | @ability.cannot :new, Range, :ignore => "me"
338 | @ability.attributes_for(:new, Range).should == {:foo => "foo", :bar => 123, :baz => "baz"}
339 | end
340 |
341 | it "should raise access denied exception if ability us unauthorized to perform a certain action" do
342 | begin
343 | @ability.authorize! :read, :foo, 1, 2, 3, :message => "Access denied!"
344 | rescue CanCan::AccessDenied => e
345 | e.message.should == "Access denied!"
346 | e.action.should == :read
347 | e.subject.should == :foo
348 | else
349 | fail "Expected CanCan::AccessDenied exception to be raised"
350 | end
351 | end
352 |
353 | it "should not raise access denied exception if ability is authorized to perform an action and return subject" do
354 | @ability.can :read, :foo
355 | lambda {
356 | @ability.authorize!(:read, :foo).should == :foo
357 | }.should_not raise_error
358 | end
359 |
360 | it "should know when block is used in conditions" do
361 | @ability.can :read, :foo
362 | @ability.should_not have_block(:read, :foo)
363 | @ability.can :read, :foo do |foo|
364 | false
365 | end
366 | @ability.should have_block(:read, :foo)
367 | end
368 |
369 | it "should know when raw sql is used in conditions" do
370 | @ability.can :read, :foo
371 | @ability.should_not have_raw_sql(:read, :foo)
372 | @ability.can :read, :foo, 'false'
373 | @ability.should have_raw_sql(:read, :foo)
374 | end
375 |
376 | it "should raise access denied exception with default message if not specified" do
377 | begin
378 | @ability.authorize! :read, :foo
379 | rescue CanCan::AccessDenied => e
380 | e.default_message = "Access denied!"
381 | e.message.should == "Access denied!"
382 | else
383 | fail "Expected CanCan::AccessDenied exception to be raised"
384 | end
385 | end
386 |
387 | it "should determine model adapter class by asking AbstractAdapter" do
388 | model_class = Object.new
389 | adapter_class = Object.new
390 | stub(CanCan::ModelAdapters::AbstractAdapter).adapter_class(model_class) { adapter_class }
391 | stub(adapter_class).new(model_class, []) { :adapter_instance }
392 | @ability.model_adapter(model_class, :read).should == :adapter_instance
393 | end
394 |
395 | it "should raise an error when attempting to use a block with a hash condition since it's not likely what they want" do
396 | lambda {
397 | @ability.can :read, Array, :published => true do
398 | false
399 | end
400 | }.should raise_error(CanCan::Error, "You are not able to supply a block with a hash of conditions in read Array ability. Use either one.")
401 | end
402 |
403 | describe "unauthorized message" do
404 | after(:each) do
405 | I18n.backend = nil
406 | end
407 |
408 | it "should use action/subject in i18n" do
409 | I18n.backend.store_translations :en, :unauthorized => {:update => {:array => "foo"}}
410 | @ability.unauthorized_message(:update, Array).should == "foo"
411 | @ability.unauthorized_message(:update, [1, 2, 3]).should == "foo"
412 | @ability.unauthorized_message(:update, :missing).should be_nil
413 | end
414 |
415 | it "should use symbol as subject directly" do
416 | I18n.backend.store_translations :en, :unauthorized => {:has => {:cheezburger => "Nom nom nom. I eated it."}}
417 | @ability.unauthorized_message(:has, :cheezburger).should == "Nom nom nom. I eated it."
418 | end
419 |
420 | it "should fall back to 'manage' and 'all'" do
421 | I18n.backend.store_translations :en, :unauthorized => {
422 | :manage => {:all => "manage all", :array => "manage array"},
423 | :update => {:all => "update all", :array => "update array"}
424 | }
425 | @ability.unauthorized_message(:update, Array).should == "update array"
426 | @ability.unauthorized_message(:update, Hash).should == "update all"
427 | @ability.unauthorized_message(:foo, Array).should == "manage array"
428 | @ability.unauthorized_message(:foo, Hash).should == "manage all"
429 | end
430 |
431 | it "should follow aliased actions" do
432 | I18n.backend.store_translations :en, :unauthorized => {:modify => {:array => "modify array"}}
433 | @ability.alias_action :update, :to => :modify
434 | @ability.unauthorized_message(:update, Array).should == "modify array"
435 | @ability.unauthorized_message(:edit, Array).should == "modify array"
436 | end
437 |
438 | it "should have variables for action and subject" do
439 | I18n.backend.store_translations :en, :unauthorized => {:manage => {:all => "%{action} %{subject}"}} # old syntax for now in case testing with old I18n
440 | @ability.unauthorized_message(:update, Array).should == "update array"
441 | @ability.unauthorized_message(:update, ArgumentError).should == "update argument error"
442 | @ability.unauthorized_message(:edit, 1..3).should == "edit range"
443 | end
444 | end
445 |
446 | describe "#merge" do
447 | it "should add the rules from the given ability" do
448 | @ability.can :use, :tools
449 | another_ability = Object.new
450 | another_ability.extend(CanCan::Ability)
451 | another_ability.can :use, :search
452 |
453 | @ability.merge(another_ability)
454 | @ability.can?(:use, :search).should be_true
455 | @ability.send(:rules).size.should == 2
456 | end
457 | end
458 | end
459 |
--------------------------------------------------------------------------------
/spec/cancan/controller_additions_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe CanCan::ControllerAdditions do
4 | before(:each) do
5 | @controller_class = Class.new
6 | @controller = @controller_class.new
7 | stub(@controller).params { {} }
8 | stub(@controller).current_user { :current_user }
9 | mock(@controller_class).helper_method(:can?, :cannot?, :current_ability)
10 | @controller_class.send(:include, CanCan::ControllerAdditions)
11 | end
12 |
13 | it "should raise ImplementationRemoved when attempting to call 'unauthorized!' on a controller" do
14 | lambda { @controller.unauthorized! }.should raise_error(CanCan::ImplementationRemoved)
15 | end
16 |
17 | it "authorize! should assign @_authorized instance variable and pass args to current ability" do
18 | mock(@controller.current_ability).authorize!(:foo, :bar)
19 | @controller.authorize!(:foo, :bar)
20 | @controller.instance_variable_get(:@_authorized).should be_true
21 | end
22 |
23 | it "should have a current_ability method which generates an ability for the current user" do
24 | @controller.current_ability.should be_kind_of(Ability)
25 | end
26 |
27 | it "should provide a can? and cannot? methods which go through the current ability" do
28 | @controller.current_ability.should be_kind_of(Ability)
29 | @controller.can?(:foo, :bar).should be_false
30 | @controller.cannot?(:foo, :bar).should be_true
31 | end
32 |
33 | it "load_and_authorize_resource should setup a before filter which passes call to ControllerResource" do
34 | stub(CanCan::ControllerResource).new(@controller, nil, :foo => :bar).mock!.load_and_authorize_resource
35 | mock(@controller_class).before_filter({}) { |options, block| block.call(@controller) }
36 | @controller_class.load_and_authorize_resource :foo => :bar
37 | end
38 |
39 | it "load_and_authorize_resource should properly pass first argument as the resource name" do
40 | stub(CanCan::ControllerResource).new(@controller, :project, :foo => :bar).mock!.load_and_authorize_resource
41 | mock(@controller_class).before_filter({}) { |options, block| block.call(@controller) }
42 | @controller_class.load_and_authorize_resource :project, :foo => :bar
43 | end
44 |
45 | it "load_and_authorize_resource with :prepend should prepend the before filter" do
46 | mock(@controller_class).prepend_before_filter({})
47 | @controller_class.load_and_authorize_resource :foo => :bar, :prepend => true
48 | end
49 |
50 | it "authorize_resource should setup a before filter which passes call to ControllerResource" do
51 | stub(CanCan::ControllerResource).new(@controller, nil, :foo => :bar).mock!.authorize_resource
52 | mock(@controller_class).before_filter(:except => :show, :if => true) { |options, block| block.call(@controller) }
53 | @controller_class.authorize_resource :foo => :bar, :except => :show, :if => true
54 | end
55 |
56 | it "load_resource should setup a before filter which passes call to ControllerResource" do
57 | stub(CanCan::ControllerResource).new(@controller, nil, :foo => :bar).mock!.load_resource
58 | mock(@controller_class).before_filter(:only => [:show, :index], :unless => false) { |options, block| block.call(@controller) }
59 | @controller_class.load_resource :foo => :bar, :only => [:show, :index], :unless => false
60 | end
61 |
62 | it "skip_authorization_check should set up a before filter which sets @_authorized to true" do
63 | mock(@controller_class).before_filter(:filter_options) { |options, block| block.call(@controller) }
64 | @controller_class.skip_authorization_check(:filter_options)
65 | @controller.instance_variable_get(:@_authorized).should be_true
66 | end
67 |
68 | it "check_authorization should trigger AuthorizationNotPerformed in after filter" do
69 | mock(@controller_class).after_filter(:only => [:test]) { |options, block| block.call(@controller) }
70 | lambda {
71 | @controller_class.check_authorization(:only => [:test])
72 | }.should raise_error(CanCan::AuthorizationNotPerformed)
73 | end
74 |
75 | it "check_authorization should not trigger AuthorizationNotPerformed when :if is false" do
76 | stub(@controller).check_auth? { false }
77 | mock(@controller_class).after_filter({}) { |options, block| block.call(@controller) }
78 | lambda {
79 | @controller_class.check_authorization(:if => :check_auth?)
80 | }.should_not raise_error(CanCan::AuthorizationNotPerformed)
81 | end
82 |
83 | it "check_authorization should not trigger AuthorizationNotPerformed when :unless is true" do
84 | stub(@controller).engine_controller? { true }
85 | mock(@controller_class).after_filter({}) { |options, block| block.call(@controller) }
86 | lambda {
87 | @controller_class.check_authorization(:unless => :engine_controller?)
88 | }.should_not raise_error(CanCan::AuthorizationNotPerformed)
89 | end
90 |
91 | it "check_authorization should not raise error when @_authorized is set" do
92 | @controller.instance_variable_set(:@_authorized, true)
93 | mock(@controller_class).after_filter(:only => [:test]) { |options, block| block.call(@controller) }
94 | lambda {
95 | @controller_class.check_authorization(:only => [:test])
96 | }.should_not raise_error(CanCan::AuthorizationNotPerformed)
97 | end
98 |
99 | it "cancan_resource_class should be ControllerResource by default" do
100 | @controller.class.cancan_resource_class.should == CanCan::ControllerResource
101 | end
102 |
103 | it "cancan_resource_class should be InheritedResource when class includes InheritedResources::Actions" do
104 | stub(@controller.class).ancestors { ["InheritedResources::Actions"] }
105 | @controller.class.cancan_resource_class.should == CanCan::InheritedResource
106 | end
107 |
108 | it "cancan_skipper should be an empty hash with :authorize and :load options and remember changes" do
109 | @controller_class.cancan_skipper.should == {:authorize => {}, :load => {}}
110 | @controller_class.cancan_skipper[:load] = true
111 | @controller_class.cancan_skipper[:load].should == true
112 | end
113 |
114 | it "skip_authorize_resource should add itself to the cancan skipper with given model name and options" do
115 | @controller_class.skip_authorize_resource(:project, :only => [:index, :show])
116 | @controller_class.cancan_skipper[:authorize][:project].should == {:only => [:index, :show]}
117 | @controller_class.skip_authorize_resource(:only => [:index, :show])
118 | @controller_class.cancan_skipper[:authorize][nil].should == {:only => [:index, :show]}
119 | @controller_class.skip_authorize_resource(:article)
120 | @controller_class.cancan_skipper[:authorize][:article].should == {}
121 | end
122 |
123 | it "skip_load_resource should add itself to the cancan skipper with given model name and options" do
124 | @controller_class.skip_load_resource(:project, :only => [:index, :show])
125 | @controller_class.cancan_skipper[:load][:project].should == {:only => [:index, :show]}
126 | @controller_class.skip_load_resource(:only => [:index, :show])
127 | @controller_class.cancan_skipper[:load][nil].should == {:only => [:index, :show]}
128 | @controller_class.skip_load_resource(:article)
129 | @controller_class.cancan_skipper[:load][:article].should == {}
130 | end
131 |
132 | it "skip_load_and_authore_resource should add itself to the cancan skipper with given model name and options" do
133 | @controller_class.skip_load_and_authorize_resource(:project, :only => [:index, :show])
134 | @controller_class.cancan_skipper[:load][:project].should == {:only => [:index, :show]}
135 | @controller_class.cancan_skipper[:authorize][:project].should == {:only => [:index, :show]}
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/spec/cancan/controller_resource_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe CanCan::ControllerResource do
4 | before(:each) do
5 | @params = HashWithIndifferentAccess.new(:controller => "projects")
6 | @controller_class = Class.new
7 | @controller = @controller_class.new
8 | @ability = Ability.new(nil)
9 | stub(@controller).params { @params }
10 | stub(@controller).current_ability { @ability }
11 | stub(@controller_class).cancan_skipper { {:authorize => {}, :load => {}} }
12 | end
13 |
14 | it "should load the resource into an instance variable if params[:id] is specified" do
15 | project = Project.create!
16 | @params.merge!(:action => "show", :id => project.id)
17 | resource = CanCan::ControllerResource.new(@controller)
18 | resource.load_resource
19 | @controller.instance_variable_get(:@project).should == project
20 | end
21 |
22 | it "should not load resource into an instance variable if already set" do
23 | @params.merge!(:action => "show", :id => "123")
24 | @controller.instance_variable_set(:@project, :some_project)
25 | resource = CanCan::ControllerResource.new(@controller)
26 | resource.load_resource
27 | @controller.instance_variable_get(:@project).should == :some_project
28 | end
29 |
30 | it "should properly load resource for namespaced controller" do
31 | project = Project.create!
32 | @params.merge!(:controller => "admin/projects", :action => "show", :id => project.id)
33 | resource = CanCan::ControllerResource.new(@controller)
34 | resource.load_resource
35 | @controller.instance_variable_get(:@project).should == project
36 | end
37 |
38 | it "should attempt to load a resource with the same namespace as the controller when using :: for namespace" do
39 | module MyEngine
40 | class Project < ::Project; end
41 | end
42 |
43 | project = MyEngine::Project.create!
44 | @params.merge!(:controller => "MyEngine::ProjectsController", :action => "show", :id => project.id)
45 | resource = CanCan::ControllerResource.new(@controller)
46 | resource.load_resource
47 | @controller.instance_variable_get(:@project).should == project
48 | end
49 |
50 | # Rails includes namespace in params, see issue #349
51 | it "should create through the namespaced params" do
52 | module MyEngine
53 | class Project < ::Project; end
54 | end
55 |
56 | @params.merge!(:controller => "MyEngine::ProjectsController", :action => "create", :my_engine_project => {:name => "foobar"})
57 | resource = CanCan::ControllerResource.new(@controller)
58 | resource.load_resource
59 | @controller.instance_variable_get(:@project).name.should == "foobar"
60 | end
61 |
62 | it "should properly load resource for namespaced controller when using '::' for namespace" do
63 | project = Project.create!
64 | @params.merge!(:controller => "Admin::ProjectsController", :action => "show", :id => project.id)
65 | resource = CanCan::ControllerResource.new(@controller)
66 | resource.load_resource
67 | @controller.instance_variable_get(:@project).should == project
68 | end
69 |
70 | it "has the specified nested resource_class when using / for namespace" do
71 | module Admin
72 | class Dashboard; end
73 | end
74 | @ability.can(:index, "admin/dashboard")
75 | @params.merge!(:controller => "admin/dashboard", :action => "index")
76 | resource = CanCan::ControllerResource.new(@controller, :authorize => true)
77 | resource.send(:resource_class).should == Admin::Dashboard
78 | end
79 |
80 | it "should build a new resource with hash if params[:id] is not specified" do
81 | @params.merge!(:action => "create", :project => {:name => "foobar"})
82 | resource = CanCan::ControllerResource.new(@controller)
83 | resource.load_resource
84 | @controller.instance_variable_get(:@project).name.should == "foobar"
85 | end
86 |
87 | it "should build a new resource for namespaced model with hash if params[:id] is not specified" do
88 | @params.merge!(:action => "create", 'sub_project' => {:name => "foobar"})
89 | resource = CanCan::ControllerResource.new(@controller, :class => ::Sub::Project)
90 | resource.load_resource
91 | @controller.instance_variable_get(:@project).name.should == "foobar"
92 | end
93 |
94 | it "should build a new resource for namespaced controller and namespaced model with hash if params[:id] is not specified" do
95 | @params.merge!(:controller => "Admin::SubProjectsController", :action => "create", 'sub_project' => {:name => "foobar"})
96 | resource = CanCan::ControllerResource.new(@controller, :class => Project)
97 | resource.load_resource
98 | @controller.instance_variable_get(:@sub_project).name.should == "foobar"
99 | end
100 |
101 | it "should build a new resource with attributes from current ability" do
102 | @params.merge!(:action => "new")
103 | @ability.can(:create, Project, :name => "from conditions")
104 | resource = CanCan::ControllerResource.new(@controller)
105 | resource.load_resource
106 | @controller.instance_variable_get(:@project).name.should == "from conditions"
107 | end
108 |
109 | it "should override initial attributes with params" do
110 | @params.merge!(:action => "new", :project => {:name => "from params"})
111 | @ability.can(:create, Project, :name => "from conditions")
112 | resource = CanCan::ControllerResource.new(@controller)
113 | resource.load_resource
114 | @controller.instance_variable_get(:@project).name.should == "from params"
115 | end
116 |
117 | it "should build a collection when on index action when class responds to accessible_by" do
118 | stub(Project).accessible_by(@ability, :index) { :found_projects }
119 | @params[:action] = "index"
120 | resource = CanCan::ControllerResource.new(@controller, :project)
121 | resource.load_resource
122 | @controller.instance_variable_get(:@project).should be_nil
123 | @controller.instance_variable_get(:@projects).should == :found_projects
124 | end
125 |
126 | it "should not build a collection when on index action when class does not respond to accessible_by" do
127 | @params[:action] = "index"
128 | resource = CanCan::ControllerResource.new(@controller)
129 | resource.load_resource
130 | @controller.instance_variable_get(:@project).should be_nil
131 | @controller.instance_variable_defined?(:@projects).should be_false
132 | end
133 |
134 | it "should not use accessible_by when defining abilities through a block" do
135 | stub(Project).accessible_by(@ability) { :found_projects }
136 | @params[:action] = "index"
137 | @ability.can(:read, Project) { |p| false }
138 | resource = CanCan::ControllerResource.new(@controller)
139 | resource.load_resource
140 | @controller.instance_variable_get(:@project).should be_nil
141 | @controller.instance_variable_defined?(:@projects).should be_false
142 | end
143 |
144 | it "should not authorize single resource in collection action" do
145 | @params[:action] = "index"
146 | @controller.instance_variable_set(:@project, :some_project)
147 | stub(@controller).authorize!(:index, Project) { raise CanCan::AccessDenied }
148 | resource = CanCan::ControllerResource.new(@controller)
149 | lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
150 | end
151 |
152 | it "should authorize parent resource in collection action" do
153 | @params[:action] = "index"
154 | @controller.instance_variable_set(:@category, :some_category)
155 | stub(@controller).authorize!(:show, :some_category) { raise CanCan::AccessDenied }
156 | resource = CanCan::ControllerResource.new(@controller, :category, :parent => true)
157 | lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
158 | end
159 |
160 | it "should perform authorization using controller action and loaded model" do
161 | @params.merge!(:action => "show", :id => "123")
162 | @controller.instance_variable_set(:@project, :some_project)
163 | stub(@controller).authorize!(:show, :some_project) { raise CanCan::AccessDenied }
164 | resource = CanCan::ControllerResource.new(@controller)
165 | lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
166 | end
167 |
168 | it "should perform authorization using controller action and non loaded model" do
169 | @params.merge!(:action => "show", :id => "123")
170 | stub(@controller).authorize!(:show, Project) { raise CanCan::AccessDenied }
171 | resource = CanCan::ControllerResource.new(@controller)
172 | lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
173 | end
174 |
175 | it "should call load_resource and authorize_resource for load_and_authorize_resource" do
176 | @params.merge!(:action => "show", :id => "123")
177 | resource = CanCan::ControllerResource.new(@controller)
178 | mock(resource).load_resource
179 | mock(resource).authorize_resource
180 | resource.load_and_authorize_resource
181 | end
182 |
183 | it "should not build a single resource when on custom collection action even with id" do
184 | @params.merge!(:action => "sort", :id => "123")
185 | resource = CanCan::ControllerResource.new(@controller, :collection => [:sort, :list])
186 | resource.load_resource
187 | @controller.instance_variable_get(:@project).should be_nil
188 | end
189 |
190 | it "should load a collection resource when on custom action with no id param" do
191 | stub(Project).accessible_by(@ability, :sort) { :found_projects }
192 | @params[:action] = "sort"
193 | resource = CanCan::ControllerResource.new(@controller)
194 | resource.load_resource
195 | @controller.instance_variable_get(:@project).should be_nil
196 | @controller.instance_variable_get(:@projects).should == :found_projects
197 | end
198 |
199 | it "should build a resource when on custom new action even when params[:id] exists" do
200 | @params.merge!(:action => "build", :id => "123")
201 | stub(Project).new { :some_project }
202 | resource = CanCan::ControllerResource.new(@controller, :new => :build)
203 | resource.load_resource
204 | @controller.instance_variable_get(:@project).should == :some_project
205 | end
206 |
207 | it "should not try to load resource for other action if params[:id] is undefined" do
208 | @params[:action] = "list"
209 | resource = CanCan::ControllerResource.new(@controller)
210 | resource.load_resource
211 | @controller.instance_variable_get(:@project).should be_nil
212 | end
213 |
214 | it "should be a parent resource when name is provided which doesn't match controller" do
215 | resource = CanCan::ControllerResource.new(@controller, :category)
216 | resource.should be_parent
217 | end
218 |
219 | it "should not be a parent resource when name is provided which matches controller" do
220 | resource = CanCan::ControllerResource.new(@controller, :project)
221 | resource.should_not be_parent
222 | end
223 |
224 | it "should be parent if specified in options" do
225 | resource = CanCan::ControllerResource.new(@controller, :project, {:parent => true})
226 | resource.should be_parent
227 | end
228 |
229 | it "should not be parent if specified in options" do
230 | resource = CanCan::ControllerResource.new(@controller, :category, {:parent => false})
231 | resource.should_not be_parent
232 | end
233 |
234 | it "should have the specified resource_class if 'name' is passed to load_resource" do
235 | class Section
236 | end
237 |
238 | resource = CanCan::ControllerResource.new(@controller, :section)
239 | resource.send(:resource_class).should == Section
240 | end
241 |
242 | it "should load parent resource through proper id parameter" do
243 | project = Project.create!
244 | @params.merge!(:controller => "categories", :action => "index", :project_id => project.id)
245 | resource = CanCan::ControllerResource.new(@controller, :project)
246 | resource.load_resource
247 | @controller.instance_variable_get(:@project).should == project
248 | end
249 |
250 | it "should load resource through the association of another parent resource using instance variable" do
251 | @params.merge!(:action => "show", :id => "123")
252 | category = Object.new
253 | @controller.instance_variable_set(:@category, category)
254 | stub(category).projects.stub!.find("123") { :some_project }
255 | resource = CanCan::ControllerResource.new(@controller, :through => :category)
256 | resource.load_resource
257 | @controller.instance_variable_get(:@project).should == :some_project
258 | end
259 |
260 | it "should load resource through the custom association name" do
261 | @params.merge!(:action => "show", :id => "123")
262 | category = Object.new
263 | @controller.instance_variable_set(:@category, category)
264 | stub(category).custom_projects.stub!.find("123") { :some_project }
265 | resource = CanCan::ControllerResource.new(@controller, :through => :category, :through_association => :custom_projects)
266 | resource.load_resource
267 | @controller.instance_variable_get(:@project).should == :some_project
268 | end
269 |
270 | it "should load resource through the association of another parent resource using method" do
271 | @params.merge!(:action => "show", :id => "123")
272 | category = Object.new
273 | stub(@controller).category { category }
274 | stub(category).projects.stub!.find("123") { :some_project }
275 | resource = CanCan::ControllerResource.new(@controller, :through => :category)
276 | resource.load_resource
277 | @controller.instance_variable_get(:@project).should == :some_project
278 | end
279 |
280 | it "should not load through parent resource if instance isn't loaded when shallow" do
281 | project = Project.create!
282 | @params.merge!(:action => "show", :id => project.id)
283 | resource = CanCan::ControllerResource.new(@controller, :through => :category, :shallow => true)
284 | resource.load_resource
285 | @controller.instance_variable_get(:@project).should == project
286 | end
287 |
288 | it "should raise AccessDenied when attempting to load resource through nil" do
289 | project = Project.create!
290 | @params.merge!(:action => "show", :id => project.id)
291 | resource = CanCan::ControllerResource.new(@controller, :through => :category)
292 | lambda {
293 | resource.load_resource
294 | }.should raise_error(CanCan::AccessDenied) { |exception|
295 | exception.action.should == :show
296 | exception.subject.should == Project
297 | }
298 | @controller.instance_variable_get(:@project).should be_nil
299 | end
300 |
301 | it "should authorize nested resource through parent association on index action" do
302 | @params.merge!(:action => "index")
303 | category = Object.new
304 | @controller.instance_variable_set(:@category, category)
305 | stub(@controller).authorize!(:index, category => Project) { raise CanCan::AccessDenied }
306 | resource = CanCan::ControllerResource.new(@controller, :through => :category)
307 | lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
308 | end
309 |
310 | it "should load through first matching if multiple are given" do
311 | @params.merge!(:action => "show", :id => "123")
312 | category = Object.new
313 | @controller.instance_variable_set(:@category, category)
314 | stub(category).projects.stub!.find("123") { :some_project }
315 | resource = CanCan::ControllerResource.new(@controller, :through => [:category, :user])
316 | resource.load_resource
317 | @controller.instance_variable_get(:@project).should == :some_project
318 | end
319 |
320 | it "should find record through has_one association with :singleton option without id param" do
321 | @params.merge!(:action => "show", :id => nil)
322 | category = Object.new
323 | @controller.instance_variable_set(:@category, category)
324 | stub(category).project { :some_project }
325 | resource = CanCan::ControllerResource.new(@controller, :through => :category, :singleton => true)
326 | resource.load_resource
327 | @controller.instance_variable_get(:@project).should == :some_project
328 | end
329 |
330 | it "should not build record through has_one association with :singleton option because it can cause it to delete it in the database" do
331 | @params.merge!(:action => "create", :project => {:name => "foobar"})
332 | category = Category.new
333 | @controller.instance_variable_set(:@category, category)
334 | resource = CanCan::ControllerResource.new(@controller, :through => :category, :singleton => true)
335 | resource.load_resource
336 | @controller.instance_variable_get(:@project).name.should == "foobar"
337 | @controller.instance_variable_get(:@project).category.should == category
338 | end
339 |
340 | it "should find record through has_one association with :singleton and :shallow options" do
341 | project = Project.create!
342 | @params.merge!(:action => "show", :id => project.id)
343 | resource = CanCan::ControllerResource.new(@controller, :through => :category, :singleton => true, :shallow => true)
344 | resource.load_resource
345 | @controller.instance_variable_get(:@project).should == project
346 | end
347 |
348 | it "should build record through has_one association with :singleton and :shallow options" do
349 | @params.merge!(:action => "create", :project => {:name => "foobar"})
350 | resource = CanCan::ControllerResource.new(@controller, :through => :category, :singleton => true, :shallow => true)
351 | resource.load_resource
352 | @controller.instance_variable_get(:@project).name.should == "foobar"
353 | end
354 |
355 | it "should only authorize :show action on parent resource" do
356 | project = Project.create!
357 | @params.merge!(:action => "new", :project_id => project.id)
358 | stub(@controller).authorize!(:show, project) { raise CanCan::AccessDenied }
359 | resource = CanCan::ControllerResource.new(@controller, :project, :parent => true)
360 | lambda { resource.load_and_authorize_resource }.should raise_error(CanCan::AccessDenied)
361 | end
362 |
363 | it "should load the model using a custom class" do
364 | project = Project.create!
365 | @params.merge!(:action => "show", :id => project.id)
366 | resource = CanCan::ControllerResource.new(@controller, :class => Project)
367 | resource.load_resource
368 | @controller.instance_variable_get(:@project).should == project
369 | end
370 |
371 | it "should load the model using a custom namespaced class" do
372 | project = Sub::Project.create!
373 | @params.merge!(:action => "show", :id => project.id)
374 | resource = CanCan::ControllerResource.new(@controller, :class => ::Sub::Project)
375 | resource.load_resource
376 | @controller.instance_variable_get(:@project).should == project
377 | end
378 |
379 | it "should authorize based on resource name if class is false" do
380 | @params.merge!(:action => "show", :id => "123")
381 | stub(@controller).authorize!(:show, :project) { raise CanCan::AccessDenied }
382 | resource = CanCan::ControllerResource.new(@controller, :class => false)
383 | lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
384 | end
385 |
386 | it "should load and authorize using custom instance name" do
387 | project = Project.create!
388 | @params.merge!(:action => "show", :id => project.id)
389 | stub(@controller).authorize!(:show, project) { raise CanCan::AccessDenied }
390 | resource = CanCan::ControllerResource.new(@controller, :instance_name => :custom_project)
391 | lambda { resource.load_and_authorize_resource }.should raise_error(CanCan::AccessDenied)
392 | @controller.instance_variable_get(:@custom_project).should == project
393 | end
394 |
395 | it "should load resource using custom ID param" do
396 | project = Project.create!
397 | @params.merge!(:action => "show", :the_project => project.id)
398 | resource = CanCan::ControllerResource.new(@controller, :id_param => :the_project)
399 | resource.load_resource
400 | @controller.instance_variable_get(:@project).should == project
401 | end
402 |
403 | # CVE-2012-5664
404 | it "should always convert id param to string" do
405 | @params.merge!(:action => "show", :the_project => { :malicious => "I am" })
406 | resource = CanCan::ControllerResource.new(@controller, :id_param => :the_project)
407 | resource.send(:id_param).class.should == String
408 | end
409 |
410 | it "should load resource using custom find_by attribute" do
411 | project = Project.create!(:name => "foo")
412 | @params.merge!(:action => "show", :id => "foo")
413 | resource = CanCan::ControllerResource.new(@controller, :find_by => :name)
414 | resource.load_resource
415 | @controller.instance_variable_get(:@project).should == project
416 | end
417 |
418 | it "should allow full find method to be passed into find_by option" do
419 | project = Project.create!(:name => "foo")
420 | @params.merge!(:action => "show", :id => "foo")
421 | resource = CanCan::ControllerResource.new(@controller, :find_by => :find_by_name)
422 | resource.load_resource
423 | @controller.instance_variable_get(:@project).should == project
424 | end
425 |
426 | it "should raise ImplementationRemoved when adding :name option" do
427 | lambda {
428 | CanCan::ControllerResource.new(@controller, :name => :foo)
429 | }.should raise_error(CanCan::ImplementationRemoved)
430 | end
431 |
432 | it "should raise ImplementationRemoved exception when specifying :resource option since it is no longer used" do
433 | lambda {
434 | CanCan::ControllerResource.new(@controller, :resource => Project)
435 | }.should raise_error(CanCan::ImplementationRemoved)
436 | end
437 |
438 | it "should raise ImplementationRemoved exception when passing :nested option" do
439 | lambda {
440 | CanCan::ControllerResource.new(@controller, :nested => :project)
441 | }.should raise_error(CanCan::ImplementationRemoved)
442 | end
443 |
444 | it "should skip resource behavior for :only actions in array" do
445 | stub(@controller_class).cancan_skipper { {:load => {nil => {:only => [:index, :show]}}} }
446 | @params.merge!(:action => "index")
447 | CanCan::ControllerResource.new(@controller).skip?(:load).should be_true
448 | CanCan::ControllerResource.new(@controller, :some_resource).skip?(:load).should be_false
449 | @params.merge!(:action => "show")
450 | CanCan::ControllerResource.new(@controller).skip?(:load).should be_true
451 | @params.merge!(:action => "other_action")
452 | CanCan::ControllerResource.new(@controller).skip?(:load).should be_false
453 | end
454 |
455 | it "should skip resource behavior for :only one action on resource" do
456 | stub(@controller_class).cancan_skipper { {:authorize => {:project => {:only => :index}}} }
457 | @params.merge!(:action => "index")
458 | CanCan::ControllerResource.new(@controller).skip?(:authorize).should be_false
459 | CanCan::ControllerResource.new(@controller, :project).skip?(:authorize).should be_true
460 | @params.merge!(:action => "other_action")
461 | CanCan::ControllerResource.new(@controller, :project).skip?(:authorize).should be_false
462 | end
463 |
464 | it "should skip resource behavior :except actions in array" do
465 | stub(@controller_class).cancan_skipper { {:load => {nil => {:except => [:index, :show]}}} }
466 | @params.merge!(:action => "index")
467 | CanCan::ControllerResource.new(@controller).skip?(:load).should be_false
468 | @params.merge!(:action => "show")
469 | CanCan::ControllerResource.new(@controller).skip?(:load).should be_false
470 | @params.merge!(:action => "other_action")
471 | CanCan::ControllerResource.new(@controller).skip?(:load).should be_true
472 | CanCan::ControllerResource.new(@controller, :some_resource).skip?(:load).should be_false
473 | end
474 |
475 | it "should skip resource behavior :except one action on resource" do
476 | stub(@controller_class).cancan_skipper { {:authorize => {:project => {:except => :index}}} }
477 | @params.merge!(:action => "index")
478 | CanCan::ControllerResource.new(@controller, :project).skip?(:authorize).should be_false
479 | @params.merge!(:action => "other_action")
480 | CanCan::ControllerResource.new(@controller).skip?(:authorize).should be_false
481 | CanCan::ControllerResource.new(@controller, :project).skip?(:authorize).should be_true
482 | end
483 |
484 | it "should skip loading and authorization" do
485 | stub(@controller_class).cancan_skipper { {:authorize => {nil => {}}, :load => {nil => {}}} }
486 | @params.merge!(:action => "new")
487 | resource = CanCan::ControllerResource.new(@controller)
488 | lambda { resource.load_and_authorize_resource }.should_not raise_error
489 | @controller.instance_variable_get(:@project).should be_nil
490 | end
491 | end
492 |
--------------------------------------------------------------------------------
/spec/cancan/exceptions_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe CanCan::AccessDenied do
4 | describe "with action and subject" do
5 | before(:each) do
6 | @exception = CanCan::AccessDenied.new(nil, :some_action, :some_subject)
7 | end
8 |
9 | it "should have action and subject accessors" do
10 | @exception.action.should == :some_action
11 | @exception.subject.should == :some_subject
12 | end
13 |
14 | it "should have a changable default message" do
15 | @exception.message.should == "You are not authorized to access this page."
16 | @exception.default_message = "Unauthorized!"
17 | @exception.message.should == "Unauthorized!"
18 | end
19 | end
20 |
21 | describe "with only a message" do
22 | before(:each) do
23 | @exception = CanCan::AccessDenied.new("Access denied!")
24 | end
25 |
26 | it "should have nil action and subject" do
27 | @exception.action.should be_nil
28 | @exception.subject.should be_nil
29 | end
30 |
31 | it "should have passed message" do
32 | @exception.message.should == "Access denied!"
33 | end
34 | end
35 |
36 | describe "i18n in the default message" do
37 | after(:each) do
38 | I18n.backend = nil
39 | end
40 |
41 | it "uses i18n for the default message" do
42 | I18n.backend.store_translations :en, :unauthorized => {:default => "This is a different message"}
43 | @exception = CanCan::AccessDenied.new
44 | @exception.message.should == "This is a different message"
45 | end
46 |
47 | it "defaults to a nice message" do
48 | @exception = CanCan::AccessDenied.new
49 | @exception.message.should == "You are not authorized to access this page."
50 | end
51 |
52 | it "does not use translation if a message is given" do
53 | @exception = CanCan::AccessDenied.new("Hey! You're not welcome here")
54 | @exception.message.should == "Hey! You're not welcome here"
55 | @exception.message.should_not == "You are not authorized to access this page."
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/spec/cancan/inherited_resource_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe CanCan::InheritedResource do
4 | before(:each) do
5 | @params = HashWithIndifferentAccess.new(:controller => "projects")
6 | @controller_class = Class.new
7 | @controller = @controller_class.new
8 | @ability = Ability.new(nil)
9 | stub(@controller).params { @params }
10 | stub(@controller).current_ability { @ability }
11 | stub(@controller_class).cancan_skipper { {:authorize => {}, :load => {}} }
12 | end
13 |
14 | it "show should load resource through @controller.resource" do
15 | @params.merge!(:action => "show", :id => 123)
16 | stub(@controller).resource { :project_resource }
17 | CanCan::InheritedResource.new(@controller).load_resource
18 | @controller.instance_variable_get(:@project).should == :project_resource
19 | end
20 |
21 | it "new should load through @controller.build_resource" do
22 | @params[:action] = "new"
23 | stub(@controller).build_resource { :project_resource }
24 | CanCan::InheritedResource.new(@controller).load_resource
25 | @controller.instance_variable_get(:@project).should == :project_resource
26 | end
27 |
28 | it "index should load through @controller.association_chain when parent" do
29 | @params[:action] = "index"
30 | stub(@controller).association_chain { @controller.instance_variable_set(:@project, :project_resource) }
31 | CanCan::InheritedResource.new(@controller, :parent => true).load_resource
32 | @controller.instance_variable_get(:@project).should == :project_resource
33 | end
34 |
35 | it "index should load through @controller.end_of_association_chain" do
36 | @params[:action] = "index"
37 | stub(Project).accessible_by(@ability, :index) { :projects }
38 | stub(@controller).end_of_association_chain { Project }
39 | CanCan::InheritedResource.new(@controller).load_resource
40 | @controller.instance_variable_get(:@projects).should == :projects
41 | end
42 |
43 | it "should build a new resource with attributes from current ability" do
44 | @params[:action] = "new"
45 | @ability.can(:create, Project, :name => "from conditions")
46 | stub(@controller).build_resource { Struct.new(:name).new }
47 | resource = CanCan::InheritedResource.new(@controller)
48 | resource.load_resource
49 | @controller.instance_variable_get(:@project).name.should == "from conditions"
50 | end
51 |
52 | it "should override initial attributes with params" do
53 | @params.merge!(:action => "new", :project => {:name => "from params"})
54 | @ability.can(:create, Project, :name => "from conditions")
55 | stub(@controller).build_resource { Struct.new(:name).new }
56 | resource = CanCan::ControllerResource.new(@controller)
57 | resource.load_resource
58 | @controller.instance_variable_get(:@project).name.should == "from params"
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/spec/cancan/matchers_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "be_able_to" do
4 | it "delegates to can?" do
5 | object = Object.new
6 | mock(object).can?(:read, 123) { true }
7 | object.should be_able_to(:read, 123)
8 | end
9 |
10 | it "reports a nice failure message for should" do
11 | object = Object.new
12 | mock(object).can?(:read, 123) { false }
13 | expect do
14 | object.should be_able_to(:read, 123)
15 | end.should raise_error('expected to be able to :read 123')
16 | end
17 |
18 | it "reports a nice failure message for should not" do
19 | object = Object.new
20 | mock(object).can?(:read, 123) { true }
21 | expect do
22 | object.should_not be_able_to(:read, 123)
23 | end.should raise_error('expected not to be able to :read 123')
24 | end
25 |
26 | it "delegates additional arguments to can? and reports in failure message" do
27 | object = Object.new
28 | mock(object).can?(:read, 123, 456) { false }
29 | expect do
30 | object.should be_able_to(:read, 123, 456)
31 | end.should raise_error('expected to be able to :read 123 456')
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/cancan/model_adapters/active_record_adapter_spec.rb:
--------------------------------------------------------------------------------
1 | if ENV["MODEL_ADAPTER"].nil? || ENV["MODEL_ADAPTER"] == "active_record"
2 | require "spec_helper"
3 |
4 | ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
5 |
6 | describe CanCan::ModelAdapters::ActiveRecordAdapter do
7 | with_model :category do
8 | table do |t|
9 | t.boolean "visible"
10 | end
11 | model do
12 | has_many :articles
13 | end
14 | end
15 |
16 | with_model :article do
17 | table do |t|
18 | t.string "name"
19 | t.boolean "published"
20 | t.boolean "secret"
21 | t.integer "priority"
22 | t.integer "category_id"
23 | t.integer "user_id"
24 | end
25 | model do
26 | belongs_to :category
27 | has_many :comments
28 | belongs_to :user
29 | end
30 | end
31 |
32 | with_model :comment do
33 | table do |t|
34 | t.boolean "spam"
35 | t.integer "article_id"
36 | end
37 | model do
38 | belongs_to :article
39 | end
40 | end
41 |
42 | with_model :user do
43 | table do |t|
44 |
45 | end
46 | model do
47 | has_many :articles
48 | end
49 | end
50 |
51 | before(:each) do
52 | Article.delete_all
53 | Comment.delete_all
54 | @ability = Object.new
55 | @ability.extend(CanCan::Ability)
56 | @article_table = Article.table_name
57 | @comment_table = Comment.table_name
58 | end
59 |
60 | it "should be for only active record classes" do
61 | CanCan::ModelAdapters::ActiveRecordAdapter.should_not be_for_class(Object)
62 | CanCan::ModelAdapters::ActiveRecordAdapter.should be_for_class(Article)
63 | CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article).should == CanCan::ModelAdapters::ActiveRecordAdapter
64 | end
65 |
66 | it "should find record" do
67 | article = Article.create!
68 | CanCan::ModelAdapters::ActiveRecordAdapter.find(Article, article.id).should == article
69 | end
70 |
71 | it "should not fetch any records when no abilities are defined" do
72 | Article.create!
73 | Article.accessible_by(@ability).should be_empty
74 | end
75 |
76 | it "should fetch all articles when one can read all" do
77 | @ability.can :read, Article
78 | article = Article.create!
79 | Article.accessible_by(@ability).should == [article]
80 | end
81 |
82 | it "should fetch only the articles that are published" do
83 | @ability.can :read, Article, :published => true
84 | article1 = Article.create!(:published => true)
85 | article2 = Article.create!(:published => false)
86 | Article.accessible_by(@ability).should == [article1]
87 | end
88 |
89 | it "should fetch any articles which are published or secret" do
90 | @ability.can :read, Article, :published => true
91 | @ability.can :read, Article, :secret => true
92 | article1 = Article.create!(:published => true, :secret => false)
93 | article2 = Article.create!(:published => true, :secret => true)
94 | article3 = Article.create!(:published => false, :secret => true)
95 | article4 = Article.create!(:published => false, :secret => false)
96 | Article.accessible_by(@ability).should == [article1, article2, article3]
97 | end
98 |
99 | it "should fetch only the articles that are published and not secret" do
100 | @ability.can :read, Article, :published => true
101 | @ability.cannot :read, Article, :secret => true
102 | article1 = Article.create!(:published => true, :secret => false)
103 | article2 = Article.create!(:published => true, :secret => true)
104 | article3 = Article.create!(:published => false, :secret => true)
105 | article4 = Article.create!(:published => false, :secret => false)
106 | Article.accessible_by(@ability).should == [article1]
107 | end
108 |
109 | it "should only read comments for articles which are published" do
110 | @ability.can :read, Comment, :article => { :published => true }
111 | comment1 = Comment.create!(:article => Article.create!(:published => true))
112 | comment2 = Comment.create!(:article => Article.create!(:published => false))
113 | Comment.accessible_by(@ability).should == [comment1]
114 | end
115 |
116 | it "should only read comments for visible categories through articles" do
117 | @ability.can :read, Comment, :article => { :category => { :visible => true } }
118 | comment1 = Comment.create!(:article => Article.create!(:category => Category.create!(:visible => true)))
119 | comment2 = Comment.create!(:article => Article.create!(:category => Category.create!(:visible => false)))
120 | Comment.accessible_by(@ability).should == [comment1]
121 | end
122 |
123 | it "should allow conditions in SQL and merge with hash conditions" do
124 | @ability.can :read, Article, :published => true
125 | @ability.can :read, Article, ["secret=?", true]
126 | article1 = Article.create!(:published => true, :secret => false)
127 | article2 = Article.create!(:published => true, :secret => true)
128 | article3 = Article.create!(:published => false, :secret => true)
129 | article4 = Article.create!(:published => false, :secret => false)
130 | Article.accessible_by(@ability).should == [article1, article2, article3]
131 | end
132 |
133 | it "should allow a scope for conditions" do
134 | @ability.can :read, Article, Article.where(:secret => true)
135 | article1 = Article.create!(:secret => true)
136 | article2 = Article.create!(:secret => false)
137 | Article.accessible_by(@ability).should == [article1]
138 | end
139 |
140 | it "should fetch only associated records when using with a scope for conditions" do
141 | @ability.can :read, Article, Article.where(:secret => true)
142 | category1 = Category.create!(:visible => false)
143 | category2 = Category.create!(:visible => true)
144 | article1 = Article.create!(:secret => true, :category => category1)
145 | article2 = Article.create!(:secret => true, :category => category2)
146 | category1.articles.accessible_by(@ability).should == [article1]
147 | end
148 |
149 | it "should raise an exception when trying to merge scope with other conditions" do
150 | @ability.can :read, Article, :published => true
151 | @ability.can :read, Article, Article.where(:secret => true)
152 | lambda { Article.accessible_by(@ability) }.should raise_error(CanCan::Error, "Unable to merge an Active Record scope with other conditions. Instead use a hash or SQL for read Article ability.")
153 | end
154 |
155 | it "should not allow to fetch records when ability with just block present" do
156 | @ability.can :read, Article do
157 | false
158 | end
159 | lambda { Article.accessible_by(@ability) }.should raise_error(CanCan::Error)
160 | end
161 |
162 | it "should not allow to check ability on object against SQL conditions without block" do
163 | @ability.can :read, Article, ["secret=?", true]
164 | lambda { @ability.can? :read, Article.new }.should raise_error(CanCan::Error)
165 | end
166 |
167 | it "should have false conditions if no abilities match" do
168 | @ability.model_adapter(Article, :read).conditions.should == "'t'='f'"
169 | end
170 |
171 | it "should return false conditions for cannot clause" do
172 | @ability.cannot :read, Article
173 | @ability.model_adapter(Article, :read).conditions.should == "'t'='f'"
174 | end
175 |
176 | it "should return SQL for single `can` definition in front of default `cannot` condition" do
177 | @ability.cannot :read, Article
178 | @ability.can :read, Article, :published => false, :secret => true
179 | @ability.model_adapter(Article, :read).conditions.should orderlessly_match(%Q["#{@article_table}"."published" = 'f' AND "#{@article_table}"."secret" = 't'])
180 | end
181 |
182 | it "should return true condition for single `can` definition in front of default `can` condition" do
183 | @ability.can :read, Article
184 | @ability.can :read, Article, :published => false, :secret => true
185 | @ability.model_adapter(Article, :read).conditions.should == "'t'='t'"
186 | end
187 |
188 | it "should return `false condition` for single `cannot` definition in front of default `cannot` condition" do
189 | @ability.cannot :read, Article
190 | @ability.cannot :read, Article, :published => false, :secret => true
191 | @ability.model_adapter(Article, :read).conditions.should == "'t'='f'"
192 | end
193 |
194 | it "should return `not (sql)` for single `cannot` definition in front of default `can` condition" do
195 | @ability.can :read, Article
196 | @ability.cannot :read, Article, :published => false, :secret => true
197 | @ability.model_adapter(Article, :read).conditions.should orderlessly_match(%Q["not (#{@article_table}"."published" = 'f' AND "#{@article_table}"."secret" = 't')])
198 | end
199 |
200 | it "should return appropriate sql conditions in complex case" do
201 | @ability.can :read, Article
202 | @ability.can :manage, Article, :id => 1
203 | @ability.can :update, Article, :published => true
204 | @ability.cannot :update, Article, :secret => true
205 | @ability.model_adapter(Article, :update).conditions.should == %Q[not ("#{@article_table}"."secret" = 't') AND (("#{@article_table}"."published" = 't') OR ("#{@article_table}"."id" = 1))]
206 | @ability.model_adapter(Article, :manage).conditions.should == {:id => 1}
207 | @ability.model_adapter(Article, :read).conditions.should == "'t'='t'"
208 | end
209 |
210 | it "should return appropriate sql conditions in complex case with nested joins" do
211 | @ability.can :read, Comment, :article => { :category => { :visible => true } }
212 | @ability.model_adapter(Comment, :read).conditions.should == { Category.table_name.to_sym => { :visible => true } }
213 | end
214 |
215 | it "should return appropriate sql conditions in complex case with nested joins of different depth" do
216 | @ability.can :read, Comment, :article => { :published => true, :category => { :visible => true } }
217 | @ability.model_adapter(Comment, :read).conditions.should == { Article.table_name.to_sym => { :published => true }, Category.table_name.to_sym => { :visible => true } }
218 | end
219 |
220 | it "should not forget conditions when calling with SQL string" do
221 | @ability.can :read, Article, :published => true
222 | @ability.can :read, Article, ['secret=?', false]
223 | adapter = @ability.model_adapter(Article, :read)
224 | 2.times do
225 | adapter.conditions.should == %Q[(secret='f') OR ("#{@article_table}"."published" = 't')]
226 | end
227 | end
228 |
229 | it "should have nil joins if no rules" do
230 | @ability.model_adapter(Article, :read).joins.should be_nil
231 | end
232 |
233 | it "should have nil joins if no nested hashes specified in conditions" do
234 | @ability.can :read, Article, :published => false
235 | @ability.can :read, Article, :secret => true
236 | @ability.model_adapter(Article, :read).joins.should be_nil
237 | end
238 |
239 | it "should merge separate joins into a single array" do
240 | @ability.can :read, Article, :project => { :blocked => false }
241 | @ability.can :read, Article, :company => { :admin => true }
242 | @ability.model_adapter(Article, :read).joins.inspect.should orderlessly_match([:company, :project].inspect)
243 | end
244 |
245 | it "should merge same joins into a single array" do
246 | @ability.can :read, Article, :project => { :blocked => false }
247 | @ability.can :read, Article, :project => { :admin => true }
248 | @ability.model_adapter(Article, :read).joins.should == [:project]
249 | end
250 |
251 | it "should merge nested and non-nested joins" do
252 | @ability.can :read, Article, :project => { :blocked => false }
253 | @ability.can :read, Article, :project => { :comments => { :spam => true } }
254 | @ability.model_adapter(Article, :read).joins.should == [{:project=>[:comments]}]
255 | end
256 |
257 | it "should merge :all conditions with other conditions" do
258 | user = User.create!
259 | article = Article.create!(:user => user)
260 | ability = Ability.new(user)
261 | ability.can :manage, :all
262 | ability.can :manage, Article, :user_id => user.id
263 | Article.accessible_by(ability).should == [article]
264 | end
265 |
266 | it "should restrict articles given a MetaWhere condition" do
267 | @ability.can :read, Article, :priority.lt => 2
268 | article1 = Article.create!(:priority => 1)
269 | article2 = Article.create!(:priority => 3)
270 | Article.accessible_by(@ability).should == [article1]
271 | @ability.should be_able_to(:read, article1)
272 | @ability.should_not be_able_to(:read, article2)
273 | end
274 |
275 | it "should merge MetaWhere and non-MetaWhere conditions" do
276 | @ability.can :read, Article, :priority.lt => 2
277 | @ability.can :read, Article, :priority => 1
278 | article1 = Article.create!(:priority => 1)
279 | article2 = Article.create!(:priority => 3)
280 | Article.accessible_by(@ability).should == [article1]
281 | @ability.should be_able_to(:read, article1)
282 | @ability.should_not be_able_to(:read, article2)
283 | end
284 |
285 | it "should match any MetaWhere condition" do
286 | adapter = CanCan::ModelAdapters::ActiveRecordAdapter
287 | article1 = Article.new(:priority => 1, :name => "Hello World")
288 | adapter.matches_condition?(article1, :priority.eq, 1).should be_true
289 | adapter.matches_condition?(article1, :priority.eq, 2).should be_false
290 | adapter.matches_condition?(article1, :priority.eq_any, [1, 2]).should be_true
291 | adapter.matches_condition?(article1, :priority.eq_any, [2, 3]).should be_false
292 | adapter.matches_condition?(article1, :priority.eq_all, [1, 1]).should be_true
293 | adapter.matches_condition?(article1, :priority.eq_all, [1, 2]).should be_false
294 | adapter.matches_condition?(article1, :priority.ne, 2).should be_true
295 | adapter.matches_condition?(article1, :priority.ne, 1).should be_false
296 | adapter.matches_condition?(article1, :priority.in, [1, 2]).should be_true
297 | adapter.matches_condition?(article1, :priority.in, [2, 3]).should be_false
298 | adapter.matches_condition?(article1, :priority.nin, [2, 3]).should be_true
299 | adapter.matches_condition?(article1, :priority.nin, [1, 2]).should be_false
300 | adapter.matches_condition?(article1, :priority.lt, 2).should be_true
301 | adapter.matches_condition?(article1, :priority.lt, 1).should be_false
302 | adapter.matches_condition?(article1, :priority.lteq, 1).should be_true
303 | adapter.matches_condition?(article1, :priority.lteq, 0).should be_false
304 | adapter.matches_condition?(article1, :priority.gt, 0).should be_true
305 | adapter.matches_condition?(article1, :priority.gt, 1).should be_false
306 | adapter.matches_condition?(article1, :priority.gteq, 1).should be_true
307 | adapter.matches_condition?(article1, :priority.gteq, 2).should be_false
308 | adapter.matches_condition?(article1, :name.like, "%ello worl%").should be_true
309 | adapter.matches_condition?(article1, :name.like, "hello world").should be_true
310 | adapter.matches_condition?(article1, :name.like, "hello%").should be_true
311 | adapter.matches_condition?(article1, :name.like, "h%d").should be_true
312 | adapter.matches_condition?(article1, :name.like, "%helo%").should be_false
313 | adapter.matches_condition?(article1, :name.like, "hello").should be_false
314 | adapter.matches_condition?(article1, :name.like, "hello.world").should be_false
315 | # For some reason this is reporting "The not_matches MetaWhere condition is not supported."
316 | # adapter.matches_condition?(article1, :name.nlike, "%helo%").should be_true
317 | # adapter.matches_condition?(article1, :name.nlike, "%ello worl%").should be_false
318 | end
319 | end
320 | end
321 |
--------------------------------------------------------------------------------
/spec/cancan/model_adapters/data_mapper_adapter_spec.rb:
--------------------------------------------------------------------------------
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 = Object.new
30 | @ability.extend(CanCan::Ability)
31 | end
32 |
33 | it "should be for only data mapper classes" do
34 | CanCan::ModelAdapters::DataMapperAdapter.should_not be_for_class(Object)
35 | CanCan::ModelAdapters::DataMapperAdapter.should be_for_class(Article)
36 | CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article).should == CanCan::ModelAdapters::DataMapperAdapter
37 | end
38 |
39 | it "should find record" do
40 | article = Article.create
41 | CanCan::ModelAdapters::DataMapperAdapter.find(Article, article.id).should == article
42 | end
43 |
44 | it "should not fetch any records when no abilities are defined" do
45 | Article.create
46 | Article.accessible_by(@ability).should be_empty
47 | end
48 |
49 | it "should fetch all articles when one can read all" do
50 | @ability.can :read, Article
51 | article = Article.create
52 | Article.accessible_by(@ability).should == [article]
53 | end
54 |
55 | it "should fetch only the articles that are published" do
56 | @ability.can :read, Article, :published => true
57 | article1 = Article.create(:published => true)
58 | article2 = Article.create(:published => false)
59 | Article.accessible_by(@ability).should == [article1]
60 | end
61 |
62 | it "should fetch any articles which are published or secret" do
63 | @ability.can :read, Article, :published => true
64 | @ability.can :read, Article, :secret => true
65 | article1 = Article.create(:published => true, :secret => false)
66 | article2 = Article.create(:published => true, :secret => true)
67 | article3 = Article.create(:published => false, :secret => true)
68 | article4 = Article.create(:published => false, :secret => false)
69 | Article.accessible_by(@ability).should == [article1, article2, article3]
70 | end
71 |
72 | it "should fetch only the articles that are published and not secret" do
73 | @ability.can :read, Article, :published => true
74 | @ability.cannot :read, Article, :secret => true
75 | article1 = Article.create(:published => true, :secret => false)
76 | article2 = Article.create(:published => true, :secret => true)
77 | article3 = Article.create(:published => false, :secret => true)
78 | article4 = Article.create(:published => false, :secret => false)
79 | Article.accessible_by(@ability).should == [article1]
80 | end
81 |
82 | it "should only read comments for articles which are published" do
83 | @ability.can :read, Comment, :article => { :published => true }
84 | comment1 = Comment.create(:article => Article.create!(:published => true))
85 | comment2 = Comment.create(:article => Article.create!(:published => false))
86 | Comment.accessible_by(@ability).should == [comment1]
87 | end
88 |
89 | it "should allow conditions in SQL and merge with hash conditions" do
90 | @ability.can :read, Article, :published => true
91 | @ability.can :read, Article, ["secret=?", true]
92 | article1 = Article.create(:published => true, :secret => false)
93 | article4 = Article.create(:published => false, :secret => false)
94 | Article.accessible_by(@ability).should == [article1]
95 | end
96 |
97 | it "should match gt comparison" do
98 | @ability.can :read, Article, :priority.gt => 3
99 | article1 = Article.create(:priority => 4)
100 | article2 = Article.create(:priority => 3)
101 | Article.accessible_by(@ability).should == [article1]
102 | @ability.should be_able_to(:read, article1)
103 | @ability.should_not be_able_to(:read, article2)
104 | end
105 |
106 | it "should match gte comparison" do
107 | @ability.can :read, Article, :priority.gte => 3
108 | article1 = Article.create(:priority => 4)
109 | article2 = Article.create(:priority => 3)
110 | article3 = Article.create(:priority => 2)
111 | Article.accessible_by(@ability).should == [article1, article2]
112 | @ability.should be_able_to(:read, article1)
113 | @ability.should be_able_to(:read, article2)
114 | @ability.should_not be_able_to(:read, article3)
115 | end
116 |
117 | # TODO: add more comparison specs
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/spec/cancan/model_adapters/default_adapter_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe CanCan::ModelAdapters::DefaultAdapter do
4 | it "should be default for generic classes" do
5 | CanCan::ModelAdapters::AbstractAdapter.adapter_class(Object).should == CanCan::ModelAdapters::DefaultAdapter
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/cancan/model_adapters/mongoid_adapter_spec.rb:
--------------------------------------------------------------------------------
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 = Object.new
24 | @ability.extend(CanCan::Ability)
25 | end
26 |
27 | after(:each) do
28 | Mongoid.master.collections.select do |collection|
29 | collection.name !~ /system/
30 | end.each(&:drop)
31 | end
32 |
33 | it "should be for only Mongoid classes" do
34 | CanCan::ModelAdapters::MongoidAdapter.should_not be_for_class(Object)
35 | CanCan::ModelAdapters::MongoidAdapter.should be_for_class(MongoidProject)
36 | CanCan::ModelAdapters::AbstractAdapter.adapter_class(MongoidProject).should == CanCan::ModelAdapters::MongoidAdapter
37 | end
38 |
39 | it "should find record" do
40 | project = MongoidProject.create
41 | CanCan::ModelAdapters::MongoidAdapter.find(MongoidProject, project.id).should == project
42 | end
43 |
44 | it "should compare properties on mongoid documents with the conditions hash" do
45 | model = MongoidProject.new
46 | @ability.can :read, MongoidProject, :id => model.id
47 | @ability.should be_able_to(:read, model)
48 | end
49 |
50 | it "should be able to read hashes when field is array" do
51 | one_to_three = MongoidProject.create(:numbers => ['one', 'two', 'three'])
52 | two_to_five = MongoidProject.create(:numbers => ['two', 'three', 'four', 'five'])
53 |
54 | @ability.can :foo, MongoidProject, :numbers => 'one'
55 | @ability.should be_able_to(:foo, one_to_three)
56 | @ability.should_not be_able_to(:foo, two_to_five)
57 | end
58 |
59 | it "should return [] when no ability is defined so no records are found" do
60 | MongoidProject.create(:title => 'Sir')
61 | MongoidProject.create(:title => 'Lord')
62 | MongoidProject.create(:title => 'Dude')
63 |
64 | MongoidProject.accessible_by(@ability, :read).entries.should == []
65 | end
66 |
67 | it "should return the correct records based on the defined ability" do
68 | @ability.can :read, MongoidProject, :title => "Sir"
69 | sir = MongoidProject.create(:title => 'Sir')
70 | lord = MongoidProject.create(:title => 'Lord')
71 | dude = MongoidProject.create(:title => 'Dude')
72 |
73 | MongoidProject.accessible_by(@ability, :read).entries.should == [sir]
74 | end
75 |
76 | it "should return the correct records when a mix of can and cannot rules in defined ability" do
77 | @ability.can :manage, MongoidProject, :title => 'Sir'
78 | @ability.cannot :destroy, MongoidProject
79 |
80 | sir = MongoidProject.create(:title => 'Sir')
81 | lord = MongoidProject.create(:title => 'Lord')
82 | dude = MongoidProject.create(:title => 'Dude')
83 |
84 | MongoidProject.accessible_by(@ability, :destroy).entries.should == [sir]
85 | end
86 |
87 | it "should be able to mix empty conditions and hashes" do
88 | @ability.can :read, MongoidProject
89 | @ability.can :read, MongoidProject, :title => 'Sir'
90 | sir = MongoidProject.create(:title => 'Sir')
91 | lord = MongoidProject.create(:title => 'Lord')
92 |
93 | MongoidProject.accessible_by(@ability, :read).count.should == 2
94 | end
95 |
96 | it "should return everything when the defined ability is manage all" do
97 | @ability.can :manage, :all
98 | sir = MongoidProject.create(:title => 'Sir')
99 | lord = MongoidProject.create(:title => 'Lord')
100 | dude = MongoidProject.create(:title => 'Dude')
101 |
102 | MongoidProject.accessible_by(@ability, :read).entries.should == [sir, lord, dude]
103 | end
104 |
105 | it "should allow a scope for conditions" do
106 | @ability.can :read, MongoidProject, MongoidProject.where(:title => 'Sir')
107 | sir = MongoidProject.create(:title => 'Sir')
108 | lord = MongoidProject.create(:title => 'Lord')
109 | dude = MongoidProject.create(:title => 'Dude')
110 |
111 | MongoidProject.accessible_by(@ability, :read).entries.should == [sir]
112 | end
113 |
114 | describe "Mongoid::Criteria where clause Symbol extensions using MongoDB expressions" do
115 | it "should handle :field.in" do
116 | obj = MongoidProject.create(:title => 'Sir')
117 | @ability.can :read, MongoidProject, :title.in => ["Sir", "Madam"]
118 | @ability.can?(:read, obj).should == true
119 | MongoidProject.accessible_by(@ability, :read).should == [obj]
120 |
121 | obj2 = MongoidProject.create(:title => 'Lord')
122 | @ability.can?(:read, obj2).should == false
123 | end
124 |
125 | describe "activates only when there are Criteria in the hash" do
126 | it "Calls where on the model class when there are criteria" do
127 | obj = MongoidProject.create(:title => 'Bird')
128 | @conditions = {:title.nin => ["Fork", "Spoon"]}
129 |
130 | @ability.can :read, MongoidProject, @conditions
131 | @ability.should be_able_to(:read, obj)
132 | end
133 | it "Calls the base version if there are no mongoid criteria" do
134 | obj = MongoidProject.new(:title => 'Bird')
135 | @conditions = {:id => obj.id}
136 | @ability.can :read, MongoidProject, @conditions
137 | @ability.should be_able_to(:read, obj)
138 | end
139 | end
140 |
141 | it "should handle :field.nin" do
142 | obj = MongoidProject.create(:title => 'Sir')
143 | @ability.can :read, MongoidProject, :title.nin => ["Lord", "Madam"]
144 | @ability.can?(:read, obj).should == true
145 | MongoidProject.accessible_by(@ability, :read).should == [obj]
146 |
147 | obj2 = MongoidProject.create(:title => 'Lord')
148 | @ability.can?(:read, obj2).should == false
149 | end
150 |
151 | it "should handle :field.size" do
152 | obj = MongoidProject.create(:titles => ['Palatin', 'Margrave'])
153 | @ability.can :read, MongoidProject, :titles.size => 2
154 | @ability.can?(:read, obj).should == true
155 | MongoidProject.accessible_by(@ability, :read).should == [obj]
156 |
157 | obj2 = MongoidProject.create(:titles => ['Palatin', 'Margrave', 'Marquis'])
158 | @ability.can?(:read, obj2).should == false
159 | end
160 |
161 | it "should handle :field.exists" do
162 | obj = MongoidProject.create(:titles => ['Palatin', 'Margrave'])
163 | @ability.can :read, MongoidProject, :titles.exists => true
164 | @ability.can?(:read, obj).should == true
165 | MongoidProject.accessible_by(@ability, :read).should == [obj]
166 |
167 | obj2 = MongoidProject.create
168 | @ability.can?(:read, obj2).should == false
169 | end
170 |
171 | it "should handle :field.gt" do
172 | obj = MongoidProject.create(:age => 50)
173 | @ability.can :read, MongoidProject, :age.gt => 45
174 | @ability.can?(:read, obj).should == true
175 | MongoidProject.accessible_by(@ability, :read).should == [obj]
176 |
177 | obj2 = MongoidProject.create(:age => 40)
178 | @ability.can?(:read, obj2).should == false
179 | end
180 |
181 | it "should handle instance not saved to database" do
182 | obj = MongoidProject.new(:title => 'Sir')
183 | @ability.can :read, MongoidProject, :title.in => ["Sir", "Madam"]
184 | @ability.can?(:read, obj).should == true
185 |
186 | # accessible_by only returns saved records
187 | MongoidProject.accessible_by(@ability, :read).entries.should == []
188 |
189 | obj2 = MongoidProject.new(:title => 'Lord')
190 | @ability.can?(:read, obj2).should == false
191 | end
192 | end
193 |
194 | it "should call where with matching ability conditions" do
195 | obj = MongoidProject.create(:foo => {:bar => 1})
196 | @ability.can :read, MongoidProject, :foo => {:bar => 1}
197 | MongoidProject.accessible_by(@ability, :read).entries.first.should == obj
198 | end
199 |
200 | it "should exclude from the result if set to cannot" do
201 | obj = MongoidProject.create(:bar => 1)
202 | obj2 = MongoidProject.create(:bar => 2)
203 | @ability.can :read, MongoidProject
204 | @ability.cannot :read, MongoidProject, :bar => 2
205 | MongoidProject.accessible_by(@ability, :read).entries.should == [obj]
206 | end
207 |
208 | it "should combine the rules" do
209 | obj = MongoidProject.create(:bar => 1)
210 | obj2 = MongoidProject.create(:bar => 2)
211 | obj3 = MongoidProject.create(:bar => 3)
212 | @ability.can :read, MongoidProject, :bar => 1
213 | @ability.can :read, MongoidProject, :bar => 2
214 | MongoidProject.accessible_by(@ability, :read).entries.should =~ [obj, obj2]
215 | end
216 |
217 | it "should not allow to fetch records when ability with just block present" do
218 | @ability.can :read, MongoidProject do
219 | false
220 | end
221 | lambda {
222 | MongoidProject.accessible_by(@ability)
223 | }.should raise_error(CanCan::Error)
224 | end
225 | end
226 | end
227 | end
228 |
--------------------------------------------------------------------------------
/spec/cancan/rule_spec.rb:
--------------------------------------------------------------------------------
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 "should return no association joins if none exist" do
12 | @rule.associations_hash.should == {}
13 | end
14 |
15 | it "should return no association for joins if just attributes" do
16 | @conditions[:foo] = :bar
17 | @rule.associations_hash.should == {}
18 | end
19 |
20 | it "should return single association for joins" do
21 | @conditions[:foo] = {:bar => 1}
22 | @rule.associations_hash.should == {:foo => {}}
23 | end
24 |
25 | it "should return multiple associations for joins" do
26 | @conditions[:foo] = {:bar => 1}
27 | @conditions[:test] = {1 => 2}
28 | @rule.associations_hash.should == {:foo => {}, :test => {}}
29 | end
30 |
31 | it "should return nested associations for joins" do
32 | @conditions[:foo] = {:bar => {1 => 2}}
33 | @rule.associations_hash.should == {:foo => {:bar => {}}}
34 | end
35 |
36 | it "should return no association joins if conditions is nil" do
37 | rule = CanCan::Rule.new(true, :read, Integer, nil, nil)
38 | rule.associations_hash.should == {}
39 | end
40 |
41 | it "should not be mergeable if conditions are not simple hashes" do
42 | meta_where = OpenStruct.new(:name => 'metawhere', :column => 'test')
43 | @conditions[meta_where] = :bar
44 |
45 | @rule.should be_unmergeable
46 | end
47 |
48 | it "should be mergeable if conditions is an empty hash" do
49 | @conditions = {}
50 | @rule.should_not be_unmergeable
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/spec/matchers.rb:
--------------------------------------------------------------------------------
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
14 |
--------------------------------------------------------------------------------
/spec/spec.opts:
--------------------------------------------------------------------------------
1 | --color
2 | --backtrace
3 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
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 :rr
16 | config.before(:each) do
17 | Project.delete_all
18 | Category.delete_all
19 | end
20 | config.extend WithModel if ENV["MODEL_ADAPTER"].nil? || ENV["MODEL_ADAPTER"] == "active_record"
21 | end
22 |
23 | # Working around CVE-2012-5664 requires us to convert all ID params
24 | # to strings. Let's switch to using string IDs in tests, otherwise
25 | # SuperModel and/or RR will fail (as strings are not fixnums).
26 |
27 | module SuperModel
28 | class Base
29 | def generate_id
30 | object_id.to_s
31 | end
32 | end
33 | end
34 |
35 | class Ability
36 | include CanCan::Ability
37 |
38 | def initialize(user)
39 | end
40 | end
41 |
42 | class Category < SuperModel::Base
43 | has_many :projects
44 | end
45 |
46 | module Sub
47 | class Project < SuperModel::Base
48 | belongs_to :category
49 | attr_accessor :category # why doesn't SuperModel do this automatically?
50 |
51 | def self.respond_to?(method, include_private = false)
52 | if method.to_s == "find_by_name!" # hack to simulate ActiveRecord
53 | true
54 | else
55 | super
56 | end
57 | end
58 | end
59 | end
60 |
61 | class Project < SuperModel::Base
62 | belongs_to :category
63 | attr_accessor :category # why doesn't SuperModel do this automatically?
64 |
65 | def self.respond_to?(method, include_private = false)
66 | if method.to_s == "find_by_name!" # hack to simulate ActiveRecord
67 | true
68 | else
69 | super
70 | end
71 | end
72 | end
73 |
--------------------------------------------------------------------------------