From 0a4ca7edd5894368d4be948f4199f2debd5ba801 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 15 Mar 2020 19:19:01 +0100 Subject: [PATCH 01/66] Update README --- README.md | 19 +++++++------------ logo/cancancan.jpg | Bin 50495 -> 0 bytes logo/cancancan.png | Bin 0 -> 137261 bytes 3 files changed, 7 insertions(+), 12 deletions(-) delete mode 100644 logo/cancancan.jpg create mode 100755 logo/cancancan.png diff --git a/README.md b/README.md index 0da17af0..e1b2184f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CanCanCan -![CanCanCan Logo](/logo/cancancan.jpg) + [![Gem Version](https://badge.fury.io/rb/cancancan.svg)](http://badge.fury.io/rb/cancancan) [![Travis badge](https://travis-ci.org/CanCanCommunity/cancancan.svg?branch=develop)](https://travis-ci.org/CanCanCommunity/cancancan) @@ -11,7 +11,7 @@ [Screencast 1](http://railscasts.com/episodes/192-authorization-with-cancan) | [Screencast 2](https://www.youtube.com/watch?v=cTYu-OjUgDw) -CanCanCan is an authorization library for Ruby >= 2.2.0 and Ruby on Rails >= 4.2 which restricts what +CanCanCan is an authorization library for Ruby and Ruby on Rails which restricts what resources a given user is allowed to access. All permissions can be defined in one or multiple ability files and not duplicated across controllers, views, @@ -24,6 +24,10 @@ and provides helpers to check for those permissions. 2. **Rails helpers** to simplify the code in Rails Controllers by performing the loading and checking of permissions of models automatically and reduce duplicated code. +## Sponsored by + +[![Renuo AG](./logo/renuo.png)](https://www.renuo.ch) + ## Installation Add this to your Gemfile: @@ -254,7 +258,6 @@ If you have any question or doubt regarding CanCanCan which you cannot find the If you find a bug please add an [issue on GitHub](https://github.com/CanCanCommunity/cancancan/issues) or fork the project and send a pull request. - ## Development CanCanCan uses [appraisals](https://github.com/thoughtbot/appraisal) to test the code base against multiple versions @@ -266,17 +269,9 @@ You can then run all appraisal files (like CI does), with `appraisal rake` or ju See the [CONTRIBUTING](https://github.com/CanCanCommunity/cancancan/blob/develop/CONTRIBUTING.md) for more information. - ## Special Thanks -[![Renuo AG](/logo/renuo.png)](https://www.renuo.ch) - Thanks to [Renuo AG](https://www.renuo.ch) for currently maintaining and supporting the project. -Also many thanks to the [CanCanCan contributors](https://github.com/CanCanCommunity/cancancan/contributors). +Many thanks to the [CanCanCan contributors](https://github.com/CanCanCommunity/cancancan/contributors). See the [CHANGELOG](https://github.com/CanCanCommunity/cancancan/blob/master/CHANGELOG.md) for the full list. - -CanCanCan was inspired by [declarative_authorization](https://github.com/stffn/declarative_authorization/) and -[aegis](https://github.com/makandra/aegis). - - diff --git a/logo/cancancan.jpg b/logo/cancancan.jpg deleted file mode 100644 index 8002610236a79ded1ff4fa4c5eb53640807b299d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50495 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

;l02hm-2+xb!7aNjS5B& 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 literal 0 HcmV?d00001 From 4012445316fc760a5876149add58d02e6d6b466f Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 15 Mar 2020 19:24:01 +0100 Subject: [PATCH 02/66] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 977c3af0..2086a556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased +## 3.1.0 * [#605](https://github.com/CanCanCommunity/cancancan/pull/605): Generate inner queries instead of join+distinct. ([@fsateler][]) * [#608](https://github.com/CanCanCommunity/cancancan/pull/608): Spec for json column regression. ([@aleksejleonov][]) From 56e879f9f3eac4300d8c29ea8c2f398048d3555b Mon Sep 17 00:00:00 2001 From: Olle Jonsson Date: Sun, 22 Mar 2020 16:30:23 +0100 Subject: [PATCH 03/66] CI: use jruby-9.2.11.0 (#626) --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index d55aa3d1..7159d00a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ rvm: - 2.7.0 - ruby-head - jruby-9.1.17.0 - - jruby-9.2.7.0 + - jruby-9.2.11.0 - jruby-head gemfile: @@ -39,9 +39,9 @@ matrix: gemfile: gemfiles/activerecord_5.0.2.gemfile - rvm: jruby-9.1.17.0 gemfile: gemfiles/activerecord_6.0.0.gemfile - - rvm: jruby-9.2.7.0 + - rvm: jruby-9.2.11.0 gemfile: gemfiles/activerecord_5.0.2.gemfile - - rvm: jruby-9.2.7.0 + - rvm: jruby-9.2.11.0 gemfile: gemfiles/activerecord_6.0.0.gemfile allow_failures: - rvm: ruby-head From a620f8d61cdee9235aea237979574012139e67c7 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Thu, 16 Apr 2020 15:25:45 +0900 Subject: [PATCH 04/66] Updated README for appraisal command. Updated to a supported version, added 'bundle exec', fixed version syntax --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e1b2184f..c10ad90d 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ of Rails, as well as the different model adapters. When first developing, you need to run `bundle install` and then `appraisal install`, to install the different sets. -You can then run all appraisal files (like CI does), with `appraisal rake` or just run a specific set `appraisal activerecord_5.0 rake`. +You can then run all appraisal files (like CI does), with `appraisal rake` or just run a specific set `bundle exec appraisal activerecord_5.2.2 rake`. See the [CONTRIBUTING](https://github.com/CanCanCommunity/cancancan/blob/develop/CONTRIBUTING.md) for more information. From 0fa32f05828074c359fd637eb8c3a81873ff3c4e Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Thu, 16 Apr 2020 16:19:14 +0900 Subject: [PATCH 05/66] added DB specification to readme appraisal example, increased verbosity on error in spec sql_helpers --- README.md | 2 +- spec/support/sql_helpers.rb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c10ad90d..7083547c 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ of Rails, as well as the different model adapters. When first developing, you need to run `bundle install` and then `appraisal install`, to install the different sets. -You can then run all appraisal files (like CI does), with `appraisal rake` or just run a specific set `bundle exec appraisal activerecord_5.2.2 rake`. +You can then run all appraisal files (like CI does), with `appraisal rake` or just run a specific set `DB='sqlite' bundle exec appraisal activerecord_5.2.2 rake`. See the [CONTRIBUTING](https://github.com/CanCanCommunity/cancancan/blob/develop/CONTRIBUTING.md) for more information. diff --git a/spec/support/sql_helpers.rb b/spec/support/sql_helpers.rb index d4cf2e40..fc1ac8cd 100644 --- a/spec/support/sql_helpers.rb +++ b/spec/support/sql_helpers.rb @@ -12,8 +12,10 @@ def connect_db ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') elsif ENV['DB'] == 'postgres' connect_postgres + elsif ENV['DB'].nil? + raise StandardError, "ENV['DB'] not specified" else - raise StandardError, 'database not supported' + raise StandardError, "database not supported: #{ENV['DB']}. Try DB='sqlite' or DB='postgres'" end end From 25ddf8d97131ba5b95f9516efd511ca4ca89491c Mon Sep 17 00:00:00 2001 From: Olle Jonsson Date: Thu, 14 May 2020 21:20:49 +0200 Subject: [PATCH 06/66] CI: Drop unused sudo: false Travis directive (#625) Co-authored-by: Alessandro Rodi --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7159d00a..e86431ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: ruby cache: bundler -sudo: false addons: postgresql: "9.6" rvm: From 61530988c20a3d93e2ef7f598879f89582d2df34 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Thu, 14 May 2020 21:26:21 +0200 Subject: [PATCH 07/66] Move wiki in the docs folder (#635) Add: wiki documentation into a separate docs folder (#621) Co-authored-by: Ben Koshy --- docs/Abilities-in-Database.md | 124 +++++++++++ docs/Ability-Precedence.md | 47 ++++ docs/Ability-for-Other-Users.md | 34 +++ docs/Accessing-request-data.md | 25 +++ docs/Action-Aliases.md | 32 +++ ...uthorization-for-Namespaced-Controllers.md | 51 +++++ docs/Authorizing-controller-actions.md | 210 ++++++++++++++++++ docs/Changing-Defaults.md | 44 ++++ docs/Checking-Abilities.md | 51 +++++ docs/Controller-Authorization-Example.md | 70 ++++++ docs/Custom-Actions.md | 27 +++ docs/Debugging-Abilities.md | 61 +++++ docs/Defining-Abilities-with-Blocks.md | 101 +++++++++ docs/Defining-Abilities-with-Hashes.md | 5 + docs/Defining-Abilities.md | 171 ++++++++++++++ docs/Defining-Abilities:-Best-Practices.md | 99 +++++++++ docs/Devise.md | 22 ++ docs/Ensure-Authorization.md | 40 ++++ docs/Exception-Handling.md | 115 ++++++++++ docs/Fetching-Records.md | 47 ++++ docs/FriendlyId-support.md | 30 +++ docs/Home.md | 43 ++++ docs/Inherited-Resources.md | 42 ++++ docs/Issue-Collaborators.md | 27 +++ docs/Link-Helpers.md | 39 ++++ docs/MetaWhere.md | 1 + docs/Migrating-from-CanCanCan-2.x-to-3.0.md | 15 ++ docs/Model-Adapter.md | 130 +++++++++++ docs/Mongoid.md | 17 ++ docs/Multiple-can-definitions.textile | 35 +++ docs/Nested-Resources.md | 160 +++++++++++++ docs/Non-RESTful-Controllers.md | 35 +++ docs/Other-Authorization-Solutions.md | 7 + docs/Role-Based-Authorization.md | 175 +++++++++++++++ docs/Rules-compression.md | 44 ++++ docs/Separate-Role-Model.md | 87 ++++++++ docs/Share-Ability-Definitions.md | 9 + docs/Strong-Parameters.md | 140 ++++++++++++ docs/Testing-Abilities.md | 99 +++++++++ docs/Translations-(i18n).md | 49 ++++ docs/mvc--deficiencies.md | 15 ++ 41 files changed, 2575 insertions(+) create mode 100644 docs/Abilities-in-Database.md create mode 100644 docs/Ability-Precedence.md create mode 100644 docs/Ability-for-Other-Users.md create mode 100644 docs/Accessing-request-data.md create mode 100644 docs/Action-Aliases.md create mode 100644 docs/Authorization-for-Namespaced-Controllers.md create mode 100644 docs/Authorizing-controller-actions.md create mode 100644 docs/Changing-Defaults.md create mode 100644 docs/Checking-Abilities.md create mode 100644 docs/Controller-Authorization-Example.md create mode 100644 docs/Custom-Actions.md create mode 100644 docs/Debugging-Abilities.md create mode 100644 docs/Defining-Abilities-with-Blocks.md create mode 100644 docs/Defining-Abilities-with-Hashes.md create mode 100644 docs/Defining-Abilities.md create mode 100644 docs/Defining-Abilities:-Best-Practices.md create mode 100644 docs/Devise.md create mode 100644 docs/Ensure-Authorization.md create mode 100644 docs/Exception-Handling.md create mode 100644 docs/Fetching-Records.md create mode 100644 docs/FriendlyId-support.md create mode 100644 docs/Home.md create mode 100644 docs/Inherited-Resources.md create mode 100644 docs/Issue-Collaborators.md create mode 100644 docs/Link-Helpers.md create mode 100644 docs/MetaWhere.md create mode 100644 docs/Migrating-from-CanCanCan-2.x-to-3.0.md create mode 100644 docs/Model-Adapter.md create mode 100644 docs/Mongoid.md create mode 100644 docs/Multiple-can-definitions.textile create mode 100644 docs/Nested-Resources.md create mode 100644 docs/Non-RESTful-Controllers.md create mode 100644 docs/Other-Authorization-Solutions.md create mode 100644 docs/Role-Based-Authorization.md create mode 100644 docs/Rules-compression.md create mode 100644 docs/Separate-Role-Model.md create mode 100644 docs/Share-Ability-Definitions.md create mode 100644 docs/Strong-Parameters.md create mode 100644 docs/Testing-Abilities.md create mode 100644 docs/Translations-(i18n).md create mode 100644 docs/mvc--deficiencies.md diff --git a/docs/Abilities-in-Database.md b/docs/Abilities-in-Database.md new file mode 100644 index 00000000..09233c53 --- /dev/null +++ b/docs/Abilities-in-Database.md @@ -0,0 +1,124 @@ +What if you or a client, wants to change permissions without having to re-deploy the application? +In that case, it may be best to store the permission logic in a database: it is very easy to use the database records when defining abilities. + +We will need a model called `Permission`. + +Each user `has_many :permissions`, and each permission has `action`, `subject_class` and `subject_id` columns. The last of which is optional. + +```ruby +class Ability + include CanCan::Ability + + def initialize(user) + can do |action, subject_class, subject| + user.permissions.where(action: aliases_for_action(action)).any? do |permission| + permission.subject_class == subject_class.to_s && + (subject.nil? || permission.subject_id.nil? || permission.subject_id == subject.id) + end + end + end +end +``` + +An alternative approach is to define a separate `can` ability for each permission. + +```ruby +def initialize(user) + user.permissions.each do |permission| + if permission.subject_id.nil? + can permission.action.to_sym, permission.subject_class.constantize + else + can permission.action.to_sym, permission.subject_class.constantize, id: permission.subject_id + end + end +end +``` + +The actual details will depend largely on your application requirements, but hopefully, you can see how it's possible to define permissions in the database and use them with CanCanCan. + +You can mix-and-match this with defining permissions in the code as well. This way you can keep the more complex logic in the code so you don't need to shoe-horn every kind of permission rule into an overly-abstract database. + + +You can also create a `Permission` model containing all possible permissions in your app. Use that code to create a rake task that fills a `Permission` table: +(The code below is not fully tested) + +To use the following code, the permissions table should have such fields :name, :user_id, :subject_class, :subject_id, :action, and :description.You can generate the permission model by the command: `rails g model Permission user_id:integer name:string subject_class:string subject_id:integer action:string description:text`. + +```ruby +class ApplicationController < ActionController::Base + ... + protected + + # Derive the model name from the controller. UsersController will return User + def self.permission + return name = controller_name.classify.constantize + end +end +``` + +```ruby +def setup_actions_controllers_db + + write_permission("all", "manage", "Everything", "All operations", true) + + controllers = Dir.new("#{Rails.root}/app/controllers").entries + controllers.each do |controller| + if controller =~ /_controller/ + foo_bar = controller.camelize.gsub(".rb","").constantize.new + end + end + # You can change ApplicationController for a super-class used by your restricted controllers + ApplicationController.subclasses.each do |controller| + if controller.respond_to?(:permission) + klass, description = controller.permission + write_permission(klass, "manage", description, "All operations") + controller.action_methods.each do |action| + if action.to_s.index("_callback").nil? + action_desc, cancan_action = eval_cancan_action(action) + write_permission(klass, cancan_action, description, action_desc) + end + end + end + end + +end + + +def eval_cancan_action(action) + case action.to_s + when "index", "show", "search" + cancan_action = "read" + action_desc = I18n.t :read + when "create", "new" + cancan_action = "create" + action_desc = I18n.t :create + when "edit", "update" + cancan_action = "update" + action_desc = I18n.t :edit + when "delete", "destroy" + cancan_action = "delete" + action_desc = I18n.t :delete + else + cancan_action = action.to_s + action_desc = "Other: " << cancan_action + end + return action_desc, cancan_action +end + +def write_permission(class_name, cancan_action, name, description, force_id_1 = false) + permission = Permission.find(:first, :conditions => ["subject_class = ? and action = ?", class_name, cancan_action]) + if not permission + permission = Permission.new + permission.id = 1 if force_id_1 + permission.subject_class = class_name + permission.action = cancan_action + permission.name = name + permission.description = description + permission.save + else + permission.name = name + permission.description = description + permission.save + end +end +``` \ No newline at end of file diff --git a/docs/Ability-Precedence.md b/docs/Ability-Precedence.md new file mode 100644 index 00000000..f2856a7d --- /dev/null +++ b/docs/Ability-Precedence.md @@ -0,0 +1,47 @@ +An ability rule will override a previous one. +For example, let's say we want the user to be able to do everything to projects except destroy them. + +This is the correct way: + +```ruby +can :manage, Project +cannot :destroy, Project +``` + +It is important that the `cannot :destroy` line comes after the `can :manage` line. If they were reversed, `cannot :destroy` would be overridden by `can :manage`. + +Adding `can` rules do not override prior rules, but instead are logically or'ed. + +```ruby +can :manage, Project, user_id: user.id +can :update, Project do |project| + !project.locked? +end +``` + +For the above, `can? :update` will always return true if the `user_id` equals `user.id`, even if the project is locked. + +This is also important when dealing with roles which have inherited behavior. For example, let's say we have two roles, moderator and admin. We want the admin to inherit the moderator's behavior. + +```ruby +if user.role? :moderator + can :manage, Project + cannot :destroy, Project + can :manage, Comment +end + +if user.role? :admin + can :destroy, Project +end +``` + +Here it is important the admin role be after the moderator so it can override the `cannot` behavior to give the admin more permissions. See [[Role Based Authorization]]. + +If you are not getting the behavior you expect, please [[post an issue|https://github.com/CanCanCommunity/cancancan/issues]]. + +## Additional Docs + +* [[Defining Abilities]] +* [[Checking Abilities]] +* [[Debugging Abilities]] +* [[Testing Abilities]] \ No newline at end of file diff --git a/docs/Ability-for-Other-Users.md b/docs/Ability-for-Other-Users.md new file mode 100644 index 00000000..598f5efe --- /dev/null +++ b/docs/Ability-for-Other-Users.md @@ -0,0 +1,34 @@ +What if you want to determine the abilities of a `User` record that is not the `current_user`? Maybe we want to see if another user can update an article. + +```ruby +some_user.ability.can? :update, @article +``` + +You can easily add an `ability` method in the `User` model. + +```ruby +def ability + @ability ||= Ability.new(self) +end +``` + +I also recommend adding delegation so `can?` can be called directly from the user. + +```ruby +class User < ActiveRecord::Base + delegate :can?, :cannot?, to: :ability + # ... +end + +some_user.can? :update, @article +``` + +Finally, if you're using this approach, it's best to override the `current_ability` method in the `ApplicationController` so it uses the same method. + +```ruby +def current_ability + @current_ability ||= current_user.ability +end +``` + +The downside of this approach is that [[Accessing Request Data]] is not as easy, so it depends on the needs of your application. \ No newline at end of file diff --git a/docs/Accessing-request-data.md b/docs/Accessing-request-data.md new file mode 100644 index 00000000..286a44f1 --- /dev/null +++ b/docs/Accessing-request-data.md @@ -0,0 +1,25 @@ +What if you need to modify the permissions based on something outside of the User object? For example, let's say you want to blacklist certain IP addresses from creating comments. The IP address is accessible through request.remote_ip but the Ability class does not have access to this. It's easy to modify what you pass to the Ability object by overriding the current_ability method in ApplicationController. + +```ruby +class ApplicationController < ActionController::Base + #... + + private + + def current_ability + @current_ability ||= Ability.new(current_user, request.remote_ip) + end +end +``` +```ruby +class Ability + include CanCan::Ability + + def initialize(user, ip_address=nil) + can :create, Comment unless BLACKLIST_IPS.include? ip_address + end +end +``` +This concept can apply to session and cookies as well. + +You may wonder, why I pass only the IP Address instead of the entire request object? I prefer to pass only the information needed, this makes testing and debugging the behavior easier. \ No newline at end of file diff --git a/docs/Action-Aliases.md b/docs/Action-Aliases.md new file mode 100644 index 00000000..be139175 --- /dev/null +++ b/docs/Action-Aliases.md @@ -0,0 +1,32 @@ +You will usually be working with four actions when [[defining|Defining Abilities]] and [[checking|Checking Abilities]] permissions: `:read`, `:create`, `:update`, `:destroy`. These aren't the same as the 7 RESTful actions in Rails. CanCanCan automatically adds some convenient aliases for mapping the controller actions. + +```ruby +alias_action :index, :show, :to => :read +alias_action :new, :to => :create +alias_action :edit, :to => :update +``` + +Notice the `edit` action is aliased to `update`. This means if the user is able to update a record he also has permission to edit it. You can define your own aliases in the `Ability` class. + +```ruby +class Ability + include CanCan::Ability + def initialize(user) + alias_action :update, :destroy, :to => :modify + can :modify, Comment + end +end + +# in controller or view +can? :update, Comment # => true +``` + +You are not restricted to just the 7 RESTful actions, you can use any action name. See [[Custom Actions]] for details. + +Please note that if you are changing the default alias_actions, the original actions associated with the alias will NOT be removed. For example, following statement will not have any effect on the alias :read, which points to :show and :index: + +```ruby +alias_action :show, :to => :read # this will have no effect on the alias :read! +``` + +If you want to change the default actions, you should use clear_aliased_actions method to remove ALL default aliases first. \ No newline at end of file diff --git a/docs/Authorization-for-Namespaced-Controllers.md b/docs/Authorization-for-Namespaced-Controllers.md new file mode 100644 index 00000000..14ffaf09 --- /dev/null +++ b/docs/Authorization-for-Namespaced-Controllers.md @@ -0,0 +1,51 @@ +The default operation for CanCanCan is to authorize based on user and the object identified in `load_resource`. So if you have a `WidgetsController` and also an `Admin::WidgetsController`, you can use some different approaches. + +Just like in the example given for [[Accessing Request Data]], you **can** also create differing authorization rules that depend on the controller namespace. + +In this case, just override the `current_ability` method in `ApplicationController` to include the controller namespace, and create an `Ability` class that knows what to do with it. + +``` ruby +class Admin::WidgetsController < ActionController::Base + #... + + private + + def current_ability + # I am sure there is a slicker way to capture the controller namespace + controller_name_segments = params[:controller].split('/') + controller_name_segments.pop + controller_namespace = controller_name_segments.join('/').camelize + @current_ability ||= Ability.new(current_user, controller_namespace) + end +end + + +class Ability + include CanCan::Ability + + def initialize(user, controller_namespace) + case controller_namespace + when 'Admin' + can :manage, :all if user.has_role? 'admin' + else + # rules for non-admin controllers here + end + end +end +``` + +Another way to achieve the same is to use a completely different Ability class in this controller: + +``` ruby +class Admin::WidgetsController < ActionController::Base + #... + + private + + def current_ability + @current_ability ||= AdminAbility.new(current_user) + end +end +``` + +and follow the [Best Practice of splitting your Ability file into multiple files](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practices#split-your-abilityrb-file). \ No newline at end of file diff --git a/docs/Authorizing-controller-actions.md b/docs/Authorizing-controller-actions.md new file mode 100644 index 00000000..73840e9d --- /dev/null +++ b/docs/Authorizing-controller-actions.md @@ -0,0 +1,210 @@ +You can use the `authorize!` method to manually handle authorization in a controller action. This will raise a `CanCan::AccessDenied` exception when the user does not have permission. See [[Exception Handling]] for how to react to this. + +```ruby +def show + @project = Project.find(params[:project]) + authorize! :show, @project +end +``` + +However that can be tedious to apply to each action. Instead you can use the `load_and_authorize_resource` method in your controller to load the resource into an instance variable and authorize it automatically for every action in that controller. + +```ruby +class ProductsController < ActionController::Base + load_and_authorize_resource +end +``` + +This is the same as calling `load_resource` and `authorize_resource` because they are two separate steps and you can choose to use one or the other. + +```ruby +class ProductsController < ActionController::Base + load_resource + authorize_resource +end +``` + +As of CanCan 1.5 you can use the `skip_load_and_authorize_resource`, `skip_load_resource` or `skip_authorize_resource` methods to skip any of the applied behavior and specify specific actions like in a before filter. For example. + +```ruby +class ProductsController < ActionController::Base + load_and_authorize_resource + skip_authorize_resource :only => :new +end +``` + +**important notice about `:manage` rules** + +Using `load_and_authorize_resource` with a rule like `can :manage, Article, id: 23` will allow rendering the `new` method of the ArticlesController, which is unexpected because this rule naively reads as _"the user can manage the existing article with id 23"_, which should have nothing to do with creating new articles. + +But in reality the rule means _"the user can manage any article object with an id field set to 23"_, which includes creating a new Article with the id set to 23 like `Article.new(id: 23)`. + +Thus `load_and_authorize_resource` will initialize a model in the `:new` action and set its id to 23, and happily render the page. Saving will not work though. + +The correct intended rule to avoid `new` being allowed would be: + +``` ruby +can [:read, :update, :destroy], Article, id: 23 +``` + +Also see [[Controller Authorization Example]], [[Ensure Authorization]] and [[Non RESTful Controllers]]. + + +## Choosing Actions + +By default this will apply to **every action** in the controller even if it is not one of the 7 RESTful actions. The action name will be passed in when authorizing. For example, if we have a `discontinue` action on `ProductsController` it will have this behavior. + +```ruby +class ProductsController < ActionController::Base + load_and_authorize_resource + def discontinue + # Automatically does the following: + # @product = Product.find(params[:id]) + # authorize! :discontinue, @product + end +end +``` + +You can specify which actions to affect using the `:except` and `:only` options, just like a `before_action`. + +```ruby +load_and_authorize_resource :only => [:index, :show] +``` +### Choosing actions on nested resources + +For this you can pass a name to skip_authorize_resource. +For example: +```ruby +class CommentsController < ApplicationController + load_and_authorize_resource :post + load_and_authorize_resource :through => :post + + skip_authorize_resource :only => :show + skip_authorize_resource :post, :only => :show +end +``` + +The first skip_authorize_resource skips authorization check for comment and the second for post. Both are needed if you want to skip all authorization checks for an action. + +## load_resource + +### index action + +As of 1.4 the index action will load the collection resource using `accessible_by`. + +```ruby +def index + # @products automatically set to Product.accessible_by(current_ability) +end +``` + +If you want custom find options such as [[includes|https://github.com/ryanb/cancan/issues#issue/259]] or pagination, you can build on this further since it is a scope. + +```ruby +def index + @products = @products.includes(:category).page(params[:page]) +end +``` + +The `@products` variable will not be set initially if `Product` does not respond to `accessible_by` (such as if you aren't using a supported ORM). It will also not be set if you are only using a block in the `can` definitions because there is no way to determine which records to fetch from the database. + +### show, edit, update and destroy actions + +These member actions simply fetch the record directly. + +```ruby +def show + # @product automatically set to Product.find(params[:id]) +end +``` + +### new and create actions + +As of 1.4 these builder actions will initialize the resource with the attributes in the hash conditions. For example, if we have this `can` definition. + +```ruby +can :manage, Product, :discontinued => false +``` + +Then the product will be built with that attribute in the controller. + +```ruby +@product = Product.new(:discontinued => false) +``` + +This way it will pass authorization when the user accesses the `new` action. + +The attributes are then overridden by whatever is passed by the user in `params[:product]`. + +### Custom class + +If the model is named differently than the controller, then you may explicitly name the model that should be loaded; however, you must specify that it is not a parent in a nested routing situation, ie: + +```ruby +class ArticlesController < ApplicationController + load_and_authorize_resource :post, :parent => false +end +``` + +If the model class is namespaced differently than the controller you will need to specify the `:class` option. + +```ruby +class ProductsController < ApplicationController + load_and_authorize_resource :class => "Store::Product" +end +``` + + +### Custom find + +If you want to fetch a resource by something other than `id` it can be done so using the `find_by` option. + +```ruby +load_resource :find_by => :permalink # will use find_by_permalink!(params[:id]) +authorize_resource +``` + +### Override loading + +The resource will only be loaded into an instance variable if it hasn't been already. This allows you to easily override how the loading happens in a separate `before_action`. + +```ruby +class BooksController < ApplicationController + before_action :find_published_book, :only => :show + load_and_authorize_resource + + private + + def find_published_book + @book = Book.released.find(params[:id]) + end +end +``` + +It is important that any custom loading behavior happens **before** the call to `load_and_authorize_resource`. If you have `authorize_resource` in your `ApplicationController` then you need to use `prepend_before_action` to do the loading in the controller subclasses so it happens before authorization. + +## authorize_resource + +Adding `authorize_resource` will install a `before_action` callback that calls `authorize!`, passing the resource instance variable if it exists. If the instance variable isn't set (such as in the index action) it will pass in the class name. For example, if we have a `ProductsController` it will do this before each action. + +```ruby +authorize!(params[:action].to_sym, @product || Product) +``` + +## More info + +For additional information see the `load_resource` and `authorize_resource` methods in the [[RDoc|http://www.rubydoc.info/github/CanCanCommunity/cancancan]]. + +Also see [[Nested Resources]] and [[Non RESTful Controllers]]. + +## Resetting Current Ability + +If you ever update a User record which may be the current user, it will make the current ability for that request stale. This means any `can?` checks will use the user record before it was updated. You will need to reset the `current_ability` instance so it will be reloaded. Do the same for the `current_user` if you are caching that too. + +```ruby +if @user.update_attributes(params[:user]) + @current_ability = nil + @current_user = nil + # ... +end +``` \ No newline at end of file diff --git a/docs/Changing-Defaults.md b/docs/Changing-Defaults.md new file mode 100644 index 00000000..2d2a8747 --- /dev/null +++ b/docs/Changing-Defaults.md @@ -0,0 +1,44 @@ +CanCanCan makes two assumptions about your application. + +* You have an `Ability` class which defines the permissions. +* You have a `current_user` method in the controller which returns the current user model. + +You can override both of these by defining the `current_ability` method in your `ApplicationController`. The current method looks like this. + +```ruby +def current_ability + @current_ability ||= Ability.new(current_user) +end +``` + +The `Ability` class and `current_user` method can easily be changed to something else. + +```ruby +# in ApplicationController +def current_ability + @current_ability ||= AccountAbility.new(current_account) +end +``` + +Sometimes you might have a gem in your project which provides its own Rails engine which also uses CanCanCan such as LocomotiveCMS. In this case the current_ability override in the ApplicationController can also be useful. + +```ruby +# in ApplicationController +def current_ability + if request.fullpath =~ /\/locomotive/ + @current_ability ||= Locomotive::Ability.new(current_user) + else + @current_ability ||= Ability.new(current_user) + end +end +``` + +If your method that returns the currently logged in user just has another name than `current_user`, it may be the easiest solution to simply alias the method in your ApplicationController like this: + +```ruby +class ApplicationController < ActionController::Base + alias_method :current_user, :name_of_your_method # Could be :current_member or :logged_in_user +end +``` + +That's it! See [[Accessing Request Data]] for a more complex example of what you can do here. \ No newline at end of file diff --git a/docs/Checking-Abilities.md b/docs/Checking-Abilities.md new file mode 100644 index 00000000..4d3dfa93 --- /dev/null +++ b/docs/Checking-Abilities.md @@ -0,0 +1,51 @@ +After [[abilities are defined|Defining Abilities]], you can use the `can?` method in the controller or view to check the user's permission for a given action and object. + +```ruby +can? :destroy, @project +``` + +The `cannot?` method is for convenience and performs the opposite check of `can?` + +```ruby +cannot? :destroy, @project +``` + +Also see [[Authorizing Controller Actions]] and [[Custom Actions]]. + +## Checking with Class + +You can also pass the class instead of an instance (if you don't have one handy). + +```rhtml +<% if can? :create, Project %> + <%= link_to "New Project", new_project_path %> +<% end %> +``` + +**Important:** If a block or hash of conditions exist they will be ignored when checking on a class, and it will return `true`. For example: + +```ruby +can :read, Project, :priority => 3 +can? :read, Project # returns true +``` + +It is impossible to answer this `can?` question completely because not enough detail is given. Here the class does not have a `priority` attribute to check on. + +Think of it as asking "can the current user read **a** project?". The user can read a project, so this returns `true`. However it depends on which specific project you're talking about. If you are doing a class check, it is important you do another check once an instance becomes available so the hash of conditions can be used. + +The reason for this behavior is because of the controller `index` action. Since the `authorize_resource` before filter has no instance to check on, it will use the `Project` class. If the authorization failed at that point then it would be impossible to filter the results later when [[Fetching Records]]. + +That is why passing a class to `can?` will return `true`. + +The code answering the question "can the user update all the articles?" would be something like: + +``` ruby +Article.accessible_by(current_ability).count == Article.count +``` + +## Additional Docs + +* [[Defining Abilities]] +* [[Ability Precedence]] +* [[Debugging Abilities]] +* [[Testing Abilities]] \ No newline at end of file diff --git a/docs/Controller-Authorization-Example.md b/docs/Controller-Authorization-Example.md new file mode 100644 index 00000000..8a65c553 --- /dev/null +++ b/docs/Controller-Authorization-Example.md @@ -0,0 +1,70 @@ +CanCan provides a convenient `load_and_authorize_resource` method in the controller, but what exactly is this doing? It sets up a before filter for every action to handle the loading and authorization of the controller. Let's say we have a typical RESTful controller with that line at the top. + +```ruby +class ProjectsController < ApplicationController + load_and_authorize_resource + # ... +end +``` + +It will add a before filter that has this behavior for the actions if they exist. This means you do not need to put code below in your controller. + +```ruby +class ProjectsController < ApplicationController + def index + authorize! :index, Project + @projects = Project.accessible_by(current_ability) + end + + def show + @project = Project.find(params[:id]) + authorize! :show, @project + end + + def new + @project = Project.new + current_ability.attributes_for(:new, Project).each do |key, value| + @project.send("#{key}=", value) + end + @project.attributes = params[:project] + authorize! :new, @project + end + + def create + @project = Project.new + current_ability.attributes_for(:create, Project).each do |key, value| + @project.send("#{key}=", value) + end + @project.attributes = params[:project] + authorize! :create, @project + end + + def edit + @project = Project.find(params[:id]) + authorize! :edit, @project + end + + def update + @project = Project.find(params[:id]) + authorize! :update, @project + end + + def destroy + @project = Project.find(params[:id]) + authorize! :destroy, @project + end + + def some_other_action + if params[:id] + @project = Project.find(params[:id]) + else + @projects = Project.accessible_by(current_ability) + end + authorize!(:some_other_action, @project || Project) + end +end +``` + +The most complex behavior is inside the new and create actions. There it is setting some initial attribute values based on what the given user has permission to access. For example, if the user is only allowed to create projects where the "visible" attribute is true, then it would automatically set this upon building it. + +See [[Authorizing Controller Actions]] for details on what options you can pass to the `load_and_authorize_resource`. \ No newline at end of file diff --git a/docs/Custom-Actions.md b/docs/Custom-Actions.md new file mode 100644 index 00000000..fbdce20c --- /dev/null +++ b/docs/Custom-Actions.md @@ -0,0 +1,27 @@ +When you define a user's abilities for a given model, you are not restricted to the 7 RESTful actions (create, update, destroy, etc.), you can create your own. + +For example, in [[Role Based Authorization]] I showed you how to define separate roles for a given user. However, you don't want all users to be able to assign roles, only admins. How do you set these fine-grained controls? Well you need to come up with a new action name. Let's call it `assign_roles`. + +```ruby +# in models/ability.rb +can :assign_roles, User if user.admin? +``` + +We can then check if the user has permission to assign roles when displaying the role checkboxes and assigning them. + +```rhtml + +<% if can? :assign_roles, @user %> + +<% end %> +``` + +```ruby +# users_controller.rb +def update + authorize! :assign_roles, @user if params[:user][:assign_roles] + # ... +end +``` + +Now only admins will be able to assign roles to users. \ No newline at end of file diff --git a/docs/Debugging-Abilities.md b/docs/Debugging-Abilities.md new file mode 100644 index 00000000..829722d7 --- /dev/null +++ b/docs/Debugging-Abilities.md @@ -0,0 +1,61 @@ +What do you do when permissions you defined in the Ability class don't seem to be working properly? First try to duplicate this problem in the `rails console` or better yet, see [[Testing Abilities]]. + +## Debugging Member Actions + +```ruby +# in rails console or test +user = User.first # fetch any user you want to test abilities on +project = Project.first # any model you want to test against +ability = Ability.new(user) +ability.can?(:create, project) # see if it returns the expected behavior for that action +``` + +Note: this assumes that the model instance is being loaded properly. If you are only using `authorize_resource` it will not have an instance to work with so it will use the class. + +```ruby +ability.can?(:create, Project) +``` + +## Debugging index Action + +```ruby +# in rails console or test +user = User.first # fetch any user you want to test abilities on +ability = Ability.new(user) +ability.can?(:index, Project) # see if user can access the class +Project.accessible_by(ability) # see if returns the records the user can access +Project.accessible_by(ability).to_sql # see what the generated SQL looks like to help determine why it's not fetching the records you want +``` + +If you find it is fetching the wrong records in complex cases, you may need to use an SQL condition instead of a hash inside the Ability class. + +```ruby +can :update, Project, ["priority < ?", 3] do |project| + project.priority < 3 +end +``` + +See [[issue #213|https://github.com/ryanb/cancan/issues#issue/213]] for a more complex example. + +## Logging AccessDenied Exception + +If you think the `CanCan::AccessDenied` exception is being raised and you are not sure why, you can log this behavior to help debug what is triggering it. + +```ruby +# in ApplicationController +rescue_from CanCan::AccessDenied do |exception| + Rails.logger.debug "Access denied on #{exception.action} #{exception.subject.inspect}" + # ... +end +``` + +## Issue Tracker + +If you are still unable to resolve the issue, please post on the [[issue tracker|https://github.com/ryanb/cancan/issues]] + +## Additional Docs + +* [[Defining Abilities]] +* [[Checking Abilities]] +* [[Ability Precedence]] +* [[Testing Abilities]] \ No newline at end of file diff --git a/docs/Defining-Abilities-with-Blocks.md b/docs/Defining-Abilities-with-Blocks.md new file mode 100644 index 00000000..14f8506b --- /dev/null +++ b/docs/Defining-Abilities-with-Blocks.md @@ -0,0 +1,101 @@ +If your conditions are too complex to define in a hash (as shown in [[Defining Abilities]] page), you can use a block to define them in Ruby. + +```ruby +can :update, Project do |project| + project.priority < 3 +end +``` + +If the block returns true then the user has that ability, otherwise he will be denied access. + +## Only for Object Attributes + +The block is **only** evaluated when an actual instance object is present. It is not evaluated when checking permissions on the class (such as in the `index` action). This means any conditions which are not dependent on the object attributes should be moved outside of the block. + +```ruby +# don't do this +can :update, Project do |project| + user.admin? # this won't be called for Project.accessible_by(current_ability, :update) +end + +# do this +can :update, Project if user.admin? +``` +Note that if you pass a block to a `can` or `cannot`, regardless of whether the block asks for parameters (ex. `|project|`) the block only executes if an instance of a class is passed to `can?` or `cannot?`. + +If you define a `can` or `cannot` with a block and an object is not passed, the check will pass. +```ruby +can :update, Project do |project| + false +end +``` +```ruby +can? :update, Project # returns true! +``` + +See [[Checking Abilities]] for more information. + +## Fetching Records + +A block's conditions are only executable through Ruby. If you are [[Fetching Records]] using `accessible_by` it will raise an exception. To fetch records from the database you need to supply an SQL string representing the condition. The SQL will go in the `WHERE` clause, if you need to do joins consider using sub-queries or scopes (below). + +```ruby +can :update, Project, ["priority < ?", 3] do |project| + project.priority < 3 +end +``` + +If you are using `load_resource` and don't supply this SQL argument, the instance variable will not be set for the `index` action since they cannot be translated to a database query. + + +## Block Conditions with Scopes + +It's also possible to pass a scope instead of an SQL string when using a block in an ability. + +```ruby +can :read, Article, Article.published do |article| + article.published_at <= Time.now +end +``` + +Generally, this breaks down to looks something like: + +```ruby +can [:ability], Model, Model.scope_to_select_on_index_action do |model_instance| + model_instance.condition_to_evaluate_for_new_create_edit_update_destroy +end +``` + +This is really useful if you have complex conditions which require `joins`. A couple of caveats: + +* You cannot use this with multiple `can` definitions that match the same action and model since it is not possible to combine them. An exception will be raised when that is the case. +* If you use this with `cannot`, the scope needs to be the inverse since it's passed directly through. For example, if you don't want someone to read discontinued products the scope will need to fetch non discontinued ones: + +```ruby +cannot :read, Product, Product.where(:discontinued => false) do |product| + product.discontinued? +end +``` + +It is only recommended to use scopes if a situation is too complex for a hash condition. + +## Overriding All Behavior + +You can override all `can` behaviour by passing no arguments, this is useful when permissions are defined outside of ruby such as when defining [[Abilities in Database]]. + +```ruby +can do |action, subject_class, subject| + # ... +end +``` + +Here the block will be triggered for every `can?` check, even when only a class is used in the check. + + +## Additional Docs + +* [[Defining Abilities]] +* [[Checking Abilities]] +* [[Testing Abilities]] +* [[Debugging Abilities]] +* [[Ability Precedence]] diff --git a/docs/Defining-Abilities-with-Hashes.md b/docs/Defining-Abilities-with-Hashes.md new file mode 100644 index 00000000..860b2594 --- /dev/null +++ b/docs/Defining-Abilities-with-Hashes.md @@ -0,0 +1,5 @@ +This section has been moved to [[Defining Abilities]] under "Hash of Conditions". + +## Checking with Class + +This section has been moved to [[Checking Abilities]]. diff --git a/docs/Defining-Abilities.md b/docs/Defining-Abilities.md new file mode 100644 index 00000000..2410894f --- /dev/null +++ b/docs/Defining-Abilities.md @@ -0,0 +1,171 @@ +The `Ability` class is where all user permissions are defined. An example class looks like this. + +```ruby +class Ability + include CanCan::Ability + + def initialize(user) + can :read, :all # permissions for every user, even if not logged in + if user.present? # additional permissions for logged in users (they can manage their posts) + can :manage, Post, user_id: user.id + if user.admin? # additional permissions for administrators + can :manage, :all + end + end + end +end +``` + +The `current_user` model is passed into the initialize method, so the permissions can be modified based on any user attributes. CanCanCan makes no assumption about how roles are handled in your application. See [[Role Based Authorization]] for an example. + +## The `can` Method + +The `can` method is used to define permissions and requires two arguments. The first one is the action you're setting the permission for, the second one is the class of object you're setting it on. + +```ruby +can :update, Article +``` + +You can pass `:manage` to represent any action and `:all` to represent any object. + +```ruby +can :manage, Article # user can perform any action on the article +can :read, :all # user can read any object +can :manage, :all # user can perform any action on any object +``` + +Common actions are `:read`, `:create`, `:update` and `:destroy` but it can be anything. See [[Action Aliases]] and [[Custom Actions]] for more information on actions. + +You can pass an array for either of these parameters to match any one. For example, here the user will have the ability to update or destroy both articles and comments. + +```ruby +can [:update, :destroy], [Article, Comment] +``` + + +**Important notice about :manage**. As you read above it represents ANY action on the object. So if you have something like: + +```ruby +can :manage, User +can :invite, User +``` + +you can get rid of the second line and the `:invite` permissions, because because `:manage` represents **any** action on object and `:manage` is not just `:create`, `:read`, `:update`, `:destroy` on object. + +If you want only CRUD actions on object, you should create custom action that called `:crud` for example, and use it instead of `:manage`: + +```ruby +def initialize(user) + alias_action :create, :read, :update, :destroy, to: :crud + if user.present? + can :crud, User + can :invite, User + end +end +``` + +## Hash of Conditions + +A hash of conditions can be passed to further restrict which records this permission applies to. Here the user will only have permission to read active projects which they own. + +```ruby +can :read, Project, active: true, user_id: user.id +``` + +It is important to only use database columns for these conditions so it can be reused for [[Fetching Records]]. + +You can use nested hashes to define conditions on associations. Here the project can only be read if the category it belongs to is visible. + +```ruby +can :read, Project, category: { visible: true } +``` + +The above will issue a query that performs an `LEFT JOIN` to query conditions on associated records. +The example below will use a scope that returns all Photos that do not belong to a group. + +```ruby +class Photo + has_and_belongs_to_many :groups + scope :unowned, -> { left_joins(:groups).where(groups: { id: nil }) } +end + +class Group + has_and_belongs_to_many :photos +end + +class Ability + def initialize(user) + can :read, Photo, Photo.unowned do |photo| + photo.groups.empty? + end + end +end +``` + +An array or range can be passed to match multiple values. Here the user can only read projects of priority 1 through 3. + +```ruby +can :read, Project, priority: 1..3 +``` + +Almost anything that you can pass to a hash of conditions in Active Record will work here. The only exception is working with model ids. You can't pass in the model objects directly, you must pass in the ids. + +```ruby +can :manage, Project, group: { id: user.group_ids } +``` + +If you have a complex case which cannot be done through a hash of conditions, see [[Defining Abilities with Blocks]]. + +## Traverse associations + +All associations can be traversed when defining a rule. + +```ruby +class User + belongs_to :account +end + +class Account + has_one :user + has_many :services +end + +class Service + belongs_to :account + has_many :parts +end + +class Part + belongs_to :service +end + +# Ability +can :manage, Part, service: { account: { user: { id: user.id } } } +``` + +## Combining Abilities + +It is possible to define multiple abilities for the same resource. Here the user will be able to read projects which are released OR available for preview. + +```ruby +can :read, Project, released: true +can :read, Project, preview: true +``` + +The `cannot` method takes the same arguments as `can` and defines which actions the user is unable to perform. This is normally done after a more generic `can` call. + +```ruby +can :manage, Project +cannot :destroy, Project +``` + +The order of these calls is important. See [[Ability Precedence]] for more details. + +## Additional Docs + +* [[Defining Abilities: Best Practices]] +* [[Defining Abilities with Blocks]] +* [[Checking Abilities]] +* [[Testing Abilities]] +* [[Debugging Abilities]] +* [[Ability Precedence]] \ No newline at end of file diff --git a/docs/Defining-Abilities:-Best-Practices.md b/docs/Defining-Abilities:-Best-Practices.md new file mode 100644 index 00000000..fc16e8a5 --- /dev/null +++ b/docs/Defining-Abilities:-Best-Practices.md @@ -0,0 +1,99 @@ +## Use hash conditions as much as possible + +Here's why: + +**1. Although scopes are fine for fetching, they pose a problem when authorizing a discrete action.** + + For example, this declaration in Ability: + + ```ruby + can :read, Article, Article.is_published + ``` + + causes this `CanCan::Error`: + + ``` + The can? and cannot? call cannot be used with a raw sql 'can' definition. + The checking code cannot be determined for :read #

. + ``` + + A better way to define the same is: + + ```ruby + can :read, Article, is_published: true + ``` + +**2. Hash conditions are DRYer.** + + By using hashes instead of blocks for all actions, you won't have to worry about translating blocks used for member controller actions (`:create`, `:destroy`, `:update`) to equivalent blocks for collection actions (`:index`, `:show`)—which require hashes anyway! + +**3. Hash conditions are OR'd in SQL, giving you maximum flexibilty.** + + Every time you define an ability with `can`, each `can` chains together with OR in the final SQL query for that model. + + So if, in addition to the `is_published` condition above, we want to allow authors to see their drafts: + + ```ruby + can :read, Article, author_id: @user.id, is_published: false + ``` + + Then the final SQL would be: + + ```sql + SELECT `articles`.* + FROM `articles` + WHERE `articles`.`is_published` = 1 + OR ( `articles`.`author_id` = 97 AND `articles`.`is_published` = 0 ) + ``` + +**4. For complex object graphs, hash conditions accommodate `joins` easily.** + + See https://github.com/CanCanCommunity/cancancan/wiki/defining-abilities#hash-of-conditions. + +## Give permissions, don't take them away + +As I suggested in this [topic on Reddit](https://www.reddit.com/r/ruby/comments/6ytka8/refactoring_cancancan_abilities_brewing_bits/) you should, when possible, give increasing permissions to your users. +CanCanCan increases permissions, it starts by giving no permissions to nobody and then increases those permissions depending on the user. A properly written ability.rb looks like that: + +```ruby +class Ability + include CanCan::Ability + + def initialize(user) + can :read, Post # start by defining rules for all users, also not logged ones + return unless user.present? + can :manage, Post, user_id: user.id # if the user is logged in can manage it's own posts + can :create, Comment # logged in users can also create comments + return unless user.manager? # if the user is a manager we give additional permissions + can :manage, Comment # like managing all comments in the website + return unless user.admin? + can :manage, :all # finally we give all remaining permissions only to the admins + end +end +``` + +following this good practice will help you to keep your permissions clean and more readable. + +The risk of giving wrong permissions to the wrong users is also decreased. + +## Split your ability.rb file + +Another help, to make CanCanCan work more in a "pundit way" is to define a separate Ability file for each model, or controller, and then use + +```ruby +def current_ability + @current_ability ||= MyAbility.new(current_user) +end +``` + +To use a specific ability file: this way you don't have to load the whole ability.rb file on each request. + +Abilities files can always be merged together, so if you need two of them in one Controller, you can simply: + +```ruby +def current_ability + @current_ability ||= ReadAbility.new(current_user).merge(WriteAbility.new(current_user)) +end +``` + +You can read more about splitting the Ability file in [this article](https://medium.com/@coorasse/cancancan-that-scales-d4e526fced3d) \ No newline at end of file diff --git a/docs/Devise.md b/docs/Devise.md new file mode 100644 index 00000000..4ea1528f --- /dev/null +++ b/docs/Devise.md @@ -0,0 +1,22 @@ +You can bypass CanCanCan's authorization for Devise controllers: + +```ruby +class ApplicationController < ActionController::Base + protect_from_forgery + check_authorization unless: :devise_controller? +end +``` + +It may be a good idea to specify the rescue from action: + +```ruby +rescue_from CanCan::Unauthorized do |exception| + if current_user.nil? + session[:next] = request.fullpath + redirect_to login_url, alert: 'You have to log in to continue.' + else + # render file: "#{Rails.root}/public/403.html", status: 403 + redirect_back(fallback_location: root_path) + end +end +``` \ No newline at end of file diff --git a/docs/Ensure-Authorization.md b/docs/Ensure-Authorization.md new file mode 100644 index 00000000..0e55cd35 --- /dev/null +++ b/docs/Ensure-Authorization.md @@ -0,0 +1,40 @@ +If you want to be certain authorization is not forgotten in some controller action, add `check_authorization` to your `ApplicationController`. + +```ruby +class ApplicationController < ActionController::Base + check_authorization +end +``` + +This will add an `after_filter` to ensure authorization takes place in every inherited controller action. If no authorization happens it will raise a `CanCan::AuthorizationNotPerformed` exception. You can skip this check by adding `skip_authorization_check` to that controller. Both of these methods take the same arguments as `before_filter` so you can exclude certain actions with `:only` and `:except`. + +```ruby +class UsersController < ApplicationController + skip_authorization_check :only => [:new, :create] + # ... +end +``` + +## Conditionally Check Authorization + +As of CanCan 1.6, the `check_authorization` method supports `:if` and `:unless` options. Either one takes a method name as a symbol. This method will be called to determine if the authorization check will be performed. This makes it very easy to skip this check on all Devise controllers since they provide a `devise_controller?` method. + +```ruby +class ApplicationController < ActionController::Base + check_authorization :unless => :devise_controller? +end +``` + +Here's another example where authorization is only ensured for the admin subdomain. + +```ruby +class ApplicationController < ActionController::Base + check_authorization :if => :admin_subdomain? + private + def admin_subdomain? + request.subdomain == "admin" + end +end +``` + +Note: The `check_authorization` only ensures that authorization is performed. If you have `authorize_resource` the authorization will still be performed no matter what is returned here. diff --git a/docs/Exception-Handling.md b/docs/Exception-Handling.md new file mode 100644 index 00000000..62407ff4 --- /dev/null +++ b/docs/Exception-Handling.md @@ -0,0 +1,115 @@ +The `CanCan::AccessDenied` exception is raised when calling `authorize!` in the controller and the user is not able to perform the given action. A message can optionally be provided. + +```ruby +authorize! :read, Article, :message => "Unable to read this article." +``` + +This exception can also be raised manually if you want more custom behavior. + +```ruby +raise CanCan::AccessDenied.new("Not authorized!", :read, Article) +``` + +The message can also be customized through internationalization. + +```yaml +# in config/locales/en.yml +en: + unauthorized: + manage: + all: "Not authorized to %{action} %{subject}." + user: "Not allowed to manage other user accounts." + update: + project: "Not allowed to update this project." +``` + +Notice `manage` and `all` can be used to generalize the subject and actions. Also `%{action}` and `%{subject}` can be used as variables in the message. + +You can catch the exception and modify its behavior in the `ApplicationController`. The behavior may vary depending on the request format. For example here we set the error message to a flash and redirect to the home page for HTML requests and return `403 Forbidden` for JSON requests. + +```ruby +class ApplicationController < ActionController::Base + rescue_from CanCan::AccessDenied do |exception| + respond_to do |format| + format.json { head :forbidden } + format.html { redirect_to main_app.root_url, :alert => exception.message } + end + end +end +``` + +The action and subject can be retrieved through the exception to customize the behavior further. + +```ruby +exception.action # => :read +exception.subject # => Article +``` + +The default error message can also be customized through the exception. This will be used if no message was provided. + +```ruby +exception.default_message = "Default error message" +exception.message # => "Default error message" +``` + +If you prefer to return the 403 Forbidden HTTP code, create a `public/403.html` file and write a rescue_from statement like this example in `ApplicationController`: + +```ruby +class ApplicationController < ActionController::Base + rescue_from CanCan::AccessDenied do |exception| + render :file => "#{Rails.root}/public/403.html", :status => 403, :layout => false + ## to avoid deprecation warnings with Rails 3.2.x (and incidentally using Ruby 1.9.3 hash syntax) + ## this render call should be: + # render file: "#{Rails.root}/public/403", formats: [:html], status: 403, layout: false + end +end +``` + +`403.html` must be pure HTML, CSS, and JavaScript--not a template. The fields of the exception are not available to it. + +If you are getting unexpected behavior when rescuing from the exception it is best to add some logging . See [[Debugging Abilities]] for details. + +## Rescuing exceptions for XML responses + +If your web application provides a web service which returns XML or JSON responses then you will likely want to handle Authorization properly with a 403 response. You can do so by rendering a response when rescuing from the exception. + +```ruby +rescue_from CanCan::AccessDenied do |exception| + respond_to do |format| + format.json { render nothing: true, status: :forbidden } + format.xml { render xml: '...', status: :forbidden } + format.html { redirect_to main_app.root_url, alert: exception.message } + end +end +``` + +## Danger of exposing sensible information + +Please read [this thread](https://github.com/CanCanCommunity/cancancan/issues/437) for more information. + +In a Rails application, if a record is not found during `load_and_authorize_resource` it raises `ActiveRecord::NotFound` before it checks _authentication_ in the `authorize` step. + +This means that secured routes can have their resources discovered without even being signed in: + +``` +$ curl -I https://app.example.com/restricted_resource/does-not-exist +HTTP/1.1 404 Not Found + +$ curl -I https://app.example.com/restricted_resource/does-exist-but-not-permitted +HTTP/1.1 302 Found +Location: https://app.example.com/sessions/new +``` + +A more secure approach is to **always** return a 404 status instead of 302: + +```ruby +class ApplicationController < ActionController::Base + rescue_from CanCan::AccessDenied do |exception| + respond_to do |format| + format.json { render nothing: true, status: :not_found } + format.html { redirect_to main_app.root_url, notice: exception.message, status: :not_found } + format.js { render nothing: true, status: :not_found } + end + end +end +``` \ No newline at end of file diff --git a/docs/Fetching-Records.md b/docs/Fetching-Records.md new file mode 100644 index 00000000..8f64115c --- /dev/null +++ b/docs/Fetching-Records.md @@ -0,0 +1,47 @@ +Sometimes you need to restrict which records are returned from the database based on what the user is able to access. This can be done with the `accessible_by` method on any Active Record model. Simply pass the current ability to find only the records which the user is able to `:index`. + +```ruby +# current_ability is a method made available by CanCanCan in your controllers +@articles = Article.accessible_by(current_ability) +``` + +You can change the action by passing it as the second argument. Here we find only the records the user has permission to update. + +```ruby +@articles = Article.accessible_by(current_ability, :update) +``` + +If you want to use the current controller's action, make sure to call `to_sym` on it: + +```ruby +@articles = Article.accessible_by(current_ability, params[:action].to_sym) +``` + +This is an Active Record scope so other scopes and pagination can be chained onto it. + +This works with multiple `can` definitions, which allows you to define complex permission logic and have it translated properly to SQL. + +Given the definition: +```ruby +class Ability + can :manage, User, manager_id: user.id + cannot :manage, User, self_managed: true + can :manage, User, id: user.id +end +``` +a call to User.accessible_by(current_ability) generates the following SQL + +```sql +SELECT * +FROM users +WHERE (id = 1) OR (not (self_managed = 't') AND (manager_id = 1)) +``` + +It will raise an exception if any requested model's ability definition is defined using just block. +You can define SQL fragment in addition to block (look for more examples in [[Defining Abilities with Blocks]]). + +If you are using something other than Active Record you can fetch the conditions hash directly from the current ability. + +```ruby +current_ability.model_adapter(TargetClass, :read).conditions +``` \ No newline at end of file diff --git a/docs/FriendlyId-support.md b/docs/FriendlyId-support.md new file mode 100644 index 00000000..8ea6cdbe --- /dev/null +++ b/docs/FriendlyId-support.md @@ -0,0 +1,30 @@ +If you are using FriendlyId you will probably like something to make cancan compatible with it. + +You do not have to write `find_by :slug` or something like that, that is always error prone. + +You just need to create a `config/initizializers/cancan.rb` file with: +```ruby +if defined?(CanCan) + class Object + def metaclass + class << self; self; end + end + end + + module CanCan + module ModelAdapters + class ActiveRecord4Adapter < AbstractAdapter + @@friendly_support = {} + + def self.find(model_class, id) + klass = + model_class.metaclass.ancestors.include?(ActiveRecord::Associations::CollectionProxy) ? + model_class.klass : model_class + @@friendly_support[klass]||=klass.metaclass.ancestors.include?(FriendlyId) + @@friendly_support[klass] == true ? model_class.friendly.find(id) : model_class.find(id) + end + end + end + end +end +``` \ No newline at end of file diff --git a/docs/Home.md b/docs/Home.md new file mode 100644 index 00000000..a92d03c0 --- /dev/null +++ b/docs/Home.md @@ -0,0 +1,43 @@ +### Getting Started + +* [[README|https://github.com/CanCanCommunity/cancancan#readme]] +* [[Defining Abilities]], [Best Practices](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practices) +* [[Checking Abilities]] +* [[Authorizing Controller Actions]] +* [[Exception Handling]] +* [[Ensure Authorization]] +* [[Changing Defaults]] +* [[Translations (i18n)]] + +### More about Abilities + +* [[Testing Abilities]] +* [[Debugging Abilities]] +* [[Ability Precedence]] +* [[Fetching Records]] +* [[Action Aliases]] +* [[Custom Actions]] +* [[Role Based Authorization]] + + +### More about Controllers & Views + +* [[Controller Authorization Example]] +* [[Nested Resources]] +* [[Strong Parameters]] +* [[Non RESTful Controllers]] +* [[Link Helpers]] + + +### Other Use Cases + +* [[Inherited Resources]] +* [[Mongoid]] +* [[Rails Admin|https://github.com/sferik/rails_admin/wiki/CanCanCan]] +* [[Devise]] +* [[Accessing Request Data]] +* [[Abilities in Database]] +* [[Ability for Other Users]] +* [[Other Authorization Solutions]] + +**Can't find what you're looking for? [Submit a Question on StackOverflow](http://stackoverflow.com/questions/ask?tags=cancancan) \ No newline at end of file diff --git a/docs/Inherited-Resources.md b/docs/Inherited-Resources.md new file mode 100644 index 00000000..6ac2bd6d --- /dev/null +++ b/docs/Inherited-Resources.md @@ -0,0 +1,42 @@ +**This guide is for cancancan < 2.0 only. +If you want to use Inherited Resources and cancancan 2.0 please check for extensions like https://github.com/TylerRick/cancan-inherited_resources** + +The `load_and_authorize_resource` call will automatically detect if you are using [[Inherited Resources|http://github.com/josevalim/inherited_resources]] and load the resource through that. The `load` part in CanCan is still necessary since Inherited Resources does lazy loading. This will also ensure the behavior is identical to normal loading. + +```ruby +class ProjectsController < InheritedResources::Base + load_and_authorize_resource +end +``` + +if you are doing nesting you will need to mention it in both Inherited Resources and CanCan. + +```ruby +class TasksController < InheritedResources::Base + belongs_to :project + load_and_authorize_resource :project + load_and_authorize_resource :task, :through => :project +end +``` +Please note that even for a `has_many :tasks` association, the `load_and_authorize_resource` needs the singular name of the associated model... + +**Warning**: when overwriting the `collection` method in a controller the `load` part of a `load_and_authorize_resource` call will not work correctly. See https://github.com/ryanb/cancan/issues/274 for the discussions. + +In this case you can override collection like +```ruby +skip_load_and_authorize_resource :only => :index + +def collection + @products ||= end_of_association_chain.accessible_by(current_ability).paginate(:page => params[:page], :per_page => 10) +end +``` + +## Mongoid +With mongoid it is necessary to reference `:project_id` instead of just `:project` + +```ruby +class TasksController < InheritedResources::Base + ... + load_and_authorize_resource :task, :through => :project_id +end +``` \ No newline at end of file diff --git a/docs/Issue-Collaborators.md b/docs/Issue-Collaborators.md new file mode 100644 index 00000000..e989aa01 --- /dev/null +++ b/docs/Issue-Collaborators.md @@ -0,0 +1,27 @@ +The CanCan issue tracker has gotten out of hand because I have not had time to work on it recently. I am bringing on several Issue Collaborators to help. My goal is to make CanCan the best it can be and getting the issue tracker under control will help give me a clear direction on where to take it in 2.0. + +**Note: even though issue collaborators have full commit access, please do not make any commits or merge in any pull requests.** I am just looking for help cleaning up the issue tracker at the moment. I will likely take on full collaborators in the future. + +### Guidelines + +* **Questions:** If someone has a question that can be solved with the [wiki docs](https://github.com/ryanb/cancan/wiki) please point them to the appropriate docs and close the issue. If the question is not clearly answered by the wiki please improve the wiki so that it is and close the issue. If you do not have time to add docs at the moment, tag it with `docs` and `help` labels and keep it open. + +* **Feature Requests:** If it is a feature request that could go in CanCan 2.0, please tag it with `2.0` and `feature` tags. If you are uncertain whether it's a good idea, add a `discuss` tag to get some feedback. I don't plan to add features to CanCan 1 at this point, if it only applies to that release please close it and add a comment saying so. + +* **Dormant Issues:** If you are uncertain if an issue is still applicable and do not want to spend time investigating it, just ask "Are you still having this problem?" and tag with `waiting`. If you do not get a response within a week or so, close the issue. Mention you can open the issue again if they respond. + +* **Duplicate Issues:** If it seems like a common issue, do a search and look for a duplicate. If it is, close the issue and link to the other original one. + +* **Bug Reports:** If CanCan is not behaving in a way that it is documented to, add the `bug` label. Please verify this bug by trying it on your own and add a `verified` label to it if you can duplicate the problem. + + If you would like to submit a pull request to fix this bug, assign the issue to yourself so others know you are working on it. If not, add a comment saying you are looking for someone to write a pull request and add a `help` label to it. When a pull request is available, close the original issue and link to it from the pull request. + +* **Pull Requests:** Please try pull requests on your local machine to see if the tests pass and the functionality works as described. If so, add a `verified` label. Also add a `bug` or `feature` label depending on the type of request. If it is urgent, add a `critical` tag and ping me at @rbates on Twitter and I'll try to get it pulled in quickly. + + I will be reluctant to merge pull requests that are large or have features I feel unnecessary. Please add a comment to pull requests explaining your thinking on if it should be merged in and if you can think of a better way to do it. + +It is a good idea to occasionally check the `help` and `discuss` tags to give your input on other issues. + +**Final note:** if you are ever uncertain about whether to close an issue or leave it open. Close it and add a note saying you will open it again if someone comments. If it is beyond your expertise, just tag it with `help` and move on. + +Thank you very much for your help in cleaning up the issue tracker. If you have any questions, send me an email and I'll update this. diff --git a/docs/Link-Helpers.md b/docs/Link-Helpers.md new file mode 100644 index 00000000..89225fd5 --- /dev/null +++ b/docs/Link-Helpers.md @@ -0,0 +1,39 @@ +Generally you only want to show new/edit/destroy links when the user has permission to perform that action. You can do so like this in the view. + +```rhtml +<% if can? :update, @project %> + <%= link_to "Edit", edit_project_path(@project) %> +<% end %> +``` + +However if you find yourself repeating this pattern often you may want to add helper methods like this. + +```ruby +# in ApplicationHelper +def show_link(object, content = "Show") + link_to(content, object) if can?(:read, object) +end + +def edit_link(object, content = "Edit") + link_to(content, [:edit, object]) if can?(:update, object) +end + +def destroy_link(object, content = "Destroy") + link_to(content, object, :method => :delete, :confirm => "Are you sure?") if can?(:destroy, object) +end + +def create_link(object, content = "New") + if can?(:create, object) + object_class = (object.kind_of?(Class) ? object : object.class) + link_to(content, [:new, object_class.name.underscore.to_sym]) + end +end +``` + +Then a link is as simple as this. + +```rhtml +<%= edit_link @project %> +``` + +I only recommend doing this if you see this pattern a lot in your application. There are times when the view code is more complex where this doesn't fit well. \ No newline at end of file diff --git a/docs/MetaWhere.md b/docs/MetaWhere.md new file mode 100644 index 00000000..d94bc646 --- /dev/null +++ b/docs/MetaWhere.md @@ -0,0 +1 @@ +MetaWhere is not supported anymore diff --git a/docs/Migrating-from-CanCanCan-2.x-to-3.0.md b/docs/Migrating-from-CanCanCan-2.x-to-3.0.md new file mode 100644 index 00000000..f7886ea7 --- /dev/null +++ b/docs/Migrating-from-CanCanCan-2.x-to-3.0.md @@ -0,0 +1,15 @@ +### Breaking changes + +* **Defining abilities without a subject is not allowed anymore.** +For example, `can :dashboard` is not going to be accepted anymore and will raise an exception. +All these kind of rules need to be rethought in terms of `can action, subject`. `can :read, :dashboard` for example. + +* **Eager loading is not automatic.** If you relied on CanCanCan to avoid N+1 queries, this will not be the case anymore. +From now on, all necessary `includes`, `preload` or `eager_load` need to be explicitly written. We strongly suggest to have +`bullet` gem installed to identify your possible N+1 issues. + +* **Use of distinct.** Uniqueness of the results is guaranteed by using the `distinct` clause in the final query. +This may cause issues with some existing queries when using clauses like `group by` or `order` on associations. +Adding a custom `select` may be necessary in these cases. + +* **aliases are now merged.** When using the method to merge different Ability files, the aliases are now also merged. This might cause some incompatibility issues. diff --git a/docs/Model-Adapter.md b/docs/Model-Adapter.md new file mode 100644 index 00000000..4bd26f42 --- /dev/null +++ b/docs/Model-Adapter.md @@ -0,0 +1,130 @@ +CanCan includes a model adapter layer which allows it to change behavior depending on the model used. The current adapters are. + +* ActiveRecord +* [[Mongoid]] + +See [[spec/README|https://github.com/CanCanCommunity/cancancan/blob/master/spec/README.rdoc]] for how to run specs for a given adapter. + +## Creating a Model Adapter + +It is easy to make your own adapter if one is not provided. Here I'll walk you through the steps to recreate the Mongoid adapter. + +### The Specs + +First, fork the CanCan GitHub project and clone that repo. Next, add the necessary gems to the Gemfile for working with the adapter in the specs. + +```ruby +case ENV["MODEL_ADAPTER"] +# ... +when "mongoid" + gem "bson_ext", "~> 1.1" + gem "mongoid", "~> 2.0.0.beta.20" +# ... +end +``` + +Next create a spec for the adapter which tests basic behavior. For example, here's a simple Mongoid spec that would go under `spec/cancan/model_adapters/mongoid_adapter_spec.rb` + +```ruby +if ENV["MODEL_ADAPTER"] == "mongoid" + require "spec_helper" + + class MongoidProject + include Mongoid::Document + end + + Mongoid.configure do |config| + config.master = Mongo::Connection.new('127.0.0.1', 27017).db("cancan_mongoid_spec") + end + + describe CanCan::ModelAdapters::MongoidAdapter do + context "Mongoid defined" do + before(:each) do + @ability = Object.new + @ability.extend(CanCan::Ability) + end + + it "should return the correct records based on the defined ability" do + @ability.can :read, MongoidProject, :title => "Sir" + sir = MongoidProject.create(:title => 'Sir') + lord = MongoidProject.create(:title => 'Lord') + MongoidProject.accessible_by(@ability, :read).entries.should == [sir] + end + end + end +end +``` + +You will need many more specs for full coverage but add them one at a time. To run the specs execute the following commands. + +```bash +MODEL_ADAPTER=mongoid bundle +MODEL_ADAPTER=mongoid rake +``` + +That will fail since we have not added the implementation. + +### The Implementation + +First add a line to `lib/cancan.rb` for including the adapter only when Mongoid is present. + +```ruby +require 'cancan/model_adapters/mongoid_adapter' if defined? Mongoid +``` + +Next create that adapter under `lib/cancan/model_adapters/mongoid_adapter.rb`. + +```ruby +module CanCan + module ModelAdapters + class MongoidAdapter < AbstractAdapter + def self.for_class?(model_class) + model_class <= Mongoid::Document + end + + def database_records + if @rules.size == 0 + @model_class.where(:_id => {'$exists' => false, '$type' => 7}) # return no records in Mongoid + else + @rules.inject(@model_class.all) do |records, rule| + if rule.base_behavior + records.or(rule.conditions) + else + records.excludes(rule.conditions) + end + end + end + end + end + end +end + +module Mongoid::Document::ClassMethods + include CanCan::ModelAdditions::ClassMethods +end +``` + +The class method called `for_class?` is used to determine if this adapter should be used for a given class. Here we just see if that model is a Mongoid document. + +The `database_records` method is used in the `accessible_by` call. Here we fetch records from `@model_class` which match the `@rules`. If there are no rules then we return a query which fetches no records. + +Otherwise we start with all the records and apply each of the rule conditions to them. The `rule.base_behavior` defines whether this rule should be additive or subtractive. It is `true` for a `can` call and `false` for a `cannot` call. + +The last three lines add the `accessible_by` method to all Mongoid classes. I expect this to not be necessary in CanCan 2.0 (see [[issue #235|https://github.com/ryanb/cancan/issues#issue/235]]). + +Some models add additional features to the conditions hash. With Mongoid you can do something like `:age.gt => 13`. To get this working a couple more methods need to be added to the adapter to override how conditions are checked. + +```ruby +# in MongoidAdapter +def self.override_conditions_hash_matching?(subject, conditions) + conditions.any? { |k,v| !k.kind_of?(Symbol) } +end + +def self.matches_conditions_hash?(subject, conditions) + subject.matches? subject.class.where(conditions).selector +end +``` + +The first one returns `true` when there's a conditions option which is not a Symbol (such as `:age.gt`). The second method will be called by CanCan when the first one returns true to check if the given subject matches the hash of conditions. + +See the actual [[mongoid_adapter_spec.rb|https://github.com/ryanb/cancan/blob/master/spec/cancan/model_adapters/mongoid_adapter_spec.rb]] and [[mongoid_adapter.rb|https://github.com/ryanb/cancan/blob/master/lib/cancan/model_adapters/mongoid_adapter.rb]] files for the full code. \ No newline at end of file diff --git a/docs/Mongoid.md b/docs/Mongoid.md new file mode 100644 index 00000000..971b6139 --- /dev/null +++ b/docs/Mongoid.md @@ -0,0 +1,17 @@ +** **Attention: Supported only on cancancan < 2.0!** ** + +CanCanCan supports [[Mongoid|http://mongoid.org]]. All you have to do is mention `mongoid` before `cancan` in your Gemfile so it is required first. + +```ruby +gem "mongoid" +gem "cancan" +``` + +That is it, you can now call `accessible_by` on any Mongoid document (which is done automatically in the `index` action). You can also use the query syntax that Mongoid provides when defining the abilities. + +```ruby +# in Ability +can :read, Article, :priority.lt => 5 +``` + +This is all done through a [[Model Adapter]]. See that page for more information and how you can add your own. \ No newline at end of file diff --git a/docs/Multiple-can-definitions.textile b/docs/Multiple-can-definitions.textile new file mode 100644 index 00000000..ada51f95 --- /dev/null +++ b/docs/Multiple-can-definitions.textile @@ -0,0 +1,35 @@ +h2. Multiple `can` definitions + +It is possible to specify multiple `can` and `cannot` definitions with hashes and have it properly translate to a single SQL query. + +```ruby +# in ability.rb +can :manage, User, id: 1 +can :manage, User, manager_id: 1 +cannot :manage, User, self_managed: true +``` + +When using `accessible_by` it will translate to SQL conditions that look like this. + +```sql +not (self_managed = 't') AND ((manager_id = 1) OR (id = 1)) +``` + +If you have the following definition: + +```ruby +can :manage, User, id: user.id +can :assign_roles, User do + user.admin? +end +``` + +and you call `can? :assign_roles, some_user` it evaluates to `true` when `current_user == some_user` because it falls back to `can :manage, User, id: user.id`. + +Proper can definition should be now: + +```ruby +can :manage, User, id: user.id +cannot :assign_roles, User +can :assign_roles, User if user.admin? +``` \ No newline at end of file diff --git a/docs/Nested-Resources.md b/docs/Nested-Resources.md new file mode 100644 index 00000000..a2adc5e7 --- /dev/null +++ b/docs/Nested-Resources.md @@ -0,0 +1,160 @@ +Let's say we have nested resources set up in our routes. + +```ruby +resources :projects do + resources :tasks +end +``` + +We can then tell CanCanCan to load the project and then load the task through that. + +```ruby +class TasksController < ApplicationController + load_and_authorize_resource :project + load_and_authorize_resource :task, through: :project +end +``` + +This will fetch the project using `Project.find(params[:project_id])` on every controller action, save it in the `@project` instance variable, and authorize it using the `:read` action to ensure the user has the ability to access that project. If you don't want to do the authorization you can simply use `load_resource`, but calling just `authorize_resource` for the parent object is insufficient. The task is then loaded through the `@project.tasks` association. + +If the name of the association doesn't match the resource name, for instance `has_many :issues, class_name: 'Task'`, you can specify the association name using `:through_association`. + +```ruby + class TasksController < ApplicationController + load_and_authorize_resource :project + load_and_authorize_resource :task, through: :project, through_association: :issues + end +``` + +If the resource name (`:project` in this case) does not match the controller then it will be considered a parent resource. You can manually specify parent/child resources using the `parent: false` option. + + +## Nested through method + +It's also possible to nest through a method, this is commonly the `current_user` method. + +```ruby +class ProjectsController < ApplicationController + load_and_authorize_resource through: :current_user +end +``` + +Here everything will be loaded through the `current_user.projects` association. + +## Shallow nesting + +The parent resource is required to be present and it will raise an exception if the parent is ever `nil`. +If you want it to be optional (such as with shallow routes), add the `shallow: true` option to the child. + +```ruby +class TasksController < ApplicationController + load_and_authorize_resource :project + load_and_authorize_resource :task, through: :project, shallow: true +end +``` + +## Singleton resource + +What if each project only had one task through a `has_one` association? To set up singleton resources you can use the `:singleton` option. + +```ruby +class TasksController < ApplicationController + load_and_authorize_resource :project + load_and_authorize_resource :task, through: :project, singleton: true +end +``` + +It will then use the `@project.task` and `@project.build_task` methods for fetching and building respectively. + +## Polymorphic associations + +Let's say tasks can either be assigned to a Project or an Event through a polymorphic association. An array can be passed into the `:through` option and it will use the first one it finds. + +```ruby +load_resource :project +load_resource :event +load_and_authorize_resource :task, through: [:project, :event] +``` + +Here it will check both the `@project` and `@event` variables and fetch the task through whichever one exists. Note that this is only loading the parent model, if you want to authorize the parent you will need to do it through a before_filter because there is special logic involved. + +```ruby +before_filter :authorize_parent + +private + +def authorize_parent + authorize! :read, (@event || @project) +end +``` + +## Accessing parent in ability + +Sometimes the child permissions are closely tied to the parent resource. For example, if there is a `user_id` column on Project, one may want to only allow access to tasks if the user owns their project. + +This will happen automatically due to the `@project` instance being authorized in the nesting. However it's still a good idea to restrict the tasks separately. You can do so by going through the project association. + +```ruby +# in Ability +can :manage, Task, project: { user_id: user.id } +``` + +This means you will need to have a project tied to the tasks which you pass into here. For example, if you are checking if the user has permission to create a new task, do that by building it through the project. + +```ruby +can? :create, @project.tasks.build +``` + +It's also possible to check permission through an association like this. + +```ruby +can? :read, @project => Task +``` + +This will use the above `:project` hash conditions and ensure `@project` meets those conditions. + +## Has_many through associations +How to load and authorize resources with a has_many :through association? + +Given that situation: + +```ruby +class User < ActiveRecord::Base + has_many :groups_users + has_many :groups, through: :groups_users +end +``` + +```ruby +class Group < ActiveRecord::Base + has_many :groups_users + has_many :users, through: :groups_users +end +``` + +```ruby +class GroupsUsers < ActiveRecord::Base + belongs_to :group, inverse_of: :groups_users + belongs_to :user, inverse_of: :groups_users +end +``` + +and in the controller: + +```ruby +class UsersController < ApplicationController + load_and_authorize_resource :group + load_and_authorize_resource through: :group +``` + +in ability.rb + +```ruby +can :create, User, groups_users: {group: {CONDITION_ON_GROUP} } +``` + +Don't forget the **inverse_of** option, is the trick to make it works correctly. + +Remember to define the ability through the **groups_users** model (i.e. don't write `can :create, User, groups: {CONDITION_ON_GROUP}`) + +You will be able to persist the association just calling `@user.save` instead of `@group.save` \ No newline at end of file diff --git a/docs/Non-RESTful-Controllers.md b/docs/Non-RESTful-Controllers.md new file mode 100644 index 00000000..56035128 --- /dev/null +++ b/docs/Non-RESTful-Controllers.md @@ -0,0 +1,35 @@ +You can use CanCan with controllers that do not follow the traditional show/new/edit/destroy actions, however you should not use the `load_and_authorize_resource` method since there is no resource to load. Instead you can call `authorize!` in each action separately. + +**NOTE:** This is **not** the same as having additional non-RESTful actions on a RESTful controller. See the Choosing Actions section of the [[Authorizing Controller Actions]] page for details. + +For example, let's say we have a controller which does some miscellaneous administration tasks such as rolling log files. We can use the `authorize!` method here. + +```ruby +class AdminController < ActionController::Base + def roll_logs + authorize! :roll, :logs + # roll the logs here + end +end +``` + +And then authorize that in the `Ability` class. + +```ruby +can :roll, :logs if user.admin? +``` + +Notice you can pass a symbol as the second argument to both `authorize!` and `can`. It doesn't have to be a model class or instance. Generally the first argument is the "action" one is trying to perform and the second argument is the "subject" the action is being performed on. It can be anything. + +## Alternative: authorize_resource + +Alternatively you can use the `authorize_resource` and specify that there's no class. This way it will pass the resource symbol instead. This is good if you still have a Resource-like controller but no model class backing it. + +```ruby +class ToolsController < ApplicationController + authorize_resource :class => false + def show + # automatically calls authorize!(:show, :tool) + end +end +``` \ No newline at end of file diff --git a/docs/Other-Authorization-Solutions.md b/docs/Other-Authorization-Solutions.md new file mode 100644 index 00000000..eed470c4 --- /dev/null +++ b/docs/Other-Authorization-Solutions.md @@ -0,0 +1,7 @@ +There are many authorization solutions available, and it is important to find one which best meets the application requirements. + +We try to keep CanCanCan minimal yet extendable so it can be used in many situations, but there are times it doesn't fit the best. + +If you find the conditions hash to be too limiting I encourage you to check out [[Pundit|https://github.com/elabs/pundit]] which offers a sophisticated DSL for handling more complex permission scenarios. This allows one to generate complex database queries based on the permissions but at the cost of a more complex DSL. + +Also consider, if you have very unique authorization requirements, the best choice may be to write your own solution instead of trying to shoe-horn an existing plugin. \ No newline at end of file diff --git a/docs/Role-Based-Authorization.md b/docs/Role-Based-Authorization.md new file mode 100644 index 00000000..86d6deda --- /dev/null +++ b/docs/Role-Based-Authorization.md @@ -0,0 +1,175 @@ +CanCanCan is decoupled from how you implement roles in the User model, but how might one set up basic role-based authorization? The pros and cons are described [here](https://github.com/kristianmandrup/cantango/wiki/CanCan-vs-CanTango). + +The following approach allows you to simply define the role abilities in Ruby and does not need a role model. Alternatively, [[Separate Role Model]] describes how to define the roles and mappings in a database. + +Since there is such a tight coupling between the list of roles and abilities, I recommend keeping the list of roles in Ruby. You can do so in a constant under the User class. + +```ruby +class User < ActiveRecord::Base + ROLES = %i[admin moderator author banned] +end +``` + +But now, how do you set up the association between the user and the roles? You'll need to decide if the user can have many roles or just one. + +## One role per user + +If a user can have only one role, it's as simple as adding a `role` string column to the `users` table. + +```bash +rails generate migration add_role_to_users role:string +rake db:migrate +``` + +In your `users_controller.rb` add `:role` to the list of permitted parameters + +```ruby +def user_params + params.require(:user).permit(:name, :email, :password, :password_confirmation, :role) +end +``` +If you're using ActiveAdmin don't forget to add `role` to the `user.rb` list of parameters as well + +```ruby + permit_params :name, :email, :role +``` + +Now you can provide a select-menu for choosing the roles in the view. + +```rhtml + +<%= f.collection_select(:role, User::ROLES, :to_s, lambda{|i| i.to_s.humanize}) %> +``` + +You may not have considered using `collection_select` when you aren't working with an association, but it will work perfectly. In this case the user will see the humanized name of the role, and the simple lower-cased version will be passed in as the value when the form is submitted. + +It's then very simple to determine the role of the user in the Ability class. + +```ruby +can :manage, :all if user.role == "admin" +``` + + +## Many roles per user + +It is possible to assign multiple roles to a user and store it into a single integer column using a [[bitmask|http://en.wikipedia.org/wiki/Mask_(computing)]]. First add a `roles_mask` integer column to your `users` table. + +```bash +rails generate migration add_roles_mask_to_users roles_mask:integer +rake db:migrate +``` + +Next you'll need to add the following code to the User model for getting and setting the list of roles a user belongs to. This will perform the necessary bitwise operations to translate an array of roles into the integer field. + +```ruby +# in models/user.rb +def roles=(roles) + roles = [*roles].map { |r| r.to_sym } + self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.inject(0, :+) +end + +def roles + ROLES.reject do |r| + ((roles_mask.to_i || 0) & 2**ROLES.index(r)).zero? + end +end +``` + +If you're using devise, don't forget to add `attr_accessible :roles` to your user model or add following to application_controller.rb + +```ruby + before_action :configure_permitted_parameters, if: :devise_controller? + protected + def configure_permitted_parameters + devise_parameter_sanitizer.for(:sign_up) { |u| u.permit( :email, :password, :password_confirmation, roles: [] ) } + end +``` +You can use checkboxes in the view for setting these roles. + +```rhtml +<% for role in User::ROLES %> + <%= check_box_tag "user[roles][#{role}]", role, @user.roles.include?(role), {:name => "user[roles][]"}%> + <%= label_tag "user_roles_#{role}", role.to_s.humanize %>
+<% end %> +<%= hidden_field_tag "user[roles][]", "" %> +``` + +Finally, you can then add a convenient way to check the user's roles in the Ability class. + +```ruby +# in models/user.rb +def has_role?(role) + roles.include?(role) +end + +# in models/ability.rb +can :manage, :all if user.has_role? :admin +``` + +See [[Custom Actions]] for a way to restrict which users can assign roles to other users. + +This functionality has also been extracted into a little gem called [[role_model|http://rubygems.org/gems/role_model]] ([[code & howto|http://github.com/martinrehfeld/role_model]]). + +If you do not like this bitmask solution, see [[Separate Role Model]] for an alternative way to handle this. + + +## Role Inheritance + +Sometimes you want one role to inherit the behavior of another role. For example, let's say there are three roles: moderator, admin, superadmin and you want each one to inherit the abilities of the one before. There is also a "role" string column in the User model. You should create a method in the User model which has the inheritance logic. + +```ruby +# in User +ROLES = %w[moderator admin superadmin] +def role?(base_role) + ROLES.index(base_role.to_s) <= ROLES.index(role) +end +``` + +You then use this in the Ability class. + +```ruby +# in Ability#initialize +if user.role? :moderator + can :manage, Post +end +if user.role? :admin + can :manage, ForumThread +end +if user.role? :superadmin + can :manage, Forum +end +``` + +Here a superadmin will be able to manage all three classes but a moderator can only manage the one. Of course you can change the role logic to fit your needs. You can add complex logic so certain roles only inherit from others. And if a given user can have multiple roles you can decide whether the lowest role takes priority or the highest one does. Or use other attributes on the user model such as a "banned", "activated", or "admin" column. + +This functionality has been extracted into a gem called [[canard|http://rubygems.org/gems/canard]] ([[code & howto|http://github.com/james2m/canard]]). + +## Alternative Role Inheritance + +If you would like to keep the inheritance rules in the Ability class instead of the User model it is easy to do so like this. + +```ruby +class Ability + include CanCan::Ability + + def initialize(user) + @user = user || User.new # for guest + @user.roles.each { |role| send(role.name_to_symbol) } + + if @user.roles.size == 0 + can :read, :all #for guest without roles + end + end + + def manager + can :manage, Employee + end + + def admin + manager + can :manage, Bill + end +end +``` + +Here each role is a separate method which is called. You can call one role inside another to define inheritance. This assumes you have a `User#roles` method which returns an array of all roles for that user. \ No newline at end of file diff --git a/docs/Rules-compression.md b/docs/Rules-compression.md new file mode 100644 index 00000000..37f307c7 --- /dev/null +++ b/docs/Rules-compression.md @@ -0,0 +1,44 @@ +# Rules compressions + +Your rules are optimized automatically at runtime. There are a set of "rules" to optimize your rules definition and they are implemented in the `RulesCompressor` class. Here you can see how this works: + +A rule without conditions is defined as `catch_all`. + + +### A catch_all rule, eliminates all previous rules and all subsequent rules of the same type + +```ruby +can :read, Book, author_id: user.id +cannot :read, Book, private: true +can :read, Book +can :read, Book, id: 1 +cannot :read, Book, private: true +``` +becomes +```ruby +can :read, Book +cannot :read, Book, private: true +``` + +### If a catch_all cannot rule is first, it can be removed + +```ruby +cannot :read, Book +can :read, Book, author_id: user.id +``` +becomes +```ruby +can :read, Book, author_id: user.id +``` + +### If all rules are cannot rules, this is equivalent to no rules + +```ruby +cannot :read, Book, private: true +``` +becomes +```ruby +# nothing +``` + +These optimizations allow you to follow the strategy of ["Give Permissions, don't take them"](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practices#give-permissions-dont-take-them-away) and automatically ignore previous rules when they are not needed. \ No newline at end of file diff --git a/docs/Separate-Role-Model.md b/docs/Separate-Role-Model.md new file mode 100644 index 00000000..a2f3f0cd --- /dev/null +++ b/docs/Separate-Role-Model.md @@ -0,0 +1,87 @@ +This approach uses a separate role and shows how to setup a many-to-many association, Assignment, between User and Role. Alternatively, [[Role Based Authorization]] describes a simple ruby based approach that defines the roles within ruby. + +```ruby +class User < ActiveRecord::Base + has_many :assignments + has_many :roles, :through => :assignments +end + +class Assignment < ActiveRecord::Base + belongs_to :user + belongs_to :role +end + +class Role < ActiveRecord::Base + has_many :assignments + has_many :users, :through => :assignments +end +``` + +You can assign roles using checkboxes when creating or updating a user model. + +```rhtml +<% for role in Role.all %> +
+ <%= check_box_tag "user[role_ids][]", role.id, @user.roles.include?(role) %> + <%=h role.name %> +
+<% end %> +<%= hidden_field_tag "user[role_ids][]", "" %> +``` + +Or you may want to [[use Formtastic|http://railscasts.com/episodes/185-formtastic-part-2]] for this. + +Next you need to determine if a user is in a specific role. You can create a method in the User model for this. + +```ruby +# in models/user.rb +def has_role?(role_sym) + roles.any? { |r| r.name.underscore.to_sym == role_sym } +end +``` + +And then you can use this in your Ability. + +```ruby +# in models/ability.rb +def initialize(user) + user ||= User.new # in case of guest + if user.has_role? :admin + can :manage, :all + else + can :read, :all + end +end +``` + +That's it! + +## Role Inheritance Within Ability.rb + +You can use the Alternative Role Inheritance strategy described in [[Role Based Authorization|https://github.com/ryanb/cancan/wiki/Role-Based-Authorization]] with one minor modification: change "send(role)" to "send(role.name.downcase)" assuming name is the column describing the role's name in the database. + +```ruby +class Ability + include CanCan::Ability + + def initialize(user) + @user = user || User.new # for guest + @user.roles.each { |role| send(role.name.downcase) } + + if @user.roles.size == 0 + can :read, :all #for guest without roles + end + end + + def manager + can :manage, Employee + end + + def admin + manager + can :manage, Bill + end +end +``` + +Here each role is a separate method which is called. You can call one role inside another to define inheritance. This assumes you have a `User#roles` method which returns an array of all roles for that user. \ No newline at end of file diff --git a/docs/Share-Ability-Definitions.md b/docs/Share-Ability-Definitions.md new file mode 100644 index 00000000..456389aa --- /dev/null +++ b/docs/Share-Ability-Definitions.md @@ -0,0 +1,9 @@ +Let's say the ability of one action depends on the ability of another. For example, what if we have a `Project` which `has_many :tasks` and we want a task's update ability to be dependent on whether the user can update the project. We can perform the `can?` call within the ability definition to check the project permission. + +```ruby +can :update, Task do |task| + can?(:update, task.project) +end +``` + +With this it is easy to define one ability based on another. \ No newline at end of file diff --git a/docs/Strong-Parameters.md b/docs/Strong-Parameters.md new file mode 100644 index 00000000..ccda8bc7 --- /dev/null +++ b/docs/Strong-Parameters.md @@ -0,0 +1,140 @@ +CanCanCan supports Strong Parameters without controller workarounds. +When using strong_parameters or Rails 4+, you have to sanitize inputs before saving the record, in actions such as `:create` and `:update`. + +By default, CanCanCan will try to sanitize the input on `:create` and `:update` routes by seeing if your controller will respond to the following methods (in order): + +### By Action + +If you specify a `create_params` or `update_params` method, CanCan will run that method depending on the action you are performing. + +```ruby +class ArticlesController < ApplicationController + load_and_authorize_resource + + def create + if @article.save + # hurray + else + render :new + end + end + + def update + if @article.update_attributes(update_params) + # hurray + else + render :edit + end + end + + private + + def create_params + params.require(:article).permit(:name, :email) + end + + def update_params + params.require(:article).permit(:name) + end +end +``` + +### By Model Name + +If you follow the convention in rails for naming your param method after the applicable model's class `_params` such as `article_params`, CanCanCan will automatically detect and run that params method. + +```ruby +class ArticlesController < ApplicationController + load_and_authorize_resource + + def create + if @article.save + # hurray + else + render :new + end + end + + private + + def article_params + params.require(:article).permit(:name) + end +end +``` + +#### When Model and Controller names differ + +When you specify `class` option note that the method will still be `articles_params` and not `post_params`, since we are in `ArticlesController`. + +```ruby +class ArticlesController < ApplicationController + load_and_authorize_resource class: 'Post' + + def create + if @article.save + # hurray + else + render :new + end + end + + private + + def article_params + params.require(:article).permit(:name) + end +end +``` + +### By Static Method Name + +CanCanCan also recognizes a static method name: `resource_params`, as a general param method name you can use to standardize on. + +```ruby +class ArticlesController < ApplicationController + load_and_authorize_resource + + def create + if @article.save + # hurray + else + render :new + end + end + + private + + def resource_params + params.require(:article).permit(:name) + end +end +``` + +### By Custom Method + +Additionally, load_and_authorize_resource can now take a `param_method` option to specify a custom method in the controller to run to sanitize input. + +```ruby +class ArticlesController < ApplicationController + load_and_authorize_resource param_method: :my_sanitizer + + def create + if @article.save + # hurray + else + render :new + end + end + + private + + def my_sanitizer + params.require(:article).permit(:name) + end +end +``` + +### No Strong Parameters + +No problem, if your controllers do not respond to any of the above methods, it will ignore and continue execution as normal. \ No newline at end of file diff --git a/docs/Testing-Abilities.md b/docs/Testing-Abilities.md new file mode 100644 index 00000000..76dc431e --- /dev/null +++ b/docs/Testing-Abilities.md @@ -0,0 +1,99 @@ +It can be difficult to thoroughly test user permissions at the functional/integration level because there are often many branching possibilities. Since CanCanCan handles all permission logic in `Ability` classes this makes it easy to have a solid set of unit test for complete coverage. + +The `can?` method can be called directly on any `Ability` (like you would in the controller or view) so it is easy to test permission logic. + +```ruby +test "user can only destroy projects which they own" do + user = User.create! + ability = Ability.new(user) + assert ability.can?(:destroy, Project.new(user: user)) + assert ability.cannot?(:destroy, Project.new) +end +``` + + +## RSpec + +If you are testing the `Ability` class through RSpec there is a `be_able_to` matcher available. This checks if the `can?` method returns `true`. + +```ruby +require "cancan/matchers" +# ... +ability.should be_able_to(:destroy, Project.new(user: user)) +ability.should_not be_able_to(:destroy, Project.new) +``` + +Pro way ;) + +```ruby +require "cancan/matchers" +# ... +describe "User" do + describe "abilities" do + subject(:ability) { Ability.new(user) } + let(:user){ nil } + + context "when is an account manager" do + let(:user){ Factory(:accounts_manager) } + + it { is_expected.to be_able_to(:manage, Account.new) } + end + end +end +``` + +## Cucumber + +By default, Cucumber will ignore the `rescue_from` call in the `ApplicationController` and report the `CanCan::AccessDenied` exception when running the features. If you want full integration testing you can change this behavior so the exception is caught by Rails. You can do so by setting this in the `env.rb` file. + +```ruby +# in features/support/env.rb +ActionController::Base.allow_rescue = true +``` + +Alternatively, if you don't want to allow rescue on everything, you can tag individual scenarios with `@allow-rescue` tag. + +```ruby +@allow-rescue +Scenario: Update Article +``` + +Here the `rescue_from` block will take effect only in this scenario. + + +## Controller Testing + +If you want to test authorization functionality at the controller level one option is to log-in the user who has the appropriate permissions. + +```ruby +user = User.create!(admin: true) +session[:user_id] = user.id # log in user however you like, alternatively stub `current_user` method +get :index +assert_template :index # render the template since they should have access +``` + +Alternatively, if you want to test the controller behaviour independently from what is inside the `Ability` class, it is easy to stub out the ability with any behaviour you want. + +```ruby +def setup + @ability = Object.new + @ability.extend(CanCan::Ability) + @controller.stubs(:current_ability).returns(@ability) +end + +test "render index if have read ability on project" do + @ability.can :read, Project + get :index + assert_template :index +end +``` + +If you have very complex permissions it can lead to many branching possibilities. If these are all tested in the controller layer then it can lead to slow and bloated tests. +Instead I recommend keeping controller authorization tests light and testing the authorization functionality more thoroughly in the Ability model through unit tests as shown at the top. + +## Additional Docs + +* [[Defining Abilities]] +* [[Checking Abilities]] +* [[Debugging Abilities]] +* [[Ability Precedence]] \ No newline at end of file diff --git a/docs/Translations-(i18n).md b/docs/Translations-(i18n).md new file mode 100644 index 00000000..8fc75b96 --- /dev/null +++ b/docs/Translations-(i18n).md @@ -0,0 +1,49 @@ +To use translations in your app define some yaml like this: +```yaml +# en.yml +en: + unauthorized: + manage: + all: "You have no access to this resource" +``` +## Translation for individual abilities +If you want to customize messages for some model or even for some ability define translation like this: + +```ruby +# models/ability.rb +... +can :create, Article +... +``` +```yaml +# en.yml +en: + unauthorized: + create: + article: "Only an admin can create an article" +``` + +### Translating custom abilities +Also translations is available for your custom abilities: +```ruby +# models/ability.rb +... +can :vote, Article +... +``` +```yaml +# en.yml +en: + unauthorized: + vote: + article: "Only users which have one or more article can vote" +``` +## Variables for translations +Finally you may use `action`(which contain ability like 'create') and `subject`(for example 'article') variables in your translation: +```yaml +# en.yml +en: + unauthorized: + manage: + all: "You do not have access to %{action} %{subject}!" +``` \ No newline at end of file diff --git a/docs/mvc--deficiencies.md b/docs/mvc--deficiencies.md new file mode 100644 index 00000000..6c6133f4 --- /dev/null +++ b/docs/mvc--deficiencies.md @@ -0,0 +1,15 @@ +Hi all, First I like cancan because it collects all resource access rules in one place, but ! + +Although there are many benefits to mvc frameworks (mainly to large dev teams), there are of course short comings. + +So, you have an app with user roles and define access to resources using cancan. Then because of mvc you have to search all your views that render links to these protected resources. This creates a lot of unnecessary noise and is not very DRY. + +It seems to me that it would be simpler if the rules created in cancan automatically generated override filters (perhaps using deface) that warden could utilise so that these useless links are removed from the rendered html. + +What do you think ? + +I would call the approach mvc+ and define it such that the only legitimate use cases are those that have one requirement that has a common effect across all mvc domains (like user roles and access rights to resources). + +I would love to create a solution but although I have 30 years as a self employed software engineer I am not fully up to speed with the rights and wrongs of the finer detail within the rails framework. + +Any suggestions ! From 92aac6de8dfe3544609c04de4b7859fd00afe23b Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Thu, 14 May 2020 21:27:51 +0200 Subject: [PATCH 08/66] Update Abilities-in-Database.md --- docs/Abilities-in-Database.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/Abilities-in-Database.md b/docs/Abilities-in-Database.md index 09233c53..603d576e 100644 --- a/docs/Abilities-in-Database.md +++ b/docs/Abilities-in-Database.md @@ -1,3 +1,5 @@ +# Abilities in Database + What if you or a client, wants to change permissions without having to re-deploy the application? In that case, it may be best to store the permission logic in a database: it is very easy to use the database records when defining abilities. @@ -121,4 +123,4 @@ def write_permission(class_name, cancan_action, name, description, force_id_1 = permission.save end end -``` \ No newline at end of file +``` From a5cb733a9991d2baadcc774d93ac165da12ffb51 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Thu, 14 May 2020 21:33:03 +0200 Subject: [PATCH 09/66] Rename Home.md to README.md --- docs/{Home.md => README.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/{Home.md => README.md} (99%) diff --git a/docs/Home.md b/docs/README.md similarity index 99% rename from docs/Home.md rename to docs/README.md index a92d03c0..a9d57283 100644 --- a/docs/Home.md +++ b/docs/README.md @@ -40,4 +40,4 @@ * [[Ability for Other Users]] * [[Other Authorization Solutions]] -**Can't find what you're looking for? [Submit a Question on StackOverflow](http://stackoverflow.com/questions/ask?tags=cancancan) \ No newline at end of file +**Can't find what you're looking for? [Submit a Question on StackOverflow](http://stackoverflow.com/questions/ask?tags=cancancan) From 010ce975f667bd45ce2611df5d2586c7d776531b Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Thu, 14 May 2020 21:36:33 +0200 Subject: [PATCH 10/66] Update README.md --- docs/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/README.md b/docs/README.md index a9d57283..e9cf0e2d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,5 @@ ### Getting Started -* [[README|https://github.com/CanCanCommunity/cancancan#readme]] * [[Defining Abilities]], [Best Practices](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practices) * [[Checking Abilities]] * [[Authorizing Controller Actions]] @@ -33,10 +32,10 @@ * [[Inherited Resources]] * [[Mongoid]] -* [[Rails Admin|https://github.com/sferik/rails_admin/wiki/CanCanCan]] +* [Rails Admin](https://github.com/sferik/rails_admin/wiki/CanCanCan) * [[Devise]] * [[Accessing Request Data]] -* [[Abilities in Database]] +* [Abilities in Database](./Abilities-in-Database.md) * [[Ability for Other Users]] * [[Other Authorization Solutions]] From b7d137734c9203c2c6667adef6039bb75bcc6674 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Thu, 14 May 2020 21:44:31 +0200 Subject: [PATCH 11/66] Update Ability-Precedence.md --- docs/Ability-Precedence.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/Ability-Precedence.md b/docs/Ability-Precedence.md index f2856a7d..5f76e394 100644 --- a/docs/Ability-Precedence.md +++ b/docs/Ability-Precedence.md @@ -1,3 +1,5 @@ +# Ability Precedence + An ability rule will override a previous one. For example, let's say we want the user to be able to do everything to projects except destroy them. @@ -35,13 +37,13 @@ if user.role? :admin end ``` -Here it is important the admin role be after the moderator so it can override the `cannot` behavior to give the admin more permissions. See [[Role Based Authorization]]. +Here it is important the admin role be after the moderator so it can override the `cannot` behavior to give the admin more permissions. See [Role Based Authorization](./Role-Based-Authorization.md). -If you are not getting the behavior you expect, please [[post an issue|https://github.com/CanCanCommunity/cancancan/issues]]. +If you are not getting the behavior you expect, please [post an issue](https://github.com/CanCanCommunity/cancancan/issues). ## Additional Docs -* [[Defining Abilities]] -* [[Checking Abilities]] -* [[Debugging Abilities]] -* [[Testing Abilities]] \ No newline at end of file +* [Defining Abilities](./Defining-Abilities.md) +* [Checking Abilities](./Checking-Abilities.md) +* [Debugging Abilities](./Debugging-Abilities.md) +* [Testing Abilities](./Testing-Abilities.md) From 9e408d3bde02ddba746b30ed94a1c7031a616df4 Mon Sep 17 00:00:00 2001 From: Olle Jonsson Date: Sat, 27 Jun 2020 09:34:39 +0200 Subject: [PATCH 12/66] CI: Use jruby-9.2.11.1 (#636) --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index e86431ad..a5a475c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ rvm: - 2.7.0 - ruby-head - jruby-9.1.17.0 - - jruby-9.2.11.0 + - jruby-9.2.11.1 - jruby-head gemfile: @@ -38,9 +38,9 @@ matrix: gemfile: gemfiles/activerecord_5.0.2.gemfile - rvm: jruby-9.1.17.0 gemfile: gemfiles/activerecord_6.0.0.gemfile - - rvm: jruby-9.2.11.0 + - rvm: jruby-9.2.11.1 gemfile: gemfiles/activerecord_5.0.2.gemfile - - rvm: jruby-9.2.11.0 + - rvm: jruby-9.2.11.1 gemfile: gemfiles/activerecord_6.0.0.gemfile allow_failures: - rvm: ruby-head From a5cdb16ddec448968b67a3fdf802e27fbbad6d54 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 12 Jul 2020 18:39:14 +0200 Subject: [PATCH 13/66] Update Defining-Abilities.md --- docs/Defining-Abilities.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/Defining-Abilities.md b/docs/Defining-Abilities.md index 2410894f..91e113bd 100644 --- a/docs/Defining-Abilities.md +++ b/docs/Defining-Abilities.md @@ -1,3 +1,5 @@ +# Defining Abilities + The `Ability` class is where all user permissions are defined. An example class looks like this. ```ruby @@ -168,4 +170,4 @@ The order of these calls is important. See [[Ability Precedence]] for more detai * [[Checking Abilities]] * [[Testing Abilities]] * [[Debugging Abilities]] -* [[Ability Precedence]] \ No newline at end of file +* [[Ability Precedence]] From 27039dade3ee39314d5689c981bdbe1b884b9330 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Mon, 27 Jul 2020 11:51:10 +0200 Subject: [PATCH 14/66] Add sponsors logos (#642) --- logo/modern_treasury.svg | 13 +++++++++++++ logo/renuo.png | Bin 5459 -> 13835 bytes 2 files changed, 13 insertions(+) create mode 100644 logo/modern_treasury.svg diff --git a/logo/modern_treasury.svg b/logo/modern_treasury.svg new file mode 100644 index 00000000..c0ac10d6 --- /dev/null +++ b/logo/modern_treasury.svg @@ -0,0 +1,13 @@ + + + diff --git a/logo/renuo.png b/logo/renuo.png index 6addfda21a752d33e1bfa5319e970c667bbc71d9..0ce669dc68937af6018c2580fe83586a4b4bc3e0 100644 GIT binary patch literal 13835 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 literal 5459 zcmV-Z6|CxsP)I zU0MeRyQoW34k{yq@gTwz;kZSxZA9FKkSig{dpUos-^hB`JG1xfz2EoEev|cmp8X{6 z%$k`sYu0zI-}>DaSXfwCSXfwCSXfx}Mq~>4*kf_Wlgf9+GfO?YT>0*(qlHCpMuSO+ zVm253zZux1n9Z$-J0DZN4=@O5ui*J@(f!SP$J~Da(6^Y)El*i~6QH@&{VgmuI-*BJ z$Q1Goz&`+o17m>sz&*umZgCu`GKG92unX{ADc@GX^IL#%~Th5XsYY_7u~Atc`!xK*AL;XL5%1d{Iy+yrc;9Uco@WM_4YjTMbC z#NE>%Ilyti7BQr10w&c+asUt5DdF7D43YyJnRd8R?&0I+1GoBkbOlG zs?^-5qa9cUECl`!m=*fLN}cqU@D^1O-J;8gK0x#i+tfgGo$&3V2YDE{5O^{oxh1?s zMQe16ZW}}!Yy35#tPBHDPDyisnU3df<9KdE=m%ek^cEJD=<6S?C~H94-&(mQ&ZXD| z3cwV{bDJ&MEv)Fi{zUgzfbrVlCr}oF{wcr90F;x`&T^l@#49_04qPAlL8}$xEt1e| zo1n)qC-j5Ms~k8c2%)FQx?dlluK_*>Zm}e{u%de}98@)P+JevzP6fUPgmhN$UF&%6 z&iXYL7A1+^8=OkY)v=QJ^?HV`Sr@VVdEiIDgT~r@6W|Z}H5L|$YjndQrUB(NQ%`8E zAb!0G6_36;seBKw(Ju@A;FKEgizwc<2LfP%c6t?XQ0NCwRCCV?C5*0auZrrhu?}Xn zw0}}O`qzPf0Y*_-B8hG%I`ATJLou6MTEfZZ2w;Dd;_DUQ>S8wcWeU`2b>-?NeP#l%87c^?Ne1s_z`LOzykEqtqRJ<^Q$dM9x_cMR~)z+Td3Bc-D?qRf0N zfklqzzJ>~5x+l``r+JnPN?G978gZ`<{oqxtKHmloMkPGi9Ob^;U#r6kslx)`5#YYi z556o{2YGgX;38SxM3T>4p&!hyayixRc<)g z>Huw^T?s69Ja-{57nl+H!Q#lInwIQe5nY?j_pD=*M|y?BGll%+#cb}~nDhT;%%qLL z@tHz?S}~hz?d|N=KJY5XbDsjfqn#e=cu~2G~i!zXQ9=OrFNneFSiv4hl9ud^DUa0~no$8aao8m0 z*uV{ens%)k3JuGzs@`Ov&C#~LBhFPjSmYdMty^UHiEB=`JVmDbhL56{iv)y zX3~%sP`>oX13BPy;IBds9a{ruIG%fP71eJ74gszban-~E;679yg1cq*kHbXZBFA&T z(;Jj%()J_dy-tMP_EDirXhQ?sf_ljw#avH6p{BN z;FrKT+Uzp~_^IQ$?}UDERR!zCMp-Ikj2Zv0z~#W3^87X_+YD?fgO91q+yXeo@!Z!# zKPc9tMqX?Cp{U#o$Ho-9Mb0gdcW>3&s!1fN9B_j1&9??_bv*a`p&vY2>ix|%I>l_R zJyXcfmXBVKVti{cn_C-qLL>4WA{I6PEyZlEWkVvl_Ca?t#J;t_2PGtc>wvErr|$%Q z2&_raCa(cLcRY6*Dtd0FDC< zKUBuwVjYvb0XPF?$ z(hhGYo$!ZM5bXotq|guUEcg7-57s!IJ6qPZp>g^!$8%?fe(+|!%<4uxrieshE>R`} zZySSg>{3nc_mbned&|;mKW&iTN(S+RleF>H1-6%x-0|EW(b1u4H7cD~2ah`Ww@EtM zNUi+aD2;n6&AHZr9s@2gPVXfqvPxWVR|B_&esE`W|1xGWWX78-BEF7qBAJ5ga8}m268gd0rQSQC4OWYe;}z|61F(nmiP-yDqW2<_YyTe#oK44Qww1tb373Bl z*xopHV(15}E13bca_6BOIU-CzY2zy)+5%aZrj;h5j=oZsWx5rmpR8Bw23kAp1srCa zJ{1*aTv2R-LH1(|se3poS7O<4)9zLqp?jdmlh$VWR5<8Btj^~aCZa_IsG#X;~nF;sWEM_*pK@>#Q&Q$H96w@(| zn<`*Z5BPxytj=V$o z@gZ?Mcc74H50o3JsWo2)oD%xMhY2&p4k*p64!;(EZE*vA31!L~qII(Iz~d=yu|`Dq zYCC)6|BfNP*Tux8z0(+!@4i03y-@9J9wX%~bUgQ2)|pHD5bv8{iQa?Q$?@F7iTB(~ zT2>7c5q%$2ppl6sz_id0?kdr7wlEYy0Ncn+Uke7JHuZ>zK1|4-R&-xk!?WAz9Ft#9 z{vMG6)YKci41C&EgNaBEkI>OCVz}eETbMyGoroTnO1YE`y3+K@>%*R1CpPp$V%BTr zh+YcwflER^xTQ?F5p>+9ss$AgQ)_nGMwC0MIWVQghMvZ(uhBk{Zee9DVtWi;j~ReR zVtBVUmm)-_kZ&$#b1jiGO}POeQ^@x%W^-%I%^q|Z<%f3jNW8k3&8;p~pB_X*7j^EB zfV+UdgnsapCdMU|?`M4HW|aGAcc2dz`oL%A_*;c?YU)ZXi}?lv5=D0{9v#$fswV&= zGll#-;L>6?_ko_N#n>PHTPQcl!I?t-8Q`j7HuqkMomUPlFs6qZdp{9pezl%w_2*Cy zE&Wmudlkx|WG_@|=Lpd@)zeeVXEVvsvaVZVi2BXI0Wl9dSUTg=F%>(i2T2}{qQ_(k`ID1; zg*Q9Or?{N$;&+**LQfhBMLV02h*V^MjSub`60eTBjD~8tA z{V1cz7MIZ+9; zol*pkly8IO?=eF5xwW>Dkh~9_6})eSey}D^VPIlDDm9LdyW?hnVm8-;3NSF^m1m>2 zT#my{>)}J+E`d6|2Ab2wbX4rNIgm~VMw`wxF&&}+n3;HvHJ;IBs8fcF zCBQk+R>(1U9hEw0F3JS(PB;DDmAYMoO4-vZ(>{lO&YyC;2GfC+Oa)=ZvZpO)mu;IC{8dQ z@!4VG+NsU)+$Vsd@!c*(MV~FKr2UQP#q;T$?t4b)2TRL_sTQ-jL^71KiC1!NeW16- zqZ{F&*NC!3$GMK@PS(rDbdI;AQ|l?V_5*(Bcy6N^)|%AGj1IH~%AS9_=(&=?qiAfX^SC4^? z=l%kfdP;Y``WWR3{aMn0VSU|u*Q^dIVdD-WdPW!x+~j!fp;5l`sB;+Up-)5kJZz_3 z`&;1aj^|#0N>2D`=m#wZ=^IdHp)sy6{NeNle=W96MsCw-iP+4c)1k>=K@ppyn6`+=xzJyRXeeM|_uJoJNhjokfF zrqOLtK}|;*kNFI^GV}w#)aDYF=)I5ZpE{mPu>|H1*XOwsW&Du8EX1$CGu-|a$;?8xAG9V(jd0hEu#B9Yn#vo5H~ ziQ(%~;HuCM&etS`SXj}$car^alr=$j0BQh!DeJemHFvv^eFndvhv_qGQxE_Z-CF=YR@F(DBplqN7D=i}kjtrSCPw)U6L? zf`&vX`E%ehl#RO*ESF`^385c^blkL|Z<@hI+UOCK3-Y-n`4`lB{FSX-{mLO*y{cA0rd*2v!w zQF=%MxnC8@WtO~iRPP70Z~Gc$0*4H;F9fDJo_jyaEd5Ps`y_?x5>)Q}IVhiqha>N| zw5GY(5wM)ixz_WQqL$~a5Bs?&ueLM>(&eEaEa~F4CywVvJCEw$bSP~kM4)^2wiCU3dpW2Ls++QCD*SH#ZXV)ua@(&M_5y> z?~%qn+>`Gdl}Rd>luJ*)rmMbVi8?enp4;4&?Upp7?+^SZ%CyjClz$HRMKPOOQDVS2 zOcWMfvA_D`z)9x*=K4&XDdc}3N{(U1^V&q@I;EJ+)%}JP^)L{X@(`<2hnQ}A@1A_; z?r!@AlQ@||epi%J#URh?zn{KeHXZ%&Iu4B ztO0H-W^+H;0QX+Au&8`SmuxXVM&%7Jhtb7sZgI?PeipI`Vi%$f=xc6$Eb;K?ic+c+ zmII^Kb3D3*#dgq0(TU%xwhUIrfOl)<)XVrqPU#rwC?xXgqYAs Date: Mon, 27 Jul 2020 11:56:20 +0200 Subject: [PATCH 15/66] Update Sponsors README (#643) --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7083547c..c12f6785 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,18 @@ of models automatically and reduce duplicated code. ## Sponsored by -[![Renuo AG](./logo/renuo.png)](https://www.renuo.ch) +
+ Renuo AG + +
+
+ + Modern Treasury + +
+
+Do you want to sponsor CanCanCan and show your logo here? +Check our [Sponsors page](https://github.com/sponsors/coorasse/). ## Installation From e88d52cbae0f17f44cb8706384173217dfdf6f21 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Mon, 27 Jul 2020 11:58:38 +0200 Subject: [PATCH 16/66] correct URL in README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c12f6785..68121ea7 100644 --- a/README.md +++ b/README.md @@ -27,17 +27,18 @@ of models automatically and reduce duplicated code. ## Sponsored by - Renuo AG + Renuo AG

- Modern Treasury + Modern Treasury

+ Do you want to sponsor CanCanCan and show your logo here? -Check our [Sponsors page](https://github.com/sponsors/coorasse/). +Check our [Sponsors Page](https://github.com/sponsors/coorasse). ## Installation From 300f54c3765c67c2ee508d047f2864f92b039643 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Mon, 27 Jul 2020 11:59:19 +0200 Subject: [PATCH 17/66] Remove Renuo from Thanks section and leave the logo --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 68121ea7..b7a5b4e8 100644 --- a/README.md +++ b/README.md @@ -283,7 +283,5 @@ See the [CONTRIBUTING](https://github.com/CanCanCommunity/cancancan/blob/develop ## Special Thanks -Thanks to [Renuo AG](https://www.renuo.ch) for currently maintaining and supporting the project. - Many thanks to the [CanCanCan contributors](https://github.com/CanCanCommunity/cancancan/contributors). See the [CHANGELOG](https://github.com/CanCanCommunity/cancancan/blob/master/CHANGELOG.md) for the full list. From ed9a511175a421bea6910f82fb72dafb314a6652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20Anthony=20Fl=C3=BCckiger?= Date: Mon, 24 Aug 2020 16:33:24 +0200 Subject: [PATCH 18/66] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2086a556..97a32fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +* [#532](https://github.com/CanCanCommunity/cancancan/issues/532): Generate inner queries instead of join+distinct. ([@mraidel][], [@gabsi20][]) + ## 3.1.0 * [#605](https://github.com/CanCanCommunity/cancancan/pull/605): Generate inner queries instead of join+distinct. ([@fsateler][]) @@ -665,3 +669,4 @@ Please read the [guide on migrating from CanCanCan 2.x to 3.0](https://github.co [@aleksejleonov]: https://github.com/aleksejleonov [@albb0920]: https://github.com/albb0920 [@ayumu838]: https://github.com/ayumu838 +[@liberatys]: https://github.com/liberatys From b70e5563219423a21ff4f5906e164ea6e01b6e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20Anthony=20Fl=C3=BCckiger?= Date: Mon, 24 Aug 2020 16:34:14 +0200 Subject: [PATCH 19/66] Update CHANGELOG.md --- CHANGELOG.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97a32fdf..2086a556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,3 @@ -## Unreleased - -* [#532](https://github.com/CanCanCommunity/cancancan/issues/532): Generate inner queries instead of join+distinct. ([@mraidel][], [@gabsi20][]) - ## 3.1.0 * [#605](https://github.com/CanCanCommunity/cancancan/pull/605): Generate inner queries instead of join+distinct. ([@fsateler][]) @@ -669,4 +665,3 @@ Please read the [guide on migrating from CanCanCan 2.x to 3.0](https://github.co [@aleksejleonov]: https://github.com/aleksejleonov [@albb0920]: https://github.com/albb0920 [@ayumu838]: https://github.com/ayumu838 -[@liberatys]: https://github.com/liberatys From 880897dd0cdc6cc1c7e28fe73b2c73a3178c6b07 Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Sun, 13 Sep 2020 10:52:29 -0500 Subject: [PATCH 20/66] Insert new model adapters at the start of the list (#640) --- lib/cancan/model_adapters/abstract_adapter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cancan/model_adapters/abstract_adapter.rb b/lib/cancan/model_adapters/abstract_adapter.rb index 06cfd6c0..4e0e51a2 100644 --- a/lib/cancan/model_adapters/abstract_adapter.rb +++ b/lib/cancan/model_adapters/abstract_adapter.rb @@ -5,7 +5,7 @@ module ModelAdapters class AbstractAdapter def self.inherited(subclass) @subclasses ||= [] - @subclasses << subclass + @subclasses.insert(0, subclass) end def self.adapter_class(model_class) From 7bd2a38d71284f8b65b9c1e97f256805340c3781 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sun, 13 Sep 2020 17:53:53 +0200 Subject: [PATCH 21/66] Update Model-Adapter.md --- docs/Model-Adapter.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/Model-Adapter.md b/docs/Model-Adapter.md index 4bd26f42..8b529e05 100644 --- a/docs/Model-Adapter.md +++ b/docs/Model-Adapter.md @@ -1,9 +1,9 @@ -CanCan includes a model adapter layer which allows it to change behavior depending on the model used. The current adapters are. +# Model Adapter -* ActiveRecord -* [[Mongoid]] +CanCan includes a model adapter layer which allows it to change behavior depending on the model used. The current adapters are. -See [[spec/README|https://github.com/CanCanCommunity/cancancan/blob/master/spec/README.rdoc]] for how to run specs for a given adapter. +* ActiveRecord (native in `cancancan` gem) +* [Mongoid](https://github.com/CanCanCommunity/cancancan-mongoid) ## Creating a Model Adapter @@ -11,7 +11,8 @@ It is easy to make your own adapter if one is not provided. Here I'll walk you t ### The Specs -First, fork the CanCan GitHub project and clone that repo. Next, add the necessary gems to the Gemfile for working with the adapter in the specs. + +First, fork the CanCanCan GitHub project and clone that repo. Next, add the necessary gems to the Gemfile for working with the adapter in the specs. ```ruby case ENV["MODEL_ADAPTER"] @@ -127,4 +128,4 @@ end The first one returns `true` when there's a conditions option which is not a Symbol (such as `:age.gt`). The second method will be called by CanCan when the first one returns true to check if the given subject matches the hash of conditions. -See the actual [[mongoid_adapter_spec.rb|https://github.com/ryanb/cancan/blob/master/spec/cancan/model_adapters/mongoid_adapter_spec.rb]] and [[mongoid_adapter.rb|https://github.com/ryanb/cancan/blob/master/lib/cancan/model_adapters/mongoid_adapter.rb]] files for the full code. \ No newline at end of file +See the actual [[mongoid_adapter_spec.rb|https://github.com/ryanb/cancan/blob/master/spec/cancan/model_adapters/mongoid_adapter_spec.rb]] and [[mongoid_adapter.rb|https://github.com/ryanb/cancan/blob/master/lib/cancan/model_adapters/mongoid_adapter.rb]] files for the full code. From 677c588670553b87bacd29ee1d89043c815caf48 Mon Sep 17 00:00:00 2001 From: Igor Victor Date: Sun, 13 Sep 2020 18:53:06 +0200 Subject: [PATCH 22/66] Add Truffleruby head to CI (#651) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a5a475c8..4ab62bf7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ rvm: - jruby-9.1.17.0 - jruby-9.2.11.1 - jruby-head + - truffleruby-head gemfile: - gemfiles/activerecord_4.2.0.gemfile From 06c103bf2645e833155be05d75b31f616c239dca Mon Sep 17 00:00:00 2001 From: Eloy Espinaco Date: Sun, 13 Sep 2020 13:53:27 -0300 Subject: [PATCH 23/66] Add #inspect on AccessDenied error (#639) Help debugging errors adding some usefull information on the AccessDenied inspect method. --- lib/cancan/exceptions.rb | 8 ++++++++ spec/cancan/exceptions_spec.rb | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/lib/cancan/exceptions.rb b/lib/cancan/exceptions.rb index d66c5eb6..11417ba0 100644 --- a/lib/cancan/exceptions.rb +++ b/lib/cancan/exceptions.rb @@ -58,5 +58,13 @@ def initialize(message = nil, action = nil, subject = nil, conditions = nil) def to_s @message || @default_message end + + def inspect + details = %i[action subject conditions message].map do |attribute| + value = instance_variable_get "@#{attribute}" + "#{attribute}: #{value.inspect}" if value.present? + end.compact.join(', ') + "#<#{self.class.name} #{details}>" + end end end diff --git a/spec/cancan/exceptions_spec.rb b/spec/cancan/exceptions_spec.rb index ee2a1f50..aa8cac85 100644 --- a/spec/cancan/exceptions_spec.rb +++ b/spec/cancan/exceptions_spec.rb @@ -19,6 +19,12 @@ @exception.default_message = 'Unauthorized!' expect(@exception.message).to eq('Unauthorized!') end + + it 'has debug information on inspect' do + expect(@exception.inspect).to eq( + '#' + ) + end end describe 'with only a message' do From c6b41395c8d9bca08e8f845c5c03a8de188a1170 Mon Sep 17 00:00:00 2001 From: Alexander Weiss Date: Sun, 13 Sep 2020 18:54:02 +0200 Subject: [PATCH 24/66] Update before_filter to before_action in docs (#641) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seems like this has been missed ☺️ (it's been updated in other places). --- docs/Nested-Resources.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Nested-Resources.md b/docs/Nested-Resources.md index a2adc5e7..3fd597d6 100644 --- a/docs/Nested-Resources.md +++ b/docs/Nested-Resources.md @@ -76,10 +76,10 @@ load_resource :event load_and_authorize_resource :task, through: [:project, :event] ``` -Here it will check both the `@project` and `@event` variables and fetch the task through whichever one exists. Note that this is only loading the parent model, if you want to authorize the parent you will need to do it through a before_filter because there is special logic involved. +Here it will check both the `@project` and `@event` variables and fetch the task through whichever one exists. Note that this is only loading the parent model, if you want to authorize the parent you will need to do it through a before_action because there is special logic involved. ```ruby -before_filter :authorize_parent +before_action :authorize_parent private @@ -157,4 +157,4 @@ Don't forget the **inverse_of** option, is the trick to make it works correctly. Remember to define the ability through the **groups_users** model (i.e. don't write `can :create, User, groups: {CONDITION_ON_GROUP}`) -You will be able to persist the association just calling `@user.save` instead of `@group.save` \ No newline at end of file +You will be able to persist the association just calling `@user.save` instead of `@group.save` From 592ae7ff6e0f0500e5079d5ec2e803b7f439d3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20C=2E=20Vieira?= Date: Sun, 13 Sep 2020 13:54:44 -0300 Subject: [PATCH 25/66] Remove duplicated word from wiki (#644) --- docs/Defining-Abilities.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Defining-Abilities.md b/docs/Defining-Abilities.md index 91e113bd..1f18656b 100644 --- a/docs/Defining-Abilities.md +++ b/docs/Defining-Abilities.md @@ -52,7 +52,7 @@ can :manage, User can :invite, User ``` -you can get rid of the second line and the `:invite` permissions, because because `:manage` represents **any** action on object and `:manage` is not just `:create`, `:read`, `:update`, `:destroy` on object. +you can get rid of the second line and the `:invite` permissions, because `:manage` represents **any** action on object and `:manage` is not just `:create`, `:read`, `:update`, `:destroy` on object. If you want only CRUD actions on object, you should create custom action that called `:crud` for example, and use it instead of `:manage`: From 5d39575bfd1a81ec3a4bd1f9b2af8e72ab40f554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20Fl=C3=BCckiger?= Date: Sat, 10 Oct 2020 14:51:45 +0200 Subject: [PATCH 26/66] Add support for single table inheritance (#649) Add support for Single Table Inheritance --- lib/cancan.rb | 1 + lib/cancan/ability.rb | 6 +- lib/cancan/class_matcher.rb | 26 ++++++ .../model_adapters/active_record_adapter.rb | 3 +- lib/cancan/model_adapters/sti_normalizer.rb | 31 +++++++ lib/cancan/rule.rb | 15 +++- .../active_record_adapter_spec.rb | 88 +++++++++++++++++++ 7 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 lib/cancan/class_matcher.rb create mode 100644 lib/cancan/model_adapters/sti_normalizer.rb diff --git a/lib/cancan.rb b/lib/cancan.rb index 9461754a..955589c1 100644 --- a/lib/cancan.rb +++ b/lib/cancan.rb @@ -16,6 +16,7 @@ if defined? ActiveRecord require 'cancan/model_adapters/conditions_extractor' require 'cancan/model_adapters/conditions_normalizer' + require 'cancan/model_adapters/sti_normalizer' require 'cancan/model_adapters/active_record_adapter' require 'cancan/model_adapters/active_record_4_adapter' require 'cancan/model_adapters/active_record_5_adapter' diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index 8b40c997..d605053a 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -302,7 +302,11 @@ def extract_subjects(subject) def alternative_subjects(subject) subject = subject.class unless subject.is_a?(Module) - [:all, *subject.ancestors, subject.class.to_s] + if subject.respond_to?(:subclasses) + [:all, *(subject.ancestors + subject.subclasses), subject.class.to_s] + else + [:all, *subject.ancestors, subject.class.to_s] + end end end end diff --git a/lib/cancan/class_matcher.rb b/lib/cancan/class_matcher.rb new file mode 100644 index 00000000..0bdf0fd9 --- /dev/null +++ b/lib/cancan/class_matcher.rb @@ -0,0 +1,26 @@ +# This class is responsible for matching classes and their subclasses as well as +# upmatching classes to their ancestors. +# This is used to generate sti connections +class SubjectClassMatcher + def self.matches_subject_class?(subjects, subject) + subjects.any? do |sub| + has_subclasses = subject.respond_to?(:subclasses) + matching_class_check(subject, sub, has_subclasses) + end + end + + def self.matching_class_check(subject, sub, has_subclasses) + matches = matches_class_or_is_related(subject, sub) + if has_subclasses + matches || subject.subclasses.include?(sub) + else + matches + end + end + + def self.matches_class_or_is_related(subject, sub) + sub.is_a?(Module) && (subject.is_a?(sub) || + subject.class.to_s == sub.to_s || + (subject.is_a?(Module) && subject.ancestors.include?(sub))) + end +end diff --git a/lib/cancan/model_adapters/active_record_adapter.rb b/lib/cancan/model_adapters/active_record_adapter.rb index 41c1057e..81beac72 100644 --- a/lib/cancan/model_adapters/active_record_adapter.rb +++ b/lib/cancan/model_adapters/active_record_adapter.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative 'conditions_extractor.rb' -require 'cancan/rules_compressor' module CanCan module ModelAdapters class ActiveRecordAdapter < AbstractAdapter @@ -16,6 +14,7 @@ def self.version_lower?(version) def initialize(model_class, rules) super @compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse + StiNormalizer.normalize(@compressed_rules) ConditionsNormalizer.normalize(model_class, @compressed_rules) end diff --git a/lib/cancan/model_adapters/sti_normalizer.rb b/lib/cancan/model_adapters/sti_normalizer.rb new file mode 100644 index 00000000..a2ac739f --- /dev/null +++ b/lib/cancan/model_adapters/sti_normalizer.rb @@ -0,0 +1,31 @@ +# this class is responsible for detecting sti classes and creating new rules for the +# relevant subclasses, using the inheritance_column as a merger +module CanCan + module ModelAdapters + class StiNormalizer + class << self + def normalize(rules) + rules_cache = [] + rules.delete_if.with_index do |rule, _index| + subjects = rule.subjects.select do |subject| + next if subject == :all || subject.descends_from_active_record? + + rules_cache.push(build_rule_for_subclass(rule, subject)) + true + end + subjects.length == rule.subjects.length + end + rules_cache.each { |rule| rules.push(rule) } + end + + private + + # create a new rule for the subclasses that links on the inheritance_column + def build_rule_for_subclass(rule, subject) + CanCan::Rule.new(rule.base_behavior, rule.actions, subject.superclass, + rule.conditions.merge(subject.inheritance_column => subject.name), rule.block) + end + end + end + end +end diff --git a/lib/cancan/rule.rb b/lib/cancan/rule.rb index 0071b884..07063496 100644 --- a/lib/cancan/rule.rb +++ b/lib/cancan/rule.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative 'conditions_matcher.rb' +require_relative 'class_matcher.rb' require_relative 'relevant.rb' module CanCan @@ -11,7 +12,7 @@ class Rule # :nodoc: include ConditionsMatcher include Relevant include ParameterValidators - attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes + attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes, :block attr_writer :expanded_actions, :conditions # The first argument when initializing is the base_behavior which is a true/false @@ -101,6 +102,18 @@ def matches_attributes?(attribute) private + def matches_action?(action) + @expanded_actions.include?(:manage) || @expanded_actions.include?(action) + end + + def matches_subject?(subject) + @subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject) + end + + def matches_subject_class?(subject) + SubjectClassMatcher.matches_subject_class?(@subjects, subject) + end + def parse_attributes_from_extra_args(args) attributes = args.shift if valid_attribute_param?(args.first) extra_args = args.shift diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index d47f9ddc..b653cff4 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -644,4 +644,92 @@ class JsonTransaction < ActiveRecord::Base expect(JsonTransaction.accessible_by(ability)).to match_array([transaction]) end end + + context 'when STI is in use' do + before do + ActiveRecord::Schema.define do + create_table(:brands) do |t| + t.string :name + end + + create_table(:vehicles) do |t| + t.string :type + end + end + + class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + end + + class Vehicle < ApplicationRecord + end + + class Car < Vehicle + end + + class Motorbike < Vehicle + end + + class Suzuki < Motorbike + end + end + + it 'recognises rules applied to the base class' do + u1 = User.create!(name: 'pippo') + + car = Car.create! + motorbike = Motorbike.create! + + ability = Ability.new(u1) + ability.can :read, Vehicle + expect(Vehicle.accessible_by(ability)).to match_array([car, motorbike]) + expect(Car.accessible_by(ability)).to match_array([car]) + expect(Motorbike.accessible_by(ability)).to match_array([motorbike]) + end + + it 'recognises rules applied to the base class multiple classes deep' do + u1 = User.create!(name: 'pippo') + + car = Car.create! + motorbike = Motorbike.create! + suzuki = Suzuki.create! + + ability = Ability.new(u1) + ability.can :read, Vehicle + expect(Vehicle.accessible_by(ability)).to match_array([suzuki, car, motorbike]) + expect(Car.accessible_by(ability)).to match_array([car]) + expect(Motorbike.accessible_by(ability)).to match_array([suzuki, motorbike]) + expect(Suzuki.accessible_by(ability)).to match_array([suzuki]) + end + + it 'recognises rules applied to subclasses' do + u1 = User.create!(name: 'pippo') + car = Car.create! + Motorbike.create! + + ability = Ability.new(u1) + ability.can :read, [Car] + expect(Vehicle.accessible_by(ability)).to match_array([car]) + expect(Car.accessible_by(ability)).to eq([car]) + expect(Motorbike.accessible_by(ability)).to eq([]) + end + + it 'recognises rules applied to subclasses on 3 level' do + u1 = User.create!(name: 'pippo') + suzuki = Suzuki.create! + Motorbike.create! + ability = Ability.new(u1) + ability.can :read, [Suzuki] + expect(Motorbike.accessible_by(ability)).to eq([suzuki]) + end + + it 'recognises rules applied to subclass of subclass even with be_able_to' do + u1 = User.create!(name: 'pippo') + motorbike = Motorbike.create! + ability = Ability.new(u1) + ability.can :read, [Motorbike] + expect(ability).to be_able_to(:read, motorbike) + expect(ability).to be_able_to(:read, Suzuki.new) + end + end end From 585e5ea54c900c6afd536f143cde962ccdf68607 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 10 Oct 2020 14:53:59 +0200 Subject: [PATCH 27/66] Update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2086a556..09b5e492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +* [#649](https://github.com/CanCanCommunity/cancancan/pull/649): Add support for Single Table Inheritance. ([@Liberatys][]) + ## 3.1.0 * [#605](https://github.com/CanCanCommunity/cancancan/pull/605): Generate inner queries instead of join+distinct. ([@fsateler][]) @@ -665,3 +669,4 @@ Please read the [guide on migrating from CanCanCan 2.x to 3.0](https://github.co [@aleksejleonov]: https://github.com/aleksejleonov [@albb0920]: https://github.com/albb0920 [@ayumu838]: https://github.com/ayumu838 +[@Liberatys]: https://github.com/Liberatys From 575fa027aeafb5529d6d11b97fc29a83b073f976 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 10 Oct 2020 15:12:30 +0200 Subject: [PATCH 28/66] Update Changing-Defaults.md --- docs/Changing-Defaults.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/Changing-Defaults.md b/docs/Changing-Defaults.md index 2d2a8747..eafe5d63 100644 --- a/docs/Changing-Defaults.md +++ b/docs/Changing-Defaults.md @@ -1,3 +1,5 @@ +# Changing Defaults + CanCanCan makes two assumptions about your application. * You have an `Ability` class which defines the permissions. @@ -41,4 +43,4 @@ class ApplicationController < ActionController::Base end ``` -That's it! See [[Accessing Request Data]] for a more complex example of what you can do here. \ No newline at end of file +That's it! See [Accessing Request Data](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Accessing-request-data.md) for a more complex example of what you can do here. From 43eabd70d98cdc5f760d7fce2587b9977c172b20 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 10 Oct 2020 15:14:12 +0200 Subject: [PATCH 29/66] Update Accessing-request-data.md --- docs/Accessing-request-data.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/Accessing-request-data.md b/docs/Accessing-request-data.md index 286a44f1..84e6ef72 100644 --- a/docs/Accessing-request-data.md +++ b/docs/Accessing-request-data.md @@ -1,3 +1,5 @@ +# Accessing request data + What if you need to modify the permissions based on something outside of the User object? For example, let's say you want to blacklist certain IP addresses from creating comments. The IP address is accessible through request.remote_ip but the Ability class does not have access to this. It's easy to modify what you pass to the Ability object by overriding the current_ability method in ApplicationController. ```ruby @@ -22,4 +24,4 @@ end ``` This concept can apply to session and cookies as well. -You may wonder, why I pass only the IP Address instead of the entire request object? I prefer to pass only the information needed, this makes testing and debugging the behavior easier. \ No newline at end of file +You may wonder, why I pass only the IP Address instead of the entire request object? I prefer to pass only the information needed, this makes testing and debugging the behavior easier. From 91c9b2b7bcc93e8fab17c909033dff7d3508a5ab Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 10 Oct 2020 15:15:39 +0200 Subject: [PATCH 30/66] Update README.md --- docs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README.md b/docs/README.md index e9cf0e2d..4af22990 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,7 +5,7 @@ * [[Authorizing Controller Actions]] * [[Exception Handling]] * [[Ensure Authorization]] -* [[Changing Defaults]] +* [Changing Defaults](./Changing-Defaults.md) * [[Translations (i18n)]] ### More about Abilities @@ -34,7 +34,7 @@ * [[Mongoid]] * [Rails Admin](https://github.com/sferik/rails_admin/wiki/CanCanCan) * [[Devise]] -* [[Accessing Request Data]] +* [Accessing Request Data](./Accessing-request-data.md) * [Abilities in Database](./Abilities-in-Database.md) * [[Ability for Other Users]] * [[Other Authorization Solutions]] From 6d80a2ec8a7e45e27fe82018343f62165f11b083 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 10 Oct 2020 15:17:09 +0200 Subject: [PATCH 31/66] Update README.md --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 4af22990..6e137e13 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ ### Getting Started -* [[Defining Abilities]], [Best Practices](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practices) +* [Defining Abilities](./Defining-Abilities.md), [Best Practices](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practices) * [[Checking Abilities]] * [[Authorizing Controller Actions]] * [[Exception Handling]] From cf4142ec7765a567e64fcbc183541c6dc9446d97 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 10 Oct 2020 15:19:59 +0200 Subject: [PATCH 32/66] Update Defining-Abilities.md --- docs/Defining-Abilities.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/Defining-Abilities.md b/docs/Defining-Abilities.md index 1f18656b..e260da99 100644 --- a/docs/Defining-Abilities.md +++ b/docs/Defining-Abilities.md @@ -161,13 +161,13 @@ can :manage, Project cannot :destroy, Project ``` -The order of these calls is important. See [[Ability Precedence]] for more details. +The order of these calls is important. See [Ability Precedence](./Ability-Precedence.md) for more details. ## Additional Docs -* [[Defining Abilities: Best Practices]] -* [[Defining Abilities with Blocks]] -* [[Checking Abilities]] -* [[Testing Abilities]] -* [[Debugging Abilities]] -* [[Ability Precedence]] +* [Defining Abilities: Best Practices](./Defining-Abilities:-Best-Practices.md) +* [Defining Abilities with Blocks](./Defining-Abilities-with-Blocks.md) +* [Checking Abilities](./Checking-Abilities.md) +* [Testing Abilities](./Testing-Abilities.md) +* [Debugging Abilities](./Debugging-Abilities.md) +* [Ability Precedence](./Ability-Precedence.md) From ddb3725c7bb8b57464eca849282bdf0261e6050f Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 10 Oct 2020 15:20:26 +0200 Subject: [PATCH 33/66] Update Defining-Abilities.md --- docs/Defining-Abilities.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Defining-Abilities.md b/docs/Defining-Abilities.md index e260da99..5e2fd564 100644 --- a/docs/Defining-Abilities.md +++ b/docs/Defining-Abilities.md @@ -116,7 +116,7 @@ Almost anything that you can pass to a hash of conditions in Active Record will can :manage, Project, group: { id: user.group_ids } ``` -If you have a complex case which cannot be done through a hash of conditions, see [[Defining Abilities with Blocks]]. +If you have a complex case which cannot be done through a hash of conditions, see [Defining Abilities with Blocks](./Defining-Abilities-with-Blocks.md). ## Traverse associations From d9eaad190ce611c0ab9dd1c37886a7ef05565d77 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 10 Oct 2020 15:21:59 +0200 Subject: [PATCH 34/66] Update Defining-Abilities:-Best-Practices.md --- docs/Defining-Abilities:-Best-Practices.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/Defining-Abilities:-Best-Practices.md b/docs/Defining-Abilities:-Best-Practices.md index fc16e8a5..f7127664 100644 --- a/docs/Defining-Abilities:-Best-Practices.md +++ b/docs/Defining-Abilities:-Best-Practices.md @@ -1,3 +1,5 @@ +# Defining Abilities: Best Practices + ## Use hash conditions as much as possible Here's why: @@ -48,7 +50,7 @@ Here's why: **4. For complex object graphs, hash conditions accommodate `joins` easily.** - See https://github.com/CanCanCommunity/cancancan/wiki/defining-abilities#hash-of-conditions. + See https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Defining-Abilities.md#hash-of-conditions. ## Give permissions, don't take them away @@ -96,4 +98,4 @@ def current_ability end ``` -You can read more about splitting the Ability file in [this article](https://medium.com/@coorasse/cancancan-that-scales-d4e526fced3d) \ No newline at end of file +You can read more about splitting the Ability file in [this article](https://medium.com/@coorasse/cancancan-that-scales-d4e526fced3d) From 695d9b274075ab2408222e2e6e8b9e2867c81744 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 10 Oct 2020 15:27:05 +0200 Subject: [PATCH 35/66] Update README.md --- docs/README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/README.md b/docs/README.md index 6e137e13..bdee930b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,42 +1,42 @@ ### Getting Started * [Defining Abilities](./Defining-Abilities.md), [Best Practices](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practices) -* [[Checking Abilities]] -* [[Authorizing Controller Actions]] -* [[Exception Handling]] -* [[Ensure Authorization]] +* [Checking Abilities](./Checking-Abilities.md) +* [Authorizing Controller Actions](./Authorizing-controller-actions.md) +* [Exception Handling](./Exception-Handling.md) +* [Ensure Authorization](./Ensure-Authorization.md) * [Changing Defaults](./Changing-Defaults.md) -* [[Translations (i18n)]] +* [Translations (i18n)](./Translations-(i18n).md) ### More about Abilities -* [[Testing Abilities]] -* [[Debugging Abilities]] -* [[Ability Precedence]] -* [[Fetching Records]] -* [[Action Aliases]] -* [[Custom Actions]] -* [[Role Based Authorization]] +* [Testing Abilities](./Testing-Abilities.md) +* [Debugging Abilities](./Debugging-Abilities.md) +* [Ability Precedence](./Ability-Precedence.md) +* [Fetching Records](./Fetching-Records.md) +* [Action Aliases](./Action-Aliases.md) +* [Custom Actions](./Custom-Actions.md) +* [Role Based Authorization](./Role-Based-Authorization.md) ### More about Controllers & Views -* [[Controller Authorization Example]] -* [[Nested Resources]] -* [[Strong Parameters]] -* [[Non RESTful Controllers]] -* [[Link Helpers]] +* [Controller Authorization Example](./Controller-Authorization-Example.md) +* [Nested Resources](./Nested-Resources.md) +* [Strong Parameters](./Strong-Parameters.md) +* [Non RESTful Controllers](./Non-RESTful-Controllers.md) +* [Link Helpers](./Link-Helpers.md) ### Other Use Cases -* [[Inherited Resources]] -* [[Mongoid]] +* [Inherited Resources](./Inherited-Resources.md) +* [Mongoid](./Mongoid.md) * [Rails Admin](https://github.com/sferik/rails_admin/wiki/CanCanCan) -* [[Devise]] +* [Devise](./Devise.md) * [Accessing Request Data](./Accessing-request-data.md) * [Abilities in Database](./Abilities-in-Database.md) -* [[Ability for Other Users]] -* [[Other Authorization Solutions]] +* [Ability for Other Users](./Ability-for-Other-Users.md) +* [Other Authorization Solutions](./Other-Authorization-Solutions.md) **Can't find what you're looking for? [Submit a Question on StackOverflow](http://stackoverflow.com/questions/ask?tags=cancancan) From 5bcba2c6a38bdeac1a0146a8b710b89fd6c5b5f1 Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Tue, 13 Oct 2020 10:46:17 -0500 Subject: [PATCH 36/66] Add option for accessible_by querying strategy --- lib/cancan.rb | 1 + lib/cancan/config.rb | 33 + .../model_adapters/active_record_4_adapter.rb | 17 +- .../model_adapters/active_record_5_adapter.rb | 16 +- .../accessible_by_integration_spec.rb | 4 +- .../active_record_4_adapter_spec.rb | 212 ++--- .../active_record_5_adapter_spec.rb | 220 ++--- .../active_record_adapter_spec.rb | 753 ++++++++++-------- .../has_and_belongs_to_many_spec.rb | 28 +- spec/spec_helper.rb | 5 + 10 files changed, 719 insertions(+), 570 deletions(-) create mode 100644 lib/cancan/config.rb diff --git a/lib/cancan.rb b/lib/cancan.rb index 955589c1..80d7a24d 100644 --- a/lib/cancan.rb +++ b/lib/cancan.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'cancan/version' +require 'cancan/config' require 'cancan/parameter_validators' require 'cancan/ability' require 'cancan/rule' diff --git a/lib/cancan/config.rb b/lib/cancan/config.rb new file mode 100644 index 00000000..9334226f --- /dev/null +++ b/lib/cancan/config.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module CanCan + VALID_ACCESSIBLE_BY_STRATEGIES = %i[ + subquery + left_join + ].freeze + + # Determines how CanCan should build queries when calling accessible_by, + # if the query will contain a join. The default strategy is `:subquery`. + # + # # config/initializers/cancan.rb + # CanCan.accessible_by_strategy = :subquery + # + # Valid strategies are: + # - :subquery - Creates a nested query with all joins, wrapped by a + # WHERE IN query. + # - :left_join - Calls the joins directly using `left_joins`, and + # ensures records are unique using `distinct`. Note that + # `distinct` is not reliable in some cases. See + # https://github.com/CanCanCommunity/cancancan/pull/605 + def self.accessible_by_strategy + @accessible_by_strategy || :subquery + end + + def self.accessible_by_strategy=(value) + unless VALID_ACCESSIBLE_BY_STRATEGIES.include?(value) + raise ArgumentError, "accessible_by_strategy must be one of #{VALID_ACCESSIBLE_BY_STRATEGIES.join(', ')}" + end + + @accessible_by_strategy = value + end +end diff --git a/lib/cancan/model_adapters/active_record_4_adapter.rb b/lib/cancan/model_adapters/active_record_4_adapter.rb index 4755240f..bd9665b1 100644 --- a/lib/cancan/model_adapters/active_record_4_adapter.rb +++ b/lib/cancan/model_adapters/active_record_4_adapter.rb @@ -36,8 +36,21 @@ def matches_condition?(subject, name, value) # in addition to `includes()` to force the outer join. def build_relation(*where_conditions) relation = @model_class.where(*where_conditions) - relation = relation.includes(joins).references(joins) if joins.present? - relation + + if joins.present? + case CanCan.accessible_by_strategy + when :subquery + inner = @model_class.unscoped do + relation.includes(joins).references(joins) + end + @model_class.where(@model_class.primary_key => inner) + + when :left_join + relation.includes(joins).references(joins) + end + else + relation + end end # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions` diff --git a/lib/cancan/model_adapters/active_record_5_adapter.rb b/lib/cancan/model_adapters/active_record_5_adapter.rb index f7c5df8d..ed45813e 100644 --- a/lib/cancan/model_adapters/active_record_5_adapter.rb +++ b/lib/cancan/model_adapters/active_record_5_adapter.rb @@ -22,13 +22,21 @@ def self.matches_condition?(subject, name, value) private def build_relation(*where_conditions) + relation = @model_class.where(*where_conditions) + if joins.present? - inner = @model_class.unscoped do - @model_class.left_joins(joins).where(*where_conditions) + case CanCan.accessible_by_strategy + when :subquery + inner = @model_class.unscoped do + relation.left_joins(joins) + end + @model_class.where(@model_class.primary_key => inner) + + when :left_join + relation.left_joins(joins).distinct end - @model_class.where(@model_class.primary_key => inner) else - @model_class.where(*where_conditions) + relation end end diff --git a/spec/cancan/model_adapters/accessible_by_integration_spec.rb b/spec/cancan/model_adapters/accessible_by_integration_spec.rb index 89f76659..f5a2ae93 100644 --- a/spec/cancan/model_adapters/accessible_by_integration_spec.rb +++ b/spec/cancan/model_adapters/accessible_by_integration_spec.rb @@ -76,7 +76,7 @@ class Editor < ActiveRecord::Base describe 'preloading of associatons' do it 'preloads associations correctly' do - posts = Post.accessible_by(ability).where(published: true).includes(likes: :user) + posts = Post.accessible_by(ability).includes(likes: :user) expect(posts[0].association(:likes)).to be_loaded expect(posts[0].likes[0].association(:user)).to be_loaded end @@ -92,7 +92,7 @@ class Editor < ActiveRecord::Base if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') describe 'selecting custom columns' do it 'extracts custom columns correctly' do - posts = Post.accessible_by(ability).where(published: true).select('title as mytitle') + posts = Post.accessible_by(ability).select('title as mytitle') expect(posts[0].mytitle).to eq 'post1' end end diff --git a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb index d238ce1e..59b3977d 100644 --- a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb @@ -4,148 +4,152 @@ if CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0') describe CanCan::ModelAdapters::ActiveRecord4Adapter do - context 'with sqlite3' do - before :each do - connect_db - ActiveRecord::Migration.verbose = false - ActiveRecord::Schema.define do - create_table(:parents) do |t| - t.timestamps null: false + CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| + context "with sqlite3 and #{strategy} strategy" do + before :each do + CanCan.accessible_by_strategy = strategy + + connect_db + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define do + create_table(:parents) do |t| + t.timestamps null: false + end + + create_table(:children) do |t| + t.timestamps null: false + t.integer :parent_id + end end - create_table(:children) do |t| - t.timestamps null: false - t.integer :parent_id + class Parent < ActiveRecord::Base + has_many :children, -> { order(id: :desc) } end - end - class Parent < ActiveRecord::Base - has_many :children, -> { order(id: :desc) } - end + class Child < ActiveRecord::Base + belongs_to :parent + end - class Child < ActiveRecord::Base - belongs_to :parent + (@ability = double).extend(CanCan::Ability) end - (@ability = double).extend(CanCan::Ability) - end + it 'respects scope on included associations' do + @ability.can :read, [Parent, Child] - it 'respects scope on included associations' do - @ability.can :read, [Parent, Child] + parent = Parent.create! + child1 = Child.create!(parent: parent, created_at: 1.hours.ago) + child2 = Child.create!(parent: parent, created_at: 2.hours.ago) - parent = Parent.create! - child1 = Child.create!(parent: parent, created_at: 1.hours.ago) - child2 = Child.create!(parent: parent, created_at: 2.hours.ago) - - expect(Parent.accessible_by(@ability).order(created_at: :asc).includes(:children).first.children) - .to eq [child2, child1] - end + expect(Parent.accessible_by(@ability).order(created_at: :asc).includes(:children).first.children) + .to eq [child2, child1] + end - if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('4.1.0') - it 'allows filters on enums' do - ActiveRecord::Schema.define do - create_table(:shapes) do |t| - t.integer :color, default: 0, null: false + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('4.1.0') + it 'allows filters on enums' do + ActiveRecord::Schema.define do + create_table(:shapes) do |t| + t.integer :color, default: 0, null: false + end end - end - class Shape < ActiveRecord::Base - enum color: %i[red green blue] unless defined_enums.key? 'color' - end + class Shape < ActiveRecord::Base + enum color: %i[red green blue] unless defined_enums.key? 'color' + end - red = Shape.create!(color: :red) - green = Shape.create!(color: :green) - blue = Shape.create!(color: :blue) + red = Shape.create!(color: :red) + green = Shape.create!(color: :green) + blue = Shape.create!(color: :blue) - # A condition with a single value. - @ability.can :read, Shape, color: Shape.colors[:green] + # A condition with a single value. + @ability.can :read, Shape, color: Shape.colors[:green] - expect(@ability.cannot?(:read, red)).to be true - expect(@ability.can?(:read, green)).to be true - expect(@ability.cannot?(:read, blue)).to be true + expect(@ability.cannot?(:read, red)).to be true + expect(@ability.can?(:read, green)).to be true + expect(@ability.cannot?(:read, blue)).to be true - accessible = Shape.accessible_by(@ability) - expect(accessible).to contain_exactly(green) + accessible = Shape.accessible_by(@ability) + expect(accessible).to contain_exactly(green) - # A condition with multiple values. - @ability.can :update, Shape, color: [Shape.colors[:red], - Shape.colors[:blue]] + # A condition with multiple values. + @ability.can :update, Shape, color: [Shape.colors[:red], + Shape.colors[:blue]] - expect(@ability.can?(:update, red)).to be true - expect(@ability.cannot?(:update, green)).to be true - expect(@ability.can?(:update, blue)).to be true + expect(@ability.can?(:update, red)).to be true + expect(@ability.cannot?(:update, green)).to be true + expect(@ability.can?(:update, blue)).to be true - accessible = Shape.accessible_by(@ability, :update) - expect(accessible).to contain_exactly(red, blue) - end + accessible = Shape.accessible_by(@ability, :update) + expect(accessible).to contain_exactly(red, blue) + end - it 'allows dual filter on enums' do - ActiveRecord::Schema.define do - create_table(:discs) do |t| - t.integer :color, default: 0, null: false - t.integer :shape, default: 3, null: false + it 'allows dual filter on enums' do + ActiveRecord::Schema.define do + create_table(:discs) do |t| + t.integer :color, default: 0, null: false + t.integer :shape, default: 3, null: false + end end - end - class Disc < ActiveRecord::Base - enum color: %i[red green blue] unless defined_enums.key? 'color' - enum shape: { triangle: 3, rectangle: 4 } unless defined_enums.key? 'shape' - end + class Disc < ActiveRecord::Base + enum color: %i[red green blue] unless defined_enums.key? 'color' + enum shape: { triangle: 3, rectangle: 4 } unless defined_enums.key? 'shape' + end - red_triangle = Disc.create!(color: Disc.colors[:red], shape: Disc.shapes[:triangle]) - green_triangle = Disc.create!(color: Disc.colors[:green], shape: Disc.shapes[:triangle]) - green_rectangle = Disc.create!(color: Disc.colors[:green], shape: Disc.shapes[:rectangle]) - blue_rectangle = Disc.create!(color: Disc.colors[:blue], shape: Disc.shapes[:rectangle]) + red_triangle = Disc.create!(color: Disc.colors[:red], shape: Disc.shapes[:triangle]) + green_triangle = Disc.create!(color: Disc.colors[:green], shape: Disc.shapes[:triangle]) + green_rectangle = Disc.create!(color: Disc.colors[:green], shape: Disc.shapes[:rectangle]) + blue_rectangle = Disc.create!(color: Disc.colors[:blue], shape: Disc.shapes[:rectangle]) - # A condition with a dual filter. - @ability.can :read, Disc, color: Disc.colors[:green], shape: Disc.shapes[:rectangle] + # A condition with a dual filter. + @ability.can :read, Disc, color: Disc.colors[:green], shape: Disc.shapes[:rectangle] - expect(@ability.cannot?(:read, red_triangle)).to be true - expect(@ability.cannot?(:read, green_triangle)).to be true - expect(@ability.can?(:read, green_rectangle)).to be true - expect(@ability.cannot?(:read, blue_rectangle)).to be true + expect(@ability.cannot?(:read, red_triangle)).to be true + expect(@ability.cannot?(:read, green_triangle)).to be true + expect(@ability.can?(:read, green_rectangle)).to be true + expect(@ability.cannot?(:read, blue_rectangle)).to be true - accessible = Disc.accessible_by(@ability) - expect(accessible).to contain_exactly(green_rectangle) + accessible = Disc.accessible_by(@ability) + expect(accessible).to contain_exactly(green_rectangle) + end end end - end - context 'with postgresql' do - before :each do - connect_db - ActiveRecord::Migration.verbose = false - ActiveRecord::Schema.define do - create_table(:parents) do |t| - t.timestamps null: false + context 'with postgresql' do + before :each do + connect_db + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define do + create_table(:parents) do |t| + t.timestamps null: false + end + + create_table(:children) do |t| + t.timestamps null: false + t.integer :parent_id + end end - create_table(:children) do |t| - t.timestamps null: false - t.integer :parent_id + class Parent < ActiveRecord::Base + has_many :children, -> { order(id: :desc) } end - end - class Parent < ActiveRecord::Base - has_many :children, -> { order(id: :desc) } - end + class Child < ActiveRecord::Base + belongs_to :parent + end - class Child < ActiveRecord::Base - belongs_to :parent + (@ability = double).extend(CanCan::Ability) end - (@ability = double).extend(CanCan::Ability) - end - - it 'allows overlapping conditions in SQL and merge with hash conditions' do - @ability.can :read, Parent, children: { parent_id: 1 } - @ability.can :read, Parent, children: { parent_id: 1 } + it 'allows overlapping conditions in SQL and merge with hash conditions' do + @ability.can :read, Parent, children: { parent_id: 1 } + @ability.can :read, Parent, children: { parent_id: 1 } - parent = Parent.create! - Child.create!(parent: parent, created_at: 1.hours.ago) - Child.create!(parent: parent, created_at: 2.hours.ago) + parent = Parent.create! + Child.create!(parent: parent, created_at: 1.hours.ago) + Child.create!(parent: parent, created_at: 2.hours.ago) - expect(Parent.accessible_by(@ability)).to eq([parent]) + expect(Parent.accessible_by(@ability)).to eq([parent]) + end end end end diff --git a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb index fb0edd5a..0b7450b9 100644 --- a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb @@ -4,158 +4,162 @@ if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') describe CanCan::ModelAdapters::ActiveRecord5Adapter do - context 'with sqlite3' do - before :each do - connect_db - ActiveRecord::Migration.verbose = false - - ActiveRecord::Schema.define do - create_table(:shapes) do |t| - t.integer :color, default: 0, null: false - end + CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| + context "with sqlite3 and #{strategy} strategy" do + before :each do + CanCan.accessible_by_strategy = strategy - create_table(:things) do |t| - t.string :size, default: 'big', null: false - end + connect_db + ActiveRecord::Migration.verbose = false + + ActiveRecord::Schema.define do + create_table(:shapes) do |t| + t.integer :color, default: 0, null: false + end - create_table(:discs) do |t| - t.integer :color, default: 0, null: false - t.integer :shape, default: 3, null: false + create_table(:things) do |t| + t.string :size, default: 'big', null: false + end + + create_table(:discs) do |t| + t.integer :color, default: 0, null: false + t.integer :shape, default: 3, null: false + end end - end - unless defined?(Thing) - class Thing < ActiveRecord::Base - enum size: { big: 'big', medium: 'average', small: 'small' } + unless defined?(Thing) + class Thing < ActiveRecord::Base + enum size: { big: 'big', medium: 'average', small: 'small' } + end end - end - unless defined?(Shape) - class Shape < ActiveRecord::Base - enum color: %i[red green blue] + unless defined?(Shape) + class Shape < ActiveRecord::Base + enum color: %i[red green blue] + end end - end - unless defined?(Disc) - class Disc < ActiveRecord::Base - enum color: %i[red green blue] - enum shape: { triangle: 3, rectangle: 4 } + unless defined?(Disc) + class Disc < ActiveRecord::Base + enum color: %i[red green blue] + enum shape: { triangle: 3, rectangle: 4 } + end end end - end - subject(:ability) { Ability.new(nil) } + subject(:ability) { Ability.new(nil) } - context 'when enums use integers as values' do - let(:red) { Shape.create!(color: :red) } - let(:green) { Shape.create!(color: :green) } - let(:blue) { Shape.create!(color: :blue) } + context 'when enums use integers as values' do + let(:red) { Shape.create!(color: :red) } + let(:green) { Shape.create!(color: :green) } + let(:blue) { Shape.create!(color: :blue) } - context 'when the condition contains a single value' do - before do - ability.can :read, Shape, color: :green - end + context 'when the condition contains a single value' do + before do + ability.can :read, Shape, color: :green + end - it 'can check ability on single models' do - is_expected.not_to be_able_to(:read, red) - is_expected.to be_able_to(:read, green) - is_expected.not_to be_able_to(:read, blue) - end + it 'can check ability on single models' do + is_expected.not_to be_able_to(:read, red) + is_expected.to be_able_to(:read, green) + is_expected.not_to be_able_to(:read, blue) + end - it 'can use accessible_by helper' do - accessible = Shape.accessible_by(ability) - expect(accessible).to contain_exactly(green) + it 'can use accessible_by helper' do + accessible = Shape.accessible_by(ability) + expect(accessible).to contain_exactly(green) + end end - end - context 'when the condition contains multiple values' do - before do - ability.can :update, Shape, color: %i[red blue] - end + context 'when the condition contains multiple values' do + before do + ability.can :update, Shape, color: %i[red blue] + end - it 'can check ability on single models' do - is_expected.to be_able_to(:update, red) - is_expected.not_to be_able_to(:update, green) - is_expected.to be_able_to(:update, blue) - end + it 'can check ability on single models' do + is_expected.to be_able_to(:update, red) + is_expected.not_to be_able_to(:update, green) + is_expected.to be_able_to(:update, blue) + end - it 'can use accessible_by helper' do - accessible = Shape.accessible_by(ability, :update) - expect(accessible).to contain_exactly(red, blue) + it 'can use accessible_by helper' do + accessible = Shape.accessible_by(ability, :update) + expect(accessible).to contain_exactly(red, blue) + end end end - end - context 'when enums use strings as values' do - let(:big) { Thing.create!(size: :big) } - let(:medium) { Thing.create!(size: :medium) } - let(:small) { Thing.create!(size: :small) } + context 'when enums use strings as values' do + let(:big) { Thing.create!(size: :big) } + let(:medium) { Thing.create!(size: :medium) } + let(:small) { Thing.create!(size: :small) } - context 'when the condition contains a single value' do - before do - ability.can :read, Thing, size: :medium - end + context 'when the condition contains a single value' do + before do + ability.can :read, Thing, size: :medium + end - it 'can check ability on single models' do - is_expected.not_to be_able_to(:read, big) - is_expected.to be_able_to(:read, medium) - is_expected.not_to be_able_to(:read, small) - end + it 'can check ability on single models' do + is_expected.not_to be_able_to(:read, big) + is_expected.to be_able_to(:read, medium) + is_expected.not_to be_able_to(:read, small) + end - it 'can use accessible_by helper' do - expect(Thing.accessible_by(ability)).to contain_exactly(medium) + it 'can use accessible_by helper' do + expect(Thing.accessible_by(ability)).to contain_exactly(medium) + end + + context 'when a rule is overriden' do + before do + ability.cannot :read, Thing, size: 'average' + end + + it 'is recognised correctly' do + is_expected.not_to be_able_to(:read, medium) + expect(Thing.accessible_by(ability)).to be_empty + end + end end - context 'when a rule is overriden' do + context 'when the condition contains multiple values' do before do - ability.cannot :read, Thing, size: 'average' + ability.can :update, Thing, size: %i[big small] end - it 'is recognised correctly' do - is_expected.not_to be_able_to(:read, medium) - expect(Thing.accessible_by(ability)).to be_empty + it 'can check ability on single models' do + is_expected.to be_able_to(:update, big) + is_expected.not_to be_able_to(:update, medium) + is_expected.to be_able_to(:update, small) + end + + it 'can use accessible_by helper' do + expect(Thing.accessible_by(ability, :update)).to contain_exactly(big, small) end end end - context 'when the condition contains multiple values' do + context 'when multiple enums are present' do + let(:red_triangle) { Disc.create!(color: :red, shape: :triangle) } + let(:green_triangle) { Disc.create!(color: :green, shape: :triangle) } + let(:green_rectangle) { Disc.create!(color: :green, shape: :rectangle) } + let(:blue_rectangle) { Disc.create!(color: :blue, shape: :rectangle) } + before do - ability.can :update, Thing, size: %i[big small] + ability.can :read, Disc, color: :green, shape: :rectangle end it 'can check ability on single models' do - is_expected.to be_able_to(:update, big) - is_expected.not_to be_able_to(:update, medium) - is_expected.to be_able_to(:update, small) + is_expected.not_to be_able_to(:read, red_triangle) + is_expected.not_to be_able_to(:read, green_triangle) + is_expected.to be_able_to(:read, green_rectangle) + is_expected.not_to be_able_to(:read, blue_rectangle) end it 'can use accessible_by helper' do - expect(Thing.accessible_by(ability, :update)).to contain_exactly(big, small) + expect(Disc.accessible_by(ability)).to contain_exactly(green_rectangle) end end end - - context 'when multiple enums are present' do - let(:red_triangle) { Disc.create!(color: :red, shape: :triangle) } - let(:green_triangle) { Disc.create!(color: :green, shape: :triangle) } - let(:green_rectangle) { Disc.create!(color: :green, shape: :rectangle) } - let(:blue_rectangle) { Disc.create!(color: :blue, shape: :rectangle) } - - before do - ability.can :read, Disc, color: :green, shape: :rectangle - end - - it 'can check ability on single models' do - is_expected.not_to be_able_to(:read, red_triangle) - is_expected.not_to be_able_to(:read, green_triangle) - is_expected.to be_able_to(:read, green_rectangle) - is_expected.not_to be_able_to(:read, blue_rectangle) - end - - it 'can use accessible_by helper' do - expect(Disc.accessible_by(ability)).to contain_exactly(green_rectangle) - end - end end end end diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index b653cff4..31268f32 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -109,338 +109,346 @@ class User < ActiveRecord::Base @comment_table = Comment.table_name end - it 'does not fires query with accessible_by() for abilities defined with association' do - user = User.create! - @ability.can :edit, Article, user.articles.unpopular - callback = ->(*) { raise 'No query expected' } - - ActiveSupport::Notifications.subscribed(callback, 'sql.active_record') do - Article.accessible_by(@ability, :edit) - nil - end - end + CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| + context "base functionality with #{strategy} strategy" do + before :each do + CanCan.accessible_by_strategy = strategy + end - it 'fetches only the articles that are published' do - @ability.can :read, Article, published: true - article1 = Article.create!(published: true) - Article.create!(published: false) - expect(Article.accessible_by(@ability)).to eq([article1]) - end + it 'does not fires query with accessible_by() for abilities defined with association' do + user = User.create! + @ability.can :edit, Article, user.articles.unpopular + callback = ->(*) { raise 'No query expected' } - it 'is for only active record classes' do - if ActiveRecord.version > Gem::Version.new('5') - expect(CanCan::ModelAdapters::ActiveRecord5Adapter).to_not be_for_class(Object) - expect(CanCan::ModelAdapters::ActiveRecord5Adapter).to be_for_class(Article) - expect(CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article)) - .to eq(CanCan::ModelAdapters::ActiveRecord5Adapter) - elsif ActiveRecord.version > Gem::Version.new('4') - expect(CanCan::ModelAdapters::ActiveRecord4Adapter).to_not be_for_class(Object) - expect(CanCan::ModelAdapters::ActiveRecord4Adapter).to be_for_class(Article) - expect(CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article)) - .to eq(CanCan::ModelAdapters::ActiveRecord4Adapter) - end - end + ActiveSupport::Notifications.subscribed(callback, 'sql.active_record') do + Article.accessible_by(@ability, :edit) + nil + end + end - it 'finds record' do - article = Article.create! - adapter = CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article) - expect(adapter.find(Article, article.id)).to eq(article) - end + it 'fetches only the articles that are published' do + @ability.can :read, Article, published: true + article1 = Article.create!(published: true) + Article.create!(published: false) + expect(Article.accessible_by(@ability)).to eq([article1]) + end - it 'does not fetch any records when no abilities are defined' do - Article.create! - expect(Article.accessible_by(@ability)).to be_empty - end + it 'is for only active record classes' do + if ActiveRecord.version > Gem::Version.new('5') + expect(CanCan::ModelAdapters::ActiveRecord5Adapter).to_not be_for_class(Object) + expect(CanCan::ModelAdapters::ActiveRecord5Adapter).to be_for_class(Article) + expect(CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article)) + .to eq(CanCan::ModelAdapters::ActiveRecord5Adapter) + elsif ActiveRecord.version > Gem::Version.new('4') + expect(CanCan::ModelAdapters::ActiveRecord4Adapter).to_not be_for_class(Object) + expect(CanCan::ModelAdapters::ActiveRecord4Adapter).to be_for_class(Article) + expect(CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article)) + .to eq(CanCan::ModelAdapters::ActiveRecord4Adapter) + end + end - it 'fetches all articles when one can read all' do - @ability.can :read, Article - article = Article.create! - expect(Article.accessible_by(@ability)).to match_array([article]) - end + it 'finds record' do + article = Article.create! + adapter = CanCan::ModelAdapters::AbstractAdapter.adapter_class(Article) + expect(adapter.find(Article, article.id)).to eq(article) + end - it 'fetches only the articles that are published' do - @ability.can :read, Article, published: true - article1 = Article.create!(published: true) - Article.create!(published: false) - expect(Article.accessible_by(@ability)).to match_array([article1]) - end + it 'does not fetch any records when no abilities are defined' do + Article.create! + expect(Article.accessible_by(@ability)).to be_empty + end - it 'fetches any articles which are published or secret' do - @ability.can :read, Article, published: true - @ability.can :read, Article, secret: true - article1 = Article.create!(published: true, secret: false) - article2 = Article.create!(published: true, secret: true) - article3 = Article.create!(published: false, secret: true) - Article.create!(published: false, secret: false) - expect(Article.accessible_by(@ability)).to match_array([article1, article2, article3]) - end + it 'fetches all articles when one can read all' do + @ability.can :read, Article + article = Article.create! + expect(Article.accessible_by(@ability)).to match_array([article]) + end - it 'fetches any articles which we are cited in' do - user = User.create! - cited = Article.create! - Article.create! - cited.mentioned_users << user - @ability.can :read, Article, mentioned_users: { id: user.id } - @ability.can :read, Article, mentions: { user_id: user.id } - expect(Article.accessible_by(@ability)).to match_array([cited]) - end + it 'fetches only the articles that are published' do + @ability.can :read, Article, published: true + article1 = Article.create!(published: true) + Article.create!(published: false) + expect(Article.accessible_by(@ability)).to match_array([article1]) + end - it 'fetches only the articles that are published and not secret' do - @ability.can :read, Article, published: true - @ability.cannot :read, Article, secret: true - article1 = Article.create!(published: true, secret: false) - Article.create!(published: true, secret: true) - Article.create!(published: false, secret: true) - Article.create!(published: false, secret: false) - expect(Article.accessible_by(@ability)).to match_array([article1]) - end + it 'fetches any articles which are published or secret' do + @ability.can :read, Article, published: true + @ability.can :read, Article, secret: true + article1 = Article.create!(published: true, secret: false) + article2 = Article.create!(published: true, secret: true) + article3 = Article.create!(published: false, secret: true) + Article.create!(published: false, secret: false) + expect(Article.accessible_by(@ability)).to match_array([article1, article2, article3]) + end - it 'only reads comments for articles which are published' do - @ability.can :read, Comment, article: { published: true } - comment1 = Comment.create!(article: Article.create!(published: true)) - Comment.create!(article: Article.create!(published: false)) - expect(Comment.accessible_by(@ability)).to match_array([comment1]) - end + it 'fetches any articles which we are cited in' do + user = User.create! + cited = Article.create! + Article.create! + cited.mentioned_users << user + @ability.can :read, Article, mentioned_users: { id: user.id } + @ability.can :read, Article, mentions: { user_id: user.id } + expect(Article.accessible_by(@ability)).to match_array([cited]) + end - it 'should only read articles which are published or in visible categories' do - @ability.can :read, Article, category: { visible: true } - @ability.can :read, Article, published: true - article1 = Article.create!(published: true) - Article.create!(published: false) - article3 = Article.create!(published: false, category: Category.create!(visible: true)) - expect(Article.accessible_by(@ability)).to match_array([article1, article3]) - end + it 'fetches only the articles that are published and not secret' do + @ability.can :read, Article, published: true + @ability.cannot :read, Article, secret: true + article1 = Article.create!(published: true, secret: false) + Article.create!(published: true, secret: true) + Article.create!(published: false, secret: true) + Article.create!(published: false, secret: false) + expect(Article.accessible_by(@ability)).to match_array([article1]) + end - it 'should only read categories once even if they have multiple articles' do - @ability.can :read, Category, articles: { published: true } - @ability.can :read, Article, published: true - category = Category.create! - Article.create!(published: true, category: category) - Article.create!(published: true, category: category) - expect(Category.accessible_by(@ability)).to match_array([category]) - end + it 'only reads comments for articles which are published' do + @ability.can :read, Comment, article: { published: true } + comment1 = Comment.create!(article: Article.create!(published: true)) + Comment.create!(article: Article.create!(published: false)) + expect(Comment.accessible_by(@ability)).to match_array([comment1]) + end - it 'only reads comments for visible categories through articles' do - @ability.can :read, Comment, article: { category: { visible: true } } - comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) - Comment.create!(article: Article.create!(category: Category.create!(visible: false))) - expect(Comment.accessible_by(@ability)).to match_array([comment1]) - expect(Comment.accessible_by(@ability).count).to eq(1) - end + it 'should only read articles which are published or in visible categories' do + @ability.can :read, Article, category: { visible: true } + @ability.can :read, Article, published: true + article1 = Article.create!(published: true) + Article.create!(published: false) + article3 = Article.create!(published: false, category: Category.create!(visible: true)) + expect(Article.accessible_by(@ability)).to match_array([article1, article3]) + end - it 'allows ordering via relations' do - @ability.can :read, Comment, article: { category: { visible: true } } - comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) - Comment.create!(article: Article.create!(category: Category.create!(visible: false))) - expect(Comment.accessible_by(@ability).joins(:article).order('articles.id')).to match_array([comment1]) - end + it 'should only read categories once even if they have multiple articles' do + @ability.can :read, Category, articles: { published: true } + @ability.can :read, Article, published: true + category = Category.create! + Article.create!(published: true, category: category) + Article.create!(published: true, category: category) + expect(Category.accessible_by(@ability)).to match_array([category]) + end - it 'allows conditions in SQL and merge with hash conditions' do - @ability.can :read, Article, published: true - @ability.can :read, Article, ['secret=?', true] - article1 = Article.create!(published: true, secret: false) - article2 = Article.create!(published: true, secret: true) - article3 = Article.create!(published: false, secret: true) - Article.create!(published: false, secret: false) - expect(Article.accessible_by(@ability)).to match_array([article1, article2, article3]) - end + it 'only reads comments for visible categories through articles' do + @ability.can :read, Comment, article: { category: { visible: true } } + comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) + Comment.create!(article: Article.create!(category: Category.create!(visible: false))) + expect(Comment.accessible_by(@ability)).to match_array([comment1]) + expect(Comment.accessible_by(@ability).count).to eq(1) + end - it 'allows a scope for conditions' do - @ability.can :read, Article, Article.where(secret: true) - article1 = Article.create!(secret: true) - Article.create!(secret: false) - expect(Article.accessible_by(@ability)).to match_array([article1]) - end + it 'allows ordering via relations' do + @ability.can :read, Comment, article: { category: { visible: true } } + comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) + Comment.create!(article: Article.create!(category: Category.create!(visible: false))) + expect(Comment.accessible_by(@ability).joins(:article).order('articles.id')).to match_array([comment1]) + end - it 'fetches only associated records when using with a scope for conditions' do - @ability.can :read, Article, Article.where(secret: true) - category1 = Category.create!(visible: false) - category2 = Category.create!(visible: true) - article1 = Article.create!(secret: true, category: category1) - Article.create!(secret: true, category: category2) - expect(category1.articles.accessible_by(@ability)).to match_array([article1]) - end + it 'allows conditions in SQL and merge with hash conditions' do + @ability.can :read, Article, published: true + @ability.can :read, Article, ['secret=?', true] + article1 = Article.create!(published: true, secret: false) + article2 = Article.create!(published: true, secret: true) + article3 = Article.create!(published: false, secret: true) + Article.create!(published: false, secret: false) + expect(Article.accessible_by(@ability)).to match_array([article1, article2, article3]) + end - it 'raises an exception when trying to merge scope with other conditions' do - @ability.can :read, Article, published: true - @ability.can :read, Article, Article.where(secret: true) - expect(-> { Article.accessible_by(@ability) }) - .to raise_error(CanCan::Error, - 'Unable to merge an Active Record scope with other conditions. '\ - 'Instead use a hash or SQL for read Article ability.') - end + it 'allows a scope for conditions' do + @ability.can :read, Article, Article.where(secret: true) + article1 = Article.create!(secret: true) + Article.create!(secret: false) + expect(Article.accessible_by(@ability)).to match_array([article1]) + end - it 'does not raise an exception when the rule with scope is suppressed' do - @ability.can :read, Article, published: true - @ability.can :read, Article, Article.where(secret: true) - @ability.cannot :read, Article - expect(-> { Article.accessible_by(@ability) }).not_to raise_error - end + it 'fetches only associated records when using with a scope for conditions' do + @ability.can :read, Article, Article.where(secret: true) + category1 = Category.create!(visible: false) + category2 = Category.create!(visible: true) + article1 = Article.create!(secret: true, category: category1) + Article.create!(secret: true, category: category2) + expect(category1.articles.accessible_by(@ability)).to match_array([article1]) + end - it 'recognises empty scopes and compresses them' do - @ability.can :read, Article, published: true - @ability.can :read, Article, Article.all - expect(-> { Article.accessible_by(@ability) }).not_to raise_error - end + it 'raises an exception when trying to merge scope with other conditions' do + @ability.can :read, Article, published: true + @ability.can :read, Article, Article.where(secret: true) + expect(-> { Article.accessible_by(@ability) }) + .to raise_error(CanCan::Error, + 'Unable to merge an Active Record scope with other conditions. '\ + 'Instead use a hash or SQL for read Article ability.') + end - it 'does not allow to fetch records when ability with just block present' do - @ability.can :read, Article do - false - end - expect(-> { Article.accessible_by(@ability) }).to raise_error(CanCan::Error) - end + it 'does not raise an exception when the rule with scope is suppressed' do + @ability.can :read, Article, published: true + @ability.can :read, Article, Article.where(secret: true) + @ability.cannot :read, Article + expect(-> { Article.accessible_by(@ability) }).not_to raise_error + end - it 'should support more than one deeply nested conditions' do - @ability.can :read, Comment, article: { - category: { - name: 'foo', visible: true - } - } - expect { Comment.accessible_by(@ability) }.to_not raise_error - end + it 'recognises empty scopes and compresses them' do + @ability.can :read, Article, published: true + @ability.can :read, Article, Article.all + expect(-> { Article.accessible_by(@ability) }).not_to raise_error + end - it 'does not allow to check ability on object against SQL conditions without block' do - @ability.can :read, Article, ['secret=?', true] - expect(-> { @ability.can? :read, Article.new }).to raise_error(CanCan::Error) - end + it 'does not allow to fetch records when ability with just block present' do + @ability.can :read, Article do + false + end + expect(-> { Article.accessible_by(@ability) }).to raise_error(CanCan::Error) + end - it 'has false conditions if no abilities match' do - expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) - end + it 'should support more than one deeply nested conditions' do + @ability.can :read, Comment, article: { + category: { + name: 'foo', visible: true + } + } + expect { Comment.accessible_by(@ability) }.to_not raise_error + end - it 'returns false conditions for cannot clause' do - @ability.cannot :read, Article - expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) - end + it 'does not allow to check ability on object against SQL conditions without block' do + @ability.can :read, Article, ['secret=?', true] + expect(-> { @ability.can? :read, Article.new }).to raise_error(CanCan::Error) + end - it 'returns SQL for single `can` definition in front of default `cannot` condition' do - @ability.cannot :read, Article - @ability.can :read, Article, published: false, secret: true - expect(@ability.model_adapter(Article, :read)).to generate_sql(%( -SELECT "articles".* -FROM "articles" -WHERE "articles"."published" = #{false_v} AND "articles"."secret" = #{true_v})) - end + it 'has false conditions if no abilities match' do + expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) + end - it 'returns true condition for single `can` definition in front of default `can` condition' do - @ability.can :read, Article - @ability.can :read, Article, published: false, secret: true - expect(@ability.model_adapter(Article, :read).conditions).to eq({}) - expect(@ability.model_adapter(Article, :read)).to generate_sql(%(SELECT "articles".* FROM "articles")) - end + it 'returns false conditions for cannot clause' do + @ability.cannot :read, Article + expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) + end - it 'returns `false condition` for single `cannot` definition in front of default `cannot` condition' do - @ability.cannot :read, Article - @ability.cannot :read, Article, published: false, secret: true - expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) - end + it 'returns SQL for single `can` definition in front of default `cannot` condition' do + @ability.cannot :read, Article + @ability.can :read, Article, published: false, secret: true + expect(@ability.model_adapter(Article, :read)).to generate_sql(%( + SELECT "articles".* + FROM "articles" + WHERE "articles"."published" = #{false_v} AND "articles"."secret" = #{true_v})) + end - it 'returns `not (sql)` for single `cannot` definition in front of default `can` condition' do - @ability.can :read, Article - @ability.cannot :read, Article, published: false, secret: true - expect(@ability.model_adapter(Article, :read).conditions) - .to orderlessly_match( - %["not (#{@article_table}"."published" = #{false_v} AND "#{@article_table}"."secret" = #{true_v})] - ) - end + it 'returns true condition for single `can` definition in front of default `can` condition' do + @ability.can :read, Article + @ability.can :read, Article, published: false, secret: true + expect(@ability.model_adapter(Article, :read).conditions).to eq({}) + expect(@ability.model_adapter(Article, :read)).to generate_sql(%(SELECT "articles".* FROM "articles")) + end - it 'returns appropriate sql conditions in complex case' do - @ability.can :read, Article - @ability.can :manage, Article, id: 1 - @ability.can :update, Article, published: true - @ability.cannot :update, Article, secret: true - expect(@ability.model_adapter(Article, :update).conditions) - .to eq(%[not ("#{@article_table}"."secret" = #{true_v}) ] + - %[AND (("#{@article_table}"."published" = #{true_v}) ] + - %[OR ("#{@article_table}"."id" = 1))]) - expect(@ability.model_adapter(Article, :manage).conditions).to eq(id: 1) - expect(@ability.model_adapter(Article, :read).conditions).to eq({}) - expect(@ability.model_adapter(Article, :read)).to generate_sql(%(SELECT "articles".* FROM "articles")) - end + it 'returns `false condition` for single `cannot` definition in front of default `cannot` condition' do + @ability.cannot :read, Article + @ability.cannot :read, Article, published: false, secret: true + expect(@ability.model_adapter(Article, :read).conditions).to eq(false_condition) + end - it 'returns appropriate sql conditions in complex case with nested joins' do - @ability.can :read, Comment, article: { category: { visible: true } } - expect(@ability.model_adapter(Comment, :read).conditions).to eq(Category.table_name.to_sym => { visible: true }) - end + it 'returns `not (sql)` for single `cannot` definition in front of default `can` condition' do + @ability.can :read, Article + @ability.cannot :read, Article, published: false, secret: true + expect(@ability.model_adapter(Article, :read).conditions) + .to orderlessly_match( + %["not (#{@article_table}"."published" = #{false_v} AND "#{@article_table}"."secret" = #{true_v})] + ) + end - it 'returns appropriate sql conditions in complex case with nested joins of different depth' do - @ability.can :read, Comment, article: { published: true, category: { visible: true } } - expect(@ability.model_adapter(Comment, :read).conditions) - .to eq(Article.table_name.to_sym => { published: true }, Category.table_name.to_sym => { visible: true }) - end + it 'returns appropriate sql conditions in complex case' do + @ability.can :read, Article + @ability.can :manage, Article, id: 1 + @ability.can :update, Article, published: true + @ability.cannot :update, Article, secret: true + expect(@ability.model_adapter(Article, :update).conditions) + .to eq(%[not ("#{@article_table}"."secret" = #{true_v}) ] + + %[AND (("#{@article_table}"."published" = #{true_v}) ] + + %[OR ("#{@article_table}"."id" = 1))]) + expect(@ability.model_adapter(Article, :manage).conditions).to eq(id: 1) + expect(@ability.model_adapter(Article, :read).conditions).to eq({}) + expect(@ability.model_adapter(Article, :read)).to generate_sql(%(SELECT "articles".* FROM "articles")) + end - it 'does not forget conditions when calling with SQL string' do - @ability.can :read, Article, published: true - @ability.can :read, Article, ['secret = ?', false] - adapter = @ability.model_adapter(Article, :read) - 2.times do - expect(adapter.conditions).to eq(%[(secret = #{false_v}) OR ("#{@article_table}"."published" = #{true_v})]) - end - end + it 'returns appropriate sql conditions in complex case with nested joins' do + @ability.can :read, Comment, article: { category: { visible: true } } + expect(@ability.model_adapter(Comment, :read).conditions).to eq(Category.table_name.to_sym => { visible: true }) + end - it 'has nil joins if no rules' do - expect(@ability.model_adapter(Article, :read).joins).to be_nil - end + it 'returns appropriate sql conditions in complex case with nested joins of different depth' do + @ability.can :read, Comment, article: { published: true, category: { visible: true } } + expect(@ability.model_adapter(Comment, :read).conditions) + .to eq(Article.table_name.to_sym => { published: true }, Category.table_name.to_sym => { visible: true }) + end - it 'has nil joins if rules got compressed' do - @ability.can :read, Comment, article: { category: { visible: true } } - @ability.can :read, Comment - expect(@ability.model_adapter(Comment, :read)) - .to generate_sql("SELECT \"#{@comment_table}\".* FROM \"#{@comment_table}\"") - expect(@ability.model_adapter(Comment, :read).joins).to be_nil - end + it 'does not forget conditions when calling with SQL string' do + @ability.can :read, Article, published: true + @ability.can :read, Article, ['secret = ?', false] + adapter = @ability.model_adapter(Article, :read) + 2.times do + expect(adapter.conditions).to eq(%[(secret = #{false_v}) OR ("#{@article_table}"."published" = #{true_v})]) + end + end - it 'has nil joins if no nested hashes specified in conditions' do - @ability.can :read, Article, published: false - @ability.can :read, Article, secret: true - expect(@ability.model_adapter(Article, :read).joins).to be_nil - end + it 'has nil joins if no rules' do + expect(@ability.model_adapter(Article, :read).joins).to be_nil + end - it 'merges separate joins into a single array' do - @ability.can :read, Article, project: { blocked: false } - @ability.can :read, Article, company: { admin: true } - expect(@ability.model_adapter(Article, :read).joins.inspect).to orderlessly_match(%i[company project].inspect) - end + it 'has nil joins if rules got compressed' do + @ability.can :read, Comment, article: { category: { visible: true } } + @ability.can :read, Comment + expect(@ability.model_adapter(Comment, :read)) + .to generate_sql("SELECT \"#{@comment_table}\".* FROM \"#{@comment_table}\"") + expect(@ability.model_adapter(Comment, :read).joins).to be_nil + end - it 'merges same joins into a single array' do - @ability.can :read, Article, project: { blocked: false } - @ability.can :read, Article, project: { admin: true } - expect(@ability.model_adapter(Article, :read).joins).to eq([:project]) - end + it 'has nil joins if no nested hashes specified in conditions' do + @ability.can :read, Article, published: false + @ability.can :read, Article, secret: true + expect(@ability.model_adapter(Article, :read).joins).to be_nil + end - it 'merges nested and non-nested joins' do - @ability.can :read, Article, project: { blocked: false } - @ability.can :read, Article, project: { comments: { spam: true } } - expect(@ability.model_adapter(Article, :read).joins).to eq([{ project: [:comments] }]) - end + it 'merges separate joins into a single array' do + @ability.can :read, Article, project: { blocked: false } + @ability.can :read, Article, company: { admin: true } + expect(@ability.model_adapter(Article, :read).joins.inspect).to orderlessly_match(%i[company project].inspect) + end - it 'merges :all conditions with other conditions' do - user = User.create! - article = Article.create!(user: user) - ability = Ability.new(user) - ability.can :manage, :all - ability.can :manage, Article, user_id: user.id - expect(Article.accessible_by(ability)).to eq([article]) - end + it 'merges same joins into a single array' do + @ability.can :read, Article, project: { blocked: false } + @ability.can :read, Article, project: { admin: true } + expect(@ability.model_adapter(Article, :read).joins).to eq([:project]) + end - it 'should not execute a scope when checking ability on the class' do - relation = Article.where(secret: true) - @ability.can :read, Article, relation do |article| - article.secret == true - end + it 'merges nested and non-nested joins' do + @ability.can :read, Article, project: { blocked: false } + @ability.can :read, Article, project: { comments: { spam: true } } + expect(@ability.model_adapter(Article, :read).joins).to eq([{ project: [:comments] }]) + end - allow(relation).to receive(:count).and_raise('Unexpected scope execution.') + it 'merges :all conditions with other conditions' do + user = User.create! + article = Article.create!(user: user) + ability = Ability.new(user) + ability.can :manage, :all + ability.can :manage, Article, user_id: user.id + expect(Article.accessible_by(ability)).to eq([article]) + end - expect { @ability.can? :read, Article }.not_to raise_error - end + it 'should not execute a scope when checking ability on the class' do + relation = Article.where(secret: true) + @ability.can :read, Article, relation do |article| + article.secret == true + end - it 'should ignore cannot rules with attributes when querying' do - user = User.create! - article = Article.create!(user: user) - ability = Ability.new(user) - ability.can :read, Article - ability.cannot :read, Article, :secret - expect(Article.accessible_by(ability)).to eq([article]) + allow(relation).to receive(:count).and_raise('Unexpected scope execution.') + + expect { @ability.can? :read, Article }.not_to raise_error + end + + it 'should ignore cannot rules with attributes when querying' do + user = User.create! + article = Article.create!(user: user) + ability = Ability.new(user) + ability.can :read, Article + ability.cannot :read, Article, :secret + expect(Article.accessible_by(ability)).to eq([article]) + end + end end context 'with namespaced models' do @@ -531,25 +539,34 @@ class Transaction < ActiveRecord::Base end end - context 'when a table is references multiple times' do - it 'can filter correctly on the different associations' do - u1 = User.create!(name: 'pippo') - u2 = User.create!(name: 'paperino') - - a1 = Article.create!(user: u1) - a2 = Article.create!(user: u2) - - ability = Ability.new(u1) - ability.can :read, Article, user: { id: u1.id } - ability.can :read, Article, mentioned_users: { name: u1.name } - ability.can :read, Article, mentioned_users: { mentioned_articles: { id: a2.id } } - ability.can :read, Article, mentioned_users: { articles: { user: { name: 'deep' } } } - ability.can :read, Article, mentioned_users: { articles: { mentioned_users: { name: 'd2' } } } - expect(Article.accessible_by(ability)).to match_array([a1]) + CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| + context "when a table is referenced multiple times with #{strategy} strategy" do + before :each do + CanCan.accessible_by_strategy = strategy + end + it 'can filter correctly on the different associations' do + u1 = User.create!(name: 'pippo') + u2 = User.create!(name: 'paperino') + + a1 = Article.create!(user: u1) + a2 = Article.create!(user: u2) + + ability = Ability.new(u1) + ability.can :read, Article, user: { id: u1.id } + ability.can :read, Article, mentioned_users: { name: u1.name } + ability.can :read, Article, mentioned_users: { mentioned_articles: { id: a2.id } } + ability.can :read, Article, mentioned_users: { articles: { user: { name: 'deep' } } } + ability.can :read, Article, mentioned_users: { articles: { mentioned_users: { name: 'd2' } } } + expect(Article.accessible_by(ability)).to match_array([a1]) + end end end - context 'has_many through is defined and referenced differently' do + context 'has_many through is defined and referenced differently - subquery strategy' do + before do + CanCan.accessible_by_strategy = :subquery + end + it 'recognises it and simplifies the query' do u1 = User.create!(name: 'pippo') u2 = User.create!(name: 'paperino') @@ -576,72 +593,110 @@ class Transaction < ActiveRecord::Base end end - context 'when a model have renamed primary_key' do + context 'has_many through is defined and referenced differently - left_join strategy' do before do - ActiveRecord::Schema.define do - create_table(:custom_pk_users, primary_key: :gid) do |t| - t.string :name - end + CanCan.accessible_by_strategy = :left_join + end - create_table(:custom_pk_transactions, primary_key: :gid) do |t| - t.integer :custom_pk_user_id - t.string :data - end + it 'recognises it and simplifies the query' do + u1 = User.create!(name: 'pippo') + u2 = User.create!(name: 'paperino') + + a1 = Article.create!(mentioned_users: [u1]) + a2 = Article.create!(mentioned_users: [u2]) + + ability = Ability.new(u1) + ability.can :read, Article, mentioned_users: { name: u1.name } + ability.can :read, Article, mentions: { user: { name: u2.name } } + expect(Article.accessible_by(ability)).to match_array([a1, a2]) + + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') + expect(ability.model_adapter(Article, :read)).to generate_sql(%( + SELECT DISTINCT "articles".* + FROM "articles" + LEFT OUTER JOIN "legacy_mentions" ON "legacy_mentions"."article_id" = "articles"."id" + LEFT OUTER JOIN "users" ON "users"."id" = "legacy_mentions"."user_id" + WHERE (("users"."name" = 'paperino') OR ("users"."name" = 'pippo')))) end + end + end - class CustomPkUser < ActiveRecord::Base - self.primary_key = 'gid' + CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| + context "when a model has renamed primary_key with #{strategy} strategy" do + before :each do + CanCan.accessible_by_strategy = strategy end + before do + ActiveRecord::Schema.define do + create_table(:custom_pk_users, primary_key: :gid) do |t| + t.string :name + end + + create_table(:custom_pk_transactions, primary_key: :gid) do |t| + t.integer :custom_pk_user_id + t.string :data + end + end + + class CustomPkUser < ActiveRecord::Base + self.primary_key = 'gid' + end - class CustomPkTransaction < ActiveRecord::Base - self.primary_key = 'gid' + class CustomPkTransaction < ActiveRecord::Base + self.primary_key = 'gid' - belongs_to :custom_pk_user + belongs_to :custom_pk_user + end end - end - it 'can filter correctly' do - user1 = CustomPkUser.create! - user2 = CustomPkUser.create! + it 'can filter correctly' do + user1 = CustomPkUser.create! + user2 = CustomPkUser.create! - transaction1 = CustomPkTransaction.create!(custom_pk_user: user1) - CustomPkTransaction.create!(custom_pk_user: user2) + transaction1 = CustomPkTransaction.create!(custom_pk_user: user1) + CustomPkTransaction.create!(custom_pk_user: user2) - ability = Ability.new(user1) - ability.can :read, CustomPkTransaction, custom_pk_user: { gid: user1.gid } + ability = Ability.new(user1) + ability.can :read, CustomPkTransaction, custom_pk_user: { gid: user1.gid } - expect(CustomPkTransaction.accessible_by(ability)).to match_array([transaction1]) + expect(CustomPkTransaction.accessible_by(ability)).to match_array([transaction1]) + end end end - context 'when a table has json type colum' do - before do - json_supported = - ActiveRecord::Base.connection.respond_to?(:supports_json?) && - ActiveRecord::Base.connection.supports_json? - - skip "Adapter don't support JSON column type" unless json_supported - - ActiveRecord::Schema.define do - create_table(:json_transactions) do |t| - t.integer :user_id - t.json :additional_data - end + CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| + context "when a table has json type column with #{strategy} strategy" do + before :each do + CanCan.accessible_by_strategy = strategy end + before do + json_supported = + ActiveRecord::Base.connection.respond_to?(:supports_json?) && + ActiveRecord::Base.connection.supports_json? + + skip "Adapter don't support JSON column type" unless json_supported + + ActiveRecord::Schema.define do + create_table(:json_transactions) do |t| + t.integer :user_id + t.json :additional_data + end + end - class JsonTransaction < ActiveRecord::Base - belongs_to :user + class JsonTransaction < ActiveRecord::Base + belongs_to :user + end end - end - it 'can filter correctly' do - user = User.create! - transaction = JsonTransaction.create!(user: user) + it 'can filter correctly' do + user = User.create! + transaction = JsonTransaction.create!(user: user) - ability = Ability.new(user) - ability.can :read, JsonTransaction, user: { id: user.id } + ability = Ability.new(user) + ability.can :read, JsonTransaction, user: { id: user.id } - expect(JsonTransaction.accessible_by(ability)).to match_array([transaction]) + expect(JsonTransaction.accessible_by(ability)).to match_array([transaction]) + end end end diff --git a/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb b/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb index df96747e..5d233a7c 100644 --- a/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb +++ b/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb @@ -45,7 +45,11 @@ class House < ActiveRecord::Base ability.can :read, House, people: { id: @person1.id } end - describe 'fetching of records' do + describe 'fetching of records - subquery strategy' do + before do + CanCan.accessible_by_strategy = :subquery + end + it 'it retreives the records correctly' do houses = House.accessible_by(ability) expect(houses).to match_array [@house2, @house1] @@ -66,4 +70,26 @@ class House < ActiveRecord::Base end end end + + describe 'fetching of records - left_join strategy' do + before do + CanCan.accessible_by_strategy = :left_join + end + + it 'it retreives the records correctly' do + houses = House.accessible_by(ability) + expect(houses).to match_array [@house2, @house1] + end + + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') + it 'generates the correct query' do + expect(ability.model_adapter(House, :read)).to generate_sql(%( + SELECT DISTINCT "houses".* + FROM "houses" + LEFT OUTER JOIN "houses_people" ON "houses_people"."house_id" = "houses"."id" + LEFT OUTER JOIN "people" ON "people"."id" = "houses_people"."person_id" + WHERE "people"."id" = #{@person1.id})) + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f8f51c16..09038fea 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -27,6 +27,11 @@ end config.include SQLHelpers + + config.after :each do + # set default values for all config + CanCan.accessible_by_strategy = :subquery + end end RSpec::Matchers.define :generate_sql do |expected| From 930c92c56ff25b2ee440724cda424cdd82e64cee Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Tue, 13 Oct 2020 10:51:38 -0500 Subject: [PATCH 37/66] add to readme --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b5e492..de2d9bbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased * [#649](https://github.com/CanCanCommunity/cancancan/pull/649): Add support for Single Table Inheritance. ([@Liberatys][]) +* [#655](https://github.com/CanCanCommunity/cancancan/pull/655): Add option for `accessible_by` querying strategy. ([@ghiculescu][]) ## 3.1.0 @@ -670,3 +671,4 @@ Please read the [guide on migrating from CanCanCan 2.x to 3.0](https://github.co [@albb0920]: https://github.com/albb0920 [@ayumu838]: https://github.com/ayumu838 [@Liberatys]: https://github.com/Liberatys +[@ghiculescu]: https://github.com/ghiculescu From 07a4a99e97370a1ab6fcf752707ec803e3bba4db Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Tue, 13 Oct 2020 10:53:57 -0500 Subject: [PATCH 38/66] lint --- .../model_adapters/active_record_4_adapter.rb | 22 +++++++---------- .../model_adapters/active_record_5_adapter.rb | 24 +++++++------------ .../model_adapters/active_record_adapter.rb | 8 +++++++ 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/lib/cancan/model_adapters/active_record_4_adapter.rb b/lib/cancan/model_adapters/active_record_4_adapter.rb index bd9665b1..c96da537 100644 --- a/lib/cancan/model_adapters/active_record_4_adapter.rb +++ b/lib/cancan/model_adapters/active_record_4_adapter.rb @@ -34,22 +34,16 @@ def matches_condition?(subject, name, value) # look inside the where clause to decide to outer join tables # you're using in the where. Instead, `references()` is required # in addition to `includes()` to force the outer join. - def build_relation(*where_conditions) - relation = @model_class.where(*where_conditions) - - if joins.present? - case CanCan.accessible_by_strategy - when :subquery - inner = @model_class.unscoped do - relation.includes(joins).references(joins) - end - @model_class.where(@model_class.primary_key => inner) - - when :left_join + def build_joins_relation(relation) + case CanCan.accessible_by_strategy + when :subquery + inner = @model_class.unscoped do relation.includes(joins).references(joins) end - else - relation + @model_class.where(@model_class.primary_key => inner) + + when :left_join + relation.includes(joins).references(joins) end end diff --git a/lib/cancan/model_adapters/active_record_5_adapter.rb b/lib/cancan/model_adapters/active_record_5_adapter.rb index ed45813e..89e9fce5 100644 --- a/lib/cancan/model_adapters/active_record_5_adapter.rb +++ b/lib/cancan/model_adapters/active_record_5_adapter.rb @@ -21,22 +21,16 @@ def self.matches_condition?(subject, name, value) private - def build_relation(*where_conditions) - relation = @model_class.where(*where_conditions) - - if joins.present? - case CanCan.accessible_by_strategy - when :subquery - inner = @model_class.unscoped do - relation.left_joins(joins) - end - @model_class.where(@model_class.primary_key => inner) - - when :left_join - relation.left_joins(joins).distinct + def build_joins_relation(relation) + case CanCan.accessible_by_strategy + when :subquery + inner = @model_class.unscoped do + relation.left_joins(joins) end - else - relation + @model_class.where(@model_class.primary_key => inner) + + when :left_join + relation.left_joins(joins).distinct end end diff --git a/lib/cancan/model_adapters/active_record_adapter.rb b/lib/cancan/model_adapters/active_record_adapter.rb index 81beac72..1798ff96 100644 --- a/lib/cancan/model_adapters/active_record_adapter.rb +++ b/lib/cancan/model_adapters/active_record_adapter.rb @@ -59,6 +59,14 @@ def database_records end end + def build_relation(*where_conditions) + relation = @model_class.where(*where_conditions) + return relation unless joins.present? + + # subclasses must implement `build_joins_relation` + build_joins_relation(relation) + end + # Returns the associations used in conditions for the :joins option of a search. # See ModelAdditions#accessible_by def joins From ce50d042f43167b240b33763342388394ff62305 Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Tue, 13 Oct 2020 17:05:53 -0500 Subject: [PATCH 39/66] ooops, https://github.com/CanCanCommunity/cancancan/pull/605#discussion_r504282782 --- spec/cancan/model_adapters/accessible_by_integration_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/cancan/model_adapters/accessible_by_integration_spec.rb b/spec/cancan/model_adapters/accessible_by_integration_spec.rb index f5a2ae93..89f76659 100644 --- a/spec/cancan/model_adapters/accessible_by_integration_spec.rb +++ b/spec/cancan/model_adapters/accessible_by_integration_spec.rb @@ -76,7 +76,7 @@ class Editor < ActiveRecord::Base describe 'preloading of associatons' do it 'preloads associations correctly' do - posts = Post.accessible_by(ability).includes(likes: :user) + posts = Post.accessible_by(ability).where(published: true).includes(likes: :user) expect(posts[0].association(:likes)).to be_loaded expect(posts[0].likes[0].association(:user)).to be_loaded end @@ -92,7 +92,7 @@ class Editor < ActiveRecord::Base if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') describe 'selecting custom columns' do it 'extracts custom columns correctly' do - posts = Post.accessible_by(ability).select('title as mytitle') + posts = Post.accessible_by(ability).where(published: true).select('title as mytitle') expect(posts[0].mytitle).to eq 'post1' end end From 82413d0df488b5b0645acc25c8351223f9cc6ed8 Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Tue, 13 Oct 2020 17:19:56 -0500 Subject: [PATCH 40/66] update tests for where behaviors differ between DBs --- .../active_record_adapter_spec.rb | 109 +++++++++++++----- spec/support/sql_helpers.rb | 12 +- 2 files changed, 87 insertions(+), 34 deletions(-) diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index 31268f32..f6159347 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -234,13 +234,6 @@ class User < ActiveRecord::Base expect(Comment.accessible_by(@ability).count).to eq(1) end - it 'allows ordering via relations' do - @ability.can :read, Comment, article: { category: { visible: true } } - comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) - Comment.create!(article: Article.create!(category: Category.create!(visible: false))) - expect(Comment.accessible_by(@ability).joins(:article).order('articles.id')).to match_array([comment1]) - end - it 'allows conditions in SQL and merge with hash conditions' do @ability.can :read, Article, published: true @ability.can :read, Article, ['secret=?', true] @@ -451,6 +444,46 @@ class User < ActiveRecord::Base end end + context 'base behaviour subquery specific' do + before :each do + CanCan.accessible_by_strategy = :subquery + end + + it 'allows ordering via relations' do + @ability.can :read, Comment, article: { category: { visible: true } } + comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) + Comment.create!(article: Article.create!(category: Category.create!(visible: false))) + expect(Comment.accessible_by(@ability).joins(:article).order('articles.id')).to match_array([comment1]) + end + end + + context 'base behaviour left_join specific' do + before :each do + CanCan.accessible_by_strategy = :left_join + end + + it 'allows ordering via relations in sqlite' do + skip unless sqlite? + + @ability.can :read, Comment, article: { category: { visible: true } } + comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) + Comment.create!(article: Article.create!(category: Category.create!(visible: false))) + + expect(Comment.accessible_by(@ability).joins(:article).order('articles.id')).to match_array([comment1]) + end + + # this fails on Postgres. see https://github.com/CanCanCommunity/cancancan/pull/608 + it 'fails to order via relations in postgres' do + skip unless postgres? + + @ability.can :read, Comment, article: { category: { visible: true } } + comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) + Comment.create!(article: Article.create!(category: Category.create!(visible: false))) + + expect { Comment.accessible_by(@ability).joins(:article).order('articles.id') }.to raise_error(ActiveRecord::StatementInvalid) + end + end + context 'with namespaced models' do before :each do ActiveRecord::Schema.define do @@ -664,39 +697,51 @@ class CustomPkTransaction < ActiveRecord::Base end end - CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| - context "when a table has json type column with #{strategy} strategy" do - before :each do - CanCan.accessible_by_strategy = strategy - end - before do - json_supported = - ActiveRecord::Base.connection.respond_to?(:supports_json?) && - ActiveRecord::Base.connection.supports_json? + context "when a table has json type column" do + before do + json_supported = + ActiveRecord::Base.connection.respond_to?(:supports_json?) && + ActiveRecord::Base.connection.supports_json? - skip "Adapter don't support JSON column type" unless json_supported + skip "Adapter don't support JSON column type" unless json_supported - ActiveRecord::Schema.define do - create_table(:json_transactions) do |t| - t.integer :user_id - t.json :additional_data - end + ActiveRecord::Schema.define do + create_table(:json_transactions) do |t| + t.integer :user_id + t.json :additional_data end + end - class JsonTransaction < ActiveRecord::Base - belongs_to :user - end + class JsonTransaction < ActiveRecord::Base + belongs_to :user end + end - it 'can filter correctly' do - user = User.create! - transaction = JsonTransaction.create!(user: user) + it 'can filter correctly if using subquery strategy' do + CanCan.accessible_by_strategy = :subquery - ability = Ability.new(user) - ability.can :read, JsonTransaction, user: { id: user.id } + user = User.create! + transaction = JsonTransaction.create!(user: user) - expect(JsonTransaction.accessible_by(ability)).to match_array([transaction]) - end + ability = Ability.new(user) + ability.can :read, JsonTransaction, user: { id: user.id } + + expect(JsonTransaction.accessible_by(ability)).to match_array([transaction]) + end + + # this fails on Postgres. see https://github.com/CanCanCommunity/cancancan/pull/608 + it 'cannot filter JSON on postgres columns using left_join strategy' do + skip unless postgres? + + CanCan.accessible_by_strategy = :left_join + + user = User.create! + transaction = JsonTransaction.create!(user: user) + + ability = Ability.new(user) + ability.can :read, JsonTransaction, user: { id: user.id } + + expect { JsonTransaction.accessible_by(ability) }.to raise_error(ActiveRecord::StatementInvalid) end end diff --git a/spec/support/sql_helpers.rb b/spec/support/sql_helpers.rb index fc1ac8cd..199a3418 100644 --- a/spec/support/sql_helpers.rb +++ b/spec/support/sql_helpers.rb @@ -5,12 +5,20 @@ def normalized_sql(adapter) adapter.database_records.to_sql.strip.squeeze(' ') end + def sqlite? + ENV['DB'] == 'sqlite' + end + + def postgres? + ENV['DB'] == 'postgres' + end + def connect_db # ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Base.logger = nil - if ENV['DB'] == 'sqlite' + if sqlite? ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') - elsif ENV['DB'] == 'postgres' + elsif postgres? connect_postgres elsif ENV['DB'].nil? raise StandardError, "ENV['DB'] not specified" From d9473adb59318a8d287d1ed2c107a0a79f5c2f7c Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Tue, 13 Oct 2020 17:23:19 -0500 Subject: [PATCH 41/66] lint --- .../model_adapters/active_record_adapter_spec.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index f6159347..9afe3db7 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -477,10 +477,11 @@ class User < ActiveRecord::Base skip unless postgres? @ability.can :read, Comment, article: { category: { visible: true } } - comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) + Comment.create!(article: Article.create!(category: Category.create!(visible: true))) Comment.create!(article: Article.create!(category: Category.create!(visible: false))) - expect { Comment.accessible_by(@ability).joins(:article).order('articles.id') }.to raise_error(ActiveRecord::StatementInvalid) + expect { Comment.accessible_by(@ability).joins(:article).order('articles.id') } + .to raise_error(ActiveRecord::StatementInvalid) end end @@ -697,7 +698,7 @@ class CustomPkTransaction < ActiveRecord::Base end end - context "when a table has json type column" do + context 'when a table has json type column' do before do json_supported = ActiveRecord::Base.connection.respond_to?(:supports_json?) && @@ -736,12 +737,13 @@ class JsonTransaction < ActiveRecord::Base CanCan.accessible_by_strategy = :left_join user = User.create! - transaction = JsonTransaction.create!(user: user) + JsonTransaction.create!(user: user) ability = Ability.new(user) ability.can :read, JsonTransaction, user: { id: user.id } - expect { JsonTransaction.accessible_by(ability) }.to raise_error(ActiveRecord::StatementInvalid) + expect { JsonTransaction.accessible_by(ability) } + .to raise_error(ActiveRecord::StatementInvalid) end end From 1a82c3603787d8a2b5c9b45fe974fd4592f081fa Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Wed, 14 Oct 2020 12:04:32 -0500 Subject: [PATCH 42/66] tweak some tests that are bad in old AR versions --- .../active_record_adapter_spec.rb | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index 9afe3db7..2a0dcf57 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -451,9 +451,17 @@ class User < ActiveRecord::Base it 'allows ordering via relations' do @ability.can :read, Comment, article: { category: { visible: true } } - comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) + comment1 = Comment.create!(article: Article.create!(name: 'B', category: Category.create!(visible: true))) + comment2 = Comment.create!(article: Article.create!(name: 'A', category: Category.create!(visible: true))) Comment.create!(article: Article.create!(category: Category.create!(visible: false))) - expect(Comment.accessible_by(@ability).joins(:article).order('articles.id')).to match_array([comment1]) + + # doesn't work without explicitly calling a join + expect { Comment.accessible_by(@ability).order('articles.name').to_a } + .to raise_error(ActiveRecord::StatementInvalid) + + # works with the explicit join + expect(Comment.accessible_by(@ability).joins(:article).order('articles.name')) + .to match_array([comment2, comment1]) end end @@ -466,10 +474,21 @@ class User < ActiveRecord::Base skip unless sqlite? @ability.can :read, Comment, article: { category: { visible: true } } - comment1 = Comment.create!(article: Article.create!(category: Category.create!(visible: true))) + comment1 = Comment.create!(article: Article.create!(name: 'B', category: Category.create!(visible: true))) + comment2 = Comment.create!(article: Article.create!(name: 'A', category: Category.create!(visible: true))) Comment.create!(article: Article.create!(category: Category.create!(visible: false))) - expect(Comment.accessible_by(@ability).joins(:article).order('articles.id')).to match_array([comment1]) + # works without explicitly calling a join + expect(Comment.accessible_by(@ability).order('articles.name')).to match_array([comment2, comment1]) + + # works with the explicit join in AR 5.2+ + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.2.0') + expect(Comment.accessible_by(@ability).joins(:article).order('articles.name')) + .to match_array([comment2, comment1]) + else + expect { Comment.accessible_by(@ability).joins(:article).order('articles.name').to_a } + .to raise_error(ActiveRecord::StatementInvalid) + end end # this fails on Postgres. see https://github.com/CanCanCommunity/cancancan/pull/608 @@ -477,10 +496,14 @@ class User < ActiveRecord::Base skip unless postgres? @ability.can :read, Comment, article: { category: { visible: true } } - Comment.create!(article: Article.create!(category: Category.create!(visible: true))) + Comment.create!(article: Article.create!(name: 'B', category: Category.create!(visible: true))) + Comment.create!(article: Article.create!(name: 'A', category: Category.create!(visible: true))) Comment.create!(article: Article.create!(category: Category.create!(visible: false))) - expect { Comment.accessible_by(@ability).joins(:article).order('articles.id') } + # doesn't work with or without the join + expect { Comment.accessible_by(@ability).order('articles.name').to_a } + .to raise_error(ActiveRecord::StatementInvalid) + expect { Comment.accessible_by(@ability).joins(:article).order('articles.name').to_a } .to raise_error(ActiveRecord::StatementInvalid) end end From ffd4976aeef68f0fc3c67dd1c0ed2d2e343bdcf7 Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Wed, 14 Oct 2020 12:08:57 -0500 Subject: [PATCH 43/66] more has many thorugh tests --- .../accessible_by_has_many_through_spec.rb | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb b/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb index 2b935302..b72920bb 100644 --- a/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb +++ b/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb @@ -72,31 +72,57 @@ class Editor < ActiveRecord::Base ability.can :read, Post, editors: { user_id: @user1 } end - describe 'preloading of associations' do - it 'preloads associations correctly' do - posts = Post.accessible_by(ability).where(published: true).includes(likes: :user) - expect(posts[0].association(:likes)).to be_loaded - expect(posts[0].likes[0].association(:user)).to be_loaded + CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| + context "using #{strategy} strategy" do + before :each do + CanCan.accessible_by_strategy = strategy + end + + describe 'preloading of associations' do + it 'preloads associations correctly' do + posts = Post.accessible_by(ability).where(published: true).includes(likes: :user) + expect(posts[0].association(:likes)).to be_loaded + expect(posts[0].likes[0].association(:user)).to be_loaded + end + end + + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') + describe 'selecting custom columns' do + it 'extracts custom columns correctly' do + posts = Post.accessible_by(ability).where(published: true).select('title as mytitle') + expect(posts[0].mytitle).to eq 'post1' + end + end + end + + describe 'filtering of results' do + it 'adds the where clause correctly' do + posts = Post.accessible_by(ability).where(published: true) + expect(posts.length).to eq 1 + end + end end end - describe 'filtering of results' do - it 'adds the where clause correctly' do - posts = Post.accessible_by(ability).where(published: true) - expect(posts.length).to eq 1 + describe 'filtering of results - subquery' do + before :each do + CanCan.accessible_by_strategy = :subquery end + it 'adds the where clause correctly with joins' do posts = Post.joins(:editors).where('editors.user_id': @user1.id).accessible_by(ability) expect(posts.length).to eq 1 end end - if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') - describe 'selecting custom columns' do - it 'extracts custom columns correctly' do - posts = Post.accessible_by(ability).where(published: true).select('title as mytitle') - expect(posts[0].mytitle).to eq 'post1' - end + describe 'filtering of results - left_joins' do + before :each do + CanCan.accessible_by_strategy = :left_join + end + + it 'adds the where clause correctly with joins' do + posts = Post.joins(:editors).where('editors.user_id': @user1.id).accessible_by(ability) + expect(posts.length).to eq 1 end end end From e5411d7fab595ba9d89452d9f7f918ed0539245e Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Wed, 14 Oct 2020 12:11:43 -0500 Subject: [PATCH 44/66] regression --- lib/cancan/model_adapters/active_record_4_adapter.rb | 2 +- lib/cancan/model_adapters/active_record_5_adapter.rb | 4 ++-- lib/cancan/model_adapters/active_record_adapter.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/cancan/model_adapters/active_record_4_adapter.rb b/lib/cancan/model_adapters/active_record_4_adapter.rb index c96da537..d7dcff0e 100644 --- a/lib/cancan/model_adapters/active_record_4_adapter.rb +++ b/lib/cancan/model_adapters/active_record_4_adapter.rb @@ -34,7 +34,7 @@ def matches_condition?(subject, name, value) # look inside the where clause to decide to outer join tables # you're using in the where. Instead, `references()` is required # in addition to `includes()` to force the outer join. - def build_joins_relation(relation) + def build_joins_relation(relation, *where_conditions) case CanCan.accessible_by_strategy when :subquery inner = @model_class.unscoped do diff --git a/lib/cancan/model_adapters/active_record_5_adapter.rb b/lib/cancan/model_adapters/active_record_5_adapter.rb index 89e9fce5..d9111775 100644 --- a/lib/cancan/model_adapters/active_record_5_adapter.rb +++ b/lib/cancan/model_adapters/active_record_5_adapter.rb @@ -21,11 +21,11 @@ def self.matches_condition?(subject, name, value) private - def build_joins_relation(relation) + def build_joins_relation(relation, *where_conditions) case CanCan.accessible_by_strategy when :subquery inner = @model_class.unscoped do - relation.left_joins(joins) + @model_class.left_joins(joins).where(*where_conditions) end @model_class.where(@model_class.primary_key => inner) diff --git a/lib/cancan/model_adapters/active_record_adapter.rb b/lib/cancan/model_adapters/active_record_adapter.rb index 1798ff96..7bd969c9 100644 --- a/lib/cancan/model_adapters/active_record_adapter.rb +++ b/lib/cancan/model_adapters/active_record_adapter.rb @@ -64,7 +64,7 @@ def build_relation(*where_conditions) return relation unless joins.present? # subclasses must implement `build_joins_relation` - build_joins_relation(relation) + build_joins_relation(relation, *where_conditions) end # Returns the associations used in conditions for the :joins option of a search. From 7841df0ae91479566a138f9f90f34b145bfc73f0 Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Wed, 14 Oct 2020 15:01:27 -0500 Subject: [PATCH 45/66] ugh, lint --- lib/cancan/model_adapters/active_record_4_adapter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cancan/model_adapters/active_record_4_adapter.rb b/lib/cancan/model_adapters/active_record_4_adapter.rb index d7dcff0e..81cc7ba7 100644 --- a/lib/cancan/model_adapters/active_record_4_adapter.rb +++ b/lib/cancan/model_adapters/active_record_4_adapter.rb @@ -34,7 +34,7 @@ def matches_condition?(subject, name, value) # look inside the where clause to decide to outer join tables # you're using in the where. Instead, `references()` is required # in addition to `includes()` to force the outer join. - def build_joins_relation(relation, *where_conditions) + def build_joins_relation(relation, *_where_conditions) case CanCan.accessible_by_strategy when :subquery inner = @model_class.unscoped do From 738dcf04254910ae1fad80b0485a658fd6d913df Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Wed, 14 Oct 2020 15:04:07 -0500 Subject: [PATCH 46/66] fix the last test --- .../accessible_by_has_many_through_spec.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb b/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb index b72920bb..da22e6b1 100644 --- a/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb +++ b/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb @@ -120,8 +120,15 @@ class Editor < ActiveRecord::Base CanCan.accessible_by_strategy = :left_join end - it 'adds the where clause correctly with joins' do - posts = Post.joins(:editors).where('editors.user_id': @user1.id).accessible_by(ability) + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.2.0') + it 'adds the where clause correctly with joins on AR 5.2+' do + posts = Post.joins(:editors).where('editors.user_id': @user1.id).accessible_by(ability) + expect(posts.length).to eq 1 + end + end + + it 'adds the where clause correctly without joins' do + posts = Post.where('editors.user_id': @user1.id).accessible_by(ability) expect(posts.length).to eq 1 end end From c66cba01805b4c5a42b7bb0673b3c216b5bc0b66 Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Wed, 14 Oct 2020 15:22:37 -0500 Subject: [PATCH 47/66] need to run the query --- spec/cancan/model_adapters/active_record_adapter_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index 2a0dcf57..e1c82814 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -765,7 +765,7 @@ class JsonTransaction < ActiveRecord::Base ability = Ability.new(user) ability.can :read, JsonTransaction, user: { id: user.id } - expect { JsonTransaction.accessible_by(ability) } + expect { JsonTransaction.accessible_by(ability).to_a } .to raise_error(ActiveRecord::StatementInvalid) end end From 58700497d9e6001c1517e5964f52444ca3e8f228 Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Wed, 14 Oct 2020 15:37:54 -0500 Subject: [PATCH 48/66] get it working in 4.2 --- .../model_adapters/active_record_4_adapter.rb | 7 ++-- .../model_adapters/active_record_adapter.rb | 4 ++ .../active_record_adapter_spec.rb | 41 +++++++++++++------ 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/lib/cancan/model_adapters/active_record_4_adapter.rb b/lib/cancan/model_adapters/active_record_4_adapter.rb index 81cc7ba7..78702a30 100644 --- a/lib/cancan/model_adapters/active_record_4_adapter.rb +++ b/lib/cancan/model_adapters/active_record_4_adapter.rb @@ -37,10 +37,9 @@ def matches_condition?(subject, name, value) def build_joins_relation(relation, *_where_conditions) case CanCan.accessible_by_strategy when :subquery - inner = @model_class.unscoped do - relation.includes(joins).references(joins) - end - @model_class.where(@model_class.primary_key => inner) + # subquery mode doesn't work with Rails 4.x + # see CanCan::ModelAdapters::ActiveRecordAdapter.supports_subqueries? + relation.includes(joins).references(joins) when :left_join relation.includes(joins).references(joins) diff --git a/lib/cancan/model_adapters/active_record_adapter.rb b/lib/cancan/model_adapters/active_record_adapter.rb index 7bd969c9..65eec9b0 100644 --- a/lib/cancan/model_adapters/active_record_adapter.rb +++ b/lib/cancan/model_adapters/active_record_adapter.rb @@ -3,6 +3,10 @@ module CanCan module ModelAdapters class ActiveRecordAdapter < AbstractAdapter + def self.supports_subqueries? + version_greater_or_equal?("5.0.0") + end + def self.version_greater_or_equal?(version) Gem::Version.new(ActiveRecord.version).release >= Gem::Version.new(version) end diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index e1c82814..bc9b79e1 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -455,9 +455,14 @@ class User < ActiveRecord::Base comment2 = Comment.create!(article: Article.create!(name: 'A', category: Category.create!(visible: true))) Comment.create!(article: Article.create!(category: Category.create!(visible: false))) - # doesn't work without explicitly calling a join - expect { Comment.accessible_by(@ability).order('articles.name').to_a } - .to raise_error(ActiveRecord::StatementInvalid) + # doesn't work without explicitly calling a join on AR 5+, but does before that (where we don't use subqueries at all) + if CanCan::ModelAdapters::ActiveRecordAdapter.supports_subqueries? + expect { Comment.accessible_by(@ability).order('articles.name').to_a } + .to raise_error(ActiveRecord::StatementInvalid) + else + expect(Comment.accessible_by(@ability).order('articles.name')) + .to match_array([comment2, comment1]) + end # works with the explicit join expect(Comment.accessible_by(@ability).joins(:article).order('articles.name')) @@ -481,30 +486,40 @@ class User < ActiveRecord::Base # works without explicitly calling a join expect(Comment.accessible_by(@ability).order('articles.name')).to match_array([comment2, comment1]) - # works with the explicit join in AR 5.2+ + # works with the explicit join in AR 5.2+ and AR 4.2 if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.2.0') expect(Comment.accessible_by(@ability).joins(:article).order('articles.name')) .to match_array([comment2, comment1]) - else + elsif CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') expect { Comment.accessible_by(@ability).joins(:article).order('articles.name').to_a } .to raise_error(ActiveRecord::StatementInvalid) + else + expect(Comment.accessible_by(@ability).joins(:article).order('articles.name')) + .to match_array([comment2, comment1]) end end # this fails on Postgres. see https://github.com/CanCanCommunity/cancancan/pull/608 - it 'fails to order via relations in postgres' do + it 'fails to order via relations in postgres on AR 5+' do skip unless postgres? @ability.can :read, Comment, article: { category: { visible: true } } - Comment.create!(article: Article.create!(name: 'B', category: Category.create!(visible: true))) - Comment.create!(article: Article.create!(name: 'A', category: Category.create!(visible: true))) + comment1 = Comment.create!(article: Article.create!(name: 'B', category: Category.create!(visible: true))) + comment2 = Comment.create!(article: Article.create!(name: 'A', category: Category.create!(visible: true))) Comment.create!(article: Article.create!(category: Category.create!(visible: false))) - # doesn't work with or without the join - expect { Comment.accessible_by(@ability).order('articles.name').to_a } - .to raise_error(ActiveRecord::StatementInvalid) - expect { Comment.accessible_by(@ability).joins(:article).order('articles.name').to_a } - .to raise_error(ActiveRecord::StatementInvalid) + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') + # doesn't work with or without the join + expect { Comment.accessible_by(@ability).order('articles.name').to_a } + .to raise_error(ActiveRecord::StatementInvalid) + expect { Comment.accessible_by(@ability).joins(:article).order('articles.name').to_a } + .to raise_error(ActiveRecord::StatementInvalid) + else + expect(Comment.accessible_by(@ability).order('articles.name')) + .to match_array([comment2, comment1]) + expect(Comment.accessible_by(@ability).joins(:article).order('articles.name')) + .to match_array([comment2, comment1]) + end end end From 932064657732dac6cb191e0f84f638b914445ded Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Wed, 14 Oct 2020 15:55:31 -0500 Subject: [PATCH 49/66] lint AGAIN --- lib/cancan/model_adapters/active_record_4_adapter.rb | 1 - lib/cancan/model_adapters/active_record_adapter.rb | 4 ---- spec/cancan/model_adapters/active_record_adapter_spec.rb | 5 +++-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/cancan/model_adapters/active_record_4_adapter.rb b/lib/cancan/model_adapters/active_record_4_adapter.rb index 78702a30..b4461950 100644 --- a/lib/cancan/model_adapters/active_record_4_adapter.rb +++ b/lib/cancan/model_adapters/active_record_4_adapter.rb @@ -38,7 +38,6 @@ def build_joins_relation(relation, *_where_conditions) case CanCan.accessible_by_strategy when :subquery # subquery mode doesn't work with Rails 4.x - # see CanCan::ModelAdapters::ActiveRecordAdapter.supports_subqueries? relation.includes(joins).references(joins) when :left_join diff --git a/lib/cancan/model_adapters/active_record_adapter.rb b/lib/cancan/model_adapters/active_record_adapter.rb index 65eec9b0..7bd969c9 100644 --- a/lib/cancan/model_adapters/active_record_adapter.rb +++ b/lib/cancan/model_adapters/active_record_adapter.rb @@ -3,10 +3,6 @@ module CanCan module ModelAdapters class ActiveRecordAdapter < AbstractAdapter - def self.supports_subqueries? - version_greater_or_equal?("5.0.0") - end - def self.version_greater_or_equal?(version) Gem::Version.new(ActiveRecord.version).release >= Gem::Version.new(version) end diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index bc9b79e1..a29ca57c 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -455,8 +455,9 @@ class User < ActiveRecord::Base comment2 = Comment.create!(article: Article.create!(name: 'A', category: Category.create!(visible: true))) Comment.create!(article: Article.create!(category: Category.create!(visible: false))) - # doesn't work without explicitly calling a join on AR 5+, but does before that (where we don't use subqueries at all) - if CanCan::ModelAdapters::ActiveRecordAdapter.supports_subqueries? + # doesn't work without explicitly calling a join on AR 5+, + # but does before that (where we don't use subqueries at all) + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') expect { Comment.accessible_by(@ability).order('articles.name').to_a } .to raise_error(ActiveRecord::StatementInvalid) else From 828245db2293551c79d553c365f832bf77c83bbc Mon Sep 17 00:00:00 2001 From: Lukas Bischof Date: Sat, 24 Oct 2020 13:40:10 +0200 Subject: [PATCH 50/66] Fix moved links --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b7a5b4e8..3ba01110 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ of models automatically and reduce duplicated code.

-Do you want to sponsor CanCanCan and show your logo here? +Do you want to sponsor CanCanCan and show your logo here? Check our [Sponsors Page](https://github.com/sponsors/coorasse). ## Installation @@ -242,11 +242,11 @@ See [Ensure Authorization](https://github.com/CanCanCommunity/cancancan/wiki/Ens ## Wiki Docs -* [Defining Abilities](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities) +* [Defining Abilities](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Defining-Abilities.md) * [Checking Abilities](https://github.com/CanCanCommunity/cancancan/wiki/Checking-Abilities) * [Authorizing Controller Actions](https://github.com/CanCanCommunity/cancancan/wiki/Authorizing-Controller-Actions) * [Exception Handling](https://github.com/CanCanCommunity/cancancan/wiki/Exception-Handling) -* [Changing Defaults](https://github.com/CanCanCommunity/cancancan/wiki/Changing-Defaults) +* [Changing Defaults](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Changing-Defaults.md) * [See more](https://github.com/CanCanCommunity/cancancan/wiki) ## Mission From 2f09fee208ff30acd18fa8fd955c0e81db6f465e Mon Sep 17 00:00:00 2001 From: Lukas Bischof Date: Sat, 24 Oct 2020 13:40:27 +0200 Subject: [PATCH 51/66] Actually provide links in Checking-Abilities.md --- docs/Checking-Abilities.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Checking-Abilities.md b/docs/Checking-Abilities.md index 4d3dfa93..dd7951aa 100644 --- a/docs/Checking-Abilities.md +++ b/docs/Checking-Abilities.md @@ -1,4 +1,4 @@ -After [[abilities are defined|Defining Abilities]], you can use the `can?` method in the controller or view to check the user's permission for a given action and object. +After [Defining Abilities](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Defining-Abilities.md), you can use the `can?` method in the controller or view to check the user's permission for a given action and object. ```ruby can? :destroy, @project @@ -45,7 +45,7 @@ Article.accessible_by(current_ability).count == Article.count ## Additional Docs -* [[Defining Abilities]] -* [[Ability Precedence]] -* [[Debugging Abilities]] -* [[Testing Abilities]] \ No newline at end of file +* [Defining Abilities](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Defining-Abilities.md) +* [Ability Precedence](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Ability-Precedence.md) +* [Debugging Abilities](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Debugging-Abilities.md) +* [Testing Abilities](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Testing-Abilities.md) From 96fb64a1721babb163f650e21f1b86e29ea4a4f5 Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Mon, 26 Oct 2020 15:14:12 -0500 Subject: [PATCH 52/66] Remove unnecessary call to resolve_column_aliases --- lib/cancan/model_adapters/active_record_5_adapter.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/cancan/model_adapters/active_record_5_adapter.rb b/lib/cancan/model_adapters/active_record_5_adapter.rb index f7c5df8d..1326aeec 100644 --- a/lib/cancan/model_adapters/active_record_5_adapter.rb +++ b/lib/cancan/model_adapters/active_record_5_adapter.rb @@ -32,7 +32,6 @@ def build_relation(*where_conditions) end end - # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions` def sanitize_sql(conditions) if conditions.is_a?(Hash) sanitize_sql_activerecord5(conditions) @@ -46,11 +45,7 @@ def sanitize_sql_activerecord5(conditions) table_metadata = ActiveRecord::TableMetadata.new(@model_class, table) predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) - conditions = predicate_builder.resolve_column_aliases(conditions) - - conditions.stringify_keys! - - predicate_builder.build_from_hash(conditions).map { |b| visit_nodes(b) }.join(' AND ') + predicate_builder.build_from_hash(conditions.stringify_keys).map { |b| visit_nodes(b) }.join(' AND ') end def visit_nodes(node) From c6e14266d9bcc3b6c834f977fe14034777cbaa07 Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Mon, 26 Oct 2020 15:19:51 -0500 Subject: [PATCH 53/66] Test against rails master --- .travis.yml | 7 +++++++ Appraisals | 17 +++++++++++++++++ gemfiles/activerecord_master.gemfile | 20 ++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 gemfiles/activerecord_master.gemfile diff --git a/.travis.yml b/.travis.yml index 4ab62bf7..71618575 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ gemfile: - gemfiles/activerecord_5.1.0.gemfile - gemfiles/activerecord_5.2.2.gemfile - gemfiles/activerecord_6.0.0.gemfile + - gemfiles/activerecord_master.gemfile env: - DB=sqlite - DB=postgres @@ -33,6 +34,12 @@ matrix: gemfile: gemfiles/activerecord_6.0.0.gemfile - rvm: 2.4.2 gemfile: gemfiles/activerecord_6.0.0.gemfile + - rvm: 2.2.6 + gemfile: gemfiles/activerecord_master.gemfile + - rvm: 2.3.5 + gemfile: gemfiles/activerecord_master.gemfile + - rvm: 2.4.2 + gemfile: gemfiles/activerecord_master.gemfile - rvm: 2.7.0 gemfile: gemfiles/activerecord_4.2.0.gemfile - rvm: jruby-9.1.17.0 diff --git a/Appraisals b/Appraisals index 9fd8ce0c..4e685555 100644 --- a/Appraisals +++ b/Appraisals @@ -83,3 +83,20 @@ appraise 'activerecord_6.0.0' do gem 'sqlite3', '~> 1.4.0' end end + +appraise 'activerecord_master' do + gem 'actionpack', github: 'rails/rails', require: 'action_pack' + gem 'activerecord', github: 'rails/rails', require: 'active_record' + gem 'activesupport', github: 'rails/rails', require: 'active_support/all' + + platforms :jruby do + gem 'activerecord-jdbcsqlite3-adapter' + gem 'jdbc-sqlite3' + gem 'jdbc-postgres' + end + + platforms :ruby, :mswin, :mingw do + gem 'pg', '~> 1.1.4' + gem 'sqlite3', '~> 1.4.0' + end +end diff --git a/gemfiles/activerecord_master.gemfile b/gemfiles/activerecord_master.gemfile new file mode 100644 index 00000000..739e08ba --- /dev/null +++ b/gemfiles/activerecord_master.gemfile @@ -0,0 +1,20 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "actionpack", github: "rails/rails", require: "action_pack" +gem "activerecord", github: "rails/rails", require: "active_record" +gem "activesupport", github: "rails/rails", require: "active_support/all" + +platforms :jruby do + gem "activerecord-jdbcsqlite3-adapter" + gem "jdbc-sqlite3" + gem "jdbc-postgres" +end + +platforms :ruby, :mswin, :mingw do + gem "pg", "~> 1.1.4" + gem "sqlite3", "~> 1.4.0" +end + +gemspec path: "../" From e45fd35a6c110d950f0e0dc21c1ac569e0492290 Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Tue, 27 Oct 2020 15:03:20 -0500 Subject: [PATCH 54/66] add jruby ignores --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 71618575..3b19f86c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,14 +42,19 @@ matrix: gemfile: gemfiles/activerecord_master.gemfile - rvm: 2.7.0 gemfile: gemfiles/activerecord_4.2.0.gemfile + - rvm: jruby-9.1.17.0 gemfile: gemfiles/activerecord_5.0.2.gemfile - rvm: jruby-9.1.17.0 gemfile: gemfiles/activerecord_6.0.0.gemfile + - rvm: jruby-9.1.17.0 + gemfile: gemfiles/activerecord_master.gemfile - rvm: jruby-9.2.11.1 gemfile: gemfiles/activerecord_5.0.2.gemfile - rvm: jruby-9.2.11.1 gemfile: gemfiles/activerecord_6.0.0.gemfile + - rvm: jruby-9.2.11.1 + gemfile: gemfiles/activerecord_master.gemfile allow_failures: - rvm: ruby-head - rvm: jruby-head From 501d378a061d573ea6966bbe553c7e85e61bc825 Mon Sep 17 00:00:00 2001 From: Nick Flueckiger Date: Sun, 1 Nov 2020 14:51:51 +0100 Subject: [PATCH 55/66] Implement matching by association Fix an issue that caused associations not to work in their usage as a subject for a rule --- lib/cancan/conditions_matcher.rb | 3 +- .../active_record_adapter_spec.rb | 119 ++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/lib/cancan/conditions_matcher.rb b/lib/cancan/conditions_matcher.rb index e6648543..8399cc99 100644 --- a/lib/cancan/conditions_matcher.rb +++ b/lib/cancan/conditions_matcher.rb @@ -78,7 +78,8 @@ def condition_match?(attribute, value) def hash_condition_match?(attribute, value) if attribute.is_a?(Array) || (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation)) - attribute.any? { |element| matches_conditions_hash?(element, value) } + match_results = attribute.map { |element| matches_conditions_hash?(element, value) } + match_results.any? else attribute && matches_conditions_hash?(attribute, value) end diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index b653cff4..fbc8f65e 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -503,6 +503,125 @@ class Course < ActiveRecord::Base end end + context 'when an association is used to create a rule' do + before do + ActiveRecord::Schema.define do + create_table(:foos) do |t| + t.string :name + end + create_table(:bars) do |t| + t.string :name + end + create_table :roles do |t| + t.string :name + + t.timestamps + end + create_table :user_roles do |t| + t.references :user, foreign_key: true + t.references :role, foreign_key: true + t.references :subject, polymorphic: true + + t.timestamps + end + end + + class Foo < ActiveRecord::Base + has_many :user_roles, as: :subject + end + + class Bar < ActiveRecord::Base + has_many :user_roles, as: :subject + end + + class Role < ActiveRecord::Base + has_many :user_roles + has_many :users, through: :user_roles + has_many :foos, through: :user_roles + has_many :bars, through: :user_roles + end + + class UserRole < ActiveRecord::Base + belongs_to :user + belongs_to :role + belongs_to :subject, polymorphic: true, required: false + end + end + + it 'allows for access with association' do + user = User.create! + foo = Foo.create(name: 'foo') + role = Role.create(name: 'adviser') + UserRole.create(user: user, role: role, subject: foo) + ability = Ability.new(user) + ability.can :read, Foo, user_roles: { user: user } + expect(ability.can?(:read, Foo)).to eq(true) + end + + it 'allows for access with association with accesible_by' do + user = User.new + foo = Foo.create(name: 'foo') + bar = Bar.create(name: 'bar') + role = Role.create(name: 'adviser') + UserRole.create(user: user, role: role, subject: foo) + UserRole.create(user: user, role: role, subject: bar) + ability = Ability.new(user) + ability.can :read, Foo, user_roles: { user: user } + expect(Foo.accessible_by(ability)).to match_array([foo]) + expect(Bar.accessible_by(ability)).to match_array([]) + end + + it 'blocks access with association' do + user = User.create! + foo = Foo.create(name: 'foo') + role = Role.create(name: 'adviser') + UserRole.create(user: user, role: role, subject: foo) + ability = Ability.new(user) + ability.cannot :read, Foo, user_roles: { user: user } + expect(ability.can?(:read, Foo)).to eq(false) + end + + it 'blocks access with association for accesible_by' do + user = User.create! + foo = Foo.create(name: 'foo') + role = Role.create(name: 'adviser') + UserRole.create(user: user, role: role, subject: foo) + ability = Ability.new(user) + ability.cannot :read, Foo, user_roles: { user: user } + expect(Foo.accessible_by(ability)).to match_array([]) + expect(ability.can?(:read, Foo)).to eq(false) + end + + it 'manages access with multiple models and users' do + (0..5).each do |index| + user = User.create! + foo = Foo.create(name: 'foo') + role = Role.create(name: "adviser_#{index}") + UserRole.create(user: user, role: role, subject: foo) + end + + user = User.first + + Foo.all.each do |foo| + role = Role.create(name: 'new_user') + UserRole.create(user: user, role: role, subject: foo) + end + + ability = Ability.new(user) + ability.can :read, Foo, user_roles: { user: user } + expect(Foo.accessible_by(ability).count).to eq(Foo.count) + + User.where.not(id: user.id).each do |limited_permission_user| + ability = Ability.new(limited_permission_user) + expect(ability.can?(:read, Foo)).to eq(false) + expect(Foo.accessible_by(ability).count).to eq(0) + ability.can :read, Foo, user_roles: { user: limited_permission_user } + expect(ability.can?(:read, Foo)).to eq(true) + expect(Foo.accessible_by(ability).count).to eq(1) + end + end + end + context 'when a table references another one twice' do before do ActiveRecord::Schema.define do From 72cda945f3d42e3ae3e686be841a5f60bde13587 Mon Sep 17 00:00:00 2001 From: Nick Flueckiger Date: Thu, 5 Nov 2020 08:44:30 +0100 Subject: [PATCH 56/66] Implement style suggestion --- lib/cancan/conditions_matcher.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/cancan/conditions_matcher.rb b/lib/cancan/conditions_matcher.rb index 8399cc99..e07bfc13 100644 --- a/lib/cancan/conditions_matcher.rb +++ b/lib/cancan/conditions_matcher.rb @@ -78,8 +78,7 @@ def condition_match?(attribute, value) def hash_condition_match?(attribute, value) if attribute.is_a?(Array) || (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation)) - match_results = attribute.map { |element| matches_conditions_hash?(element, value) } - match_results.any? + attribute.to_a.any? { |element| matches_conditions_hash?(element, value) } else attribute && matches_conditions_hash?(attribute, value) end From 75d796e0959f051137de9cc76c160d62763737f3 Mon Sep 17 00:00:00 2001 From: Nick Flueckiger Date: Thu, 5 Nov 2020 21:25:21 +0100 Subject: [PATCH 57/66] Adding a guard to check if is inherited from ActiveRecrodBase --- lib/cancan/ability.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index d605053a..b8b3531f 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -302,7 +302,7 @@ def extract_subjects(subject) def alternative_subjects(subject) subject = subject.class unless subject.is_a?(Module) - if subject.respond_to?(:subclasses) + if subject.respond_to?(:subclasses) && subject < (ActiveRecord::Base) [:all, *(subject.ancestors + subject.subclasses), subject.class.to_s] else [:all, *subject.ancestors, subject.class.to_s] From 8075f5e4292725ff5c557a756cb8ed2c9d2263b8 Mon Sep 17 00:00:00 2001 From: Nick Flueckiger Date: Fri, 6 Nov 2020 11:25:01 +0100 Subject: [PATCH 58/66] Lint --- lib/cancan/ability.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index b8b3531f..a114bba3 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -302,7 +302,7 @@ def extract_subjects(subject) def alternative_subjects(subject) subject = subject.class unless subject.is_a?(Module) - if subject.respond_to?(:subclasses) && subject < (ActiveRecord::Base) + if subject.respond_to?(:subclasses) && subject < ActiveRecord::Base [:all, *(subject.ancestors + subject.subclasses), subject.class.to_s] else [:all, *subject.ancestors, subject.class.to_s] From 3c17def3a058c7f20175388998c82690d6eebe6d Mon Sep 17 00:00:00 2001 From: Alex Ghiculescu Date: Mon, 9 Nov 2020 13:49:38 -0600 Subject: [PATCH 59/66] accessible_by_strategy only works for Rails 5+ --- lib/cancan/config.rb | 30 ++++-- .../model_adapters/active_record_4_adapter.rb | 9 +- .../accessible_by_has_many_through_spec.rb | 18 ++-- .../active_record_4_adapter_spec.rb | 3 +- .../active_record_5_adapter_spec.rb | 2 +- .../active_record_adapter_spec.rb | 96 ++++++++++--------- .../has_and_belongs_to_many_spec.rb | 40 ++++---- spec/spec_helper.rb | 3 +- 8 files changed, 109 insertions(+), 92 deletions(-) diff --git a/lib/cancan/config.rb b/lib/cancan/config.rb index 9334226f..74f7253a 100644 --- a/lib/cancan/config.rb +++ b/lib/cancan/config.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true module CanCan - VALID_ACCESSIBLE_BY_STRATEGIES = %i[ - subquery - left_join - ].freeze + def self.valid_accessible_by_strategies + strategies = [:left_join] + strategies << :subquery unless CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0') + strategies + end # Determines how CanCan should build queries when calling accessible_by, # if the query will contain a join. The default strategy is `:subquery`. @@ -20,12 +21,27 @@ module CanCan # `distinct` is not reliable in some cases. See # https://github.com/CanCanCommunity/cancancan/pull/605 def self.accessible_by_strategy - @accessible_by_strategy || :subquery + @accessible_by_strategy || default_accessible_by_strategy + end + + def self.default_accessible_by_strategy + if CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0') + # see https://github.com/CanCanCommunity/cancancan/pull/655 for where this was added + # the `subquery` strategy (from https://github.com/CanCanCommunity/cancancan/pull/619 + # only works in Rails 5 and higher + :left_join + else + :subquery + end end def self.accessible_by_strategy=(value) - unless VALID_ACCESSIBLE_BY_STRATEGIES.include?(value) - raise ArgumentError, "accessible_by_strategy must be one of #{VALID_ACCESSIBLE_BY_STRATEGIES.join(', ')}" + unless valid_accessible_by_strategies.include?(value) + raise ArgumentError, "accessible_by_strategy must be one of #{valid_accessible_by_strategies.join(', ')}" + end + + if value == :subquery && CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0') + raise ArgumentError, 'accessible_by_strategy = :subquery requires ActiveRecord 5 or newer' end @accessible_by_strategy = value diff --git a/lib/cancan/model_adapters/active_record_4_adapter.rb b/lib/cancan/model_adapters/active_record_4_adapter.rb index b4461950..b2e9185f 100644 --- a/lib/cancan/model_adapters/active_record_4_adapter.rb +++ b/lib/cancan/model_adapters/active_record_4_adapter.rb @@ -35,14 +35,7 @@ def matches_condition?(subject, name, value) # you're using in the where. Instead, `references()` is required # in addition to `includes()` to force the outer join. def build_joins_relation(relation, *_where_conditions) - case CanCan.accessible_by_strategy - when :subquery - # subquery mode doesn't work with Rails 4.x - relation.includes(joins).references(joins) - - when :left_join - relation.includes(joins).references(joins) - end + relation.includes(joins).references(joins) end # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions` diff --git a/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb b/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb index da22e6b1..3f5eb212 100644 --- a/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb +++ b/spec/cancan/model_adapters/accessible_by_has_many_through_spec.rb @@ -72,7 +72,7 @@ class Editor < ActiveRecord::Base ability.can :read, Post, editors: { user_id: @user1 } end - CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| + CanCan.valid_accessible_by_strategies.each do |strategy| context "using #{strategy} strategy" do before :each do CanCan.accessible_by_strategy = strategy @@ -104,14 +104,16 @@ class Editor < ActiveRecord::Base end end - describe 'filtering of results - subquery' do - before :each do - CanCan.accessible_by_strategy = :subquery - end + unless CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0') + describe 'filtering of results - subquery' do + before :each do + CanCan.accessible_by_strategy = :subquery + end - it 'adds the where clause correctly with joins' do - posts = Post.joins(:editors).where('editors.user_id': @user1.id).accessible_by(ability) - expect(posts.length).to eq 1 + it 'adds the where clause correctly with joins' do + posts = Post.joins(:editors).where('editors.user_id': @user1.id).accessible_by(ability) + expect(posts.length).to eq 1 + end end end diff --git a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb index 59b3977d..f439db5b 100644 --- a/spec/cancan/model_adapters/active_record_4_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_4_adapter_spec.rb @@ -4,7 +4,8 @@ if CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0') describe CanCan::ModelAdapters::ActiveRecord4Adapter do - CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| + # only the `left_join` strategy works in AR4 + CanCan.valid_accessible_by_strategies.each do |strategy| context "with sqlite3 and #{strategy} strategy" do before :each do CanCan.accessible_by_strategy = strategy diff --git a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb index 0b7450b9..87786007 100644 --- a/spec/cancan/model_adapters/active_record_5_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_5_adapter_spec.rb @@ -4,7 +4,7 @@ if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') describe CanCan::ModelAdapters::ActiveRecord5Adapter do - CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| + CanCan.valid_accessible_by_strategies.each do |strategy| context "with sqlite3 and #{strategy} strategy" do before :each do CanCan.accessible_by_strategy = strategy diff --git a/spec/cancan/model_adapters/active_record_adapter_spec.rb b/spec/cancan/model_adapters/active_record_adapter_spec.rb index a29ca57c..e53227ce 100644 --- a/spec/cancan/model_adapters/active_record_adapter_spec.rb +++ b/spec/cancan/model_adapters/active_record_adapter_spec.rb @@ -109,7 +109,7 @@ class User < ActiveRecord::Base @comment_table = Comment.table_name end - CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| + CanCan.valid_accessible_by_strategies.each do |strategy| context "base functionality with #{strategy} strategy" do before :each do CanCan.accessible_by_strategy = strategy @@ -444,30 +444,32 @@ class User < ActiveRecord::Base end end - context 'base behaviour subquery specific' do - before :each do - CanCan.accessible_by_strategy = :subquery - end + unless CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0') + context 'base behaviour subquery specific' do + before :each do + CanCan.accessible_by_strategy = :subquery + end - it 'allows ordering via relations' do - @ability.can :read, Comment, article: { category: { visible: true } } - comment1 = Comment.create!(article: Article.create!(name: 'B', category: Category.create!(visible: true))) - comment2 = Comment.create!(article: Article.create!(name: 'A', category: Category.create!(visible: true))) - Comment.create!(article: Article.create!(category: Category.create!(visible: false))) + it 'allows ordering via relations' do + @ability.can :read, Comment, article: { category: { visible: true } } + comment1 = Comment.create!(article: Article.create!(name: 'B', category: Category.create!(visible: true))) + comment2 = Comment.create!(article: Article.create!(name: 'A', category: Category.create!(visible: true))) + Comment.create!(article: Article.create!(category: Category.create!(visible: false))) - # doesn't work without explicitly calling a join on AR 5+, - # but does before that (where we don't use subqueries at all) - if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') - expect { Comment.accessible_by(@ability).order('articles.name').to_a } - .to raise_error(ActiveRecord::StatementInvalid) - else - expect(Comment.accessible_by(@ability).order('articles.name')) + # doesn't work without explicitly calling a join on AR 5+, + # but does before that (where we don't use subqueries at all) + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') + expect { Comment.accessible_by(@ability).order('articles.name').to_a } + .to raise_error(ActiveRecord::StatementInvalid) + else + expect(Comment.accessible_by(@ability).order('articles.name')) + .to match_array([comment2, comment1]) + end + + # works with the explicit join + expect(Comment.accessible_by(@ability).joins(:article).order('articles.name')) .to match_array([comment2, comment1]) end - - # works with the explicit join - expect(Comment.accessible_by(@ability).joins(:article).order('articles.name')) - .to match_array([comment2, comment1]) end end @@ -612,7 +614,7 @@ class Transaction < ActiveRecord::Base end end - CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| + CanCan.valid_accessible_by_strategies.each do |strategy| context "when a table is referenced multiple times with #{strategy} strategy" do before :each do CanCan.accessible_by_strategy = strategy @@ -635,33 +637,35 @@ class Transaction < ActiveRecord::Base end end - context 'has_many through is defined and referenced differently - subquery strategy' do - before do - CanCan.accessible_by_strategy = :subquery - end + unless CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0') + context 'has_many through is defined and referenced differently - subquery strategy' do + before do + CanCan.accessible_by_strategy = :subquery + end - it 'recognises it and simplifies the query' do - u1 = User.create!(name: 'pippo') - u2 = User.create!(name: 'paperino') + it 'recognises it and simplifies the query' do + u1 = User.create!(name: 'pippo') + u2 = User.create!(name: 'paperino') - a1 = Article.create!(mentioned_users: [u1]) - a2 = Article.create!(mentioned_users: [u2]) + a1 = Article.create!(mentioned_users: [u1]) + a2 = Article.create!(mentioned_users: [u2]) - ability = Ability.new(u1) - ability.can :read, Article, mentioned_users: { name: u1.name } - ability.can :read, Article, mentions: { user: { name: u2.name } } - expect(Article.accessible_by(ability)).to match_array([a1, a2]) - if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') - expect(ability.model_adapter(Article, :read)).to generate_sql(%( - SELECT "articles".* - FROM "articles" - WHERE "articles"."id" IN - (SELECT "articles"."id" + ability = Ability.new(u1) + ability.can :read, Article, mentioned_users: { name: u1.name } + ability.can :read, Article, mentions: { user: { name: u2.name } } + expect(Article.accessible_by(ability)).to match_array([a1, a2]) + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') + expect(ability.model_adapter(Article, :read)).to generate_sql(%( + SELECT "articles".* FROM "articles" - LEFT OUTER JOIN "legacy_mentions" ON "legacy_mentions"."article_id" = "articles"."id" - LEFT OUTER JOIN "users" ON "users"."id" = "legacy_mentions"."user_id" - WHERE (("users"."name" = 'paperino') OR ("users"."name" = 'pippo'))) - )) + WHERE "articles"."id" IN + (SELECT "articles"."id" + FROM "articles" + LEFT OUTER JOIN "legacy_mentions" ON "legacy_mentions"."article_id" = "articles"."id" + LEFT OUTER JOIN "users" ON "users"."id" = "legacy_mentions"."user_id" + WHERE (("users"."name" = 'paperino') OR ("users"."name" = 'pippo'))) + )) + end end end end @@ -694,7 +698,7 @@ class Transaction < ActiveRecord::Base end end - CanCan::VALID_ACCESSIBLE_BY_STRATEGIES.each do |strategy| + CanCan.valid_accessible_by_strategies.each do |strategy| context "when a model has renamed primary_key with #{strategy} strategy" do before :each do CanCan.accessible_by_strategy = strategy diff --git a/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb b/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb index 5d233a7c..0d112e9e 100644 --- a/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb +++ b/spec/cancan/model_adapters/has_and_belongs_to_many_spec.rb @@ -45,28 +45,30 @@ class House < ActiveRecord::Base ability.can :read, House, people: { id: @person1.id } end - describe 'fetching of records - subquery strategy' do - before do - CanCan.accessible_by_strategy = :subquery - end + unless CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0') + describe 'fetching of records - subquery strategy' do + before do + CanCan.accessible_by_strategy = :subquery + end - it 'it retreives the records correctly' do - houses = House.accessible_by(ability) - expect(houses).to match_array [@house2, @house1] - end + it 'it retreives the records correctly' do + houses = House.accessible_by(ability) + expect(houses).to match_array [@house2, @house1] + end - if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') - it 'generates the correct query' do - expect(ability.model_adapter(House, :read)) - .to generate_sql("SELECT \"houses\".* - FROM \"houses\" - WHERE \"houses\".\"id\" IN - (SELECT \"houses\".\"id\" + if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0') + it 'generates the correct query' do + expect(ability.model_adapter(House, :read)) + .to generate_sql("SELECT \"houses\".* FROM \"houses\" - LEFT OUTER JOIN \"houses_people\" ON \"houses_people\".\"house_id\" = \"houses\".\"id\" - LEFT OUTER JOIN \"people\" ON \"people\".\"id\" = \"houses_people\".\"person_id\" - WHERE \"people\".\"id\" = #{@person1.id}) - ") + WHERE \"houses\".\"id\" IN + (SELECT \"houses\".\"id\" + FROM \"houses\" + LEFT OUTER JOIN \"houses_people\" ON \"houses_people\".\"house_id\" = \"houses\".\"id\" + LEFT OUTER JOIN \"people\" ON \"people\".\"id\" = \"houses_people\".\"person_id\" + WHERE \"people\".\"id\" = #{@person1.id}) + ") + end end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 09038fea..7f2a4ab8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -29,8 +29,7 @@ config.include SQLHelpers config.after :each do - # set default values for all config - CanCan.accessible_by_strategy = :subquery + CanCan.accessible_by_strategy = CanCan.default_accessible_by_strategy end end From bf5172ed5b5b2f13d538c4c8e7d5abf9078b13f4 Mon Sep 17 00:00:00 2001 From: Lukas Bischof Date: Thu, 19 Nov 2020 10:08:10 +0100 Subject: [PATCH 60/66] Make docs folder source of truth and links relative --- README.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 3ba01110..1758bae2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Travis badge](https://travis-ci.org/CanCanCommunity/cancancan.svg?branch=develop)](https://travis-ci.org/CanCanCommunity/cancancan) [![Code Climate Badge](https://codeclimate.com/github/CanCanCommunity/cancancan.svg)](https://codeclimate.com/github/CanCanCommunity/cancancan) -[Wiki](https://github.com/CanCanCommunity/cancancan/wiki) | +[Wiki](./docs) | [RDocs](http://rdoc.info/projects/CanCanCommunity/cancancan) | [Screencast 1](http://railscasts.com/episodes/192-authorization-with-cancan) | [Screencast 2](https://www.youtube.com/watch?v=cTYu-OjUgDw) @@ -73,7 +73,7 @@ class Ability end ``` -See [Defining Abilities](https://github.com/CanCanCommunity/cancancan/wiki/defining-abilities) for details on how to +See [Defining Abilities](./docs/Defining-Abilities.md) for details on how to define your rules. @@ -87,7 +87,7 @@ The current user's permissions can then be checked using the `can?` and `cannot? <% end %> ``` -See [Checking Abilities](https://github.com/CanCanCommunity/cancancan/wiki/checking-abilities) for more information +See [Checking Abilities](./docs/Checking-Abilities.md) for more information on how you can use these helpers. ## Fetching records @@ -101,13 +101,13 @@ The following: ``` will use your rules to ensure that the user retrieves only a list of posts that can be read. -See [Fetching records](https://github.com/CanCanCommunity/cancancan/wiki/Fetching-Records) for details. +See [Fetching records](./docs/Fetching-Records.md) for details. ## Controller helpers CanCanCan expects a `current_user` method to exist in the controller. First, set up some authentication (such as [Devise](https://github.com/plataformatec/devise) or [Authlogic](https://github.com/binarylogic/authlogic)). -See [Changing Defaults](https://github.com/CanCanCommunity/cancancan/wiki/changing-defaults) if you need a different behavior. +See [Changing Defaults](./docs/Changing-Defaults.md) if you need a different behavior. ### 3.1 Authorizations @@ -140,7 +140,7 @@ class PostsController < ApplicationController end ``` -See [Authorizing Controller Actions](https://github.com/CanCanCommunity/cancancan/wiki/authorizing-controller-actions) +See [Authorizing Controller Actions](./docs/Authorizing-controller-actions.md) for more information. @@ -204,7 +204,7 @@ Finally, it's possible to associate `param_method` with a Proc object which will load_and_authorize_resource param_method: Proc.new { |c| c.params.require(:post).permit(:name) } -See [Strong Parameters](https://github.com/CanCanCommunity/cancancan/wiki/Strong-Parameters) for more information. +See [Strong Parameters](./docs/Strong-Parameters.md) for more information. ## Handle Unauthorized Access @@ -223,7 +223,7 @@ class ApplicationController < ActionController::Base end ``` -See [Exception Handling](https://github.com/CanCanCommunity/cancancan/wiki/exception-handling) for more information. +See [Exception Handling](./docs/Exception-Handling.md) for more information. ## Lock It Down @@ -238,16 +238,16 @@ end 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/CanCanCommunity/cancancan/wiki/Ensure-Authorization) for more information. +See [Ensure Authorization](./docs/Ensure-Authorization.md) for more information. ## Wiki Docs -* [Defining Abilities](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Defining-Abilities.md) -* [Checking Abilities](https://github.com/CanCanCommunity/cancancan/wiki/Checking-Abilities) -* [Authorizing Controller Actions](https://github.com/CanCanCommunity/cancancan/wiki/Authorizing-Controller-Actions) -* [Exception Handling](https://github.com/CanCanCommunity/cancancan/wiki/Exception-Handling) -* [Changing Defaults](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/Changing-Defaults.md) -* [See more](https://github.com/CanCanCommunity/cancancan/wiki) +* [Defining Abilities](./docs/Defining-Abilities.md) +* [Checking Abilities](./docs/Checking-Abilities.md) +* [Authorizing Controller Actions](./docs/Authorizing-controller-actions.md) +* [Exception Handling](./docs/Exception-Handling.md) +* [Changing Defaults](./docs/Changing-Defaults.md) +* [See more](./docs) ## Mission @@ -261,7 +261,7 @@ Any help is greatly appreciated, feel free to submit pull-requests or open issue ## Questions? If you have any question or doubt regarding CanCanCan which you cannot find the solution to in the -[documentation](https://github.com/CanCanCommunity/cancancan/wiki) or our +[documentation](./docs) or our [mailing list](http://groups.google.com/group/cancancan), please [open a question on Stackoverflow](http://stackoverflow.com/questions/ask?tags=cancancan) with tag [cancancan](http://stackoverflow.com/questions/tagged/cancancan) @@ -279,7 +279,7 @@ When first developing, you need to run `bundle install` and then `appraisal inst You can then run all appraisal files (like CI does), with `appraisal rake` or just run a specific set `DB='sqlite' bundle exec appraisal activerecord_5.2.2 rake`. -See the [CONTRIBUTING](https://github.com/CanCanCommunity/cancancan/blob/develop/CONTRIBUTING.md) for more information. +See the [CONTRIBUTING](./CONTRIBUTING.md) for more information. ## Special Thanks From 3f45086aa5fe13c9c638f4ae552888810e7dc26a Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 4 Dec 2020 14:39:27 +0100 Subject: [PATCH 61/66] Exclude Rails 4 tests on truffleruby-head `truffleruby-head` targets Ruby 2.7 now, and Rails 4 is also excluded on CRuby 2.7. --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4ab62bf7..db1150ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,8 @@ matrix: gemfile: gemfiles/activerecord_6.0.0.gemfile - rvm: 2.7.0 gemfile: gemfiles/activerecord_4.2.0.gemfile + - rvm: truffleruby-head + gemfile: gemfiles/activerecord_4.2.0.gemfile - rvm: jruby-9.1.17.0 gemfile: gemfiles/activerecord_5.0.2.gemfile - rvm: jruby-9.1.17.0 From d7bacb51db7df7a83b60be01ffee849a7f2be987 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 12 Dec 2020 18:53:45 +0100 Subject: [PATCH 62/66] Changelog for 3.2.0 --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b5e492..b2d9eb07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ -## Unreleased +## 3.2.0 * [#649](https://github.com/CanCanCommunity/cancancan/pull/649): Add support for Single Table Inheritance. ([@Liberatys][]) +* [#640](https://github.com/CanCanCommunity/cancancan/pull/640): Simplify implementation of new model adapters. ([@ghiculescu][]) +* [#650](https://github.com/CanCanCommunity/cancancan/pull/650): Support associations in rules. ([@Liberatys][]) +* [#657](https://github.com/CanCanCommunity/cancancan/pull/657): Support for Rails 6.1. ([@ghiculescu][]) ## 3.1.0 @@ -670,3 +673,4 @@ Please read the [guide on migrating from CanCanCan 2.x to 3.0](https://github.co [@albb0920]: https://github.com/albb0920 [@ayumu838]: https://github.com/ayumu838 [@Liberatys]: https://github.com/Liberatys +[@ghiculescu]: https://github.com/ghiculescu From c9529d372a601fcfc3d59299195c69ae27ff4a9f Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 12 Dec 2020 18:57:27 +0100 Subject: [PATCH 63/66] Remove useless doc file --- docs/mvc--deficiencies.md | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 docs/mvc--deficiencies.md diff --git a/docs/mvc--deficiencies.md b/docs/mvc--deficiencies.md deleted file mode 100644 index 6c6133f4..00000000 --- a/docs/mvc--deficiencies.md +++ /dev/null @@ -1,15 +0,0 @@ -Hi all, First I like cancan because it collects all resource access rules in one place, but ! - -Although there are many benefits to mvc frameworks (mainly to large dev teams), there are of course short comings. - -So, you have an app with user roles and define access to resources using cancan. Then because of mvc you have to search all your views that render links to these protected resources. This creates a lot of unnecessary noise and is not very DRY. - -It seems to me that it would be simpler if the rules created in cancan automatically generated override filters (perhaps using deface) that warden could utilise so that these useless links are removed from the rendered html. - -What do you think ? - -I would call the approach mvc+ and define it such that the only legitimate use cases are those that have one requirement that has a common effect across all mvc domains (like user roles and access rights to resources). - -I would love to create a solution but although I have 30 years as a self employed software engineer I am not fully up to speed with the rights and wrongs of the finer detail within the rails framework. - -Any suggestions ! From 7985f3823064cd8845533833bd775c0648e0cb73 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 12 Dec 2020 19:11:28 +0100 Subject: [PATCH 64/66] Add tests for Rails 6.1.0 to the matrix --- .travis.yml | 10 +++++----- Appraisals | 17 +++++++++++++++++ gemfiles/activerecord_6.1.0.gemfile | 20 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 gemfiles/activerecord_6.1.0.gemfile diff --git a/.travis.yml b/.travis.yml index a32911d2..38edf69e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ cache: bundler addons: postgresql: "9.6" rvm: - - 2.3.5 - 2.4.2 - 2.5.1 - 2.6.3 @@ -20,6 +19,7 @@ gemfile: - gemfiles/activerecord_5.1.0.gemfile - gemfiles/activerecord_5.2.2.gemfile - gemfiles/activerecord_6.0.0.gemfile + - gemfiles/activerecord_6.1.0.gemfile - gemfiles/activerecord_master.gemfile env: - DB=sqlite @@ -28,10 +28,6 @@ env: matrix: fast_finish: true exclude: - - rvm: 2.2.6 - gemfile: gemfiles/activerecord_6.0.0.gemfile - - rvm: 2.3.5 - gemfile: gemfiles/activerecord_6.0.0.gemfile - rvm: 2.4.2 gemfile: gemfiles/activerecord_6.0.0.gemfile - rvm: 2.2.6 @@ -48,12 +44,16 @@ matrix: gemfile: gemfiles/activerecord_5.0.2.gemfile - rvm: jruby-9.1.17.0 gemfile: gemfiles/activerecord_6.0.0.gemfile + - rvm: jruby-9.1.17.0 + gemfile: gemfiles/activerecord_6.1.0.gemfile - rvm: jruby-9.1.17.0 gemfile: gemfiles/activerecord_master.gemfile - rvm: jruby-9.2.11.1 gemfile: gemfiles/activerecord_5.0.2.gemfile - rvm: jruby-9.2.11.1 gemfile: gemfiles/activerecord_6.0.0.gemfile + - rvm: jruby-9.2.11.1 + gemfile: gemfiles/activerecord_6.1.0.gemfile - rvm: jruby-9.2.11.1 gemfile: gemfiles/activerecord_master.gemfile allow_failures: diff --git a/Appraisals b/Appraisals index 4e685555..8d178d3d 100644 --- a/Appraisals +++ b/Appraisals @@ -84,6 +84,23 @@ appraise 'activerecord_6.0.0' do end end +appraise 'activerecord_6.1.0' do + gem 'actionpack', '~> 6.1.0', require: 'action_pack' + gem 'activerecord', '~> 6.1.0', require: 'active_record' + gem 'activesupport', '~> 6.1.0', require: 'active_support/all' + + platforms :jruby do + gem 'activerecord-jdbcsqlite3-adapter' + gem 'jdbc-sqlite3' + gem 'jdbc-postgres' + end + + platforms :ruby, :mswin, :mingw do + gem 'pg', '~> 1.1.4' + gem 'sqlite3', '~> 1.4.0' + end +end + appraise 'activerecord_master' do gem 'actionpack', github: 'rails/rails', require: 'action_pack' gem 'activerecord', github: 'rails/rails', require: 'active_record' diff --git a/gemfiles/activerecord_6.1.0.gemfile b/gemfiles/activerecord_6.1.0.gemfile new file mode 100644 index 00000000..6c556b84 --- /dev/null +++ b/gemfiles/activerecord_6.1.0.gemfile @@ -0,0 +1,20 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "actionpack", "~> 6.1.0", require: "action_pack" +gem "activerecord", "~> 6.1.0", require: "active_record" +gem "activesupport", "~> 6.1.0", require: "active_support/all" + +platforms :jruby do + gem "activerecord-jdbcsqlite3-adapter" + gem "jdbc-sqlite3" + gem "jdbc-postgres" +end + +platforms :ruby, :mswin, :mingw do + gem "pg", "~> 1.1.4" + gem "sqlite3", "~> 1.4.0" +end + +gemspec path: "../" From f9b73b5139594a2fd103961a9a9e704f9e1a7186 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 12 Dec 2020 21:38:28 +0100 Subject: [PATCH 65/66] Exclude latest ruby and Rails 4.2 combination from Travis matrix --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 38edf69e..645521b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,6 +38,8 @@ matrix: gemfile: gemfiles/activerecord_master.gemfile - rvm: 2.7.0 gemfile: gemfiles/activerecord_4.2.0.gemfile + - rvm: ruby-head + gemfile: gemfiles/activerecord_4.2.0.gemfile - rvm: truffleruby-head gemfile: gemfiles/activerecord_4.2.0.gemfile - rvm: jruby-9.1.17.0 From abfa504947d08c7dfdf976a129d643b04f543b17 Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Sat, 12 Dec 2020 21:39:13 +0100 Subject: [PATCH 66/66] Bump version 3.2.0 --- lib/cancan/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cancan/version.rb b/lib/cancan/version.rb index 6bea04b2..68d5417a 100644 --- a/lib/cancan/version.rb +++ b/lib/cancan/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module CanCan - VERSION = '3.1.0'.freeze + VERSION = '3.2.0'.freeze end