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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +50 -0
- data/Rakefile +11 -0
- data/app/assets/builds/application.css +4764 -0
- data/app/assets/builds/application.js +8236 -0
- data/app/assets/builds/application.js.map +7 -0
- data/app/assets/builds/masks/application.css +1 -0
- data/app/assets/builds/masks/application.js +7122 -0
- data/app/assets/builds/masks/application.js.map +7 -0
- data/app/assets/images/masks.png +0 -0
- data/app/assets/javascripts/application.js +2 -0
- data/app/assets/javascripts/controllers/application.js +9 -0
- data/app/assets/javascripts/controllers/emails_controller.js +28 -0
- data/app/assets/javascripts/controllers/index.js +12 -0
- data/app/assets/javascripts/controllers/keys_controller.js +20 -0
- data/app/assets/javascripts/controllers/recover_controller.js +21 -0
- data/app/assets/javascripts/controllers/recover_password_controller.js +21 -0
- data/app/assets/javascripts/controllers/session_controller.js +94 -0
- data/app/assets/manifest.js +2 -0
- data/app/assets/masks_manifest.js +2 -0
- data/app/assets/stylesheets/application.css +26 -0
- data/app/controllers/concerns/masks/controller.rb +114 -0
- data/app/controllers/masks/actors_controller.rb +15 -0
- data/app/controllers/masks/application_controller.rb +35 -0
- data/app/controllers/masks/backup_codes_controller.rb +34 -0
- data/app/controllers/masks/debug_controller.rb +9 -0
- data/app/controllers/masks/devices_controller.rb +20 -0
- data/app/controllers/masks/emails_controller.rb +60 -0
- data/app/controllers/masks/error_controller.rb +14 -0
- data/app/controllers/masks/keys_controller.rb +45 -0
- data/app/controllers/masks/manage/actor_controller.rb +35 -0
- data/app/controllers/masks/manage/actors_controller.rb +12 -0
- data/app/controllers/masks/manage/base_controller.rb +12 -0
- data/app/controllers/masks/one_time_code_controller.rb +49 -0
- data/app/controllers/masks/passwords_controller.rb +33 -0
- data/app/controllers/masks/recoveries_controller.rb +43 -0
- data/app/controllers/masks/sessions_controller.rb +53 -0
- data/app/helpers/masks/application_helper.rb +49 -0
- data/app/jobs/masks/application_job.rb +7 -0
- data/app/jobs/masks/expire_actors_job.rb +15 -0
- data/app/jobs/masks/expire_recoveries_job.rb +15 -0
- data/app/mailers/masks/actor_mailer.rb +22 -0
- data/app/mailers/masks/application_mailer.rb +15 -0
- data/app/models/concerns/masks/access.rb +162 -0
- data/app/models/concerns/masks/actor.rb +132 -0
- data/app/models/concerns/masks/adapter.rb +68 -0
- data/app/models/concerns/masks/role.rb +9 -0
- data/app/models/concerns/masks/scoped.rb +54 -0
- data/app/models/masks/access/actor_password.rb +20 -0
- data/app/models/masks/access/actor_scopes.rb +18 -0
- data/app/models/masks/access/actor_signup.rb +22 -0
- data/app/models/masks/actors/anonymous.rb +40 -0
- data/app/models/masks/actors/system.rb +24 -0
- data/app/models/masks/adapters/active_record.rb +85 -0
- data/app/models/masks/application_model.rb +15 -0
- data/app/models/masks/application_record.rb +8 -0
- data/app/models/masks/check.rb +192 -0
- data/app/models/masks/credential.rb +166 -0
- data/app/models/masks/credentials/backup_code.rb +30 -0
- data/app/models/masks/credentials/device.rb +59 -0
- data/app/models/masks/credentials/email.rb +48 -0
- data/app/models/masks/credentials/factor2.rb +71 -0
- data/app/models/masks/credentials/key.rb +38 -0
- data/app/models/masks/credentials/last_login.rb +12 -0
- data/app/models/masks/credentials/masquerade.rb +32 -0
- data/app/models/masks/credentials/nickname.rb +63 -0
- data/app/models/masks/credentials/one_time_code.rb +34 -0
- data/app/models/masks/credentials/password.rb +28 -0
- data/app/models/masks/credentials/recovery.rb +71 -0
- data/app/models/masks/credentials/session.rb +67 -0
- data/app/models/masks/device.rb +30 -0
- data/app/models/masks/error.rb +51 -0
- data/app/models/masks/event.rb +14 -0
- data/app/models/masks/mask.rb +255 -0
- data/app/models/masks/rails/actor.rb +190 -0
- data/app/models/masks/rails/actor_role.rb +12 -0
- data/app/models/masks/rails/device.rb +47 -0
- data/app/models/masks/rails/email.rb +96 -0
- data/app/models/masks/rails/key.rb +61 -0
- data/app/models/masks/rails/recovery.rb +116 -0
- data/app/models/masks/rails/role.rb +20 -0
- data/app/models/masks/rails/scope.rb +15 -0
- data/app/models/masks/session.rb +447 -0
- data/app/models/masks/sessions/access.rb +26 -0
- data/app/models/masks/sessions/inline.rb +16 -0
- data/app/models/masks/sessions/request.rb +42 -0
- data/app/resources/masks/actor_resource.rb +9 -0
- data/app/resources/masks/session_resource.rb +15 -0
- data/app/views/layouts/masks/application.html.erb +17 -0
- data/app/views/layouts/masks/mailer.html.erb +17 -0
- data/app/views/layouts/masks/mailer.text.erb +1 -0
- data/app/views/layouts/masks/manage.html.erb +25 -0
- data/app/views/masks/actor_mailer/recover_credentials.html.erb +33 -0
- data/app/views/masks/actor_mailer/recover_credentials.text.erb +1 -0
- data/app/views/masks/actor_mailer/verify_email.html.erb +34 -0
- data/app/views/masks/actor_mailer/verify_email.text.erb +8 -0
- data/app/views/masks/actors/current.html.erb +152 -0
- data/app/views/masks/application/_header.html.erb +31 -0
- data/app/views/masks/backup_codes/new.html.erb +103 -0
- data/app/views/masks/emails/new.html.erb +103 -0
- data/app/views/masks/emails/verify.html.erb +51 -0
- data/app/views/masks/keys/new.html.erb +127 -0
- data/app/views/masks/manage/actor/show.html.erb +126 -0
- data/app/views/masks/manage/actors/index.html.erb +40 -0
- data/app/views/masks/one_time_code/new.html.erb +150 -0
- data/app/views/masks/passwords/edit.html.erb +58 -0
- data/app/views/masks/recoveries/new.html.erb +71 -0
- data/app/views/masks/recoveries/password.html.erb +64 -0
- data/app/views/masks/sessions/new.html.erb +153 -0
- data/config/brakeman.ignore +28 -0
- data/config/locales/en.yml +286 -0
- data/config/routes.rb +46 -0
- data/db/migrate/20231205173845_create_actors.rb +94 -0
- data/lib/generators/masks/install/USAGE +8 -0
- data/lib/generators/masks/install/install_generator.rb +33 -0
- data/lib/generators/masks/install/templates/initializer.rb +5 -0
- data/lib/generators/masks/install/templates/masks.json +6 -0
- data/lib/masks/configuration.rb +236 -0
- data/lib/masks/engine.rb +25 -0
- data/lib/masks/middleware.rb +70 -0
- data/lib/masks/version.rb +5 -0
- data/lib/masks.rb +183 -0
- data/lib/tasks/masks_tasks.rake +71 -0
- data/masks.json +274 -0
- 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,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
|