cancan 1.4.1 → 1.5.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. data/CHANGELOG.rdoc +21 -0
  2. data/Gemfile +17 -0
  3. data/LICENSE +1 -1
  4. data/README.rdoc +16 -77
  5. data/Rakefile +8 -0
  6. data/lib/cancan.rb +8 -3
  7. data/lib/cancan/ability.rb +24 -26
  8. data/lib/cancan/controller_additions.rb +50 -0
  9. data/lib/cancan/controller_resource.rb +33 -15
  10. data/lib/cancan/exceptions.rb +3 -0
  11. data/lib/cancan/model_adapters/abstract_adapter.rb +40 -0
  12. data/lib/cancan/model_adapters/active_record_adapter.rb +119 -0
  13. data/lib/cancan/model_adapters/data_mapper_adapter.rb +33 -0
  14. data/lib/cancan/model_adapters/default_adapter.rb +7 -0
  15. data/lib/cancan/model_adapters/mongoid_adapter.rb +41 -0
  16. data/lib/cancan/{active_record_additions.rb → model_additions.rb} +5 -16
  17. data/lib/cancan/{can_definition.rb → rule.rb} +29 -25
  18. data/lib/generators/cancan/ability/USAGE +4 -0
  19. data/lib/generators/cancan/ability/ability_generator.rb +11 -0
  20. data/lib/generators/cancan/ability/templates/ability.rb +28 -0
  21. data/spec/README.rdoc +28 -0
  22. data/spec/cancan/ability_spec.rb +11 -3
  23. data/spec/cancan/controller_additions_spec.rb +30 -0
  24. data/spec/cancan/controller_resource_spec.rb +68 -2
  25. data/spec/cancan/inherited_resource_spec.rb +3 -1
  26. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +185 -0
  27. data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +115 -0
  28. data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
  29. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +168 -0
  30. data/spec/cancan/rule_spec.rb +39 -0
  31. data/spec/spec_helper.rb +2 -24
  32. metadata +26 -17
  33. data/lib/cancan/query.rb +0 -97
  34. data/spec/cancan/active_record_additions_spec.rb +0 -75
  35. data/spec/cancan/can_definition_spec.rb +0 -57
  36. data/spec/cancan/query_spec.rb +0 -107
@@ -2,6 +2,9 @@ module CanCan
2
2
  # A general CanCan exception
3
3
  class Error < StandardError; end
4
4
 
5
+ # Raised when behavior is not implemented, usually used in an abstract class.
6
+ class NotImplemented < Error; end
7
+
5
8
  # Raised when removed code is called, an alternative solution is provided in message.
6
9
  class ImplementationRemoved < Error; end
7
10
 
