strongbolt 0.3.6

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 (145) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +33 -0
  3. data/.gitignore +18 -0
  4. data/.rspec +1 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/Gemfile +4 -0
  8. data/Gemfile.lock +130 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +182 -0
  11. data/Rakefile +1 -0
  12. data/app/assets/javascripts/strongbolt.js +1 -0
  13. data/app/assets/javascripts/strongbolt/role-capabilities.js +80 -0
  14. data/app/controllers/strongbolt/capabilities_controller.rb +77 -0
  15. data/app/controllers/strongbolt/roles_controller.rb +92 -0
  16. data/app/controllers/strongbolt/security_controller.rb +8 -0
  17. data/app/controllers/strongbolt/user_groups_controller.rb +76 -0
  18. data/app/controllers/strongbolt/user_groups_users_controller.rb +35 -0
  19. data/app/controllers/strongbolt_controller.rb +2 -0
  20. data/app/views/strongbolt/_menu.html.erb +13 -0
  21. data/app/views/strongbolt/capabilities/index.html.erb +53 -0
  22. data/app/views/strongbolt/capabilities/show.html.erb +53 -0
  23. data/app/views/strongbolt/roles/_capabilities.html.erb +47 -0
  24. data/app/views/strongbolt/roles/_capability.html.erb +21 -0
  25. data/app/views/strongbolt/roles/_form.html.erb +12 -0
  26. data/app/views/strongbolt/roles/edit.html.erb +14 -0
  27. data/app/views/strongbolt/roles/index.html.erb +54 -0
  28. data/app/views/strongbolt/roles/new.html.erb +11 -0
  29. data/app/views/strongbolt/roles/show.html.erb +52 -0
  30. data/app/views/strongbolt/user_groups/_form.html.erb +12 -0
  31. data/app/views/strongbolt/user_groups/edit.html.erb +14 -0
  32. data/app/views/strongbolt/user_groups/index.html.erb +46 -0
  33. data/app/views/strongbolt/user_groups/new.html.erb +13 -0
  34. data/app/views/strongbolt/user_groups/show.html.erb +88 -0
  35. data/lib/generators/strongbolt/fix_generator.rb +23 -0
  36. data/lib/generators/strongbolt/indexes_generator.rb +19 -0
  37. data/lib/generators/strongbolt/install_generator.rb +29 -0
  38. data/lib/generators/strongbolt/templates/fix.rb +5 -0
  39. data/lib/generators/strongbolt/templates/indexes.rb +21 -0
  40. data/lib/generators/strongbolt/templates/migration.rb +73 -0
  41. data/lib/generators/strongbolt/templates/strongbolt.rb +45 -0
  42. data/lib/generators/strongbolt/views_generator.rb +26 -0
  43. data/lib/strongbolt.rb +219 -0
  44. data/lib/strongbolt/base.rb +7 -0
  45. data/lib/strongbolt/bolted.rb +125 -0
  46. data/lib/strongbolt/bolted_controller.rb +297 -0
  47. data/lib/strongbolt/capabilities_role.rb +15 -0
  48. data/lib/strongbolt/capability.rb +165 -0
  49. data/lib/strongbolt/configuration.rb +111 -0
  50. data/lib/strongbolt/controllers/url_helpers.rb +37 -0
  51. data/lib/strongbolt/engine.rb +44 -0
  52. data/lib/strongbolt/errors.rb +38 -0
  53. data/lib/strongbolt/generators/migration.rb +35 -0
  54. data/lib/strongbolt/helpers.rb +18 -0
  55. data/lib/strongbolt/rails/routes.rb +20 -0
  56. data/lib/strongbolt/role.rb +46 -0
  57. data/lib/strongbolt/roles_user_group.rb +15 -0
  58. data/lib/strongbolt/rspec.rb +29 -0
  59. data/lib/strongbolt/rspec/user.rb +90 -0
  60. data/lib/strongbolt/tenantable.rb +304 -0
  61. data/lib/strongbolt/user_abilities.rb +292 -0
  62. data/lib/strongbolt/user_group.rb +24 -0
  63. data/lib/strongbolt/user_groups_user.rb +16 -0
  64. data/lib/strongbolt/users_tenant.rb +12 -0
  65. data/lib/strongbolt/version.rb +3 -0
  66. data/lib/tasks/strongbolt_tasks.rake +29 -0
  67. data/spec/controllers/strongbolt/capabilities_controller_spec.rb +254 -0
  68. data/spec/controllers/strongbolt/roles_controller_spec.rb +228 -0
  69. data/spec/controllers/strongbolt/user_groups_controller_spec.rb +216 -0
  70. data/spec/controllers/strongbolt/user_groups_users_controller_spec.rb +69 -0
  71. data/spec/controllers/without_authorization_controller_spec.rb +20 -0
  72. data/spec/dummy/.rspec +2 -0
  73. data/spec/dummy/README.rdoc +28 -0
  74. data/spec/dummy/Rakefile +6 -0
  75. data/spec/dummy/app/assets/images/.keep +0 -0
  76. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  77. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  78. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  79. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  80. data/spec/dummy/app/controllers/posts_controller.rb +18 -0
  81. data/spec/dummy/app/controllers/test_controller.rb +3 -0
  82. data/spec/dummy/app/controllers/without_authorization_controller.rb +5 -0
  83. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  84. data/spec/dummy/app/mailers/.keep +0 -0
  85. data/spec/dummy/app/models/.keep +0 -0
  86. data/spec/dummy/app/models/concerns/.keep +0 -0
  87. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  88. data/spec/dummy/bin/bundle +3 -0
  89. data/spec/dummy/bin/rails +4 -0
  90. data/spec/dummy/bin/rake +4 -0
  91. data/spec/dummy/config.ru +4 -0
  92. data/spec/dummy/config/application.rb +29 -0
  93. data/spec/dummy/config/boot.rb +5 -0
  94. data/spec/dummy/config/database.yml +25 -0
  95. data/spec/dummy/config/environment.rb +5 -0
  96. data/spec/dummy/config/environments/development.rb +37 -0
  97. data/spec/dummy/config/environments/production.rb +78 -0
  98. data/spec/dummy/config/environments/test.rb +39 -0
  99. data/spec/dummy/config/initializers/assets.rb +8 -0
  100. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  101. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  102. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  103. data/spec/dummy/config/initializers/inflections.rb +16 -0
  104. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  105. data/spec/dummy/config/initializers/session_store.rb +3 -0
  106. data/spec/dummy/config/initializers/strongbolt.rb +32 -0
  107. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  108. data/spec/dummy/config/locales/en.yml +23 -0
  109. data/spec/dummy/config/routes.rb +12 -0
  110. data/spec/dummy/config/secrets.yml +22 -0
  111. data/spec/dummy/db/development.sqlite3 +0 -0
  112. data/spec/dummy/db/migrate/20150630212236_create_strongbolt_tables.rb +54 -0
  113. data/spec/dummy/db/migrate/20150630212251_create_strongbolt_tables_indexes.rb +21 -0
  114. data/spec/dummy/db/schema.rb +84 -0
  115. data/spec/dummy/db/test.sqlite3 +0 -0
  116. data/spec/dummy/lib/assets/.keep +0 -0
  117. data/spec/dummy/public/404.html +67 -0
  118. data/spec/dummy/public/422.html +67 -0
  119. data/spec/dummy/public/500.html +66 -0
  120. data/spec/dummy/public/favicon.ico +0 -0
  121. data/spec/fabricators/capability_fabricator.rb +4 -0
  122. data/spec/fabricators/role_fabricator.rb +9 -0
  123. data/spec/fabricators/user_fabricator.rb +3 -0
  124. data/spec/fabricators/user_group_fabricator.rb +9 -0
  125. data/spec/fixtures/application.rb +28 -0
  126. data/spec/fixtures/controllers.rb +5 -0
  127. data/spec/spec_helper.rb +89 -0
  128. data/spec/strongbolt/bolted_controller_spec.rb +706 -0
  129. data/spec/strongbolt/bolted_spec.rb +136 -0
  130. data/spec/strongbolt/capability_spec.rb +251 -0
  131. data/spec/strongbolt/configuration_spec.rb +119 -0
  132. data/spec/strongbolt/controllers/url_helpers_spec.rb +34 -0
  133. data/spec/strongbolt/helpers_spec.rb +43 -0
  134. data/spec/strongbolt/role_spec.rb +90 -0
  135. data/spec/strongbolt/tenantable_spec.rb +281 -0
  136. data/spec/strongbolt/user_abilities_spec.rb +509 -0
  137. data/spec/strongbolt/user_group_spec.rb +37 -0
  138. data/spec/strongbolt/users_tenant_spec.rb +36 -0
  139. data/spec/strongbolt_spec.rb +274 -0
  140. data/spec/support/controller_macros.rb +11 -0
  141. data/spec/support/db_setup.rb +134 -0
  142. data/spec/support/helpers.rb +62 -0
  143. data/spec/support/transactional_specs.rb +17 -0
  144. data/strongbolt.gemspec +32 -0
  145. metadata +407 -0
