booth 0.0.1
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/CHANGELOG.md +4 -0
- data/LICENSE.md +22 -0
- data/README.md +372 -0
- data/app/assets/config/booth_manifest.js +15 -0
- data/app/assets/images/booth/browsers/README.md +2 -0
- data/app/assets/images/booth/browsers/chrome.svg +1 -0
- data/app/assets/images/booth/browsers/edge.svg +1 -0
- data/app/assets/images/booth/browsers/firefox.svg +1 -0
- data/app/assets/images/booth/browsers/internet_explorer.svg +1 -0
- data/app/assets/images/booth/browsers/opera.svg +1 -0
- data/app/assets/images/booth/browsers/safari.svg +1 -0
- data/app/assets/images/booth/browsers/unknown.svg +1 -0
- data/app/assets/images/booth/platforms/README.md +2 -0
- data/app/assets/images/booth/platforms/android.svg +6 -0
- data/app/assets/images/booth/platforms/apple.svg +6 -0
- data/app/assets/images/booth/platforms/linux.svg +6 -0
- data/app/assets/images/booth/platforms/unknown.svg +1 -0
- data/app/assets/images/booth/platforms/windows.svg +6 -0
- data/app/assets/javascripts/booth/all.js +162 -0
- data/app/assets/javascripts/booth/all.js.map +1 -0
- data/app/assets/javascripts/booth/booth.ts +194 -0
- data/app/assets/javascripts/booth/webauthn-json.ts +99 -0
- data/config/locales/de.yml +84 -0
- data/config/locales/en.yml +79 -0
- data/lib/booth/adminland/credentials/create.rb +30 -0
- data/lib/booth/adminland/onboardings/create.rb +63 -0
- data/lib/booth/adminland/onboardings/destroy.rb +50 -0
- data/lib/booth/adminland/onboardings/find.rb +93 -0
- data/lib/booth/adminland/onboardings/index.rb +23 -0
- data/lib/booth/adminland/periodic_cleanup.rb +11 -0
- data/lib/booth/adminland/recoveries/consume.rb +70 -0
- data/lib/booth/adminland.rb +48 -0
- data/lib/booth/audits/register/added_otp.rb +22 -0
- data/lib/booth/audits/register/changed_otp.rb +22 -0
- data/lib/booth/audits/register/completed_onboarding.rb +22 -0
- data/lib/booth/audits/register/correct_otp.rb +42 -0
- data/lib/booth/audits/register/correct_password.rb +43 -0
- data/lib/booth/audits/register/logout.rb +22 -0
- data/lib/booth/audits/register/requested_password_reset.rb +22 -0
- data/lib/booth/audits/register/wrong_otp.rb +22 -0
- data/lib/booth/audits/register/wrong_password.rb +25 -0
- data/lib/booth/authenticators/confirm.rb +34 -0
- data/lib/booth/authenticators/credential_mode_after_confirmation.rb +25 -0
- data/lib/booth/authenticators/step.rb +19 -0
- data/lib/booth/concerns/action.rb +58 -0
- data/lib/booth/concerns/transition.rb +17 -0
- data/lib/booth/configuration.rb +116 -0
- data/lib/booth/configure.rb +37 -0
- data/lib/booth/contests/get.rb +36 -0
- data/lib/booth/contests/respond.rb +78 -0
- data/lib/booth/contests/set_for_login.rb +28 -0
- data/lib/booth/cooldowns/distance_of_time.rb +46 -0
- data/lib/booth/cooldowns/otp.rb +22 -0
- data/lib/booth/cooldowns/password.rb +44 -0
- data/lib/booth/cooldowns/password_reset.rb +24 -0
- data/lib/booth/cooldowns/strategies/exponential.rb +82 -0
- data/lib/booth/cooldowns/strategies/global.rb +62 -0
- data/lib/booth/cooldowns/strategies/result.rb +22 -0
- data/lib/booth/credentials/create.rb +28 -0
- data/lib/booth/credentials/create_with_onboarding.rb +26 -0
- data/lib/booth/credentials/find_by_username.rb +45 -0
- data/lib/booth/credentials/mode.rb +69 -0
- data/lib/booth/credentials/modes/otp_addable.rb +23 -0
- data/lib/booth/credentials/modes/otp_changeable.rb +23 -0
- data/lib/booth/credentials/modes/otp_manageable.rb +17 -0
- data/lib/booth/credentials/modes/otp_removable.rb +23 -0
- data/lib/booth/credentials/modes/password_addable.rb +29 -0
- data/lib/booth/credentials/modes/password_changeable.rb +31 -0
- data/lib/booth/credentials/modes/password_manageable.rb +17 -0
- data/lib/booth/credentials/modes/password_removable.rb +24 -0
- data/lib/booth/credentials/modes/password_removal_requires_user_verifiable_webauth.rb +16 -0
- data/lib/booth/credentials/modes/webauth_addable.rb +26 -0
- data/lib/booth/credentials/modes/webauth_manageable.rb +16 -0
- data/lib/booth/credentials/modes/webauth_removable.rb +25 -0
- data/lib/booth/credentials/otp_authentication.rb +59 -0
- data/lib/booth/credentials/password_authentication.rb +72 -0
- data/lib/booth/credentials/webauth_challenge.rb +28 -0
- data/lib/booth/engine.rb +25 -0
- data/lib/booth/errors.rb +86 -0
- data/lib/booth/geolocation.rb +20 -0
- data/lib/booth/hooks/after_fetch.rb +54 -0
- data/lib/booth/hooks/before_logout.rb +29 -0
- data/lib/booth/hooks/serialize_from_session.rb +24 -0
- data/lib/booth/hooks/serialize_into_session.rb +14 -0
- data/lib/booth/logger.rb +41 -0
- data/lib/booth/logging.rb +59 -0
- data/lib/booth/method_object.rb +73 -0
- data/lib/booth/mode.rb +22 -0
- data/lib/booth/models/application_record.rb +7 -0
- data/lib/booth/models/audit.rb +24 -0
- data/lib/booth/models/authenticator.rb +45 -0
- data/lib/booth/models/concerns/modeable.rb +50 -0
- data/lib/booth/models/concerns/otpable.rb +37 -0
- data/lib/booth/models/concerns/passwordable.rb +58 -0
- data/lib/booth/models/contest.rb +55 -0
- data/lib/booth/models/contests/scopes/recently_created.rb +23 -0
- data/lib/booth/models/contests/scopes/recently_responded.rb +32 -0
- data/lib/booth/models/credential.rb +61 -0
- data/lib/booth/models/onboarding.rb +61 -0
- data/lib/booth/models/password_reset.rb +41 -0
- data/lib/booth/models/recovery.rb +32 -0
- data/lib/booth/models/registration.rb +10 -0
- data/lib/booth/models/session.rb +47 -0
- data/lib/booth/models/user_agent.rb +50 -0
- data/lib/booth/modes/base.rb +25 -0
- data/lib/booth/modes/username_and_password.rb +7 -0
- data/lib/booth/modes/username_and_webauth.rb +7 -0
- data/lib/booth/modes/username_password_and_otp.rb +7 -0
- data/lib/booth/modes/username_password_and_webauth.rb +7 -0
- data/lib/booth/onboardings/find.rb +35 -0
- data/lib/booth/onboardings/propagate_to_credential.rb +63 -0
- data/lib/booth/onboardings/step.rb +68 -0
- data/lib/booth/password_resets/create.rb +57 -0
- data/lib/booth/password_resets/find.rb +36 -0
- data/lib/booth/password_resets/propagate_to_credential.rb +36 -0
- data/lib/booth/password_resets/step.rb +18 -0
- data/lib/booth/recoveries/create.rb +45 -0
- data/lib/booth/request.rb +106 -0
- data/lib/booth/requests/agent.rb +14 -0
- data/lib/booth/requests/authentication.rb +47 -0
- data/lib/booth/requests/ip.rb +28 -0
- data/lib/booth/requests/return_path.rb +34 -0
- data/lib/booth/requests/session.rb +106 -0
- data/lib/booth/requests/storage.rb +62 -0
- data/lib/booth/requests/storages/login.rb +108 -0
- data/lib/booth/requests/storages/otp.rb +54 -0
- data/lib/booth/requests/storages/password.rb +49 -0
- data/lib/booth/requests/storages/password_reset.rb +35 -0
- data/lib/booth/requests/storages/recovery.rb +35 -0
- data/lib/booth/requests/storages/registration.rb +27 -0
- data/lib/booth/requests/storages/webauth.rb +38 -0
- data/lib/booth/requests/sudo.rb +110 -0
- data/lib/booth/routes/userland.rb +80 -0
- data/lib/booth/sessions/create_and_login.rb +46 -0
- data/lib/booth/sessions/historical_locations.rb +18 -0
- data/lib/booth/sessions/index.rb +59 -0
- data/lib/booth/sessions/revoke.rb +51 -0
- data/lib/booth/sessions/revoke_all_others.rb +43 -0
- data/lib/booth/sessions/to_passport.rb +51 -0
- data/lib/booth/syntaxes/contest_code.rb +58 -0
- data/lib/booth/syntaxes/email.rb +97 -0
- data/lib/booth/syntaxes/ip.rb +37 -0
- data/lib/booth/syntaxes/otp.rb +57 -0
- data/lib/booth/syntaxes/scope.rb +21 -0
- data/lib/booth/syntaxes/scope_comparison.rb +28 -0
- data/lib/booth/syntaxes/secret_key.rb +64 -0
- data/lib/booth/syntaxes/username.rb +85 -0
- data/lib/booth/syntaxes/uuid.rb +23 -0
- data/lib/booth/test/helpers.rb +63 -0
- data/lib/booth/test/support/assert_all_partials_were_covered.rb +63 -0
- data/lib/booth/test/support/assert_logged_in.rb +49 -0
- data/lib/booth/test/support/assert_logged_out.rb +30 -0
- data/lib/booth/test/support/assert_partial.rb +29 -0
- data/lib/booth/test/support/force_login.rb +26 -0
- data/lib/booth/test/support/get_session_value.rb +35 -0
- data/lib/booth/test/support/otp_code_from_session.rb +30 -0
- data/lib/booth/test/support/soft_reset_session.rb +22 -0
- data/lib/booth/test/userland/logins/missing_authenticators.rb +72 -0
- data/lib/booth/test/userland/logins/missing_onboarding.rb +35 -0
- data/lib/booth/test/userland/logins/username_and_password.rb +40 -0
- data/lib/booth/test/userland/logins/username_and_webauth.rb +75 -0
- data/lib/booth/test/userland/logins/username_password_and_otp.rb +45 -0
- data/lib/booth/test/userland/logins/username_password_and_webauth.rb +86 -0
- data/lib/booth/test/userland/onboardings/already_logged_in.rb +64 -0
- data/lib/booth/test/userland/onboardings/otp.rb +63 -0
- data/lib/booth/test/userland/onboardings/password.rb +49 -0
- data/lib/booth/test/userland/onboardings/timeout.rb +47 -0
- data/lib/booth/test/userland/otps/manage.rb +86 -0
- data/lib/booth/test/userland/password_resets/reset.rb +102 -0
- data/lib/booth/test/userland.rb +38 -0
- data/lib/booth/test/webauthn/disable.rb +17 -0
- data/lib/booth/test/webauthn/enable.rb +19 -0
- data/lib/booth/test/webauthn/virtual_authenticators/create.rb +38 -0
- data/lib/booth/test/webauthn/virtual_authenticators/destroy.rb +20 -0
- data/lib/booth/test.rb +53 -0
- data/lib/booth/to_struct.rb +11 -0
- data/lib/booth/userland/extract_flash_messages.rb +35 -0
- data/lib/booth/userland/logins/create.rb +28 -0
- data/lib/booth/userland/logins/destroy.rb +37 -0
- data/lib/booth/userland/logins/new.rb +70 -0
- data/lib/booth/userland/logins/transitions/create/choose_username.rb +41 -0
- data/lib/booth/userland/logins/transitions/create/enter_otp.rb +70 -0
- data/lib/booth/userland/logins/transitions/create/skip_remotes.rb +24 -0
- data/lib/booth/userland/logins/transitions/create/verify_password.rb +70 -0
- data/lib/booth/userland/logins/transitions/create/webauth_authentication_initiation.rb +55 -0
- data/lib/booth/userland/logins/transitions/create/webauth_authentication_verification.rb +80 -0
- data/lib/booth/userland/logins/transitions/new/already_logged_in.rb +21 -0
- data/lib/booth/userland/logins/transitions/new/fallible.rb +27 -0
- data/lib/booth/userland/logins/transitions/new/mode_first_time.rb +20 -0
- data/lib/booth/userland/logins/transitions/new/mode_username_and_password.rb +20 -0
- data/lib/booth/userland/logins/transitions/new/mode_username_and_webauth.rb +26 -0
- data/lib/booth/userland/logins/transitions/new/mode_username_password_and_otp.rb +24 -0
- data/lib/booth/userland/logins/transitions/new/mode_username_password_and_webauth.rb +24 -0
- data/lib/booth/userland/logins/transitions/new/no_username_chosen.rb +19 -0
- data/lib/booth/userland/logins/transitions/new/remote_session_available.rb +52 -0
- data/lib/booth/userland/logins/transitions/new/timed_out.rb +25 -0
- data/lib/booth/userland/onboardings/show.rb +74 -0
- data/lib/booth/userland/onboardings/transitions/update/choose_mode.rb +58 -0
- data/lib/booth/userland/onboardings/transitions/update/choose_password.rb +41 -0
- data/lib/booth/userland/onboardings/transitions/update/choose_webauth_nickname.rb +50 -0
- data/lib/booth/userland/onboardings/transitions/update/confirm_otp.rb +58 -0
- data/lib/booth/userland/onboardings/transitions/update/confirm_password.rb +49 -0
- data/lib/booth/userland/onboardings/transitions/update/register_otp.rb +31 -0
- data/lib/booth/userland/onboardings/transitions/update/reset_otp.rb +40 -0
- data/lib/booth/userland/onboardings/transitions/update/reset_password.rb +35 -0
- data/lib/booth/userland/onboardings/transitions/update/reset_webauth.rb +46 -0
- data/lib/booth/userland/onboardings/transitions/update/webauth_authentication_initiation.rb +40 -0
- data/lib/booth/userland/onboardings/transitions/update/webauth_authentication_verification.rb +59 -0
- data/lib/booth/userland/onboardings/transitions/update/webauth_registration_initiation.rb +46 -0
- data/lib/booth/userland/onboardings/transitions/update/webauth_registration_verification.rb +56 -0
- data/lib/booth/userland/onboardings/update.rb +68 -0
- data/lib/booth/userland/otps/destroy.rb +42 -0
- data/lib/booth/userland/otps/edit.rb +72 -0
- data/lib/booth/userland/otps/guards/manageable.rb +21 -0
- data/lib/booth/userland/otps/guards/sudo.rb +23 -0
- data/lib/booth/userland/otps/show.rb +36 -0
- data/lib/booth/userland/otps/sudo.rb +51 -0
- data/lib/booth/userland/otps/transitions/update/confirm.rb +84 -0
- data/lib/booth/userland/otps/transitions/update/register.rb +40 -0
- data/lib/booth/userland/otps/transitions/update/reset.rb +31 -0
- data/lib/booth/userland/otps/update.rb +34 -0
- data/lib/booth/userland/password_resets/create.rb +73 -0
- data/lib/booth/userland/password_resets/guards/logged_out.rb +21 -0
- data/lib/booth/userland/password_resets/new.rb +57 -0
- data/lib/booth/userland/password_resets/show.rb +77 -0
- data/lib/booth/userland/password_resets/transitions/update/choose_password.rb +48 -0
- data/lib/booth/userland/password_resets/transitions/update/confirm_password.rb +54 -0
- data/lib/booth/userland/password_resets/transitions/update/reset_password.rb +29 -0
- data/lib/booth/userland/password_resets/update.rb +65 -0
- data/lib/booth/userland/passwords/destroy.rb +41 -0
- data/lib/booth/userland/passwords/edit.rb +54 -0
- data/lib/booth/userland/passwords/guards/manageable.rb +21 -0
- data/lib/booth/userland/passwords/guards/removable.rb +21 -0
- data/lib/booth/userland/passwords/guards/sudo.rb +21 -0
- data/lib/booth/userland/passwords/remove.rb +34 -0
- data/lib/booth/userland/passwords/show.rb +32 -0
- data/lib/booth/userland/passwords/sudo.rb +55 -0
- data/lib/booth/userland/passwords/transitions/remove/step.rb +27 -0
- data/lib/booth/userland/passwords/transitions/update/choose_password.rb +62 -0
- data/lib/booth/userland/passwords/transitions/update/confirm_password.rb +82 -0
- data/lib/booth/userland/passwords/update.rb +33 -0
- data/lib/booth/userland/personal_contests/show.rb +60 -0
- data/lib/booth/userland/personal_contests/update.rb +37 -0
- data/lib/booth/userland/recoveries/create.rb +48 -0
- data/lib/booth/userland/recoveries/new.rb +35 -0
- data/lib/booth/userland/registrations/create.rb +56 -0
- data/lib/booth/userland/registrations/new.rb +39 -0
- data/lib/booth/userland/sessions/destroy_one_or_other.rb +41 -0
- data/lib/booth/userland/sessions/index.rb +27 -0
- data/lib/booth/userland/sessions/show.rb +31 -0
- data/lib/booth/userland/sessions/transitions/destroy/enter_password.rb +50 -0
- data/lib/booth/userland/sessions/transitions/destroy/enter_webauth.rb +56 -0
- data/lib/booth/userland/sessions/transitions/destroy/verify_password.rb +83 -0
- data/lib/booth/userland/sessions/transitions/destroy/webauth_authentication_initiation.rb +38 -0
- data/lib/booth/userland/sessions/transitions/destroy/webauth_authentication_verification.rb +61 -0
- data/lib/booth/userland/sessions/transitions/show/enter_webauth.rb +56 -0
- data/lib/booth/userland/webauths/create.rb +83 -0
- data/lib/booth/userland/webauths/destroy.rb +60 -0
- data/lib/booth/userland/webauths/guards/manageable.rb +21 -0
- data/lib/booth/userland/webauths/guards/sudo.rb +22 -0
- data/lib/booth/userland/webauths/index.rb +43 -0
- data/lib/booth/userland/webauths/new.rb +70 -0
- data/lib/booth/userland/webauths/sudo.rb +25 -0
- data/lib/booth/userland/webauths/transitions/create/authentication_initiation.rb +52 -0
- data/lib/booth/userland/webauths/transitions/create/authentication_verification.rb +64 -0
- data/lib/booth/userland/webauths/transitions/create/choose_nickname.rb +50 -0
- data/lib/booth/userland/webauths/transitions/create/registration_initiation.rb +61 -0
- data/lib/booth/userland/webauths/transitions/create/registration_verification.rb +68 -0
- data/lib/booth/userland/webauths/transitions/create/reset.rb +36 -0
- data/lib/booth/userland/webauths/transitions/new/step.rb +23 -0
- data/lib/booth/userland/webauths/transitions/sudo/authentication_initiation.rb +47 -0
- data/lib/booth/userland/webauths/transitions/sudo/authentication_verification.rb +34 -0
- data/lib/booth/userland.rb +192 -0
- data/lib/booth/version.rb +3 -0
- data/lib/booth/webauth/authentication_verification.rb +68 -0
- data/lib/booth/webauth/demand_user_verification.rb +29 -0
- data/lib/booth/webauth/options_for_create.rb +46 -0
- data/lib/booth/webauth/options_for_get.rb +29 -0
- data/lib/booth.rb +267 -0
- data/lib/generators/booth/migration/migration_generator.rb +25 -0
- data/lib/generators/booth/migration/templates/add_credential_to_users.erb +18 -0
- data/lib/generators/booth/migration/templates/create_booth_mode_types.erb +20 -0
- data/lib/generators/booth/migration/templates/create_booth_tables.erb +135 -0
- metadata +861 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module PersonalContests
|
|
4
|
+
class Show
|
|
5
|
+
include ::Booth::Concerns::Action
|
|
6
|
+
|
|
7
|
+
def call
|
|
8
|
+
request.must_be_get!
|
|
9
|
+
request.must_be_html!
|
|
10
|
+
request.must_be_logged_in!
|
|
11
|
+
|
|
12
|
+
debug { 'You want to receive your personal contest...' }
|
|
13
|
+
|
|
14
|
+
if contest.failure?
|
|
15
|
+
debug { "This credential doesn't have any contest right now." }
|
|
16
|
+
return Tron.success :not_contested, step: :not_contested
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if contest.recently_responded
|
|
20
|
+
debug { 'The current contest has already been responded to' }
|
|
21
|
+
return Tron.success :contest_responded, step: :contest_solved
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
step = case contest.reason
|
|
25
|
+
when :login then :remote_login
|
|
26
|
+
when :support then :support_authentication
|
|
27
|
+
else raise "Unknown contest reason: #{contest.reason}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
debug { 'You have a contest to respond to' }
|
|
31
|
+
Tron.success :you_are_contested,
|
|
32
|
+
ip: contest.ip,
|
|
33
|
+
agent: contest.agent.presence,
|
|
34
|
+
location: contest.location.presence,
|
|
35
|
+
browser_name: contest.browser_name,
|
|
36
|
+
platform_name: contest.platform_name,
|
|
37
|
+
browser_image_path: contest.browser_image_path,
|
|
38
|
+
platform_image_path: contest.platform_image_path,
|
|
39
|
+
step:
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def credential
|
|
45
|
+
return @credential if defined?(@credential)
|
|
46
|
+
|
|
47
|
+
id = request.authentication.credential_id
|
|
48
|
+
@credential = ::Booth::Models::Credential.find_by(id:)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def contest
|
|
52
|
+
return unless credential
|
|
53
|
+
return @contest if defined?(@contest)
|
|
54
|
+
|
|
55
|
+
@contest = ::Booth::Contests::Get.call(credential_id: credential.id)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module PersonalContests
|
|
4
|
+
class Update
|
|
5
|
+
include ::Booth::Concerns::Action
|
|
6
|
+
|
|
7
|
+
def call
|
|
8
|
+
request.must_be_patch!
|
|
9
|
+
|
|
10
|
+
do_check_logged_in
|
|
11
|
+
.on_success { do_respond }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def do_check_logged_in
|
|
15
|
+
return Tron.success :logged_in if credential
|
|
16
|
+
|
|
17
|
+
Tron.failure :not_logged_in
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def do_respond
|
|
21
|
+
::Booth::Contests::Respond.call(scope:, contest: credential.contest, request:)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
delegate :contest, to: :credential, private: true
|
|
27
|
+
|
|
28
|
+
def credential
|
|
29
|
+
return @credential if defined?(@credential)
|
|
30
|
+
|
|
31
|
+
credential_id = request.authentication.credential_id
|
|
32
|
+
@credential = ::Booth::Models::Credential.find_by(id: credential_id)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module Recoveries
|
|
4
|
+
class Create
|
|
5
|
+
include ::Booth::Concerns::Action
|
|
6
|
+
|
|
7
|
+
def call
|
|
8
|
+
request.must_be_post!
|
|
9
|
+
|
|
10
|
+
do_remember_email
|
|
11
|
+
.on_success { do_create_password_reset }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
delegate :username, to: :storage, private: true
|
|
17
|
+
|
|
18
|
+
def do_remember_email
|
|
19
|
+
storage.email = ::Booth::Syntaxes::Email.call(email_param).normalized_invalid_email
|
|
20
|
+
|
|
21
|
+
Tron.success :remembered_email
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def do_create_password_reset
|
|
25
|
+
creation = ::Booth::Recoveries::Create.call(
|
|
26
|
+
scope:,
|
|
27
|
+
email: email_param,
|
|
28
|
+
ip: request.ip
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
creation.on_success do
|
|
32
|
+
storage.email = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
creation
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def storage
|
|
39
|
+
request.storage.recovery
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def email_param
|
|
43
|
+
params.require(:recovery).permit(:email)[:email]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module Recoveries
|
|
4
|
+
class New
|
|
5
|
+
include ::Booth::Concerns::Action
|
|
6
|
+
|
|
7
|
+
def call
|
|
8
|
+
request.must_be_get!
|
|
9
|
+
request.must_be_html!
|
|
10
|
+
|
|
11
|
+
if already_logged_in?
|
|
12
|
+
call_already_logged_in
|
|
13
|
+
else
|
|
14
|
+
Tron.failure :enter_email, step: :enter_email, email: storage.email
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def call_already_logged_in
|
|
21
|
+
debug { "Looks like you're already logged in" }
|
|
22
|
+
Tron.success :logged_in, step: :already_logged_in
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def already_logged_in?
|
|
26
|
+
request.authentication.logged_in?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def storage
|
|
30
|
+
request.storage.recovery
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module Registrations
|
|
4
|
+
class Create
|
|
5
|
+
include ::Booth::Concerns::Action
|
|
6
|
+
|
|
7
|
+
option :allowed_modes
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
request.must_be_post!
|
|
11
|
+
|
|
12
|
+
debug { "You want to register the username #{username_param.inspect}, let's see if it is taken." }
|
|
13
|
+
finding = ::Booth::Credentials::FindByUsername.call(username: username_param)
|
|
14
|
+
registration_storage.username = finding.normalized_invalid_username
|
|
15
|
+
|
|
16
|
+
finding.on_success do
|
|
17
|
+
debug { 'That username is taken. You should try to login instead.' }
|
|
18
|
+
login_storage.credential_for_username = finding.credential
|
|
19
|
+
public_message = I18n.t('booth.username_already_exists')
|
|
20
|
+
return Tron.success :username_exists, public_message:
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
debug { 'That username is available.' }
|
|
24
|
+
return finding unless finding.failure == :credential_not_found
|
|
25
|
+
|
|
26
|
+
creation = ::Booth::Credentials::CreateWithOnboarding.call(
|
|
27
|
+
username: finding.normalized_username,
|
|
28
|
+
allowed_modes:,
|
|
29
|
+
scope:
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
creation.on_success do
|
|
33
|
+
::Booth::Sessions::CreateAndLogin.call(credential: creation.credential, request:)
|
|
34
|
+
return Tron.success :username_is_available, public_message: 'You have registered an account.'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Tron.failure :registration_failed, error: creation.error
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def username_param
|
|
43
|
+
params.require(:registration).permit(:username)[:username]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def login_storage
|
|
47
|
+
request.storage.login
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def registration_storage
|
|
51
|
+
request.storage.registration
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module Registrations
|
|
4
|
+
class New
|
|
5
|
+
include ::Booth::Concerns::Action
|
|
6
|
+
|
|
7
|
+
def call
|
|
8
|
+
request.must_be_get!
|
|
9
|
+
request.must_be_html!
|
|
10
|
+
|
|
11
|
+
if already_logged_in?
|
|
12
|
+
call_already_logged_in
|
|
13
|
+
else
|
|
14
|
+
Tron.failure :choose_username, step: :choose_username, username: storage.username
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def call_already_logged_in
|
|
21
|
+
debug { "Looks like you're already logged in" }
|
|
22
|
+
Tron.success :logged_in, step: :already_logged_in
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def already_logged_in?
|
|
26
|
+
request.authentication.logged_in?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def username
|
|
30
|
+
storage.username
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def storage
|
|
34
|
+
request.storage.registration
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module Sessions
|
|
4
|
+
class DestroyOneOrOther
|
|
5
|
+
include ::Booth::Concerns::Action
|
|
6
|
+
|
|
7
|
+
def call
|
|
8
|
+
request.must_be_delete!
|
|
9
|
+
request.must_be_logged_in!
|
|
10
|
+
|
|
11
|
+
do_transition
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def transitions
|
|
17
|
+
if request.authentication.mode == :username_and_webauth
|
|
18
|
+
webauth_transitions
|
|
19
|
+
else
|
|
20
|
+
password_transitions
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def webauth_transitions
|
|
25
|
+
[
|
|
26
|
+
::Booth::Userland::Sessions::Transitions::Destroy::EnterWebauth,
|
|
27
|
+
::Booth::Userland::Sessions::Transitions::Destroy::WebauthAuthenticationInitiation,
|
|
28
|
+
::Booth::Userland::Sessions::Transitions::Destroy::WebauthAuthenticationVerification,
|
|
29
|
+
]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def password_transitions
|
|
33
|
+
[
|
|
34
|
+
::Booth::Userland::Sessions::Transitions::Destroy::EnterPassword,
|
|
35
|
+
::Booth::Userland::Sessions::Transitions::Destroy::VerifyPassword,
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module Sessions
|
|
4
|
+
class Index
|
|
5
|
+
include ::Booth::Concerns::Action
|
|
6
|
+
|
|
7
|
+
def call
|
|
8
|
+
request.must_be_get!
|
|
9
|
+
request.must_be_logged_in!
|
|
10
|
+
|
|
11
|
+
do_fetch_sessions
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def do_fetch_sessions
|
|
17
|
+
::Booth::Sessions::Index.call credential_id: authentication.credential_id,
|
|
18
|
+
current_session_id: authentication.session_id
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def authentication
|
|
22
|
+
request.authentication
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module Sessions
|
|
4
|
+
# `DELETE /sessions/123` may present a page with a WebAuth authentication challenge.
|
|
5
|
+
# That challenge is responded to asynchronously and in that response the server destroys the session.
|
|
6
|
+
# After that, the page is reloaded by JS using `GET /sessions/123`, which is the `show` action.
|
|
7
|
+
# But we don't actually have a show action for sessions. We just informatively redirect to the index action.
|
|
8
|
+
class Show
|
|
9
|
+
include ::Booth::Concerns::Action
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
request.must_be_get!
|
|
13
|
+
request.must_be_html!
|
|
14
|
+
request.must_be_logged_in!
|
|
15
|
+
|
|
16
|
+
unless request.authentication.mode == :username_and_webauth
|
|
17
|
+
return Tron.failure :only_applicable_when_passwordless
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
::Booth::Userland::Sessions::Transitions::Show::EnterWebauth.call request:
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def authentication
|
|
26
|
+
request.authentication
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module Sessions
|
|
4
|
+
module Transitions
|
|
5
|
+
module Destroy
|
|
6
|
+
class EnterPassword
|
|
7
|
+
include ::Booth::Concerns::Transition
|
|
8
|
+
|
|
9
|
+
def self.applicable?(params:)
|
|
10
|
+
!params[:revocation]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
if sudo.password?
|
|
15
|
+
if session_id_param
|
|
16
|
+
debug { 'Having password sudo, revoking the desired session...' }
|
|
17
|
+
return ::Booth::Sessions::Revoke.call credential_id: authentication.credential_id,
|
|
18
|
+
session_id: session_id_param
|
|
19
|
+
else
|
|
20
|
+
debug { 'Having password sudo, revoking all other sessions...' }
|
|
21
|
+
return ::Booth::Sessions::RevokeAllOthers.call credential_id: authentication.credential_id,
|
|
22
|
+
surviving_session_id: authentication.session_id
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if session_id_param
|
|
27
|
+
Tron.failure :need_sudo_to_destroy_session,
|
|
28
|
+
step: :enter_password_to_destroy,
|
|
29
|
+
session_id: session_id_param
|
|
30
|
+
else
|
|
31
|
+
Tron.failure :need_sudo_to_destroy_all_other_sessions,
|
|
32
|
+
step: :enter_password_to_destroy_all_others
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def session_id_param
|
|
37
|
+
# If params[:id] is a UUID, then it's an ID for a `Booth::Models::Session` in the DB.
|
|
38
|
+
# If params[:id] is something else, then it's just a WebAuth Ceremony argument.
|
|
39
|
+
::Booth::Syntaxes::Uuid.call(request.params[:id], raise_if_invalid: false).uuid
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
delegate :authentication, to: :request
|
|
43
|
+
|
|
44
|
+
delegate :sudo, to: :request
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module Sessions
|
|
4
|
+
module Transitions
|
|
5
|
+
module Destroy
|
|
6
|
+
class EnterWebauth
|
|
7
|
+
include ::Booth::Concerns::Transition
|
|
8
|
+
|
|
9
|
+
def self.applicable?(params:)
|
|
10
|
+
!params[:webauth]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
if sudo.webauth?
|
|
15
|
+
if session_id_param
|
|
16
|
+
debug { 'Having webauth sudo, revoking the desired session...' }
|
|
17
|
+
return ::Booth::Sessions::Revoke.call credential_id: authentication.credential_id,
|
|
18
|
+
session_id: session_id_param
|
|
19
|
+
else
|
|
20
|
+
debug { 'Having webauth sudo, revoking all other sessions...' }
|
|
21
|
+
return ::Booth::Sessions::RevokeAllOthers.call credential_id: authentication.credential_id,
|
|
22
|
+
surviving_session_id: authentication.session_id
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if session_id_param
|
|
27
|
+
Tron.failure :need_sudo_to_destroy_session,
|
|
28
|
+
step: :enter_webauth_to_destroy,
|
|
29
|
+
session_id: session_id_param
|
|
30
|
+
else
|
|
31
|
+
Tron.failure :need_sudo_to_destroy_all_other_sessions,
|
|
32
|
+
step: :enter_webauth_to_destroy_all_others
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def session_id_param
|
|
39
|
+
# If params[:id] is a UUID, then it's an ID for a `Booth::Models::Session` in the DB.
|
|
40
|
+
# If params[:id] is something else, then it's just a WebAuth Ceremony argument.
|
|
41
|
+
::Booth::Syntaxes::Uuid.call(request.params[:id], raise_if_invalid: false).uuid
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def authentication
|
|
45
|
+
request.authentication
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def sudo
|
|
49
|
+
request.sudo
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module Sessions
|
|
4
|
+
module Transitions
|
|
5
|
+
module Destroy
|
|
6
|
+
class VerifyPassword
|
|
7
|
+
include ::Booth::Concerns::Transition
|
|
8
|
+
|
|
9
|
+
def self.applicable?(params:)
|
|
10
|
+
params.dig(:revocation, :password)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
do_find_credential
|
|
15
|
+
.on_success { do_revoke_sessions }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def do_find_credential
|
|
21
|
+
@credential = ::Booth::Models::Credential.find_by(id: authentication.credential_id)
|
|
22
|
+
return Tron.success :found_credential if @credential
|
|
23
|
+
|
|
24
|
+
Tron.failure :missing_credential
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def do_revoke_sessions
|
|
28
|
+
checking = ::Booth::Credentials::PasswordAuthentication.call(
|
|
29
|
+
credential: @credential,
|
|
30
|
+
password: password_param,
|
|
31
|
+
ip: request.ip,
|
|
32
|
+
agent: request.agent
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if checking.success?
|
|
36
|
+
if session_id_param
|
|
37
|
+
debug { 'Having password sudo, revoking the desired session...' }
|
|
38
|
+
return ::Booth::Sessions::Revoke.call credential_id: authentication.credential_id,
|
|
39
|
+
session_id: session_id_param
|
|
40
|
+
else
|
|
41
|
+
debug { 'Having password sudo, revoking all other sessions...' }
|
|
42
|
+
return ::Booth::Sessions::RevokeAllOthers.call credential_id: authentication.credential_id,
|
|
43
|
+
surviving_session_id: authentication.session_id
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
public_message = checking.public_message if checking.respond_to?(:public_message)
|
|
48
|
+
|
|
49
|
+
if session_id_param
|
|
50
|
+
Tron.failure :need_sudo_to_destroy_session,
|
|
51
|
+
step: :enter_password_to_destroy,
|
|
52
|
+
session_id: session_id_param,
|
|
53
|
+
public_message: public_message
|
|
54
|
+
else
|
|
55
|
+
Tron.failure :need_sudo_to_destroy_all_other_sessions,
|
|
56
|
+
step: :enter_password_to_destroy_all_others,
|
|
57
|
+
public_message: public_message
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def session_id_param
|
|
62
|
+
# If params[:id] is a UUID, then it's an ID for a `Booth::Models::Session` in the DB.
|
|
63
|
+
# If params[:id] is something else, then it's just a WebAuth Ceremony argument.
|
|
64
|
+
::Booth::Syntaxes::Uuid.call(request.params[:id], raise_if_invalid: false).uuid
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def password_param
|
|
68
|
+
request.params.require(:revocation).permit(:password)[:password]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def authentication
|
|
72
|
+
request.authentication
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def sudo
|
|
76
|
+
request.sudo
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module Sessions
|
|
4
|
+
module Transitions
|
|
5
|
+
module Destroy
|
|
6
|
+
class WebauthAuthenticationInitiation
|
|
7
|
+
include ::Booth::Concerns::Transition
|
|
8
|
+
|
|
9
|
+
def self.applicable?(params:)
|
|
10
|
+
params[:webauth] && !params[:type]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
debug { 'Preparing webauth challenge...' }
|
|
15
|
+
credential = ::Booth::Models::Credential.find(authentication.credential_id)
|
|
16
|
+
challenging = Booth::Credentials::WebauthChallenge.call(credential:)
|
|
17
|
+
result = Tron.success :webauth_for_you, public_json: challenging.options_for_get, http_status: :ok
|
|
18
|
+
debug { "The challenge is #{challenging.challenge}" }
|
|
19
|
+
sudo.webauthn_challenge = challenging.challenge
|
|
20
|
+
debug { "Responding with JSON: #{result.public_json}" }
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def sudo
|
|
27
|
+
request.sudo
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def authentication
|
|
31
|
+
request.authentication
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Booth
|
|
2
|
+
module Userland
|
|
3
|
+
module Sessions
|
|
4
|
+
module Transitions
|
|
5
|
+
module Destroy
|
|
6
|
+
class WebauthAuthenticationVerification
|
|
7
|
+
include ::Booth::Concerns::Transition
|
|
8
|
+
|
|
9
|
+
def self.applicable?(params:)
|
|
10
|
+
params[:webauth] && params[:type]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
do_find_challenge
|
|
15
|
+
.on_success { do_check_webauth }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Helpers
|
|
19
|
+
|
|
20
|
+
def do_find_challenge
|
|
21
|
+
return Tron.success :challenge_ongoing if sudo.webauthn_challenge.present?
|
|
22
|
+
|
|
23
|
+
debug { 'There is no corresponding challenge in the session' }
|
|
24
|
+
Tron.failure :no_session_challenge, public_json: {}, http_status: :unprocessable_entity
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def do_check_webauth
|
|
28
|
+
verification = ::Booth::Webauth::AuthenticationVerification.call(
|
|
29
|
+
request:,
|
|
30
|
+
credential_id: authentication.credential_id,
|
|
31
|
+
challenge: sudo.webauthn_challenge
|
|
32
|
+
)
|
|
33
|
+
return verification if verification.failure?
|
|
34
|
+
|
|
35
|
+
if session_id_param
|
|
36
|
+
::Booth::Sessions::Revoke.call credential_id: authentication.credential_id,
|
|
37
|
+
session_id: session_id_param
|
|
38
|
+
else
|
|
39
|
+
::Booth::Sessions::RevokeAllOthers.call credential_id: authentication.credential_id,
|
|
40
|
+
surviving_session_id: authentication.session_id
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
Tron.success :session_revocation_successful, public_json: {},
|
|
44
|
+
http_status: :created
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def session_id_param
|
|
48
|
+
# If params[:id] is a UUID, then it's an ID for a `Booth::Models::Session` in the DB.
|
|
49
|
+
# If params[:id] is something else, then it's just a WebAuth Ceremony argument.
|
|
50
|
+
::Booth::Syntaxes::Uuid.call(request.params[:id], raise_if_invalid: false).uuid
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
delegate :authentication, to: :request
|
|
54
|
+
|
|
55
|
+
delegate :sudo, to: :request
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|