@@ -0,0 +1,40 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class AbstractAdapter
4
+ def self.inherited(subclass)
5
+ @subclasses ||= []
6
+ @subclasses << subclass
7
+ end
8
+
9
+ def self.adapter_class(model_class)
10
+ @subclasses.detect { |subclass| subclass.for_class?(model_class) } || DefaultAdapter
11
+ end
12
+
13
+ # Used to determine if the given adapter should be used for the passed in class.
14
+ def self.for_class?(member_class)
15
+ false # override in subclass
16
+ end
17
+
18
+ # Used to determine if this model adapter will override the matching behavior for a hash of conditions.
19
+ # If this returns true then matches_conditions_hash? will be called. See Rule#matches_conditions_hash
20
+ def self.override_conditions_hash_matching?(subject, conditions)
21
+ false
22
+ end
23
+
24
+ # Override if override_conditions_hash_matching? returns true
25
+ def self.matches_conditions_hash?(subject, conditions)
26
+ raise NotImplemented, "This model adapter does not support matching on a conditions hash."
27
+ end
28
+
29
+ def initialize(model_class, rules)
30
+ @model_class = model_class
31
+ @rules = rules
32
+ end
33
+
34
+ def database_records
35
+ # This should be overridden in a subclass to return records which match @rules
36
+ raise NotImplemented, "This model adapter does not support fetching records from the database."
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,119 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class ActiveRecordAdapter < AbstractAdapter
4
+ def self.for_class?(model_class)
5
+ model_class <= ActiveRecord::Base
6
+ end
7
+
8
+ # Returns conditions intended to be used inside a database query. Normally you will not call this
9
+ # method directly, but instead go through ModelAdditions#accessible_by.
10
+ #
11
+ # If there is only one "can" definition, a hash of conditions will be returned matching the one defined.
12
+ #
13
+ # can :manage, User, :id => 1
14
+ # query(:manage, User).conditions # => { :id => 1 }
15
+ #
16
+ # If there are multiple "can" definitions, a SQL string will be returned to handle complex cases.
17
+ #
18
+ # can :manage, User, :id => 1
19
+ # can :manage, User, :manager_id => 1
20
+ # cannot :manage, User, :self_managed => true
21
+ # query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
22
+ #
23
+ def conditions
24
+ if @rules.size == 1 && @rules.first.base_behavior
25
+ # Return the conditions directly if there's just one definition
26
+ tableized_conditions(@rules.first.conditions).dup
27
+ else
28
+ @rules.reverse.inject(false_sql) do |sql, rule|
29
+ merge_conditions(sql, tableized_conditions(rule.conditions).dup, rule.base_behavior)
30
+ end
31
+ end
32
+ end
33
+
34
+ def tableized_conditions(conditions)
35
+ return conditions unless conditions.kind_of? Hash
36
+ conditions.inject({}) do |result_hash, (name, value)|
37
+ if value.kind_of? Hash
38
+ name = @model_class.reflect_on_association(name).table_name
39
+ value = tableized_conditions(value)
40
+ end
41
+ result_hash[name] = value
42
+ result_hash
43
+ end
44
+ end
45
+
46
+ # Returns the associations used in conditions for the :joins option of a search.
47
+ # See ModelAdditions#accessible_by
48
+ def joins
49
+ joins_hash = {}
50
+ @rules.each do |rule|
51
+ merge_joins(joins_hash, rule.associations_hash)
52
+ end
53
+ clean_joins(joins_hash) unless joins_hash.empty?
54
+ end
55
+
56
+ def database_records
57
+ if @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
58
+ @model_class.where(conditions).joins(joins)
59
+ else
60
+ @model_class.scoped(:conditions => conditions, :joins => joins)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def merge_conditions(sql, conditions_hash, behavior)
67
+ if conditions_hash.blank?
68
+ behavior ? true_sql : false_sql
69
+ else
70
+ conditions = sanitize_sql(conditions_hash)
71
+ case sql
72
+ when true_sql
73
+ behavior ? true_sql : "not (#{conditions})"
74
+ when false_sql
75
+ behavior ? conditions : false_sql
76
+ else
77
+ behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
78
+ end
79
+ end
80
+ end
81
+
82
+ def false_sql
83
+ sanitize_sql(['?=?', true, false])
84
+ end
85
+
86
+ def true_sql
87
+ sanitize_sql(['?=?', true, true])
88
+ end
89
+
90
+ def sanitize_sql(conditions)
91
+ @model_class.send(:sanitize_sql, conditions)
92
+ end
93
+
94
+ # Takes two hashes and does a deep merge.
95
+ def merge_joins(base, add)
96
+ add.each do |name, nested|
97
+ if base[name].is_a?(Hash) && !nested.empty?
98
+ merge_joins(base[name], nested)
99
+ else
100
+ base[name] = nested
101
+ end
102
+ end
103
+ end
104
+
105
+ # Removes empty hashes and moves everything into arrays.
106
+ def clean_joins(joins_hash)
107
+ joins = []
108
+ joins_hash.each do |name, nested|
109
+ joins << (nested.empty? ? name : {name => clean_joins(nested)})
110
+ end
111
+ joins
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ ActiveRecord::Base.class_eval do
118
+ include CanCan::ModelAdditions
119
+ end
@@ -0,0 +1,33 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class DataMapperAdapter < AbstractAdapter
4
+ def self.for_class?(model_class)
5
+ model_class <= DataMapper::Resource
6
+ end
7
+
8
+ def self.override_conditions_hash_matching?(subject, conditions)
9
+ conditions.any? { |k,v| !k.kind_of?(Symbol) }
10
+ end
11
+
12
+ def self.matches_conditions_hash?(subject, conditions)
13
+ subject.class.all(:conditions => conditions).include?(subject) # TODO don't use a database query here for performance and other instances
14
+ end
15
+
16
+ def database_records
17
+ scope = @model_class.all(:conditions => ["0=1"])
18
+ conditions.each do |condition|
19
+ scope += @model_class.all(:conditions => condition)
20
+ end
21
+ scope
22
+ end
23
+
24
+ def conditions
25
+ @rules.map(&:conditions)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ DataMapper::Model.class_eval do
32
+ include CanCan::ModelAdditions::ClassMethods
33
+ end
@@ -0,0 +1,7 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class DefaultAdapter < AbstractAdapter
4
+ # This adapter is used when no matching adapter is found
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class MongoidAdapter < AbstractAdapter
4
+ def self.for_class?(model_class)
5
+ model_class <= Mongoid::Document
6
+ end
7
+
8
+ def self.override_conditions_hash_matching?(subject, conditions)
9
+ conditions.any? { |k,v| !k.kind_of?(Symbol) }
10
+ end
11
+
12
+ def self.matches_conditions_hash?(subject, conditions)
13
+ # To avoid hitting the db, retrieve the raw Mongo selector from
14
+ # the Mongoid Criteria and use Mongoid::Matchers#matches?
15
+ subject.matches?( subject.class.where(conditions).selector )
16
+ end
17
+
18
+ def database_records
19
+ @model_class.where(conditions)
20
+ end
21
+
22
+ def conditions
23
+ if @rules.size == 0
24
+ false_query
25
+ else
26
+ @rules.first.conditions
27
+ end
28
+ end
29
+
30
+ def false_query
31
+ # this query is sure to return no results
32
+ {:_id => {'$exists' => false, '$type' => 7}} # type 7 is an ObjectID (default for _id)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ # simplest way to add `accessible_by` to all Mongoid Documents
39
+ module Mongoid::Document::ClassMethods
40
+ include CanCan::ModelAdditions::ClassMethods
41
+ end
@@ -1,6 +1,7 @@
1
1
  module CanCan
