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.
- 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
|