strongbolt 0.3.6

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