@@ -0,0 +1,304 @@
1
+ module Strongbolt
2
+ module Tenantable
3
+ module ClassMethods
4
+
5
+ def tenant?() (@tenant.present? && @tenant) || Strongbolt.tenants.include?(name); end
6
+
7
+ #
8
+ # Returns associations potential name
9
+ #
10
+ def singular_association_name
11
+ @singular_association_name ||= self.name.demodulize.underscore.to_sym
12
+ end
13
+ def plural_association_name
14
+ @plural_association_name ||= self.name.demodulize.underscore.pluralize.to_sym
15
+ end
16
+
17
+ private
18
+
19
+ #
20
+ # Specifies that the class can be tenanted
21
+ # It will traverse all the has_many relationships
22
+ # and add a has_one :tenant if not specified
23
+ #
24
+ def tenant opts = {}
25
+ # Stops if already configured
26
+ return if tenant?
27
+
28
+ Strongbolt.logger.debug "-------------------------------------------------------------------\n" +
29
+ "Configuring tenant #{self.name}\n" +
30
+ "-------------------------------------------------------------------\n\n"
31
+ #
32
+ # We're traversing using BFS the relationships
33
+ #
34
+ # Keep track of traversed models and their relationship to the tenant
35
+ @models_traversed = {self.name => self}
36
+ # File of models/associations to traverse
37
+ models_to_traverse = reflect_on_all_associations
38
+ while models_to_traverse.size > 0
39
+ # BFS search, shiftin first elt of array (older)
40
+ current_association = models_to_traverse.shift
41
+ # We don't check has_many :through association,
42
+ # only first degree relationships. It makes sense as we'll
43
+ # obviously also be checking the models concerned with the through
44
+ # relationship, using the intermediate model before.
45
+
46
+ # So unless we've already traversed this model, or that's a through relationship
47
+ # or a polymorphic association
48
+ # Also we don't go following belongs_to relationship, it becomes crazy
49
+ if should_visit? current_association
50
+ # We setup the model using the association given
51
+ method = setup_model(current_association)
52
+ # We flag the model, storing the name of the method used to link to tenant
53
+ @models_traversed[current_association.klass.name] = method
54
+ # And add its relationships into the array, at the end, if method not nil
55
+ if method.present?
56
+ models_to_traverse.concat current_association.klass.reflect_on_all_associations
57
+ end
58
+ end
59
+ end
60
+
61
+ # We add models name to Configuration
62
+ Strongbolt::Configuration.models = @models_traversed.keys
63
+
64
+ create_users_tenant_subclass
65
+ setup_association_on_user
66
+
67
+ @tenant = true
68
+ end
69
+
70
+ #
71
+ # Setup a model and returns the method name in symbol of the
72
+ # implemented link to the tenant
73
+ #
74
+ def setup_model association
75
+ # Source class
76
+ original_class = association.active_record
77
+ # Current class
78
+ klass = association.klass
79
+ # Get the link of original class to tenant
80
+ link = @models_traversed[original_class.name]
81
+
82
+ # If the original class is the actual tenant, it should have defined
83
+ # the reverse association as we cannot guess it
84
+ if original_class == self
85
+ # Inverse association
86
+ inverse = inverse_of(association)
87
+ # We first check the model doesn't have an association already created to the tenant
88
+ # We may have one but with a different name, and we don't care
89
+ if inverse.present?
90
+ assoc = inverse.name
91
+ else
92
+ raise DirectAssociationNotConfigured, "Class #{klass.name} is 1 degree from #{self.name} but the association isn't configured, you should implement it before using tenant method"
93
+ end
94
+
95
+ # The coming class has a relationship to the tenant
96
+ else
97
+ # If already created, we don't need to go further
98
+ return singular_association_name if klass.new.respond_to?(singular_association_name)
99
+ return plural_association_name if klass.new.respond_to?(plural_association_name)
100
+
101
+ # Inverse association
102
+ inverse = inverse_of(association)
103
+
104
+ # If no inverse, we cannot go further
105
+ if inverse.nil?
106
+ raise InverseAssociationNotConfigured, "Assocation #{association.name} on #{association.klass.name} could not be configured correctly as no inverse has been found"
107
+ elsif inverse.options.has_key? :polymorphic
108
+ return nil
109
+ end
110
+
111
+
112
+ # Common options
113
+ options = {
114
+ through: inverse.name,
115
+ autosave: false
116
+ }
117
+
118
+ # If the target is linked through some sort of has_many
119
+ if link == plural_association_name || inverse.collection?
120
+ # Has many
121
+ assoc = plural_association_name
122
+ # Setup the association
123
+ # Setup the scope with_name_of_plural_associations
124
+ # Current tenant table name
125
+ klass.has_many assoc, options
126
+
127
+ Strongbolt.logger.debug "#{klass.name} has_many #{plural_association_name} through: #{options[:through]}\n\n"
128
+
129
+ # Otherwise, it's linked through a has one
130
+ else
131
+ # Has one
132
+ assoc = singular_association_name
133
+ # Setup the association
134
+ # Setup the scope with_name_of_plural_associations
135
+ klass.has_one assoc, options
136
+
137
+ Strongbolt.logger.debug "#{klass.name} has_one #{singular_association_name} through: #{options[:through]}\n\n"
138
+ end
139
+ end
140
+
141
+ #
142
+ # Now includes scopes
143
+ #
144
+ klass.class_exec(plural_association_name, assoc, table_name) do |plur, assoc, table_name|
145
+ scope "with_#{plur}", -> { includes assoc }
146
+
147
+ scope "where_#{plur}_among", ->(values) do
148
+ if values.is_a? Array
149
+ # If objects
150
+ values = values.map(&:id) if values.first.respond_to? :id
151
+ else
152
+ # If object
153
+ values = values.id if values.respond_to?(:id)
154
+ end
155
+
156
+ includes(assoc).where(table_name => {id: values})
157
+ end
158
+ end
159
+
160
+ # And return name of association
161
+ return assoc
162
+ end #/setup_model
163
+
164
+
165
+ #
166
+ # The initial idea of using a polymorphic association on UsersTenant
167
+ # leads to some problems* when the tenant is a subclass of a STI schema
168
+ # and not the whole schema. Using instead STI for the UsersTenant model
169
+ # allows to achieve the same results without the edge effects.
170
+ #
171
+ # *For instance, let's say we have a Resource STI model, with Client
172
+ # and User as subclasses. Client is a tenant, User is not.
173
+ # If using the original idea of polymorphic association on UsersTenant,
174
+ # Helpers like user.client_ids = [5] wouldn't work.
175
+ # This comes from the fact that AR use the base_class name of the STI model
176
+ # and not the actual class name to be stored in the _type column.
177
+ #
178
+ #
179
+ def create_users_tenant_subclass
180
+ unless Strongbolt.const_defined?("Users#{self.name}")
181
+ users_tenant_subclass = Class.new(Strongbolt::UsersTenant)
182
+ users_tenant_subclass.class_eval <<-RUBY
183
+ # Ensures permissions on UsersTenant are applied here
184
+ authorize_as "Strongbolt::UsersTenant"
185
+ # The association to the actual tenant model
186
+ belongs_to :#{singular_association_name},
187
+ :foreign_key => :tenant_id,
188
+ :class_name => "#{self.name}"
189
+
190
+ # We have to create this association every time to have
191
+ # The correct inverse_of
192
+ belongs_to :user, class_name: Configuration.user_class,
193
+ :inverse_of => :users_#{plural_association_name}
194
+
195
+ validates :#{singular_association_name}, :presence => true
196
+ RUBY
197
+ Strongbolt.const_set "Users#{self.name}", users_tenant_subclass
198
+ end
199
+ end #/create_users_tenant_subclass
200
+
201
+ #
202
+ # Setups the has_many thru association on the User class
203
+ #
204
+ def setup_association_on_user
205
+ begin
206
+ user_class = Configuration.user_class.constantize
207
+
208
+ # Setup the association
209
+ # The first one should never be there before
210
+ user_class.has_many :"users_#{plural_association_name}",
211
+ :class_name => "Strongbolt::Users#{self.name}",
212
+ :inverse_of => :user,
213
+ :dependent => :delete_all,
214
+ :foreign_key => :user_id
215
+
216
+ # This one may have been overriden by the developer
217
+ unless user_class.respond_to? plural_association_name
218
+ user_class.has_many plural_association_name,
219
+ :source => :"#{singular_association_name}",
220
+ :class_name => self.name,
221
+ :through => :"users_#{plural_association_name}"
222
+ end
223
+
224
+ # Setup a quick method to get accessible clients directly
225
+ unless user_class.respond_to? "accessible_#{plural_association_name}"
226
+ user_class.class_exec(self, plural_association_name) do |klass, plur|
227
+ define_method "accessible_#{plur}" do
228
+ # If can find ALL the tenants
229
+ if can? :find, klass, :any, true
230
+ # Then it can access all of them
231
+ klass.all
232
+ else
233
+ # Otherwise, only the ones he manages
234
+ send plur
235
+ end
236
+ end
237
+ end
238
+ end
239
+ rescue NameError => e
240
+ Strongbolt.logger.error "User #{Configuration.user_class} could not have his association to tenant #{name} created"
241
+ end
242
+ end #/setup_association_on_user
243
+
244
+ #
245
+ # Returns the inverse of specified association, using what's given
246
+ # as inverse_of or trying to guess it
247
+ #
248
+ def inverse_of association
249
+ # If specified in association configuration
250
+ return association.inverse_of if association.has_inverse?
251
+
252
+ polymorphic_associations = []
253
+
254
+ # Else we need to find it, using the class as reference
255
+ association.klass.reflect_on_all_associations.each do |assoc|
256
+ # If the association is polymorphic
257
+ if assoc.options.has_key? :polymorphic
258
+ polymorphic_associations << assoc
259
+
260
+ # If same class than the original source of the association
261
+ elsif assoc.klass == association.active_record
262
+
263
+ Strongbolt.logger.debug "Selected inverse of #{association.name} between #{association.active_record} " +
264
+ "and #{association.klass} is #{assoc.name}.\n " +
265
+ "If not, please configure manually the inverse of #{association.name}\n"
266
+
267
+ return assoc
268
+ end
269
+ end
270
+
271
+ if polymorphic_associations.size == 1
272
+ return polymorphic_associations.first
273
+ end
274
+
275
+ return nil
276
+ end
277
+
278
+ #
279
+ # Returns true if should visit the association
280
+ #
281
+ # The BFS should visit the next model if the model hasn't been visited yet
282
+ # or was already visited but through a polymorphic association (hence no inverse)
283
+ # if the model is a HasMany, HasManyAndBelongsTo or HasOne association (ie no BelongsTo)
284
+ # and not HasManyThrough, unless it's AR v >= 4.1.0 && < 4.2.0 where
285
+ # they define a HasManyAndBelongsTo as a HasManyThrough in the reflections
286
+ #
287
+ def should_visit? association
288
+ ! (association.is_a?(ActiveRecord::Reflection::ThroughReflection) ||
289
+ association.macro == :belongs_to ||
290
+ (@models_traversed.has_key?(association.klass.name) &&
291
+ @models_traversed[association.klass.name].present?) )
292
+ end
293
+
294
+ end
295
+
296
+ module InstanceMethods
297
+ end
298
+
299
+ def self.included(receiver)
300
+ receiver.extend ClassMethods
301
+ receiver.send :include, InstanceMethods
302
+ end
303
+ end
304
+ end
@@ -0,0 +1,292 @@
1
+ module Strongbolt
2
+ module UserAbilities
3
+ module ClassMethods
4
+
5
+ end
6
+
7
+ module InstanceMethods
8
+ #----------------------------------------------------------#
9
+ # #
10
+ # Returns all the user's capabilities plus inherited ones #
11
+ # #
12
+ #----------------------------------------------------------#
13
+ def capabilities
14
+ @capabilities_cache ||= Strongbolt::Capability.unscoped.joins(:roles)
15
+ .joins('INNER JOIN strongbolt_roles as children_roles ON strongbolt_roles.lft <= children_roles.lft AND children_roles.rgt <= strongbolt_roles.rgt')
16
+ .joins('INNER JOIN strongbolt_roles_user_groups rug ON rug.role_id = children_roles.id')
17
+ .joins('INNER JOIN strongbolt_user_groups_users ugu ON ugu.user_group_id = rug.user_group_id')
18
+ .where('ugu.user_id = ?', self.id).distinct.to_a.concat(Strongbolt.default_capabilities)
19
+ end
20
+
21
+ #
22
+ # Adds a managed tenant to the user
23
+ #
24
+ def add_tenant tenant
25
+ sing_tenant_name = tenant.class.name.demodulize.underscore
26
+ send("users_#{sing_tenant_name.pluralize}").create! sing_tenant_name => tenant
27
+ # users_tenants.create! tenant: tenant
28
+ end
29
+
30
+ #
31
+ # Main method for user, used to check whether the user
32
+ # is authorized to perform a certain action on an instance/class
33
+ #
34
+ def can? action, instance, attrs = :any, all_instance = false
35
+ without_grant do
36
+
37
+ # Get the actual instance if we were given AR
38
+ instance = instance.try(:first) if instance.is_a?(ActiveRecord::Relation)
39
+ return false if instance.nil?
40
+
41
+ # We require this to be an *existing* user, that the action and attribute be symbols
42
+ # and that the instance is a class or a String
43
+ raise ArgumentError, "Action must be a symbol and instance must be Class, String, Symbol or AR" unless self.id.present? && action.is_a?(Symbol) &&
44
+ (instance.is_a?(ActiveRecord::Base) || instance.is_a?(Class) || instance.is_a?(String)) && attrs.is_a?(Symbol)
45
+
46
+ # Pre-populate all the capabilities into a results cache for quick lookup. Permissions for all "non-owned" objects are
47
+ # immediately available; additional lookups are required for owned objects (e.g. User, CheckoutBag, etc.).
48
+ # The results cache key is formatted as "action model attribute" (attribute can be any, all or an actual attribute)
49
+ # -any, all, an ID, or "owned" (if the ID will be verified later) is appended to the key based on which instances
50
+ # a user has access to
51
+ populate_capabilities_cache unless @results_cache.present?
52
+ # Determine the model name and the actual model (if we need to traverse the hierarchy)
53
+ if instance.is_a?(ActiveRecord::Base)
54
+ model = instance.class
55
+ model_name = model.name_for_authorization
56
+ elsif instance.is_a?(Class)
57
+ model = instance
58
+ model_name = model.name_for_authorization
59
+ else
60
+ model = nil # We could do model_name.constantize, but there's a big cost to doing this
61
+ # if we don't need it, so just defer until we determine there's an actual need
62
+ model_name = instance
63
+ end
64
+
65
+ # Look up the various possible valid entries in the cache that would allow us to see this
66
+ return capability_in_cache?(action, instance, model_name, attrs, all_instance)
67
+
68
+ end #end w/o grant
69
+ end
70
+
71
+ #
72
+ # Convenient method
73
+ #
74
+ def cannot? *args
75
+ !can? *args
76
+ end
77
+
78
+ #
79
+ # Checks if the user owns the instance given
80
+ #
81
+ def owns? instance
82
+ raise ArgumentError unless instance.is_a?(Object) && !instance.is_a?(Class)
83
+ # If the user id is set, does this (a) user id match the user_id field of the instance
84
+ # or (b) if this is a User instance, does the user id match the instance id?
85
+ key = instance.is_a?(User) ? :id : :user_id
86
+ return !id.nil? && instance.try(key) == id
87
+ end
88
+
89
+
90
+
91
+ #
92
+ # Populate the capabilities cache
93
+ #
94
+ def populate_capabilities_cache
95
+ beginning = Time.now
96
+
97
+ @results_cache ||= {}
98
+ @model_ancestor_cache ||= {}
99
+
100
+ # User can find itself by default
101
+ @results_cache["findUserany-any"] = true
102
+ @results_cache["findUserany-#{id}"] = true
103
+
104
+ #
105
+ # Store every capability fetched
106
+ #
107
+ capabilities.each do |capability|
108
+
109
+ k = "#{capability.action}#{capability.model}"
110
+ attr_k = capability.attr || 'all'
111
+
112
+ @results_cache["#{k}#{attr_k}-any"] = true
113
+ @results_cache["#{k}any-any"] = true
114
+
115
+ if capability.require_ownership
116
+ user_id = self.try(:id)
117
+ # We can use the ID of the User object for the key here because
118
+ # there's only one of them
119
+ if capability.model == Strongbolt::Configuration.user_class
120
+ @results_cache["#{k}#{attr_k}-#{user_id}"] = true
121
+ @results_cache["#{k}any-#{user_id}"] = true
122
+ else
123
+ # On the other hand, it doesn't make sense to pre-populate the valid
124
+ # IDs for the models with a lot of instances when we probably are never
125
+ # going to need to know this. Instead, adding 'owned' is a hint to actually look
126
+ # up later if we own a particular geography.
127
+ @results_cache["#{k}#{attr_k}-owned"] = true
128
+ @results_cache["#{k}any-owned"] = true
129
+ end
130
+ elsif capability.require_tenant_access # If tenant access required
131
+ @results_cache["#{k}#{attr_k}-tenanted"] = true
132
+ @results_cache["#{k}any-tenanted"] = true
133
+ else
134
+ @results_cache["#{k}#{attr_k}-all"] = true
135
+ @results_cache["#{k}any-all"] = true
136
+ end
137
+ end # End each capability
138
+
139
+ Strongbolt.logger.info "Populated capabilities in #{(Time.now - beginning)*1000}ms"
140
+
141
+ @results_cache
142
+ end # End Populate capabilities Cache
143
+
144
+
145
+
146
+
147
+
148
+ #----------------------------------------------------------#
149
+ # #
150
+ # Checks if the user can perform 'action' on 'instance' #
151
+ # #
152
+ #----------------------------------------------------------#
153
+
154
+ def capability_in_cache?(action, instance, model_name, attrs = :any, all_instance = false)
155
+ action_model = "#{action}#{model_name}"
156
+
157
+ Strongbolt.logger.warn "User has no results cache" if @results_cache.empty?
158
+ Strongbolt.logger.debug { "Authorizing user to perform #{action} on #{instance.inspect}" }
159
+
160
+ # we don't know or care about tenants or if this is a new record
161
+ if instance.is_a?(ActiveRecord::Base) && !instance.new_record?
162
+ # First, check if we have a hash/cache hit for User being able to do this action to every instance of the model/class
163
+ return true if @results_cache["#{action_model}all-all"] #Access to all attributes on ENTIRE class?
164
+ return true if @results_cache["#{action_model}#{attrs}-all"] #Access to this specific attribute on ENTIRE class?
165
+
166
+ # If we're checking on a specific instance of the class, not the general model,
167
+ # append the id to the key
168
+ id = instance.try(:id)
169
+ return true if @results_cache["#{action_model}all-#{id}"] # Access to all this instance's attributes?
170
+ return true if @results_cache["#{action_model}#{attrs}-#{id}"] #Access to this instance's attribute?
171
+
172
+ # Checking ownership and tenant access
173
+ # Block access for non tenanted instance
174
+ valid_tenants = has_access_to_tenants?(instance)
175
+
176
+ # Then if the model is owned but isn't preloaded yet
177
+ if instance.class.owned?
178
+ # Tests if the owner id of the instance is the same than the user
179
+ if (own_instance = instance.strongbolt_owner_id == self.id)
180
+ @results_cache["#{action_model}all-#{id}"] = own_instance && valid_tenants && @results_cache["#{action_model}all-owned"]
181
+ @results_cache["#{action_model}#{attrs}-#{id}"] = own_instance && valid_tenants && @results_cache["#{action_model}#{attrs}-owned"]
182
+ return true if @results_cache["#{action_model}all-#{id}"] || @results_cache["#{action_model}#{attrs}-#{id}"]
183
+ else
184
+ @results_cache["#{action_model}all-#{id}"] = false
185
+ @results_cache["#{action_model}#{attrs}-#{id}"] = false
186
+ end
187
+ end
188
+
189
+ # Finally we check for tenanted instances
190
+ @results_cache["#{action_model}all-#{id}"] = @results_cache["#{action_model}all-tenanted"] && valid_tenants #Access to all attributes on tenanted class?
191
+ @results_cache["#{action_model}#{attrs}-#{id}"] = @results_cache["#{action_model}#{attrs}-tenanted"] && valid_tenants #Access to this specific attribute on tenanted class?
192
+ return true if @results_cache["#{action_model}all-#{id}"] || @results_cache["#{action_model}#{attrs}-#{id}"]
193
+ elsif instance.is_a?(ActiveRecord::Base) && instance.new_record?
194
+ return true if @results_cache["#{action_model}all-all"] #Access to all attributes on ENTIRE class?
195
+ return true if @results_cache["#{action_model}#{attrs}-all"] #Access to this specific attribute on ENTIRE class?
196
+ # Checking if the instance is from valid tenants (if necessary)
197
+ valid_tenants = has_access_to_tenants?(instance)
198
+ return true if @results_cache["#{action_model}all-tenanted"] && valid_tenants #Access to all attributes on tenanted class?
199
+ return true if @results_cache["#{action_model}#{attrs}-tenanted"] && valid_tenants #Access to this specific attribute on tenanted class?
200
+
201
+ # Finally, in the case where it's a non tenanted model (it still need to have valid_tenants == true)
202
+ return true if @results_cache["#{action_model}all-any"] && valid_tenants
203
+ return true if @results_cache["#{action_model}#{attrs}-any"] && valid_tenants
204
+ else
205
+ # First, check if we have a hash/cache hit for User being able to do this action to every instance of the model/class
206
+ return true if @results_cache["#{action_model}all-all"] #Access to all attributes on ENTIRE class?
207
+ return true if @results_cache["#{action_model}#{attrs}-all"] #Access to this specific attribute on ENTIRE class?
208
+ return true if @results_cache["#{action_model}all-any"] && ! all_instance #Access to all attributes on at least once instance?
209
+ return true if @results_cache["#{action_model}#{attrs}-any"] && ! all_instance #Access to this specific attribute on at least once instance?
210
+ end
211
+ #logger.info "Cache miss for checking access to #{key}"
212
+
213
+ return false
214
+ end
215
+
216
+ #
217
+ # Checks if the instance given fulfills tenant management rules
218
+ #
219
+ def has_access_to_tenants? instance, tenants = nil
220
+ # If no tenants list given, we take all
221
+ tenants ||= Strongbolt.tenants
222
+ # Populate the cache if needed
223
+ populate_tenants_cache
224
+
225
+ # Go over each tenants and check if we access to at least one of the tenant
226
+ # models linked to it
227
+ tenants.inject(true) do |result, tenant|
228
+ begin
229
+ if instance.class == tenant
230
+ tenant_ids = [instance.id]
231
+ elsif instance.respond_to?(tenant.singular_association_name)
232
+ if instance.send(tenant.singular_association_name).present?
233
+ tenant_ids = [instance.send(tenant.singular_association_name).id]
234
+ else
235
+ tenant_ids = []
236
+ end
237
+ elsif instance.respond_to?(tenant.plural_association_name)
238
+ tenant_ids = instance.send("#{tenant.singular_association_name}_ids")
239
+ else
240
+ next result
241
+ end
242
+ # When we perform a :select on a model, we may omit
243
+ # the attribute(s) that link(s) to the tenant.
244
+ # In that case, we have to suppose the user has access
245
+ rescue ActiveModel::MissingAttributeError
246
+ tenant_ids = []
247
+ end
248
+ result && (tenant_ids.size == 0 || (@tenants_cache[tenant.name] & tenant_ids).present?)
249
+ end
250
+ end
251
+
252
+ #
253
+ # Populate a hash of tenants as keys and ids array as values
254
+ #
255
+ def populate_tenants_cache
256
+ return if @tenants_cache.present?
257
+
258
+ Strongbolt.logger.debug "Populating tenants cache for user #{self.id}"
259
+
260
+ @tenants_cache = {}
261
+ # Go over each tenants
262
+ Strongbolt.tenants.each do |tenant|
263
+ @tenants_cache[tenant.name] = send("accessible_#{tenant.plural_association_name}").pluck(:id)
264
+ Strongbolt.logger.debug "#{@tenants_cache[tenant.name].size} #{tenant.name}"
265
+ end
266
+ end
267
+
268
+
269
+ end # End InstanceMethods
270
+
271
+ def self.included(receiver)
272
+ receiver.extend ClassMethods
273
+ receiver.send :include, InstanceMethods
274
+
275
+ receiver.class_eval do
276
+ has_many :user_groups_users,
277
+ :class_name => "Strongbolt::UserGroupsUser",
278
+ :dependent => :delete_all,
279
+ :inverse_of => :user,
280
+ :foreign_key => :user_id
281
+ has_many :user_groups, :through => :user_groups_users
282
+
283
+ has_many :roles, through: :user_groups
284
+ end
285
+
286
+ # Sets up user association
287
+ Strongbolt.tenants.each do |tenant|
288
+ tenant.send :setup_association_on_user
289
+ end
290
+ end
291
+ end
292
+ end