masks 0.2.0

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