2
- # This module is automatically included into all Active Record models.
3
- module ActiveRecordAdditions
2
+
3
+ # This module adds the accessible_by class method to a model. It is included in the model adapters.
4
+ module ModelAdditions
4
5
  module ClassMethods
5
6
  # Returns a scope which fetches only the records that the passed ability
6
7
  # can perform a given action on. The action defaults to :read. This
@@ -17,15 +18,9 @@ module CanCan
17
18
  #
18
19
  # @articles = Article.accessible_by(current_ability, :update)
19
20
  #
20
- # Here only the articles which the user can update are returned. This
21
- # internally uses Ability#conditions method, see that for more information.
21
+ # Here only the articles which the user can update are returned.
22
22
  def accessible_by(ability, action = :read)
23
- query = ability.query(action, self)
24
- if respond_to?(:where) && respond_to?(:joins)
25
- where(query.conditions).joins(query.joins)
26
- else
27
- scoped(:conditions => query.conditions, :joins => query.joins)
28
- end
23
+ ability.model_adapter(self, action).database_records
29
24
  end
30
25
  end
31
26
 
@@ -34,9 +29,3 @@ module CanCan
34
29
  end
35
30
  end
36
31
  end
37
-
38
- if defined? ActiveRecord
39
- ActiveRecord::Base.class_eval do
40
- include CanCan::ActiveRecordAdditions
41
- end
42
- end
@@ -2,8 +2,8 @@ module CanCan
2
2
  # This class is used internally and should only be called through Ability.
3
3
  # it holds the information about a "can" call made on Ability and provides
4
4
  # helpful methods to determine permission checking and conditions hash generation.
5
- class CanDefinition # :nodoc:
6
- attr_reader :base_behavior, :actions
5
+ class Rule # :nodoc:
6
+ attr_reader :base_behavior, :actions, :conditions
7
7
  attr_writer :expanded_actions
8
8
 
9
9
  # The first argument when initializing is the base_behavior which is a true/false
@@ -41,18 +41,6 @@ module CanCan
41
41
  end
42
42
  end
43
43
 
44
- def tableized_conditions(conditions = @conditions)
45
- return conditions unless conditions.kind_of? Hash
46
- conditions.inject({}) do |result_hash, (name, value)|
47
- if value.kind_of? Hash
48
- name = name.to_s.tableize.to_sym
49
- value = tableized_conditions(value)
50
- end
51
- result_hash[name] = value
52
- result_hash
53
- end
54
- end
55
-
56
44
  def only_block?
57
45
  conditions_empty? && !@block.nil?
58
46
  end
@@ -100,19 +88,31 @@ module CanCan
100
88
  @subjects.any? { |sub| sub.kind_of?(Module) && (subject.kind_of?(sub) || subject.class.to_s == sub.to_s || subject.kind_of?(Module) && subject.ancestors.include?(sub)) }
101
89
  end
102
90
 
