Skip to content

Adds support for rules at the attribute/method level #474

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
76b9bce
Merge branch 'release/2.1.0'
coorasse Nov 10, 2017
b0172b4
Merge branch 'release/2.1.1'
coorasse Nov 13, 2017
aef8e47
Merge branch 'release/2.1.2'
coorasse Nov 22, 2017
f19392c
Changes Rule initialize method to take the more common &block style
phaedryx Dec 11, 2017
4ac0b1b
add an additional 'attributes' parameter to rules
phaedryx Dec 13, 2017
e88b3aa
the nils aren't needed
phaedryx Jan 19, 2018
1086662
minor test description tweak
phaedryx Jan 20, 2018
bf3c0d9
add rule test to demonstrate nil for attribute
phaedryx Jan 20, 2018
5978058
define a version of pg to see if travis will pass
phaedryx Jan 22, 2018
0170b0a
remove stray #
phaedryx Jan 22, 2018
d74d596
adds specific (minimal) versions of the 'pg' gem for activerecord
phaedryx Jan 25, 2018
b5c75ba
small change
phaedryx Feb 6, 2018
25a251e
adds a method to get attributes for strong parameter permissions
phaedryx Feb 6, 2018
ccf4a15
tighten permitted_attributes up a bit
phaedryx Feb 7, 2018
4fff325
please rubocop
phaedryx Feb 7, 2018
cb09f41
I liked the suggestion
phaedryx Feb 7, 2018
f2cb830
tighten permitted_attributes more
phaedryx Feb 8, 2018
a5d85d2
be more thorough when checking attributes
phaedryx Feb 8, 2018
24bff9b
Merge branch 'feature/3.0.0' into incorporate-ryanbs-attribute-code
phaedryx Feb 8, 2018
350033d
rubocop fix
phaedryx Feb 8, 2018
361cf6d
because being able to drop into a debugging environment in your tests…
phaedryx Feb 8, 2018
29270a3
incompatible with jruby, nevermind
phaedryx Feb 8, 2018
bf5aab1
Fixes bug where cannot rule should be ignored when subject is a class…
phaedryx Apr 2, 2018
290672e
resolve conflicts
phaedryx Apr 2, 2018
b1de291
Fixes some issues. Primarily refactors strong parameter support
phaedryx Apr 3, 2018
2fbb28d
adds additional tests for permitted_attributes method
phaedryx Apr 3, 2018
c5579c0
a little more testing
phaedryx Apr 3, 2018
7a8dfac
fix bug with cannot rules with attributes and accessible by
phaedryx Apr 6, 2018
8f43acc
make test consistent with other tests
phaedryx Apr 6, 2018
a99eb75
Merge branch 'feature/3.0.0' into incorporate-ryanbs-attribute-code
phaedryx Apr 9, 2018
795a51c
Merge branch 'feature/3.0.0' into incorporate-ryanbs-attribute-code
coorasse Jun 22, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Lint/AmbiguousBlockAssociation:


AllCops:
TargetRubyVersion: 2.0
TargetRubyVersion: 2.2.0
Exclude:
- 'gemfiles/vendor/bundle/**/*'
- 'Appraisals'
Expand Down
6 changes: 3 additions & 3 deletions Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ appraise 'activerecord_4.2' do

gemfile.platforms :ruby, :mswin, :mingw do
gem 'sqlite3'
gem 'pg'
gem 'pg', '~> 0.15'
end
end

Expand All @@ -27,7 +27,7 @@ appraise 'activerecord_5.0.2' do

gemfile.platforms :ruby, :mswin, :mingw do
gem 'sqlite3'
gem 'pg'
gem 'pg', '~> 0.18'
end
end

Expand All @@ -43,6 +43,6 @@ appraise 'activerecord_5.1.0' do

gemfile.platforms :ruby, :mswin, :mingw do
gem 'sqlite3'
gem 'pg'
gem 'pg', '~> 0.18'
end
end
4 changes: 2 additions & 2 deletions cancancan.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ Gem::Specification.new do |s|

s.required_ruby_version = '>= 2.2.0'

s.add_development_dependency 'appraisal', '~> 2.0', '>= 2.0.0'
s.add_development_dependency 'bundler', '~> 1.3'
s.add_development_dependency 'rubocop', '~> 0.48.1'
s.add_development_dependency 'rake', '~> 10.1', '>= 10.1.1'
s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0'
s.add_development_dependency 'appraisal', '~> 2.0', '>= 2.0.0'
s.add_development_dependency 'rubocop', '~> 0.48.1'
end
2 changes: 1 addition & 1 deletion gemfiles/activerecord_4.2.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ end

platforms :ruby, :mswin, :mingw do
gem "sqlite3"
gem "pg"
gem "pg", "~> 0.15"
end

gemspec path: "../"
2 changes: 1 addition & 1 deletion gemfiles/activerecord_5.0.2.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ end

platforms :ruby, :mswin, :mingw do
gem "sqlite3"
gem "pg"
gem "pg", "~> 0.18"
end

gemspec path: "../"
2 changes: 1 addition & 1 deletion gemfiles/activerecord_5.1.0.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ end

