masks 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
module Credentials
|
5
|
+
# Checks for a known, valid :device.
|
6
|
+
#
|
7
|
+
# If the device is not associated with the session's actor, it will be.
|
8
|
+
# Identification is based on the +user_agent+ and a few other facets.
|
9
|
+
class Device < Masks::Credential
|
10
|
+
checks :device
|
11
|
+
|
12
|
+
def lookup
|
13
|
+
return unless actor
|
14
|
+
|
15
|
+
device = config.find_device(session, actor:)
|
16
|
+
|
17
|
+
session.extras(device:)
|
18
|
+
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def maskup
|
23
|
+
device = session.extra(:device)
|
24
|
+
|
25
|
+
return deny! unless device
|
26
|
+
|
27
|
+
session_key = session.data[:device_key]
|
28
|
+
|
29
|
+
# ensure devices match across sessions, which would only happen if a
|
30
|
+
# session cookie happened is shared across machines. this destroys
|
31
|
+
# the entire session and cleans up everything involved.
|
32
|
+
if session_key && device.session_key != session_key
|
33
|
+
raise "invalid device"
|
34
|
+
end
|
35
|
+
|
36
|
+
# store devices that are found in a database of known devices
|
37
|
+
if device.known?
|
38
|
+
session.data[:device_key] = device.session_key
|
39
|
+
actor.devices << device
|
40
|
+
approve!
|
41
|
+
else
|
42
|
+
cleanup
|
43
|
+
deny!
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def backup
|
48
|
+
session.extra(:device)&.touch(:accessed_at) if session&.passed?
|
49
|
+
end
|
50
|
+
|
51
|
+
def cleanup
|
52
|
+
device = session.extra(:device)
|
53
|
+
device&.reset_version
|
54
|
+
|
55
|
+
session.data[:device_key] = nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
module Credentials
|
5
|
+
# Checks for an :actor given a matching email.
|
6
|
+
class Email < Masks::Credential
|
7
|
+
checks :actor
|
8
|
+
|
9
|
+
def lookup
|
10
|
+
return if actor || !email
|
11
|
+
|
12
|
+
actor = config.find_actor(session, email:)
|
13
|
+
actor ||=
|
14
|
+
config.build_actor(
|
15
|
+
session,
|
16
|
+
email:,
|
17
|
+
nickname: generate_nickname(email)
|
18
|
+
)
|
19
|
+
actor.signup = true
|
20
|
+
actor
|
21
|
+
end
|
22
|
+
|
23
|
+
def maskup
|
24
|
+
approve! if actor && email && valid?
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def email
|
30
|
+
@email ||=
|
31
|
+
[session_params[:email], session_params[:nickname]].find do |param|
|
32
|
+
ValidateEmail.valid?(param)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def generate_nickname(email)
|
37
|
+
return email if !nickname_format || nickname_format.match?(email)
|
38
|
+
|
39
|
+
parts = email.split("@")
|
40
|
+
name = parts[0].gsub(/[^\w\d]/, "")
|
41
|
+
|
42
|
+
prefix_nickname(
|
43
|
+
"#{name.downcase.slice(0, 16)}#{SecureRandom.hex.slice(0, 8)}"
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
module Credentials
|
5
|
+
# Base class for factor2 credentials.
|
6
|
+
module Factor2
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
validates :secret, presence: true, if: :enabled?
|
11
|
+
validates :code, presence: { allow_nil: true }
|
12
|
+
|
13
|
+
attribute :code
|
14
|
+
|
15
|
+
checks :factor2
|
16
|
+
end
|
17
|
+
|
18
|
+
def lookup
|
19
|
+
# nothing to do here
|
20
|
+
end
|
21
|
+
|
22
|
+
def maskup
|
23
|
+
check.optional = false if actor&.factor2?
|
24
|
+
|
25
|
+
return unless enabled?
|
26
|
+
|
27
|
+
code_param = session_params&.fetch(param, nil)&.presence
|
28
|
+
|
29
|
+
self.code = verify(code_param) if code_param
|
30
|
+
|
31
|
+
if code
|
32
|
+
approve!
|
33
|
+
elsif code_param
|
34
|
+
deny!
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def verified?
|
39
|
+
code
|
40
|
+
end
|
41
|
+
|
42
|
+
def enabled?
|
43
|
+
secret.present?
|
44
|
+
end
|
45
|
+
|
46
|
+
def param
|
47
|
+
raise NotImplementedError
|
48
|
+
end
|
49
|
+
|
50
|
+
def secret
|
51
|
+
raise NotImplementedError
|
52
|
+
end
|
53
|
+
|
54
|
+
def enable(code, secret:)
|
55
|
+
raise NotImplementedError
|
56
|
+
end
|
57
|
+
|
58
|
+
def verify(code)
|
59
|
+
raise NotImplementedError
|
60
|
+
end
|
61
|
+
|
62
|
+
def verify_on_enable?
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
def generate_secret
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
module Credentials
|
5
|
+
# Checks :key given a valid Authorization header.
|
6
|
+
class Key < Masks::Credential
|
7
|
+
checks :key
|
8
|
+
|
9
|
+
attribute :accessed
|
10
|
+
|
11
|
+
def lookup
|
12
|
+
secret = session.request.authorization&.split&.last
|
13
|
+
key = session.config.find_key(session, secret:)
|
14
|
+
|
15
|
+
return unless key
|
16
|
+
|
17
|
+
session.extras(key:)
|
18
|
+
session.scoped = key
|
19
|
+
self.accessed = true
|
20
|
+
key.actor
|
21
|
+
end
|
22
|
+
|
23
|
+
def maskup
|
24
|
+
key = session.extra(:key)
|
25
|
+
|
26
|
+
if key&.actor == session&.actor && session.scoped == key
|
27
|
+
approve!
|
28
|
+
else
|
29
|
+
deny!
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def backup
|
34
|
+
session.scoped.touch(:accessed_at) if session&.passed? && accessed
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
module Credentials
|
5
|
+
# As of now, simply keeps track of +last_login+ times on the actor.
|
6
|
+
class LastLogin < Masks::Credential
|
7
|
+
def backup
|
8
|
+
actor&.touch(:last_login_at) if session&.passed?
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
module Credentials
|
5
|
+
# Checks for an :actor to masquerade as.
|
6
|
+
class Masquerade < Masks::Credential
|
7
|
+
checks :actor
|
8
|
+
|
9
|
+
def lookup
|
10
|
+
return if session.actor
|
11
|
+
|
12
|
+
value = session.data[:as]
|
13
|
+
|
14
|
+
@loaded =
|
15
|
+
case value
|
16
|
+
when Masks::ANON
|
17
|
+
Actors::Anonymous.new(session:) if session.mask&.allow_anonymous?
|
18
|
+
when session.mask.actor_scope
|
19
|
+
value
|
20
|
+
when ValidateEmail.valid?(value)
|
21
|
+
config.find_actor(session, email: value)
|
22
|
+
when String
|
23
|
+
config.find_actor(session, nickname: prefix_nickname(value))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def maskup
|
28
|
+
approve! if @loaded
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
module Credentials
|
5
|
+
# Checks for an :actor given a matching nickname.
|
6
|
+
class Nickname < Masks::Credential
|
7
|
+
checks :actor
|
8
|
+
|
9
|
+
delegate :actor_id, :nickname, to: :actor, allow_nil: true
|
10
|
+
|
11
|
+
validates :nickname, :actor, presence: true
|
12
|
+
validate :validates_custom, if: :nickname
|
13
|
+
|
14
|
+
def lookup
|
15
|
+
return if actor
|
16
|
+
|
17
|
+
actor =
|
18
|
+
(
|
19
|
+
if nickname_param
|
20
|
+
config.find_actor(session, nickname: nickname_param)
|
21
|
+
end
|
22
|
+
)
|
23
|
+
actor ||=
|
24
|
+
if nickname_param
|
25
|
+
config
|
26
|
+
.build_actor(session, nickname: nickname_param)
|
27
|
+
.tap { |actor| actor.signup = true }
|
28
|
+
end
|
29
|
+
|
30
|
+
if actor&.new_record?
|
31
|
+
actor.nickname =
|
32
|
+
prefix_nickname(actor.nickname, default: actor.nickname)
|
33
|
+
end
|
34
|
+
|
35
|
+
actor
|
36
|
+
end
|
37
|
+
|
38
|
+
def maskup
|
39
|
+
approve! if valid?
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def nickname_param
|
45
|
+
@nickname_param ||=
|
46
|
+
prefix_nickname(
|
47
|
+
session_params[:nickname],
|
48
|
+
default: session_params[:nickname]
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def validates_custom
|
53
|
+
return unless nickname
|
54
|
+
|
55
|
+
validates_length :nickname, nickname_config&.length
|
56
|
+
|
57
|
+
return unless nickname_format
|
58
|
+
|
59
|
+
errors.add(:nickname, :format) unless nickname_format.match?(nickname)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
module Credentials
|
5
|
+
# Checks :factor2 for a valid one-time code.
|
6
|
+
class OneTimeCode < Masks::Credential
|
7
|
+
include Factor2
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def param
|
12
|
+
:one_time_code
|
13
|
+
end
|
14
|
+
|
15
|
+
def secret
|
16
|
+
actor&.totp_secret
|
17
|
+
end
|
18
|
+
|
19
|
+
def verify(code)
|
20
|
+
valid_code?(code, secret)
|
21
|
+
end
|
22
|
+
|
23
|
+
def valid_code?(code, secret)
|
24
|
+
if code && secret
|
25
|
+
actor.totp.verify(code)
|
26
|
+
else
|
27
|
+
false
|
28
|
+
end
|
29
|
+
rescue OpenSSL::HMACError
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
module Credentials
|
5
|
+
# Checks :password for a match.
|
6
|
+
class Password < Masks::Credential
|
7
|
+
checks :password
|
8
|
+
|
9
|
+
def lookup
|
10
|
+
actor.password = password if actor&.new_record? && password
|
11
|
+
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def maskup
|
16
|
+
return unless password
|
17
|
+
|
18
|
+
actor&.authenticate(password) && actor&.valid? ? approve! : deny!
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def password
|
24
|
+
session_params[:password]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
module Credentials
|
5
|
+
# Checks for a :recovery on the session.
|
6
|
+
class Recovery < Masks::Credential
|
7
|
+
checks :recovery
|
8
|
+
|
9
|
+
def lookup
|
10
|
+
return if actor
|
11
|
+
|
12
|
+
@recovery =
|
13
|
+
if recovery_key
|
14
|
+
config.find_recovery(session, token: recovery_key)
|
15
|
+
else
|
16
|
+
config.build_recovery(
|
17
|
+
session,
|
18
|
+
nickname: nickname_param,
|
19
|
+
email: email_param,
|
20
|
+
phone: phone_param,
|
21
|
+
value: recovery_value
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
session.extras(recovery: @recovery)
|
26
|
+
|
27
|
+
@recovery&.actor
|
28
|
+
end
|
29
|
+
|
30
|
+
def maskup
|
31
|
+
return unless valid? && actor
|
32
|
+
|
33
|
+
if recovery_key
|
34
|
+
if recovery_password && @recovery&.valid?
|
35
|
+
@recovery.reset_password!(recovery_password)
|
36
|
+
approve!
|
37
|
+
end
|
38
|
+
elsif recovery_value && @recovery&.valid?
|
39
|
+
@recovery.notify!
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def recovery_value
|
46
|
+
params.dig(:recover, :value) if writable?
|
47
|
+
end
|
48
|
+
|
49
|
+
def recovery_key
|
50
|
+
params[:token]
|
51
|
+
end
|
52
|
+
|
53
|
+
def recovery_password
|
54
|
+
params.dig(:recover, :password) if writable?
|
55
|
+
end
|
56
|
+
|
57
|
+
def phone_param
|
58
|
+
@phone_param ||= Phonelib.valid?(recovery_value) ? recovery_value : nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def email_param
|
62
|
+
@email_param ||=
|
63
|
+
ValidateEmail.valid?(recovery_value) ? recovery_value : nil
|
64
|
+
end
|
65
|
+
|
66
|
+
def nickname_param
|
67
|
+
@nickname_param ||= prefix_nickname(recovery_value, default: nil)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
module Credentials
|
5
|
+
# Checks for past :actor(s) on a session.
|
6
|
+
#
|
7
|
+
# This can be used in lieu of supplying identifying details like an
|
8
|
+
# email/nickname (usually provided they were supplied in the past).
|
9
|
+
class Session < Masks::Credential
|
10
|
+
checks :actor
|
11
|
+
|
12
|
+
def lookup
|
13
|
+
return if session_params[:actor_id]
|
14
|
+
|
15
|
+
actor_ids = session.data[:actors]&.keys || []
|
16
|
+
actor_id = session.data[:actor]
|
17
|
+
actors = (actor_ids.any? ? config.find_actors(session, actor_ids) : [])
|
18
|
+
|
19
|
+
# only lookup and return the current actor if
|
20
|
+
# it's not provided via a param (e.g. someone
|
21
|
+
# is trying to login)
|
22
|
+
actor =
|
23
|
+
if actor_id
|
24
|
+
actors.find do |a|
|
25
|
+
a.actor_id == actor_id &&
|
26
|
+
a.session_key == session.data[:actors][a.actor_id]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
actor = Actors::Anonymous.new(session:) if optional? && !actors.present?
|
31
|
+
|
32
|
+
actor
|
33
|
+
end
|
34
|
+
|
35
|
+
def maskup
|
36
|
+
return approve! if optional? && actor&.anonymous?
|
37
|
+
|
38
|
+
actor_id = actor&.actor_id
|
39
|
+
|
40
|
+
return unless actor_id && session.data[:actors]&.fetch(actor_id, nil)
|
41
|
+
return unless session.data[:actor] == actor_id
|
42
|
+
|
43
|
+
if session.data.dig(:actors, actor_id) == actor.session_key
|
44
|
+
approve!
|
45
|
+
else
|
46
|
+
cleanup
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def backup
|
51
|
+
return unless actor && passed?
|
52
|
+
|
53
|
+
session.data[:actors] ||= {}
|
54
|
+
session.data[:actors][actor.actor_id] = actor.session_key
|
55
|
+
session.data[:actor] = actor.actor_id
|
56
|
+
end
|
57
|
+
|
58
|
+
def cleanup
|
59
|
+
actor_id = actor&.actor_id || session.data[:actor]
|
60
|
+
|
61
|
+
session.data[:actor] = nil
|
62
|
+
session.data[:actors] ||= {}
|
63
|
+
session.data[:actors].delete(actor_id)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
# Represents a device, used for interacting with a session.
|
5
|
+
#
|
6
|
+
# Device detection is optionally added with the +Device+ credential,
|
7
|
+
# using the +device-detector+ gem.
|
8
|
+
#
|
9
|
+
# @see Masks::Credentials::Device Masks::Credentials::Device
|
10
|
+
class Device < ApplicationModel
|
11
|
+
attribute :session
|
12
|
+
|
13
|
+
def fingerprint
|
14
|
+
return unless detector.known?
|
15
|
+
|
16
|
+
input = [
|
17
|
+
detector.name,
|
18
|
+
detector.os_name,
|
19
|
+
detector.device_name,
|
20
|
+
detector.device_type
|
21
|
+
].compact.join("-")
|
22
|
+
|
23
|
+
Digest::SHA512.hexdigest(input)
|
24
|
+
end
|
25
|
+
|
26
|
+
def detector
|
27
|
+
@detector ||= DeviceDetector.new(session.user_agent)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
module Error
|
5
|
+
# Base class for Masks errors
|
6
|
+
class Base < RuntimeError
|
7
|
+
end
|
8
|
+
|
9
|
+
# Thrown when a session is required but not provided
|
10
|
+
class InvalidSession < Base
|
11
|
+
def initialize
|
12
|
+
super("a session was expected, but none was provided")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Thrown when invalid configuration is detected
|
17
|
+
class InvalidConfiguration < Base
|
18
|
+
def initialize(value)
|
19
|
+
super("cannot use '#{value}' for masks.json")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Thrown when configuration is not found
|
24
|
+
class ConfigurationNotFound < Base
|
25
|
+
def initialize
|
26
|
+
super("cannot find masks.json")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Thrown when Masks encounters an unauthorized session
|
31
|
+
class Unauthorized < Base
|
32
|
+
def initialize
|
33
|
+
super("unauthorized")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Thrown when Masks cannot find a mask for a session
|
38
|
+
class UnknownMask < Base
|
39
|
+
def initialize(session)
|
40
|
+
super("could not determine mask for #{session}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Thrown when Masks cannot find an access class for the given name
|
45
|
+
class UnknownAccess < Base
|
46
|
+
def initialize(name)
|
47
|
+
super("could not determine access class for #{name}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Masks
|
4
|
+
# Publishes events using +ActiveSupport::Notifications+
|
5
|
+
class Event
|
6
|
+
class << self
|
7
|
+
def emit(name, **opts, &block)
|
8
|
+
ActiveSupport::Notifications.instrument("masks.#{name}", **opts) do
|
9
|
+
block.call
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|