Module: Authz::Scopables::Base Private
- Defined in:
- lib/authz/scopables/base.rb
Overview
This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.
Any scopables created by the host application should extend this module. The module provides all the functionality that a scopable needs.
Defined Under Namespace
Classes: AmbiguousAssociationName, MisconfiguredAssociation, NoApplicableScopables, NoAssociationFound, UnresolvableKeyword
Constant Summary collapse
- @@scopables =
This classvariable is part of a private API. You should avoid using this classvariable if possible, as it may be removed or be changed in the future.
Scopable::Base tracks all available Scopables
[]
Class Method Summary collapse
-
.extended(scopable) ⇒ Object
private
When Scopables::Base is extended, run within the context of the extending scopable ===================================================================.
-
.get_applicable_scopables(collection_or_class) ⇒ Object
private
Returns all the applicable scopable modules for the given collection_or_class.
-
.get_applicable_scopables!(collection_or_class) ⇒ Object
private
Returns all the applicable scopable modules for the given collection_or_class and raises an error if none are found.
-
.get_scopables_modules ⇒ Object
private
Returns an array of the scoping module instances.
-
.get_scopables_names ⇒ Object
private
Returns an array with the names of the modules in camelcase (string).
-
.register_scopable(scopable) ⇒ Object
private
Contains a handle to each scopable.
-
.scopable_by?(collection_or_class, scopable) ⇒ Boolean
private
Returns true if the given collection_or_class is scopable by the given scopable module.
-
.scopable_exists?(scopable_name) ⇒ Boolean
private
Returns true if the given scopable name exists as a valid scopable @scopable_name: the string name of the scopable to test @return: true or false.
-
.special_keywords ⇒ Object
private
Returns an array with the special keywords.
Instance Method Summary collapse
-
#apply_scopable_method_name ⇒ Object
private
Returns the mame of the method used to apply the scopable keyword on the scoped class.
-
#associated_scoping_instances_ids(instance_to_check) ⇒ Object
private
Receives an instance of any class that is scopable by this scopable and returns an array of ids of the associated scoping instances.
-
#association_method_name ⇒ Object
private
Returns the name of the method used to get the name of the association for this scopable.
-
#available_keywords ⇒ Array<String>
Available keywords for creating scoping rules.
-
#normalize_if_special_keyword(keyword) ⇒ Object
private
Normalizes the keyword if it is a special keyword.
-
#plural_association_name ⇒ Object
private
Symbol of a plural association following Rails’ conventions.
-
#resolve_keyword(keyword, requester) ⇒ Array<Integers>
The ids that the given keywords resolve to.
-
#resolve_keyword!(keyword, requester) ⇒ Object
private
Resolution Calls .resolve_keyword and ensures that the returned value is valid.
-
#scoping_class ⇒ Object
private
Returns the Active Record Class of the Model used to scope.
-
#scoping_class_name ⇒ Object
private
Returns the string name of the class used to scope.
-
#singular_association_name ⇒ Object
private
Symbol of a singular association following Rails’conventions.
-
#valid_keyword?(keyword) ⇒ Boolean
private
Returns true if the given keyword is valid.
-
#within_scope_of_keyword?(instance_to_check, keyword, requester) ⇒ Boolean
private
Returns true if the given instance_to_check is within the scoping privileges of the given keyword, optionally passing the requester to aide the resolution of the keyword.
Class Method Details
.extended(scopable) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
When Scopables::Base is extended, run within the context of the extending scopable
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 |
# File 'lib/authz/scopables/base.rb', line 309 def self.extended(scopable) # self = Authz::Scopable::Base # scopable = scopable module that extended scopable.extend ActiveSupport::Concern self.register_scopable(scopable) # Any class that extends a Scopable gets these class methods # =================================================================== scopable.class_methods do # self = The class being scoped (the class that includes an scopable) # Defines a method that returns the name of the association to be used # for scoping. # For example, if Report includes ScopableByCity this will create a # scopable_by_city_association_name method. # # The method infers the association name to be used with the scopable. # If ambiguity is found, raises an Exception. # # This method should be overriden to manually set the association name. define_method scopable.association_method_name do association_name = (self.reflect_on_all_associations.map(&:name) & [scopable.singular_association_name.to_sym, scopable.plural_association_name.to_sym]) if association_name.size > 1 raise AmbiguousAssociationName, scoped_class: self.model_name.to_s, scopable: scopable, association_names: association_name end association_name.last end # Provides scoped classes with a convenient method to override the automatically inferred # association name for a given scopable. # # Usage: # include ScopableByCity # set_scopable_by_city_association_name :province define_method "set_#{scopable.association_method_name}" do |assoc_name| unless %w[Symbol String].include? assoc_name.class.name raise 'only strings or symbols are allowed' end define_singleton_method(scopable.association_method_name) { assoc_name.to_sym } end # Applies the scopable keyword on the class # @return a collection of the scoped class record after applying the scope define_method scopable.apply_scopable_method_name do |keyword, requester| keyword = scopable.normalize_if_special_keyword(keyword) if self.name == scopable.scoping_class_name # If the scoped class is the same scoping class # (e.g City and ScopableByCity) # Treatment for special keywords return self.all if keyword == :all scoped_ids = scopable.resolve_keyword!(keyword, requester) return self.where(id: scoped_ids) elsif (association_name = self.send(scopable.association_method_name)) # If the scoped class scoped by the scoping class # (e.g Report and ScopableByCity) Join through the association to query joined_collection = self.left_outer_joins(association_name) # Always left_outer_joins to account for records that are not # associated with the scoping class (e.g. reports with no city) # Report.left_outer_joins(:city) # Treatment for special keywords # TODO: the collection is forced to get joined to ensure structural # compatibility with ActiveRecord#or return joined_collection.all if keyword == :all scoped_ids = scopable.resolve_keyword!(keyword, requester) return joined_collection.merge(scopable.scoping_class.where(id: scoped_ids)) # Report.joins(:city).merge(City.where(id: [1,2,3])) else raise NoAssociationFound, scoped_class: self.model_name.to_s, scopable: scopable, scoping_class: scopable.scoping_class_name end end end end |
.get_applicable_scopables(collection_or_class) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns all the applicable scopable modules for the given collection_or_class
43 44 45 46 47 |
# File 'lib/authz/scopables/base.rb', line 43 def self.get_applicable_scopables collection_or_class get_scopables_modules.select do |scopable| scopable_by?(collection_or_class, scopable) end end |
.get_applicable_scopables!(collection_or_class) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns all the applicable scopable modules for the given collection_or_class and raises an error if none are found
52 53 54 55 56 |
# File 'lib/authz/scopables/base.rb', line 52 def self.get_applicable_scopables! collection_or_class app_scopables = get_applicable_scopables(collection_or_class) return app_scopables if app_scopables.any? raise NoApplicableScopables, scoped_class: collection_or_class end |
.get_scopables_modules ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns an array of the scoping module instances
22 23 24 |
# File 'lib/authz/scopables/base.rb', line 22 def self.get_scopables_modules @@scopables end |
.get_scopables_names ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns an array with the names of the modules in camelcase (string)
17 18 19 |
# File 'lib/authz/scopables/base.rb', line 17 def self.get_scopables_names @@scopables.map{ |s| s.name } end |
.register_scopable(scopable) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Contains a handle to each scopable
12 13 14 |
# File 'lib/authz/scopables/base.rb', line 12 def self.register_scopable(scopable) @@scopables << scopable unless @@scopables.include?(scopable) end |
.scopable_by?(collection_or_class, scopable) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns true if the given collection_or_class is scopable by the given scopable module
37 38 39 |
# File 'lib/authz/scopables/base.rb', line 37 def self.scopable_by? collection_or_class, scopable collection_or_class.respond_to?(scopable.association_method_name) end |
.scopable_exists?(scopable_name) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns true if the given scopable name exists as a valid scopable @scopable_name: the string name of the scopable to
test
@return: true or false
31 32 33 |
# File 'lib/authz/scopables/base.rb', line 31 def self.scopable_exists?(scopable_name) get_scopables_names.include?(scopable_name.to_s) end |
.special_keywords ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns an array with the special keywords
59 60 61 62 |
# File 'lib/authz/scopables/base.rb', line 59 def self.special_keywords # TODO: consider adding keyword none [:all] end |
Instance Method Details
#apply_scopable_method_name ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns the mame of the method used to apply the scopable keyword on the scoped class
189 190 191 |
# File 'lib/authz/scopables/base.rb', line 189 def apply_scopable_method_name "apply_#{to_s.underscore}" end |
#associated_scoping_instances_ids(instance_to_check) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Receives an instance of any class that is scopable by this scopable and returns an array of ids of the associated scoping instances.
For example:
-
Receives a report and returns an array with the
the id of the city associated with the report [32] or [] if not associated
-
Receives an announcement and returns an array with the
ids of the cites in which it is available [1,2,3] or [] if not associated
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 |
# File 'lib/authz/scopables/base.rb', line 270 def associated_scoping_instances_ids(instance_to_check) scoped_class = instance_to_check.class # When the instance is an instances of the Scoping Class # (e.g when we are checking the associated cities of a city) return [instance_to_check.id] if scoped_class == scoping_class assoc_method = scoped_class.send(association_method_name) instance_scope = instance_to_check.send(assoc_method) # instance_scope = report.city => a city instance / nil # instance_scope = announcement.cities => AR Relation of Cities / (may be empty) if instance_scope.class == scoping_class # When the instance is associated with ONE instance of the scoping class # (e.g report is associated with one city) instance_scope_ids = [instance_scope.id] elsif instance_scope.nil? # When the instance is associated with ONE instance of scoping class # but the association is empty (e.g. a record with no city) instance_scope_ids = [] elsif instance_scope.respond_to? 'pluck' # When the instance is associated with MANY instances of the scoping # class. Even if the association is empty # (e.g announcement is available in many cities) instance_scope_ids = instance_scope.pluck(:id) else raise MisconfiguredAssociation, scoped_class: scoped_class, scopable: self, association_method: assoc_method end instance_scope_ids end |
#association_method_name ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns the name of the method used to get the name of the association for this scopable. Eg: “scopable_by_city_association_name”
183 184 185 |
# File 'lib/authz/scopables/base.rb', line 183 def association_method_name "scopable_by_#{scoping_class_name.underscore}_association_name" end |
#available_keywords ⇒ Array<String>
Returns available keywords for creating scoping rules.
408 409 410 411 412 |
# File 'lib/authz/scopables/base.rb', line 408 def available_keywords raise NotImplementedError, "#{self}. All Scopables must implement a method that returns the available scoping keywords" end |
#normalize_if_special_keyword(keyword) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Normalizes the keyword if it is a special keyword
203 204 205 206 |
# File 'lib/authz/scopables/base.rb', line 203 def normalize_if_special_keyword(keyword) norm = keyword.downcase.to_sym Authz::Scopables::Base.special_keywords.include?(norm) ? norm : keyword end |
#plural_association_name ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Symbol of a plural association following Rails’ conventions
176 177 178 |
# File 'lib/authz/scopables/base.rb', line 176 def plural_association_name scoping_class.model_name.plural.to_sym end |
#resolve_keyword(keyword, requester) ⇒ Array<Integers>
Returns The ids that the given keywords resolve to.
419 420 421 422 423 424 425 |
# File 'lib/authz/scopables/base.rb', line 419 def resolve_keyword(keyword, requester) msg = "#{self} must implement a method " \ ' that takes in a keyword and the requester' \ ' (e.g. the user) and returns an array of ids of ' \ "#{self.scoping_class_name} for that keyword" raise NotImplementedError, msg end |
#resolve_keyword!(keyword, requester) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Resolution
Calls .resolve_keyword and ensures that the returned value is valid.
211 212 213 214 215 216 217 218 219 220 |
# File 'lib/authz/scopables/base.rb', line 211 def resolve_keyword!(keyword, requester) resolved_ids = resolve_keyword(keyword, requester) if resolved_ids.is_a? Array resolved_ids else raise UnresolvableKeyword, scopable: self, keyword: keyword, requester: requester end end |
#scoping_class ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns the Active Record Class of the Model used to scope
166 167 168 |
# File 'lib/authz/scopables/base.rb', line 166 def scoping_class scoping_class_name.constantize end |
#scoping_class_name ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns the string name of the class used to scope
161 162 163 |
# File 'lib/authz/scopables/base.rb', line 161 def scoping_class_name self.to_s.remove('ScopableBy') end |
#singular_association_name ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Symbol of a singular association following Rails’conventions
171 172 173 |
# File 'lib/authz/scopables/base.rb', line 171 def singular_association_name scoping_class.model_name.singular.to_sym end |
#valid_keyword?(keyword) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns true if the given keyword is valid
197 198 199 |
# File 'lib/authz/scopables/base.rb', line 197 def valid_keyword?(keyword) available_keywords.include?(keyword) end |
#within_scope_of_keyword?(instance_to_check, keyword, requester) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns true if the given instance_to_check is within the scoping privileges of the given keyword, optionally passing the requester to aide the resolution of the keyword.
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/authz/scopables/base.rb', line 225 def within_scope_of_keyword?(instance_to_check, keyword, requester) keyword = normalize_if_special_keyword(keyword) # Shortcut treatment for special keywords return true if keyword == :all instance_scope_ids = associated_scoping_instances_ids(instance_to_check) role_scope_ids = resolve_keyword!(keyword, requester) # Resolution if instance_scope_ids.any? # When instance is associated to scoping class (report with city) # Resolve by intersection # TODO: if this becomes a problem, we could add # another parameter indicating the type of the match # e.g. match: :any, match: :all # "any" If announcement is available in 1,2,3 and I have 3 then I can see it # "all" If I am trying to create an announcement for 1,2, and I only have 1 then it should be denied (instance_scope_ids & role_scope_ids).any? else # When instance is not associated to scoping class # (e.g report with no city, announcement not available, # in any city, city that has not been persisted) # Fix singularity problem that allowed resolved keywords # that include nil to consider non persisted instances # of scoping classes (e.g. a city that has not been saved # yet) within scope. (ultimately allowing the creation # to out of scope) return false if instance_to_check.class == scoping_class role_scope_ids.include? nil end end |