masks 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +50 -0
  4. data/Rakefile +11 -0
  5. data/app/assets/builds/application.css +4764 -0
  6. data/app/assets/builds/application.js +8236 -0
  7. data/app/assets/builds/application.js.map +7 -0
  8. data/app/assets/builds/masks/application.css +1 -0
  9. data/app/assets/builds/masks/application.js +7122 -0
  10. data/app/assets/builds/masks/application.js.map +7 -0
  11. data/app/assets/images/masks.png +0 -0
  12. data/app/assets/javascripts/application.js +2 -0
  13. data/app/assets/javascripts/controllers/application.js +9 -0
  14. data/app/assets/javascripts/controllers/emails_controller.js +28 -0
  15. data/app/assets/javascripts/controllers/index.js +12 -0
  16. data/app/assets/javascripts/controllers/keys_controller.js +20 -0
  17. data/app/assets/javascripts/controllers/recover_controller.js +21 -0
  18. data/app/assets/javascripts/controllers/recover_password_controller.js +21 -0
  19. data/app/assets/javascripts/controllers/session_controller.js +94 -0
  20. data/app/assets/manifest.js +2 -0
  21. data/app/assets/masks_manifest.js +2 -0
  22. data/app/assets/stylesheets/application.css +26 -0
  23. data/app/controllers/concerns/masks/controller.rb +114 -0
  24. data/app/controllers/masks/actors_controller.rb +15 -0
  25. data/app/controllers/masks/application_controller.rb +35 -0
  26. data/app/controllers/masks/backup_codes_controller.rb +34 -0
  27. data/app/controllers/masks/debug_controller.rb +9 -0
  28. data/app/controllers/masks/devices_controller.rb +20 -0
  29. data/app/controllers/masks/emails_controller.rb +60 -0
  30. data/app/controllers/masks/error_controller.rb +14 -0
  31. data/app/controllers/masks/keys_controller.rb +45 -0
  32. data/app/controllers/masks/manage/actor_controller.rb +35 -0
  33. data/app/controllers/masks/manage/actors_controller.rb +12 -0
  34. data/app/controllers/masks/manage/base_controller.rb +12 -0
  35. data/app/controllers/masks/one_time_code_controller.rb +49 -0
  36. data/app/controllers/masks/passwords_controller.rb +33 -0
  37. data/app/controllers/masks/recoveries_controller.rb +43 -0
  38. data/app/controllers/masks/sessions_controller.rb +53 -0
  39. data/app/helpers/masks/application_helper.rb +49 -0
  40. data/app/jobs/masks/application_job.rb +7 -0
  41. data/app/jobs/masks/expire_actors_job.rb +15 -0
  42. data/app/jobs/masks/expire_recoveries_job.rb +15 -0
  43. data/app/mailers/masks/actor_mailer.rb +22 -0
  44. data/app/mailers/masks/application_mailer.rb +15 -0
  45. data/app/models/concerns/masks/access.rb +162 -0
  46. data/app/models/concerns/masks/actor.rb +132 -0
  47. data/app/models/concerns/masks/adapter.rb +68 -0
  48. data/app/models/concerns/masks/role.rb +9 -0
  49. data/app/models/concerns/masks/scoped.rb +54 -0
  50. data/app/models/masks/access/actor_password.rb +20 -0
  51. data/app/models/masks/access/actor_scopes.rb +18 -0
  52. data/app/models/masks/access/actor_signup.rb +22 -0
  53. data/app/models/masks/actors/anonymous.rb +40 -0
  54. data/app/models/masks/actors/system.rb +24 -0
  55. data/app/models/masks/adapters/active_record.rb +85 -0
  56. data/app/models/masks/application_model.rb +15 -0
  57. data/app/models/masks/application_record.rb +8 -0
  58. data/app/models/masks/check.rb +192 -0
  59. data/app/models/masks/credential.rb +166 -0
  60. data/app/models/masks/credentials/backup_code.rb +30 -0
  61. data/app/models/masks/credentials/device.rb +59 -0
  62. data/app/models/masks/credentials/email.rb +48 -0
  63. data/app/models/masks/credentials/factor2.rb +71 -0
  64. data/app/models/masks/credentials/key.rb +38 -0
  65. data/app/models/masks/credentials/last_login.rb +12 -0
  66. data/app/models/masks/credentials/masquerade.rb +32 -0
  67. data/app/models/masks/credentials/nickname.rb +63 -0
  68. data/app/models/masks/credentials/one_time_code.rb +34 -0
  69. data/app/models/masks/credentials/password.rb +28 -0
  70. data/app/models/masks/credentials/recovery.rb +71 -0
  71. data/app/models/masks/credentials/session.rb +67 -0
  72. data/app/models/masks/device.rb +30 -0
  73. data/app/models/masks/error.rb +51 -0
  74. data/app/models/masks/event.rb +14 -0
  75. data/app/models/masks/mask.rb +255 -0
  76. data/app/models/masks/rails/actor.rb +190 -0
  77. data/app/models/masks/rails/actor_role.rb +12 -0
  78. data/app/models/masks/rails/device.rb +47 -0
  79. data/app/models/masks/rails/email.rb +96 -0
  80. data/app/models/masks/rails/key.rb +61 -0
  81. data/app/models/masks/rails/recovery.rb +116 -0
  82. data/app/models/masks/rails/role.rb +20 -0
  83. data/app/models/masks/rails/scope.rb +15 -0
  84. data/app/models/masks/session.rb +447 -0
  85. data/app/models/masks/sessions/access.rb +26 -0
  86. data/app/models/masks/sessions/inline.rb +16 -0
  87. data/app/models/masks/sessions/request.rb +42 -0
  88. data/app/resources/masks/actor_resource.rb +9 -0
  89. data/app/resources/masks/session_resource.rb +15 -0
  90. data/app/views/layouts/masks/application.html.erb +17 -0
  91. data/app/views/layouts/masks/mailer.html.erb +17 -0
  92. data/app/views/layouts/masks/mailer.text.erb +1 -0
  93. data/app/views/layouts/masks/manage.html.erb +25 -0
  94. data/app/views/masks/actor_mailer/recover_credentials.html.erb +33 -0
  95. data/app/views/masks/actor_mailer/recover_credentials.text.erb +1 -0
  96. data/app/views/masks/actor_mailer/verify_email.html.erb +34 -0
  97. data/app/views/masks/actor_mailer/verify_email.text.erb +8 -0
  98. data/app/views/masks/actors/current.html.erb +152 -0
  99. data/app/views/masks/application/_header.html.erb +31 -0
  100. data/app/views/masks/backup_codes/new.html.erb +103 -0
  101. data/app/views/masks/emails/new.html.erb +103 -0
  102. data/app/views/masks/emails/verify.html.erb +51 -0
  103. data/app/views/masks/keys/new.html.erb +127 -0
  104. data/app/views/masks/manage/actor/show.html.erb +126 -0
  105. data/app/views/masks/manage/actors/index.html.erb +40 -0
  106. data/app/views/masks/one_time_code/new.html.erb +150 -0
  107. data/app/views/masks/passwords/edit.html.erb +58 -0
  108. data/app/views/masks/recoveries/new.html.erb +71 -0
  109. data/app/views/masks/recoveries/password.html.erb +64 -0
  110. data/app/views/masks/sessions/new.html.erb +153 -0
  111. data/config/brakeman.ignore +28 -0
  112. data/config/locales/en.yml +286 -0
  113. data/config/routes.rb +46 -0
  114. data/db/migrate/20231205173845_create_actors.rb +94 -0
  115. data/lib/generators/masks/install/USAGE +8 -0
  116. data/lib/generators/masks/install/install_generator.rb +33 -0
  117. data/lib/generators/masks/install/templates/initializer.rb +5 -0
  118. data/lib/generators/masks/install/templates/masks.json +6 -0
  119. data/lib/masks/configuration.rb +236 -0
  120. data/lib/masks/engine.rb +25 -0
  121. data/lib/masks/middleware.rb +70 -0
  122. data/lib/masks/version.rb +5 -0
  123. data/lib/masks.rb +183 -0
  124. data/lib/tasks/masks_tasks.rake +71 -0
  125. data/masks.json +274 -0
  126. metadata +416 -0
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ module Rails
5
+ class Recovery < ApplicationRecord
6
+ self.table_name = "recoveries"
7
+
8
+ scope :recent,
9
+ lambda {
10
+ where(
11
+ "created_at >= ?",
12
+ Masks.configuration.lifetimes[:recovery_email]&.ago ||
13
+ 1.hour.ago
14
+ )
15
+ }
16
+ scope :expired,
17
+ lambda {
18
+ where(
19
+ "created_at < ?",
20
+ Masks.configuration.lifetimes[:recovery_email]&.ago ||
21
+ 1.hour.ago
22
+ )
23
+ }
24
+
25
+ belongs_to :actor, class_name: Masks.configuration.models[:actor]
26
+
27
+ validates :token, presence: true, uniqueness: { scope: :actor_id }
28
+ validates :contact, presence: true, if: :actor
29
+ validate :validates_actor
30
+
31
+ attribute :configuration
32
+ attribute :session
33
+ attribute :value
34
+
35
+ after_initialize :generate_token
36
+ after_initialize :load_actor, unless: :actor_id
37
+
38
+ def to
39
+ case contact
40
+ when Email
41
+ contact.email
42
+ end
43
+ end
44
+
45
+ def notified?
46
+ notified_at.present?
47
+ end
48
+
49
+ def reset_password!(password)
50
+ return unless actor && persisted?
51
+
52
+ actor.password = password
53
+
54
+ return unless actor.valid?
55
+
56
+ actor.save!
57
+ destroy!
58
+ end
59
+
60
+ def notify!
61
+ return if !valid? || notified?
62
+
63
+ self.notified_at = Time.current
64
+ save!
65
+
66
+ case contact
67
+ when Email
68
+ ActorMailer.with(recovery: id).recover_credentials.deliver_later
69
+ end
70
+ end
71
+
72
+ def contact
73
+ return unless actor
74
+
75
+ @contact ||=
76
+ begin
77
+ contact =
78
+ if email
79
+ actor.emails.find_by(verified: true, email:)
80
+ elsif phone
81
+ # TODO
82
+ end
83
+
84
+ contact ||= actor.emails.where(verified: true).first
85
+ contact
86
+ end
87
+ end
88
+
89
+ def to_param
90
+ token
91
+ end
92
+
93
+ private
94
+
95
+ def generate_token
96
+ self.token ||= SecureRandom.hex(64)
97
+ end
98
+
99
+ def validates_actor
100
+ return if !actor || actor.valid?
101
+
102
+ actor.errors.full_messages.each { |error| errors.add(:base, error) }
103
+ end
104
+
105
+ def load_actor
106
+ return unless value
107
+
108
+ loaded =
109
+ self.actor = configuration.find_actor(session, email:, nickname:)
110
+
111
+ loaded.recoveries << self if loaded
112
+ loaded
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ module Rails
5
+ class Role < ApplicationRecord
6
+ include Masks::Role
7
+
8
+ self.table_name = "roles"
9
+
10
+ belongs_to :actor, polymorphic: true, autosave: true
11
+ belongs_to :record, polymorphic: true, autosave: true
12
+
13
+ validates :type,
14
+ presence: true,
15
+ uniqueness: {
16
+ scope: %i[actor_id actor_type record_id record_type]
17
+ }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ module Rails
5
+ class Scope < ApplicationRecord
6
+ self.table_name = "scopes"
7
+
8
+ validates :name, presence: true, uniqueness: { scope: :actor_id }
9
+
10
+ belongs_to :actor,
11
+ polymorphic: true,
12
+ class_name: Masks.configuration.models[:actor]
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,447 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ # Interface for sessions, which keep track of attempts to access resources.
5
+ #
6
+ # The helper methods provided by the +Masks+ module are wrappers around
7
+ # the +Masks::Session+ class. Masks creates different types of sessions
8
+ # dependending on the context, calls their +mask!+ method, and records
9
+ # the results.
10
+ #
11
+ # This class is designed to be sub-classed. Sub-classes must provide a +data+,
12
+ # +params+, and +matches_mask?+ method. The latter method is how a session
13
+ # is able to find a suitable mask from the configuration.
14
+ #
15
+ # After a session's +mask!+ method is called, it will report an +actor+,
16
+ # whether or not the checks it ran have +passed?+, and any +errors+ (just like
17
+ # an +ActiveRecord+ model).
18
+ #
19
+ # @see Masks::Check Masks::Check
20
+ # @see Masks::Credentials Masks::Credentials
21
+ # @see Masks::Sessions Masks::Sessions
22
+ # @see Masks::Mask Masks::Mask
23
+ class Session < ApplicationModel
24
+ CHECK_KEY = :masks
25
+
26
+ attribute :name
27
+ attribute :actor
28
+ attribute :scopes
29
+ attribute :scoped
30
+ attribute :credentials
31
+ attribute :checks, default: -> { {} }
32
+ attribute :config
33
+ attribute :error_messages
34
+
35
+ validates :actor, presence: true
36
+
37
+ class << self
38
+ def mask!(*args, **opts)
39
+ new(*args, **opts).tap(&:mask!)
40
+ end
41
+ end
42
+
43
+ # Returns an identifier for the session.
44
+ #
45
+ # This value can be used to reference the session in backend processes.
46
+ #
47
+ # @return [String]
48
+ def id
49
+ data[:session_id] || "anonymous"
50
+ end
51
+
52
+ # Returns the session's IP address or nil in cases where no IP is present.
53
+ #
54
+ # @return [String] or nil
55
+ def ip_address
56
+ nil
57
+ end
58
+
59
+ # Returns the session's user agent.
60
+ #
61
+ # Ideally this is always specified, but there are contexts where it cannot be supplied.
62
+ #
63
+ # @return [String] or nil
64
+ def user_agent
65
+ nil
66
+ end
67
+
68
+ # Returns a user-supplied "fingerprint" for the session.
69
+ #
70
+ # Generally speaking, this is a low-trust value.
71
+ #
72
+ # @return [String] or nil
73
+ def fingerprint
74
+ nil
75
+ end
76
+
77
+ # Returns a detected device based on the +user_agent+.
78
+ #
79
+ # If the user agent isn't present a detected device is still returned, but
80
+ # it's attributes will return nil and its +known?+ method will return false.
81
+ #
82
+ # @return [DeviceDetector]
83
+ def device
84
+ @device ||= DeviceDetector.new(user_agent)
85
+ end
86
+
87
+ # Whether or not the session's actor is anonymous?
88
+ delegate :anonymous?, to: :actor, allow_nil: true
89
+
90
+ # A hash of persisted session data.
91
+ #
92
+ # @return [Hash]
93
+ def data
94
+ raise NotImplementedError
95
+ end
96
+
97
+ # Incoming "request" params that could affect the session.
98
+ #
99
+ # @return [Hash]
100
+ def params
101
+ raise NotImplementedError
102
+ end
103
+
104
+ # Normalizes +params[:session]+ and returns it.
105
+ #
106
+ # Paramaters intended for the session should be nested under the +session+ key.
107
+ #
108
+ # @return [Hash]
109
+ def session_params
110
+ (params[:session] || {}).deep_symbolize_keys
111
+ end
112
+
113
+ # Additional short-lived data (not session or request data)
114
+ #
115
+ # @param [Hash] opts a hash of extra data to merge
116
+ def extras(**opts)
117
+ @extras ||= {}
118
+ @extras.merge!(**opts.stringify_keys) if opts.keys
119
+ @extras
120
+ end
121
+
122
+ # Returns a specific key from the session +extras+ or nil.
123
+ #
124
+ # @param [Symbol|String] key the name of the key
125
+ # @return [any]
126
+ def extra(key)
127
+ @extras&.fetch(key.to_s, nil)
128
+ end
129
+
130
+ # Sets the actor on the session.
131
+ #
132
+ # If the actor is already set then an error will be
133
+ # added to the session, preventing it from passing.
134
+ #
135
+ # @param [Masks::Actor] actor
136
+ def actor=(actor)
137
+ if self.actor && actor != self.actor
138
+ errors.add(:base, :multiple_actors)
139
+ else
140
+ super(actor)
141
+ end
142
+ end
143
+
144
+ delegate :scopes,
145
+ :scope?,
146
+ :role?,
147
+ :roles_for,
148
+ :role_records,
149
+ to: :scoped,
150
+ allow_nil: true
151
+
152
+ # Returns the "scoped" actor, which may be different from the actor itself.
153
+ #
154
+ # For example, in some cases access is gained via an API key or some
155
+ # person/system with "admin" rights. In both cases there is an agent
156
+ # agent operating the system that is granted the ability to behave
157
+ # as someone else.
158
+ #
159
+ # This scoped actor may respond to the methods available for interrogating
160
+ # scopes and roles differently than the actor itself—e.g. an access key may
161
+ # return a smaller set of scopes than the actor. An admin may temporarily
162
+ # allow additional scopes...
163
+ #
164
+ # @return [Masks::Scoped]
165
+ def scoped
166
+ super || actor
167
+ end
168
+
169
+ # Returns a check by the name provided.
170
+ #
171
+ # @param [Symbol|String] key
172
+ # @return [Masks::Check]
173
+ def find_check(key)
174
+ return unless key&.present?
175
+
176
+ id = key.to_sym
177
+
178
+ unless checks[id]
179
+ defaults = mask.checks[id] || {}
180
+ checks[id] = Check.new(key: id, **defaults)
181
+ end
182
+
183
+ checks.fetch(id)
184
+ end
185
+
186
+ # Masks the session.
187
+
188
+ # @return [self]
189
+ def mask!
190
+ Masks.event "session", session: self do
191
+ return if mask&.skip?
192
+
193
+ self.credentials =
194
+ mask.credentials.map do |cls|
195
+ cred = cls.new(session: self)
196
+
197
+ # give credentials a chance to populate the session...
198
+ # typically this is by providing an actor to validate
199
+ if (actor = cred.lookup)
200
+ self.actor = actor
201
+ end
202
+
203
+ cred
204
+ end
205
+
206
+ self.checks = load_checks(mask.checks)
207
+
208
+ # session data can reveal checks that are still valid
209
+ # based on duration. skip checking again in this case.
210
+ return if passed?
211
+
212
+ transaction do
213
+ # each credential is given a chance to mask the session
214
+ credentials.each do |cred|
215
+ cred.mask!
216
+
217
+ if cred.errors.any?
218
+ cred.errors.full_messages.each do |message|
219
+ errors.add(:base, message)
220
+ end
221
+ end
222
+ rescue RuntimeError
223
+ return cleanup!
224
+ end
225
+
226
+ actor&.session = self
227
+
228
+ # to ensure that we are going to be passed?
229
+ if actor && !actor.valid?(:mask)
230
+ actor.errors.full_messages.each do |message|
231
+ errors.add(:base, message)
232
+ end
233
+ elsif !passed_checks?
234
+ errors.add(:base, :credentials)
235
+ elsif !passed?
236
+ errors.add(:base, :access)
237
+ elsif !actor&.mask!
238
+ errors.add(:base, actor.errors.full_messages.first || :credentials)
239
+ end
240
+
241
+ credentials.each(&:backup!)
242
+ commit_to_session
243
+ end
244
+
245
+ self
246
+ end
247
+ end
248
+
249
+ # Cleans up all session data, akin to logout.
250
+ #
251
+ # @return [self]
252
+ def cleanup!
253
+ Masks.event "cleanup", session: self do
254
+ mask.credentials.each do |cls|
255
+ cred = cls.new(session: self)
256
+ cred.cleanup!
257
+ end
258
+
259
+ if actor
260
+ data[:masks] ||= {}
261
+ data[:masks].delete(actor.session_key)
262
+ end
263
+
264
+ self
265
+ end
266
+ end
267
+
268
+ # Returns an access class based on this session.
269
+ #
270
+ # @param [String] name
271
+ # @raise [Masks::Error]
272
+ # @return [Masks::Access]
273
+ def access(name)
274
+ Masks.access(name, self)
275
+ end
276
+
277
+ # Whether or not the session is "writable".
278
+ #
279
+ # Some credentials only allow certain operations when in
280
+ # this state, which is akin to the difference between
281
+ # GET and POST.
282
+ #
283
+ # @return [Boolean]
284
+ def writable?
285
+ # certain operations should only happen when the credential is in writable
286
+ # mode, e.g. GET vs POST requests. override this method to customize the behaviour
287
+ true
288
+ end
289
+
290
+ # Whether or not to allow access to the actor identified by the session.
291
+ #
292
+ # This method aims to be a simple test to determine whether or not
293
+ # a session has passed all checks.
294
+ #
295
+ # @return [Boolean]
296
+ def passed?
297
+ return true if mask&.skip?
298
+ return false if !passed_checks? || errors.any?
299
+ return false unless matches_mask?(mask)
300
+ return false unless actor
301
+ return false unless mask.matches_session?(self)
302
+ return false unless pass?
303
+
304
+ true
305
+ end
306
+
307
+ # Returns the time the session passed all checks, provided they have.
308
+ #
309
+ # This method may return a time in the past—for example when credential
310
+ # checks passed in that past, but within their configured lifetime.
311
+ #
312
+ # @return [Datetime]
313
+ def passed_at
314
+ return unless passed?
315
+
316
+ checks.values.map(&:passed_at).compact.max
317
+ end
318
+
319
+ # Returns a hash of all checks that happened in the past.
320
+ #
321
+ # These checks are stored in the session data, under +CHECK_KEY+.
322
+ #
323
+ # @return [Hash]
324
+ def past_checks
325
+ @past_checks ||= data[CHECK_KEY]&.clone || {}
326
+ end
327
+
328
+ # Returns a hash of checks for a given type.
329
+ #
330
+ # If any of the checks in the type exist on the session, it will be
331
+ # returned. Otherwise a new check is constructed and included in the
332
+ # set.
333
+ #
334
+ # This is useful for introspecting the state of a session according
335
+ # to the rules of another type, but keep in mind that this does not
336
+ # allow the credentials configured on the type to run, so checks
337
+ # may report a passing status despite being stale.
338
+ #
339
+ # @return [Hash]
340
+ def checks_for(type)
341
+ return false unless actor_checks
342
+
343
+ load_checks(config.data.dig(:types, type.to_sym, :checks))
344
+ end
345
+
346
+ # Returns whether or not the session checks report a passing status.
347
+ #
348
+ # Pass an optional type to see if the session's checks pass according
349
+ # to the type's checks, useful for determining the potential state of
350
+ # a session.
351
+ #
352
+ # @param [String] type
353
+ # @return [Boolean]
354
+ def passed_checks?(type = nil)
355
+ return true unless checks.any?
356
+ return false unless actor_checks
357
+
358
+ to_check =
359
+ (
360
+ if type
361
+ load_checks(config.data.dig(:types, type.to_sym, :checks))
362
+ else
363
+ checks.slice(*mask.checks.keys)
364
+ end
365
+ )
366
+ to_check.values.all?(&:passed?)
367
+ end
368
+
369
+ # A single error message for the session, as opposed to a list of errors
370
+ #
371
+ # @return [String] or nil
372
+ def error_message
373
+ errors.full_messages.last
374
+ end
375
+
376
+ # The mask the session will use.
377
+ #
378
+ # @return [Masks::Mask]
379
+ # @raise [Masks::Error::UnknownMask]
380
+ def mask
381
+ @mask ||=
382
+ begin
383
+ mask = config.masks.find { |m| matches_mask?(m) }
384
+ raise Error::UnknownMask, self unless mask
385
+ mask
386
+ end
387
+ end
388
+
389
+ protected
390
+
391
+ # Whether or not the given mask matches the session.
392
+ #
393
+ # Sub-classes must implement this method, which is used to match the session
394
+ # itself to a mask from +Masks.configuration+.
395
+ #
396
+ # @return [Boolean]
397
+ def matches_mask?(mask)
398
+ raise NotImplementedError
399
+ end
400
+
401
+ # Sub-classes can return a falsey value to fail the session for any reason.
402
+ #
403
+ # @return [Boolean]
404
+ def pass?
405
+ true
406
+ end
407
+
408
+ private
409
+
410
+ def transaction(&block)
411
+ if actor.respond_to?(:transaction)
412
+ actor.transaction(&block)
413
+ elsif actor&.class.respond_to?(:transaction)
414
+ actor.class.transaction(&block)
415
+ else
416
+ block.call
417
+ end
418
+ end
419
+
420
+ def commit_to_session
421
+ return unless mask&.backup?
422
+ return unless actor&.backup?
423
+
424
+ data[:masks] ||= {}
425
+ data[:masks][actor.session_key] ||= {}
426
+ checks.each do |id, check|
427
+ data[:masks][actor.session_key][id.to_s] = check.to_session
428
+ end
429
+ end
430
+
431
+ def load_checks(defaults)
432
+ defaults ||= {}
433
+ past = actor_checks || {}
434
+
435
+ defaults
436
+ .deep_merge(past.deep_symbolize_keys)
437
+ .to_h { |key, opts| [key, Check.new(opts.merge(key:))] }
438
+ .slice(*defaults.keys)
439
+ end
440
+
441
+ def actor_checks
442
+ return unless actor&.session_key
443
+
444
+ past_checks[actor.session_key] ||= {}
445
+ end
446
+ end
447
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ module Sessions
5
+ # Session for masking access classes.
6
+ class Access < Masks::Session
7
+ attribute :name
8
+ attribute :original
9
+
10
+ delegate :actor,
11
+ :config,
12
+ :params,
13
+ :data,
14
+ :writable?,
15
+ :extras,
16
+ :extra,
17
+ to: :original
18
+
19
+ def matches_mask?(mask)
20
+ return false unless mask.access == name.to_s
21
+
22
+ original.mask.access&.try(:include?, name.to_s) || original.mask.access
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ module Sessions
5
+ # Session for masking inline ruby code.
6
+ class Inline < Masks::Session
7
+ attribute :name
8
+ attribute :data
9
+ attribute :params
10
+
11
+ def matches_mask?(mask)
12
+ true if mask.name == name
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ module Sessions
5
+ # Session for masking +ActionDispatch::Request+ and +Rack::Request+.
6
+ class Request < Masks::Session
7
+ attribute :request
8
+
9
+ def to_s
10
+ "mask(#{request.method.upcase} #{request.path})"
11
+ end
12
+
13
+ def ip_address
14
+ request.remote_ip
15
+ end
16
+
17
+ def user_agent
18
+ request.user_agent
19
+ end
20
+
21
+ def fingerprint
22
+ params[:_fingerprint]
23
+ end
24
+
25
+ def params
26
+ request.params
27
+ end
28
+
29
+ def data
30
+ request.session
31
+ end
32
+
33
+ def matches_mask?(mask)
34
+ mask.matches_request?(request)
35
+ end
36
+
37
+ def writable?
38
+ request.post?
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ class ActorResource
5
+ include Alba::Resource
6
+
7
+ attributes :nickname
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Masks
4
+ class SessionResource
5
+ include Alba::Resource
6
+
7
+ attributes :id, :ip_address, :user_agent, :fingerprint, :scopes
8
+
9
+ attribute :authorized, &:passed?
10
+
11
+ attribute :actor do |session|
12
+ ActorResource.new(session.actor).to_h
13
+ end
14
+ end
15
+ end