91
+ # Checks if the given subject matches the given conditions hash.
92
+ # This behavior can be overriden by a model adapter by defining two class methods:
93
+ # override_matching_for_conditions?(subject, conditions) and
94
+ # matches_conditions_hash?(subject, conditions)
103
95
  def matches_conditions_hash?(subject, conditions = @conditions)
104
- conditions.all? do |name, value|
105
- attribute = subject.send(name)
106
- if value.kind_of?(Hash)
107
- if attribute.kind_of? Array
108
- attribute.any? { |element| matches_conditions_hash? element, value }
109
- else
110
- matches_conditions_hash? attribute, value
111
- end
112
- elsif value.kind_of?(Array) || value.kind_of?(Range)
113
- value.include? attribute
96
+ if conditions.empty?
97
+ true
98
+ else
99
+ if model_adapter(subject).override_conditions_hash_matching? subject, conditions
100
+ model_adapter(subject).matches_conditions_hash? subject, conditions
114
101
  else
115
- attribute == value
102
+ conditions.all? do |name, value|
103
+ attribute = subject.send(name)
104
+ if value.kind_of?(Hash)
105
+ if attribute.kind_of? Array
106
+ attribute.any? { |element| matches_conditions_hash? element, value }
107
+ else
108
+ matches_conditions_hash? attribute, value
109
+ end
110
+ elsif value.kind_of?(Array) || value.kind_of?(Range)
111
+ value.include? attribute
112
+ else
113
+ attribute == value
114
+ end
115
+ end
116
116
  end
117
117
  end
118
118
  end
@@ -129,5 +129,9 @@ module CanCan
129
129
  @block.call(action, subject.class, subject, *extra_args)
130
130
  end
131
131
  end
132
+
133
+ def model_adapter(subject)
134
+ ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
135
+ end
132
136
  end
133
137
  end
@@ -0,0 +1,4 @@
1
+ Description:
2
+ The cancan:ability generator creates an Ability class in the models
3
+ directory. You can move this file anywhere you want as long as it
4
+ is in the load path.
@@ -0,0 +1,11 @@
1
+ module Cancan
2
+ module Generators
3
+ class AbilityGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ def generate_ability
7
+ copy_file "ability.rb", "app/models/ability.rb"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ class Ability
2
+ include CanCan::Ability
3
+
4
+ def initialize(user)
5
+ # Define abilities for the passed in user here. For example:
6
+ #
7
+ # user ||= User.new # guest user (not logged in)
8
+ # if user.admin?
9
+ # can :manage, :all
10
+ # else
11
+ # can :read, :all
12
+ # end
13
+ #
14
+ # The first argument to `can` is the action you are giving the user permission to do.
15
+ # If you pass :manage it will apply to every action. Other common actions here are
16
+ # :read, :create, :update and :destroy.
17
+ #
18
+ # The second argument is the resource the user can perform the action on. If you pass
19
+ # :all it will apply to every resource. Otherwise pass a Ruby class of the resource.
20
+ #
21
+ # The third argument is an optional hash of conditions to further filter the objects.
22
+ # For example, here the user can only update published articles.
23
+ #
24
+ # can :update, Article, :published => true
25
+ #
26
+ # See the wiki for details: https://github.com/ryanb/cancan/wiki/Defining-Abilities
27
+ end
28
+ end
data/spec/README.rdoc ADDED
@@ -0,0 +1,28 @@
1
+ = CanCan Specs
2
+
3
+ == Running the specs
4
+
5
+ To run the specs first run the +bundle+ command to install the necessary gems and the +rake+ command to run the specs.
6
+
7
+ bundle
8
+ rake
9
+
10
+ The specs currently require Ruby 1.8.7. Ruby 1.9.2 support will be coming soon.
11
+
12
+
13
+ == Model Adapters
14
+
15
+ CanCan offers separate specs for different model adapters (such as Mongoid and Data Mapper). By default it will use Active Record but you can change this by setting the +MODEL_ADAPTER+ environment variable before running. You can run the +bundle+ command with this as well to ensure you have the installed gems.
16
+
17
+ MODEL_ADAPTER=data_mapper bundle
18
+ MODEL_ADAPTER=data_mapper rake
19
+
20
+ The different model adapters you can specify are:
21
+
22
+ * active_record (default)
23
+ * data_mapper
24
+ * mongoid
25
+
26
+ You can also run the +spec_all+ rake task to run specs for each adapter.
27
+
28
+ rake spec_all