platforms :ruby, :mswin, :mingw do
gem "sqlite3"
gem "pg"
gem "pg", "~> 0.18"
end

gemspec path: "../"
1 change: 1 addition & 0 deletions lib/cancan.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'cancan/version'
require 'cancan/parameter_validators'
require 'cancan/ability'
require 'cancan/rule'
require 'cancan/controller_resource'
Expand Down
29 changes: 18 additions & 11 deletions lib/cancan/ability.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require_relative 'ability/rules.rb'
require_relative 'ability/actions.rb'
require_relative 'ability/strong_parameter_support'

module CanCan
# This module is designed to be included into an Ability class. This will
# provide the "can" methods for defining and checking abilities.
Expand All @@ -19,6 +21,7 @@ module CanCan
module Ability
include CanCan::Ability::Rules
include CanCan::Ability::Actions
include StrongParameterSupport

# Check if the user has permission to perform a given action on an object.
#
Expand Down Expand Up @@ -64,10 +67,10 @@ module Ability
# end
#
# Also see the RSpec Matchers to aid in testing.
def can?(action, subject, *extra_args)
def can?(action, subject, attribute = nil, *extra_args)
match = extract_subjects(subject).lazy.map do |a_subject|
relevant_rules_for_match(action, a_subject).detect do |rule|
rule.matches_conditions?(action, a_subject, extra_args)
relevant_rules_for_match(action, a_subject, attribute).detect do |rule|
rule.matches_conditions?(action, a_subject, attribute, *extra_args)
end
end.reject(&:nil?).first
match ? match.base_behavior : false
Expand Down Expand Up @@ -134,8 +137,8 @@ def cannot?(*args)
# # check the database and return true/false
# end
#
def can(action = nil, subject = nil, conditions = nil, &block)
add_rule(Rule.new(true, action, subject, conditions, block))
def can(action = nil, subject = nil, *attributes_and_conditions, &block)
add_rule(Rule.new(true, action, subject, *attributes_and_conditions, &block))
end

# Defines an ability which cannot be done. Accepts the same arguments as "can".
Expand All @@ -150,8 +153,8 @@ def can(action = nil, subject = nil, conditions = nil, &block)
# product.invisible?
# end
#
def cannot(action = nil, subject = nil, conditions = nil, &block)
add_rule(Rule.new(false, action, subject, conditions, block))
def cannot(action = nil, subject = nil, *attributes_and_conditions, &block)
add_rule(Rule.new(false, action, subject, *attributes_and_conditions, &block))
end

# User shouldn't specify targets with names of real actions or it will cause Seg fault
Expand Down Expand Up @@ -239,19 +242,23 @@ def merge(ability)
#
# Where can_hash and cannot_hash are formatted thusly:
# {
# action: array_of_objects
# action: { subject: [attributes] }
# }
def permissions
permissions_list = { can: {}, cannot: {} }
permissions_list = {
can: Hash.new { |actions, k1| actions[k1] = Hash.new { |subjects, k2| subjects[k2] = [] } },
cannot: Hash.new { |actions, k1| actions[k1] = Hash.new { |subjects, k2| subjects[k2] = [] } }
}
rules.each { |rule| extract_rule_in_permissions(permissions_list, rule) }
permissions_list
end

def extract_rule_in_permissions(permissions_list, rule)
expand_actions(rule.actions).each do |action|
container = rule.base_behavior ? :can : :cannot
permissions_list[container][action] ||= []
permissions_list[container][action] += rule.subjects.map(&:to_s)
rule.subjects.each do |subject|
permissions_list[container][action][subject.to_s] += rule.attributes
end
end
end

Expand Down
10 changes: 5 additions & 5 deletions lib/cancan/ability/rules.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ def add_rule_to_index(rule, position)

# Returns an array of Rule instances which match the action and subject
# This does not take into consideration any hash conditions or block statements
def relevant_rules(action, subject)
def relevant_rules(action, subject, attribute = nil)
return [] unless @rules
relevant = possible_relevant_rules(subject).select do |rule|
rule.expanded_actions = expand_actions(rule.actions)
rule.relevant? action, subject
rule.relevant? action, subject, attribute
end
relevant.reverse!.uniq!
optimize_order! relevant
Expand All @@ -50,12 +50,12 @@ def possible_relevant_rules(subject)
end
end

def relevant_rules_for_match(action, subject)
relevant_rules(action, subject).each do |rule|
def relevant_rules_for_match(action, subject, attribute)
relevant_rules(action, subject, attribute).each do |rule|
next unless rule.only_raw_sql?
raise Error,
"The can? and cannot? call cannot be used with a raw sql 'can' definition."\
" The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
" The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
end
end

Expand Down
25 changes: 25 additions & 0 deletions lib/cancan/ability/strong_parameter_support.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module CanCan
module Ability
module StrongParameterSupport
# Return an array of attributes suitable for use with strong parameters
def permitted_attributes(action, subject)
@permitted_attributes ||= {}
@permitted_attributes[[action, subject]] ||= allowed_attributes(action, subject)
end

