strongbolt 0.3.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +33 -0
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +130 -0
- data/LICENSE.txt +22 -0
- data/README.md +182 -0
- data/Rakefile +1 -0
- data/app/assets/javascripts/strongbolt.js +1 -0
- data/app/assets/javascripts/strongbolt/role-capabilities.js +80 -0
- data/app/controllers/strongbolt/capabilities_controller.rb +77 -0
- data/app/controllers/strongbolt/roles_controller.rb +92 -0
- data/app/controllers/strongbolt/security_controller.rb +8 -0
- data/app/controllers/strongbolt/user_groups_controller.rb +76 -0
- data/app/controllers/strongbolt/user_groups_users_controller.rb +35 -0
- data/app/controllers/strongbolt_controller.rb +2 -0
- data/app/views/strongbolt/_menu.html.erb +13 -0
- data/app/views/strongbolt/capabilities/index.html.erb +53 -0
- data/app/views/strongbolt/capabilities/show.html.erb +53 -0
- data/app/views/strongbolt/roles/_capabilities.html.erb +47 -0
- data/app/views/strongbolt/roles/_capability.html.erb +21 -0
- data/app/views/strongbolt/roles/_form.html.erb +12 -0
- data/app/views/strongbolt/roles/edit.html.erb +14 -0
- data/app/views/strongbolt/roles/index.html.erb +54 -0
- data/app/views/strongbolt/roles/new.html.erb +11 -0
- data/app/views/strongbolt/roles/show.html.erb +52 -0
- data/app/views/strongbolt/user_groups/_form.html.erb +12 -0
- data/app/views/strongbolt/user_groups/edit.html.erb +14 -0
- data/app/views/strongbolt/user_groups/index.html.erb +46 -0
- data/app/views/strongbolt/user_groups/new.html.erb +13 -0
- data/app/views/strongbolt/user_groups/show.html.erb +88 -0
- data/lib/generators/strongbolt/fix_generator.rb +23 -0
- data/lib/generators/strongbolt/indexes_generator.rb +19 -0
- data/lib/generators/strongbolt/install_generator.rb +29 -0
- data/lib/generators/strongbolt/templates/fix.rb +5 -0
- data/lib/generators/strongbolt/templates/indexes.rb +21 -0
- data/lib/generators/strongbolt/templates/migration.rb +73 -0
- data/lib/generators/strongbolt/templates/strongbolt.rb +45 -0
- data/lib/generators/strongbolt/views_generator.rb +26 -0
- data/lib/strongbolt.rb +219 -0
- data/lib/strongbolt/base.rb +7 -0
- data/lib/strongbolt/bolted.rb +125 -0
- data/lib/strongbolt/bolted_controller.rb +297 -0
- data/lib/strongbolt/capabilities_role.rb +15 -0
- data/lib/strongbolt/capability.rb +165 -0
- data/lib/strongbolt/configuration.rb +111 -0
- data/lib/strongbolt/controllers/url_helpers.rb +37 -0
- data/lib/strongbolt/engine.rb +44 -0
- data/lib/strongbolt/errors.rb +38 -0
- data/lib/strongbolt/generators/migration.rb +35 -0
- data/lib/strongbolt/helpers.rb +18 -0
- data/lib/strongbolt/rails/routes.rb +20 -0
- data/lib/strongbolt/role.rb +46 -0
- data/lib/strongbolt/roles_user_group.rb +15 -0
- data/lib/strongbolt/rspec.rb +29 -0
- data/lib/strongbolt/rspec/user.rb +90 -0
- data/lib/strongbolt/tenantable.rb +304 -0
- data/lib/strongbolt/user_abilities.rb +292 -0
- data/lib/strongbolt/user_group.rb +24 -0
- data/lib/strongbolt/user_groups_user.rb +16 -0
- data/lib/strongbolt/users_tenant.rb +12 -0
- data/lib/strongbolt/version.rb +3 -0
- data/lib/tasks/strongbolt_tasks.rake +29 -0
- data/spec/controllers/strongbolt/capabilities_controller_spec.rb +254 -0
- data/spec/controllers/strongbolt/roles_controller_spec.rb +228 -0
- data/spec/controllers/strongbolt/user_groups_controller_spec.rb +216 -0
- data/spec/controllers/strongbolt/user_groups_users_controller_spec.rb +69 -0
- data/spec/controllers/without_authorization_controller_spec.rb +20 -0
- data/spec/dummy/.rspec +2 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/controllers/posts_controller.rb +18 -0
- data/spec/dummy/app/controllers/test_controller.rb +3 -0
- data/spec/dummy/app/controllers/without_authorization_controller.rb +5 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +29 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +78 -0
- data/spec/dummy/config/environments/test.rb +39 -0
- data/spec/dummy/config/initializers/assets.rb +8 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/strongbolt.rb +32 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +12 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20150630212236_create_strongbolt_tables.rb +54 -0
- data/spec/dummy/db/migrate/20150630212251_create_strongbolt_tables_indexes.rb +21 -0
- data/spec/dummy/db/schema.rb +84 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/fabricators/capability_fabricator.rb +4 -0
- data/spec/fabricators/role_fabricator.rb +9 -0
- data/spec/fabricators/user_fabricator.rb +3 -0
- data/spec/fabricators/user_group_fabricator.rb +9 -0
- data/spec/fixtures/application.rb +28 -0
- data/spec/fixtures/controllers.rb +5 -0
- data/spec/spec_helper.rb +89 -0
- data/spec/strongbolt/bolted_controller_spec.rb +706 -0
- data/spec/strongbolt/bolted_spec.rb +136 -0
- data/spec/strongbolt/capability_spec.rb +251 -0
- data/spec/strongbolt/configuration_spec.rb +119 -0
- data/spec/strongbolt/controllers/url_helpers_spec.rb +34 -0
- data/spec/strongbolt/helpers_spec.rb +43 -0
- data/spec/strongbolt/role_spec.rb +90 -0
- data/spec/strongbolt/tenantable_spec.rb +281 -0
- data/spec/strongbolt/user_abilities_spec.rb +509 -0
- data/spec/strongbolt/user_group_spec.rb +37 -0
- data/spec/strongbolt/users_tenant_spec.rb +36 -0
- data/spec/strongbolt_spec.rb +274 -0
- data/spec/support/controller_macros.rb +11 -0
- data/spec/support/db_setup.rb +134 -0
- data/spec/support/helpers.rb +62 -0
- data/spec/support/transactional_specs.rb +17 -0
- data/strongbolt.gemspec +32 -0
- 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
|