├── .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 | {Gem Version}[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 | --------------------------------------------------------------------------------