private

def allowed_attributes(action, subject)
attributes = relevant_rules(action, subject).reduce([]) do |array, rule|

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternatively

attributes = relevant_rules(action, subject).flat_map do |rule|
  if rule.attributes.empty? && subject.class == Class # empty attributes is an 'all'
    subject.instance_methods.map(&:to_sym)
  else
    rule.attributes
  end
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if rule.attributes.empty? && subject.class == Class # empty attributes is an 'all'
array + subject.instance_methods.map(&:to_sym)
else
array + rule.attributes
end
end
attributes.uniq!
attributes.select { |attribute| can?(action, subject, attribute) }
end
end
end
end
11 changes: 7 additions & 4 deletions lib/cancan/conditions_matcher.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
module CanCan
module ConditionsMatcher
# Matches the block or conditions hash
def matches_conditions?(action, subject, extra_args)
return call_block_with_all(action, subject, extra_args) if @match_all
return @block.call(subject, *extra_args) if @block && !subject_class?(subject)
def matches_conditions?(action, subject, attribute, *extra_args)
return call_block_with_all(action, subject, *extra_args) if @match_all
if @block && !subject_class?(subject)
return @block.call(subject, attribute, *extra_args) if attribute
return @block.call(subject, *extra_args)
end
matches_non_block_conditions(subject)
end

Expand Down Expand Up @@ -74,7 +77,7 @@ def hash_condition_match?(attribute, value)
end
end

def call_block_with_all(action, subject, extra_args)
def call_block_with_all(action, subject, *extra_args)
if subject.class == Class
@block.call(action, subject, nil, *extra_args)
else
Expand Down
6 changes: 6 additions & 0 deletions lib/cancan/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ class ImplementationRemoved < Error; end
# Raised when using check_authorization without calling authorized!
class AuthorizationNotPerformed < Error; end

# Raised when a rule is created with both a block and a hash of conditions
class BlockAndConditionsError < Error; end

# Raised when an unexpected argument is passed as an attribute
class AttributeArgumentError < Error; end

# This error is raised when a user isn't allowed to access a given controller action.
# This usually happens within a call to ControllerAdditions#authorize! but can be
# raised manually.
Expand Down
7 changes: 7 additions & 0 deletions lib/cancan/parameter_validators.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module CanCan
module ParameterValidators
def valid_attribute_param?(attribute)
attribute.nil? || attribute.is_a?(Symbol) || (attribute.is_a?(Array) && attribute.first.is_a?(Symbol))
end
end
end
40 changes: 31 additions & 9 deletions lib/cancan/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,32 @@ module CanCan
# helpful methods to determine permission checking and conditions hash generation.
class Rule # :nodoc:
include ConditionsMatcher
attr_reader :base_behavior, :subjects, :actions, :conditions
include ParameterValidators
attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes
attr_writer :expanded_actions

# The first argument when initializing is the base_behavior which is a true/false
# value. True for "can" and false for "cannot". The next two arguments are the action
# and subject respectively (such as :read, @project). The third argument is a hash
# of conditions and the last one is the block passed to the "can" call.
def initialize(base_behavior, action, subject, conditions, block)
both_block_and_hash_error = 'You are not able to supply a block with a hash of conditions in '\
"#{action} #{subject} ability. Use either one."
raise Error, both_block_and_hash_error if conditions.is_a?(Hash) && block
def initialize(base_behavior, action, subject, *extra_args, &block)
# for backwards compatibility, attributes are an optional parameter. Check if
# attributes were passed or are actually conditions
attributes, extra_args = parse_attributes_from_extra_args(extra_args)
condition_and_block_check(extra_args, block, action, subject)
@match_all = action.nil? && subject.nil?
@base_behavior = base_behavior
@actions = Array(action)
@subjects = Array(subject)
@conditions = conditions || {}
@attributes = Array(attributes)
@conditions = extra_args || {}
@block = block
end

# Matches both the subject and action, not necessarily the conditions
def relevant?(action, subject)
# Matches the action, subject, and attribute; not necessarily the conditions
def relevant?(action, subject, attribute = nil)
subject = subject.values.first if subject.class == Hash
@match_all || (matches_action?(action) && matches_subject?(subject))
@match_all || (matches_action?(action) && matches_subject?(subject) && matches_attribute?(attribute))
end

def only_block?
Expand Down Expand Up @@ -73,12 +76,31 @@ def matches_subject?(subject)
@subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject)
end

def matches_attribute?(attribute)
return true if @attributes.empty?
return @base_behavior if attribute.nil?
@attributes.include?(attribute.to_sym)
end

def matches_subject_class?(subject)
@subjects.any? do |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

def parse_attributes_from_extra_args(args)
attributes = args.shift if valid_attribute_param?(args.first)
extra_args = args.shift

[attributes, extra_args]
end

def condition_and_block_check(conditions, block, action, subject)
return unless conditions.is_a?(Hash) && block
raise BlockAndConditionsError, 'A hash of conditions is mutually exclusive with a block.'\
"Check #{action} #{subject} ability."
end
end
end
Loading