cancan 1.4.1 → 1.5.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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