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