hyper-max-mod 0.0.1
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.
- checksums.yaml +7 -0
- data/cancancan-3.6.1/cancancan.gemspec +29 -0
- data/cancancan-3.6.1/init.rb +3 -0
- data/cancancan-3.6.1/lib/cancan/ability/actions.rb +93 -0
- data/cancancan-3.6.1/lib/cancan/ability/rules.rb +96 -0
- data/cancancan-3.6.1/lib/cancan/ability/strong_parameter_support.rb +41 -0
- data/cancancan-3.6.1/lib/cancan/ability.rb +312 -0
- data/cancancan-3.6.1/lib/cancan/class_matcher.rb +30 -0
- data/cancancan-3.6.1/lib/cancan/conditions_matcher.rb +147 -0
- data/cancancan-3.6.1/lib/cancan/config.rb +101 -0
- data/cancancan-3.6.1/lib/cancan/controller_additions.rb +399 -0
- data/cancancan-3.6.1/lib/cancan/controller_resource.rb +141 -0
- data/cancancan-3.6.1/lib/cancan/controller_resource_builder.rb +26 -0
- data/cancancan-3.6.1/lib/cancan/controller_resource_finder.rb +42 -0
- data/cancancan-3.6.1/lib/cancan/controller_resource_loader.rb +120 -0
- data/cancancan-3.6.1/lib/cancan/controller_resource_name_finder.rb +23 -0
- data/cancancan-3.6.1/lib/cancan/controller_resource_sanitizer.rb +32 -0
- data/cancancan-3.6.1/lib/cancan/exceptions.rb +70 -0
- data/cancancan-3.6.1/lib/cancan/matchers.rb +56 -0
- data/cancancan-3.6.1/lib/cancan/model_adapters/abstract_adapter.rb +77 -0
- data/cancancan-3.6.1/lib/cancan/model_adapters/active_record_4_adapter.rb +62 -0
- data/cancancan-3.6.1/lib/cancan/model_adapters/active_record_5_adapter.rb +61 -0
- data/cancancan-3.6.1/lib/cancan/model_adapters/active_record_adapter.rb +229 -0
- data/cancancan-3.6.1/lib/cancan/model_adapters/conditions_extractor.rb +75 -0
- data/cancancan-3.6.1/lib/cancan/model_adapters/conditions_normalizer.rb +49 -0
- data/cancancan-3.6.1/lib/cancan/model_adapters/default_adapter.rb +9 -0
- data/cancancan-3.6.1/lib/cancan/model_adapters/sti_normalizer.rb +47 -0
- data/cancancan-3.6.1/lib/cancan/model_adapters/strategies/base.rb +40 -0
- data/cancancan-3.6.1/lib/cancan/model_adapters/strategies/joined_alias_each_rule_as_exists_subquery.rb +93 -0
- data/cancancan-3.6.1/lib/cancan/model_adapters/strategies/joined_alias_exists_subquery.rb +31 -0
- data/cancancan-3.6.1/lib/cancan/model_adapters/strategies/left_join.rb +11 -0
- data/cancancan-3.6.1/lib/cancan/model_adapters/strategies/subquery.rb +18 -0
- data/cancancan-3.6.1/lib/cancan/model_additions.rb +34 -0
- data/cancancan-3.6.1/lib/cancan/parameter_validators.rb +9 -0
- data/cancancan-3.6.1/lib/cancan/relevant.rb +29 -0
- data/cancancan-3.6.1/lib/cancan/rule.rb +140 -0
- data/cancancan-3.6.1/lib/cancan/rules_compressor.rb +41 -0
- data/cancancan-3.6.1/lib/cancan/sti_detector.rb +12 -0
- data/cancancan-3.6.1/lib/cancan/unauthorized_message_resolver.rb +24 -0
- data/cancancan-3.6.1/lib/cancan/version.rb +5 -0
- data/cancancan-3.6.1/lib/cancan.rb +29 -0
- data/cancancan-3.6.1/lib/cancancan.rb +6 -0
- data/cancancan-3.6.1/lib/generators/cancan/ability/USAGE +4 -0
- data/cancancan-3.6.1/lib/generators/cancan/ability/ability_generator.rb +13 -0
- data/cancancan-3.6.1/lib/generators/cancan/ability/templates/ability.rb +32 -0
- data/hyper-max-mod.gemspec +12 -0
- metadata +86 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanCan
|
|
4
|
+
module ModelAdapters
|
|
5
|
+
class ActiveRecord5Adapter < ActiveRecord4Adapter
|
|
6
|
+
AbstractAdapter.inherited(self)
|
|
7
|
+
|
|
8
|
+
def self.for_class?(model_class)
|
|
9
|
+
version_greater_or_equal?('5.0.0') && model_class <= ActiveRecord::Base
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# rails 5 is capable of using strings in enum
|
|
13
|
+
# but often people use symbols in rules
|
|
14
|
+
def self.matches_condition?(subject, name, value)
|
|
15
|
+
return super if Array.wrap(value).all? { |x| x.is_a? Integer }
|
|
16
|
+
|
|
17
|
+
attribute = subject.send(name)
|
|
18
|
+
raw_attribute = subject.class.send(name.to_s.pluralize)[attribute]
|
|
19
|
+
!(Array(value).map(&:to_s) & [attribute, raw_attribute]).empty?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def build_joins_relation(relation, *where_conditions)
|
|
25
|
+
strategy_class.new(adapter: self, relation: relation, where_conditions: where_conditions).execute!
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def strategy_class
|
|
29
|
+
strategy_class_name = CanCan.accessible_by_strategy.to_s.camelize
|
|
30
|
+
CanCan::ModelAdapters::Strategies.const_get(strategy_class_name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def sanitize_sql(conditions)
|
|
34
|
+
if conditions.is_a?(Hash)
|
|
35
|
+
sanitize_sql_activerecord5(conditions)
|
|
36
|
+
else
|
|
37
|
+
@model_class.send(:sanitize_sql, conditions)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def sanitize_sql_activerecord5(conditions)
|
|
42
|
+
table = @model_class.send(:arel_table)
|
|
43
|
+
table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
|
|
44
|
+
predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
|
|
45
|
+
|
|
46
|
+
predicate_builder.build_from_hash(conditions.stringify_keys).map { |b| visit_nodes(b) }.join(' AND ')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def visit_nodes(node)
|
|
50
|
+
# Rails 5.2 adds a BindParam node that prevents the visitor method from properly compiling the SQL query
|
|
51
|
+
if self.class.version_greater_or_equal?('5.2.0')
|
|
52
|
+
connection = @model_class.send(:connection)
|
|
53
|
+
collector = Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new)
|
|
54
|
+
connection.visitor.accept(node, collector).value
|
|
55
|
+
else
|
|
56
|
+
@model_class.send(:connection).visitor.compile(node)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/AbcSize
|
|
4
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
5
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
6
|
+
module CanCan
|
|
7
|
+
module ModelAdapters
|
|
8
|
+
class ActiveRecordAdapter < AbstractAdapter
|
|
9
|
+
def self.version_greater_or_equal?(version)
|
|
10
|
+
Gem::Version.new(ActiveRecord.version).release >= Gem::Version.new(version)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.version_lower?(version)
|
|
14
|
+
Gem::Version.new(ActiveRecord.version).release < Gem::Version.new(version)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :compressed_rules
|
|
18
|
+
|
|
19
|
+
def initialize(model_class, rules)
|
|
20
|
+
super
|
|
21
|
+
@compressed_rules = if CanCan.rules_compressor_enabled
|
|
22
|
+
RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
|
|
23
|
+
else
|
|
24
|
+
@rules
|
|
25
|
+
end
|
|
26
|
+
StiNormalizer.normalize(@compressed_rules)
|
|
27
|
+
ConditionsNormalizer.normalize(model_class, @compressed_rules)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# When belongs_to parent_id is a condition for a model,
|
|
32
|
+
# we want to check the parent when testing ability for a hash {parent => model}
|
|
33
|
+
def override_nested_subject_conditions_matching?(parent, child, all_conditions)
|
|
34
|
+
parent_child_conditions(parent, child, all_conditions).present?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# parent_id condition can be an array of integer or one integer, we check the parent against this
|
|
38
|
+
def nested_subject_matches_conditions?(parent, child, all_conditions)
|
|
39
|
+
id_condition = parent_child_conditions(parent, child, all_conditions)
|
|
40
|
+
return id_condition.include?(parent.id) if id_condition.is_a? Array
|
|
41
|
+
return id_condition == parent.id if id_condition.is_a? Integer
|
|
42
|
+
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def parent_child_conditions(parent, child, all_conditions)
|
|
47
|
+
child_class = child.is_a?(Class) ? child : child.class
|
|
48
|
+
parent_class = parent.is_a?(Class) ? parent : parent.class
|
|
49
|
+
|
|
50
|
+
foreign_key = child_class.reflect_on_all_associations(:belongs_to).find do |association|
|
|
51
|
+
# Do not match on polymorphic associations or it will throw an error (klass cannot be determined)
|
|
52
|
+
!association.polymorphic? && association.klass == parent.class
|
|
53
|
+
end&.foreign_key&.to_sym
|
|
54
|
+
|
|
55
|
+
# Search again in case of polymorphic associations, this time matching on the :has_many side
|
|
56
|
+
# via the :as option, as well as klass
|
|
57
|
+
foreign_key ||= parent_class.reflect_on_all_associations(:has_many).find do |has_many_assoc|
|
|
58
|
+
matching_parent_child_polymorphic_association(has_many_assoc, child_class)
|
|
59
|
+
end&.foreign_key&.to_sym
|
|
60
|
+
|
|
61
|
+
foreign_key.nil? ? nil : all_conditions[foreign_key]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def matching_parent_child_polymorphic_association(parent_assoc, child_class)
|
|
65
|
+
return nil unless parent_assoc.klass == child_class
|
|
66
|
+
return nil if parent_assoc&.options[:as].nil?
|
|
67
|
+
|
|
68
|
+
child_class.reflect_on_all_associations(:belongs_to).find do |child_assoc|
|
|
69
|
+
# Only match this way for polymorphic associations
|
|
70
|
+
child_assoc.polymorphic? && child_assoc.name == parent_assoc.options[:as]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def child_association_to_parent(parent, child)
|
|
75
|
+
child_class = child.is_a?(Class) ? child : child.class
|
|
76
|
+
parent_class = parent.is_a?(Class) ? parent : parent.class
|
|
77
|
+
|
|
78
|
+
association = child_class.reflect_on_all_associations(:belongs_to).find do |belongs_to_assoc|
|
|
79
|
+
# Do not match on polymorphic associations or it will throw an error (klass cannot be determined)
|
|
80
|
+
!belongs_to_assoc.polymorphic? && belongs_to_assoc.klass == parent.class
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
return association if association
|
|
84
|
+
|
|
85
|
+
parent_class.reflect_on_all_associations(:has_many).each do |has_many_assoc|
|
|
86
|
+
association ||= matching_parent_child_polymorphic_association(has_many_assoc, child_class)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
association
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def parent_condition_name(parent, child)
|
|
93
|
+
child_association_to_parent(parent, child)&.name || parent.class.name.downcase.to_sym
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Returns conditions intended to be used inside a database query. Normally you will not call this
|
|
98
|
+
# method directly, but instead go through ModelAdditions#accessible_by.
|
|
99
|
+
#
|
|
100
|
+
# If there is only one "can" definition, a hash of conditions will be returned matching the one defined.
|
|
101
|
+
#
|
|
102
|
+
# can :manage, User, :id => 1
|
|
103
|
+
# query(:manage, User).conditions # => { :id => 1 }
|
|
104
|
+
#
|
|
105
|
+
# If there are multiple "can" definitions, a SQL string will be returned to handle complex cases.
|
|
106
|
+
#
|
|
107
|
+
# can :manage, User, :id => 1
|
|
108
|
+
# can :manage, User, :manager_id => 1
|
|
109
|
+
# cannot :manage, User, :self_managed => true
|
|
110
|
+
# query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
|
|
111
|
+
#
|
|
112
|
+
def conditions
|
|
113
|
+
conditions_extractor = ConditionsExtractor.new(@model_class)
|
|
114
|
+
if @compressed_rules.size == 1 && @compressed_rules.first.base_behavior
|
|
115
|
+
# Return the conditions directly if there's just one definition
|
|
116
|
+
conditions_extractor.tableize_conditions(@compressed_rules.first.conditions).dup
|
|
117
|
+
else
|
|
118
|
+
extract_multiple_conditions(conditions_extractor, @compressed_rules)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def extract_multiple_conditions(conditions_extractor, rules)
|
|
123
|
+
rules.reverse.inject(false_sql) do |sql, rule|
|
|
124
|
+
merge_conditions(sql, conditions_extractor.tableize_conditions(rule.conditions).dup, rule.base_behavior)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def database_records
|
|
129
|
+
if override_scope
|
|
130
|
+
@model_class.where(nil).merge(override_scope)
|
|
131
|
+
elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
|
|
132
|
+
build_relation(conditions)
|
|
133
|
+
else
|
|
134
|
+
@model_class.all(conditions: conditions, joins: joins)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def build_relation(*where_conditions)
|
|
139
|
+
relation = @model_class.where(*where_conditions)
|
|
140
|
+
return relation unless joins.present?
|
|
141
|
+
|
|
142
|
+
# subclasses must implement `build_joins_relation`
|
|
143
|
+
build_joins_relation(relation, *where_conditions)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Returns the associations used in conditions for the :joins option of a search.
|
|
147
|
+
# See ModelAdditions#accessible_by
|
|
148
|
+
def joins
|
|
149
|
+
joins_hash = {}
|
|
150
|
+
@compressed_rules.reverse_each do |rule|
|
|
151
|
+
deep_merge(joins_hash, rule.associations_hash)
|
|
152
|
+
end
|
|
153
|
+
deep_clean(joins_hash) unless joins_hash.empty?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
# Removes empty hashes and moves everything into arrays.
|
|
159
|
+
def deep_clean(joins_hash)
|
|
160
|
+
joins_hash.map { |name, nested| nested.empty? ? name : { name => deep_clean(nested) } }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Takes two hashes and does a deep merge.
|
|
164
|
+
def deep_merge(base_hash, added_hash)
|
|
165
|
+
added_hash.each do |key, value|
|
|
166
|
+
if base_hash[key].is_a?(Hash)
|
|
167
|
+
deep_merge(base_hash[key], value) unless value.empty?
|
|
168
|
+
else
|
|
169
|
+
base_hash[key] = value
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def override_scope
|
|
175
|
+
conditions = @compressed_rules.map(&:conditions).compact
|
|
176
|
+
return unless conditions.any? { |c| c.is_a?(ActiveRecord::Relation) }
|
|
177
|
+
return conditions.first if conditions.size == 1
|
|
178
|
+
|
|
179
|
+
raise_override_scope_error
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def raise_override_scope_error
|
|
183
|
+
rule_found = @compressed_rules.detect { |rule| rule.conditions.is_a?(ActiveRecord::Relation) }
|
|
184
|
+
raise Error,
|
|
185
|
+
'Unable to merge an Active Record scope with other conditions. ' \
|
|
186
|
+
"Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def merge_conditions(sql, conditions_hash, behavior)
|
|
190
|
+
if conditions_hash.blank?
|
|
191
|
+
behavior ? true_sql : false_sql
|
|
192
|
+
else
|
|
193
|
+
merge_non_empty_conditions(behavior, conditions_hash, sql)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def merge_non_empty_conditions(behavior, conditions_hash, sql)
|
|
198
|
+
conditions = sanitize_sql(conditions_hash)
|
|
199
|
+
case sql
|
|
200
|
+
when true_sql
|
|
201
|
+
behavior ? true_sql : "not (#{conditions})"
|
|
202
|
+
when false_sql
|
|
203
|
+
behavior ? conditions : false_sql
|
|
204
|
+
else
|
|
205
|
+
behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def false_sql
|
|
210
|
+
sanitize_sql(['?=?', true, false])
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def true_sql
|
|
214
|
+
sanitize_sql(['?=?', true, true])
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def sanitize_sql(conditions)
|
|
218
|
+
@model_class.send(:sanitize_sql, conditions)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
224
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
225
|
+
# rubocop:enable Metrics/AbcSize
|
|
226
|
+
|
|
227
|
+
ActiveSupport.on_load(:active_record) do
|
|
228
|
+
send :include, CanCan::ModelAdditions
|
|
229
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# this class is responsible of converting the hash of conditions
|
|
4
|
+
# in "where conditions" to generate the sql query
|
|
5
|
+
# it consists of a names_cache that helps calculating the next name given to the association
|
|
6
|
+
# it tries to reflect the behavior of ActiveRecord when generating aliases for tables.
|
|
7
|
+
module CanCan
|
|
8
|
+
module ModelAdapters
|
|
9
|
+
class ConditionsExtractor
|
|
10
|
+
def initialize(model_class)
|
|
11
|
+
@names_cache = { model_class.table_name => [] }.with_indifferent_access
|
|
12
|
+
@root_model_class = model_class
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def tableize_conditions(conditions, model_class = @root_model_class, path_to_key = 0)
|
|
16
|
+
return conditions unless conditions.is_a? Hash
|
|
17
|
+
|
|
18
|
+
conditions.each_with_object({}) do |(key, value), result_hash|
|
|
19
|
+
if value.is_a? Hash
|
|
20
|
+
result_hash.merge!(calculate_result_hash(key, model_class, path_to_key, result_hash, value))
|
|
21
|
+
else
|
|
22
|
+
result_hash[key] = value
|
|
23
|
+
end
|
|
24
|
+
result_hash
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def calculate_result_hash(key, model_class, path_to_key, result_hash, value)
|
|
31
|
+
reflection = model_class.reflect_on_association(key)
|
|
32
|
+
nested_resulted = calculate_nested(model_class, result_hash, key, value.dup, path_to_key)
|
|
33
|
+
association_class = reflection.klass.name.constantize
|
|
34
|
+
tableize_conditions(nested_resulted, association_class, "#{path_to_key}_#{key}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def calculate_nested(model_class, result_hash, relation_name, value, path_to_key)
|
|
38
|
+
value.each_with_object({}) do |(k, v), nested|
|
|
39
|
+
if v.is_a? Hash
|
|
40
|
+
value.delete(k)
|
|
41
|
+
nested[k] = v
|
|
42
|
+
else
|
|
43
|
+
table_alias = generate_table_alias(model_class, relation_name, path_to_key)
|
|
44
|
+
result_hash[table_alias] = value
|
|
45
|
+
end
|
|
46
|
+
nested
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def generate_table_alias(model_class, relation_name, path_to_key)
|
|
51
|
+
table_alias = model_class.reflect_on_association(relation_name).table_name.to_sym
|
|
52
|
+
|
|
53
|
+
if already_used?(table_alias, relation_name, path_to_key)
|
|
54
|
+
table_alias = "#{relation_name.to_s.pluralize}_#{model_class.table_name}".to_sym
|
|
55
|
+
|
|
56
|
+
index = 1
|
|
57
|
+
while already_used?(table_alias, relation_name, path_to_key)
|
|
58
|
+
table_alias = "#{table_alias}_#{index += 1}".to_sym
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
add_to_cache(table_alias, relation_name, path_to_key)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def already_used?(table_alias, relation_name, path_to_key)
|
|
65
|
+
@names_cache[table_alias].try(:exclude?, "#{path_to_key}_#{relation_name}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def add_to_cache(table_alias, relation_name, path_to_key)
|
|
69
|
+
@names_cache[table_alias] ||= []
|
|
70
|
+
@names_cache[table_alias] << "#{path_to_key}_#{relation_name}"
|
|
71
|
+
table_alias
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# this class is responsible of normalizing the hash of conditions
|
|
2
|
+
# by exploding has_many through associations
|
|
3
|
+
# when a condition is defined with an has_many through association this is exploded in all its parts
|
|
4
|
+
# TODO: it could identify STI and normalize it
|
|
5
|
+
module CanCan
|
|
6
|
+
module ModelAdapters
|
|
7
|
+
class ConditionsNormalizer
|
|
8
|
+
class << self
|
|
9
|
+
def normalize(model_class, rules)
|
|
10
|
+
rules.each { |rule| rule.conditions = normalize_conditions(model_class, rule.conditions) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def normalize_conditions(model_class, conditions)
|
|
14
|
+
return conditions unless conditions.is_a? Hash
|
|
15
|
+
|
|
16
|
+
conditions.each_with_object({}) do |(key, value), result_hash|
|
|
17
|
+
if value.is_a? Hash
|
|
18
|
+
result_hash.merge!(calculate_result_hash(model_class, key, value))
|
|
19
|
+
else
|
|
20
|
+
result_hash[key] = value
|
|
21
|
+
end
|
|
22
|
+
result_hash
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def calculate_result_hash(model_class, key, value)
|
|
29
|
+
reflection = model_class.reflect_on_association(key)
|
|
30
|
+
unless reflection
|
|
31
|
+
raise WrongAssociationName, "Association '#{key}' not defined in model '#{model_class.name}'"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if normalizable_association? reflection
|
|
35
|
+
key = reflection.options[:through]
|
|
36
|
+
value = { reflection.source_reflection_name => value }
|
|
37
|
+
reflection = model_class.reflect_on_association(key)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
{ key => normalize_conditions(reflection.klass.name.constantize, value) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def normalizable_association?(reflection)
|
|
44
|
+
reflection.options[:through].present? && !reflection.options[:source_type].present?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require_relative '../sti_detector'
|
|
2
|
+
|
|
3
|
+
# this class is responsible for detecting sti classes and creating new rules for the
|
|
4
|
+
# relevant subclasses, using the inheritance_column as a merger
|
|
5
|
+
module CanCan
|
|
6
|
+
module ModelAdapters
|
|
7
|
+
class StiNormalizer
|
|
8
|
+
class << self
|
|
9
|
+
def normalize(rules)
|
|
10
|
+
rules_cache = []
|
|
11
|
+
return unless defined?(ActiveRecord::Base)
|
|
12
|
+
|
|
13
|
+
rules.delete_if do |rule|
|
|
14
|
+
subjects = rule.subjects.select do |subject|
|
|
15
|
+
update_rule(subject, rule, rules_cache)
|
|
16
|
+
end
|
|
17
|
+
subjects.length == rule.subjects.length
|
|
18
|
+
end
|
|
19
|
+
rules_cache.each { |rule| rules.push(rule) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def update_rule(subject, rule, rules_cache)
|
|
25
|
+
return false unless StiDetector.sti_class?(subject)
|
|
26
|
+
|
|
27
|
+
rules_cache.push(build_rule_for_subclass(rule, subject))
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# create a new rule for the subclasses that links on the inheritance_column
|
|
32
|
+
def build_rule_for_subclass(rule, subject)
|
|
33
|
+
sti_conditions = { subject.inheritance_column => subject.sti_name }
|
|
34
|
+
new_rule_conditions =
|
|
35
|
+
if rule.with_scope?
|
|
36
|
+
rule.conditions.where(sti_conditions)
|
|
37
|
+
else
|
|
38
|
+
rule.conditions.merge(sti_conditions)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
CanCan::Rule.new(rule.base_behavior, rule.actions, subject.superclass,
|
|
42
|
+
new_rule_conditions, rule.block)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module CanCan
|
|
2
|
+
module ModelAdapters
|
|
3
|
+
class Strategies
|
|
4
|
+
class Base
|
|
5
|
+
attr_reader :adapter, :relation, :where_conditions
|
|
6
|
+
|
|
7
|
+
delegate(
|
|
8
|
+
:compressed_rules,
|
|
9
|
+
:extract_multiple_conditions,
|
|
10
|
+
:joins,
|
|
11
|
+
:model_class,
|
|
12
|
+
:quoted_primary_key,
|
|
13
|
+
:quoted_aliased_table_name,
|
|
14
|
+
:quoted_table_name,
|
|
15
|
+
to: :adapter
|
|
16
|
+
)
|
|
17
|
+
delegate :connection, :quoted_primary_key, to: :model_class
|
|
18
|
+
delegate :quote_table_name, to: :connection
|
|
19
|
+
|
|
20
|
+
def initialize(adapter:, relation:, where_conditions:)
|
|
21
|
+
@adapter = adapter
|
|
22
|
+
@relation = relation
|
|
23
|
+
@where_conditions = where_conditions
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def aliased_table_name
|
|
27
|
+
@aliased_table_name ||= "#{model_class.table_name}_alias"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def quoted_aliased_table_name
|
|
31
|
+
@quoted_aliased_table_name ||= quote_table_name(aliased_table_name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def quoted_table_name
|
|
35
|
+
@quoted_table_name ||= quote_table_name(model_class.table_name)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
module CanCan
|
|
4
|
+
module ModelAdapters
|
|
5
|
+
class Strategies
|
|
6
|
+
class JoinedAliasEachRuleAsExistsSubquery < Base
|
|
7
|
+
def execute!
|
|
8
|
+
model_class
|
|
9
|
+
.joins(
|
|
10
|
+
"JOIN #{quoted_table_name} AS #{quoted_aliased_table_name} ON " \
|
|
11
|
+
"#{quoted_aliased_table_name}.#{quoted_primary_key} = #{quoted_table_name}.#{quoted_primary_key}"
|
|
12
|
+
)
|
|
13
|
+
.where(double_exists_sql)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def double_exists_sql
|
|
17
|
+
double_exists_sql = ''
|
|
18
|
+
|
|
19
|
+
compressed_rules.each_with_index do |rule, index|
|
|
20
|
+
double_exists_sql << ' OR ' if index.positive?
|
|
21
|
+
double_exists_sql << "EXISTS (#{sub_query_for_rule(rule).to_sql})"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
double_exists_sql
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def sub_query_for_rule(rule)
|
|
28
|
+
conditions_extractor = ConditionsExtractor.new(model_class)
|
|
29
|
+
rule_where_conditions = extract_multiple_conditions(conditions_extractor, [rule])
|
|
30
|
+
joins_hash, left_joins_hash = extract_joins_from_rule(rule)
|
|
31
|
+
sub_query_for_rules_and_join_hashes(rule_where_conditions, joins_hash, left_joins_hash)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def sub_query_for_rules_and_join_hashes(rule_where_conditions, joins_hash, left_joins_hash)
|
|
35
|
+
model_class
|
|
36
|
+
.select('1')
|
|
37
|
+
.joins(joins_hash)
|
|
38
|
+
.left_joins(left_joins_hash)
|
|
39
|
+
.where(
|
|
40
|
+
"#{quoted_table_name}.#{quoted_primary_key} = " \
|
|
41
|
+
"#{quoted_aliased_table_name}.#{quoted_primary_key}"
|
|
42
|
+
)
|
|
43
|
+
.where(rule_where_conditions)
|
|
44
|
+
.limit(1)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def extract_joins_from_rule(rule)
|
|
48
|
+
joins = {}
|
|
49
|
+
left_joins = {}
|
|
50
|
+
|
|
51
|
+
extra_joins_recursive([], rule.conditions, joins, left_joins)
|
|
52
|
+
[joins, left_joins]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def extra_joins_recursive(current_path, conditions, joins, left_joins)
|
|
56
|
+
conditions.each do |key, value|
|
|
57
|
+
if value.is_a?(Hash)
|
|
58
|
+
current_path << key
|
|
59
|
+
extra_joins_recursive(current_path, value, joins, left_joins)
|
|
60
|
+
current_path.pop
|
|
61
|
+
else
|
|
62
|
+
extra_joins_recursive_merge_joins(current_path, value, joins, left_joins)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def extra_joins_recursive_merge_joins(current_path, value, joins, left_joins)
|
|
68
|
+
hash_joins = current_path_to_hash(current_path)
|
|
69
|
+
|
|
70
|
+
if value.nil?
|
|
71
|
+
left_joins.deep_merge!(hash_joins)
|
|
72
|
+
else
|
|
73
|
+
joins.deep_merge!(hash_joins)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Converts an array like [:child, :grand_child] into a hash like {child: {grand_child: {}}
|
|
78
|
+
def current_path_to_hash(current_path)
|
|
79
|
+
hash_joins = {}
|
|
80
|
+
current_hash_joins = hash_joins
|
|
81
|
+
|
|
82
|
+
current_path.each do |path_part|
|
|
83
|
+
new_hash = {}
|
|
84
|
+
current_hash_joins[path_part] = new_hash
|
|
85
|
+
current_hash_joins = new_hash
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
hash_joins
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanCan
|
|
4
|
+
module ModelAdapters
|
|
5
|
+
class Strategies
|
|
6
|
+
class JoinedAliasExistsSubquery < Base
|
|
7
|
+
def execute!
|
|
8
|
+
model_class
|
|
9
|
+
.joins(
|
|
10
|
+
"JOIN #{quoted_table_name} AS #{quoted_aliased_table_name} ON " \
|
|
11
|
+
"#{quoted_aliased_table_name}.#{quoted_primary_key} = #{quoted_table_name}.#{quoted_primary_key}"
|
|
12
|
+
)
|
|
13
|
+
.where("EXISTS (#{joined_alias_exists_subquery_inner_query.to_sql})")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def joined_alias_exists_subquery_inner_query
|
|
17
|
+
model_class
|
|
18
|
+
.unscoped
|
|
19
|
+
.select('1')
|
|
20
|
+
.left_joins(joins)
|
|
21
|
+
.where(*where_conditions)
|
|
22
|
+
.where(
|
|
23
|
+
"#{quoted_table_name}.#{quoted_primary_key} = " \
|
|
24
|
+
"#{quoted_aliased_table_name}.#{quoted_primary_key}"
|
|
25
|
+
)
|
|
26
|
+
.limit(1)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module CanCan
|
|
2
|
+
module ModelAdapters
|
|
3
|
+
class Strategies
|
|
4
|
+
class Subquery < Base
|
|
5
|
+
def execute!
|
|
6
|
+
build_joins_relation_subquery(where_conditions)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def build_joins_relation_subquery(where_conditions)
|
|
10
|
+
inner = model_class.unscoped do
|
|
11
|
+
model_class.left_joins(joins).where(*where_conditions)
|
|
12
|
+
end
|
|
13
|
+
model_class.where(model_class.primary_key => inner)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|