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.
Files changed (285) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +4 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +372 -0
  5. data/app/assets/config/booth_manifest.js +15 -0
  6. data/app/assets/images/booth/browsers/README.md +2 -0
  7. data/app/assets/images/booth/browsers/chrome.svg +1 -0
  8. data/app/assets/images/booth/browsers/edge.svg +1 -0
  9. data/app/assets/images/booth/browsers/firefox.svg +1 -0
  10. data/app/assets/images/booth/browsers/internet_explorer.svg +1 -0
  11. data/app/assets/images/booth/browsers/opera.svg +1 -0
  12. data/app/assets/images/booth/browsers/safari.svg +1 -0
  13. data/app/assets/images/booth/browsers/unknown.svg +1 -0
  14. data/app/assets/images/booth/platforms/README.md +2 -0
  15. data/app/assets/images/booth/platforms/android.svg +6 -0
  16. data/app/assets/images/booth/platforms/apple.svg +6 -0
  17. data/app/assets/images/booth/platforms/linux.svg +6 -0
  18. data/app/assets/images/booth/platforms/unknown.svg +1 -0
  19. data/app/assets/images/booth/platforms/windows.svg +6 -0
  20. data/app/assets/javascripts/booth/all.js +162 -0
  21. data/app/assets/javascripts/booth/all.js.map +1 -0
  22. data/app/assets/javascripts/booth/booth.ts +194 -0
  23. data/app/assets/javascripts/booth/webauthn-json.ts +99 -0
  24. data/config/locales/de.yml +84 -0
  25. data/config/locales/en.yml +79 -0
  26. data/lib/booth/adminland/credentials/create.rb +30 -0
  27. data/lib/booth/adminland/onboardings/create.rb +63 -0
  28. data/lib/booth/adminland/onboardings/destroy.rb +50 -0
  29. data/lib/booth/adminland/onboardings/find.rb +93 -0
  30. data/lib/booth/adminland/onboardings/index.rb +23 -0
  31. data/lib/booth/adminland/periodic_cleanup.rb +11 -0
  32. data/lib/booth/adminland/recoveries/consume.rb +70 -0
  33. data/lib/booth/adminland.rb +48 -0
  34. data/lib/booth/audits/register/added_otp.rb +22 -0
  35. data/lib/booth/audits/register/changed_otp.rb +22 -0
  36. data/lib/booth/audits/register/completed_onboarding.rb +22 -0
  37. data/lib/booth/audits/register/correct_otp.rb +42 -0
  38. data/lib/booth/audits/register/correct_password.rb +43 -0
  39. data/lib/booth/audits/register/logout.rb +22 -0
  40. data/lib/booth/audits/register/requested_password_reset.rb +22 -0
  41. data/lib/booth/audits/register/wrong_otp.rb +22 -0
  42. data/lib/booth/audits/register/wrong_password.rb +25 -0
  43. data/lib/booth/authenticators/confirm.rb +34 -0
  44. data/lib/booth/authenticators/credential_mode_after_confirmation.rb +25 -0
  45. data/lib/booth/authenticators/step.rb +19 -0
  46. data/lib/booth/concerns/action.rb +58 -0
  47. data/lib/booth/concerns/transition.rb +17 -0
  48. data/lib/booth/configuration.rb +116 -0
  49. data/lib/booth/configure.rb +37 -0
  50. data/lib/booth/contests/get.rb +36 -0
  51. data/lib/booth/contests/respond.rb +78 -0
  52. data/lib/booth/contests/set_for_login.rb +28 -0
  53. data/lib/booth/cooldowns/distance_of_time.rb +46 -0
  54. data/lib/booth/cooldowns/otp.rb +22 -0
  55. data/lib/booth/cooldowns/password.rb +44 -0
  56. data/lib/booth/cooldowns/password_reset.rb +24 -0
  57. data/lib/booth/cooldowns/strategies/exponential.rb +82 -0
  58. data/lib/booth/cooldowns/strategies/global.rb +62 -0
  59. data/lib/booth/cooldowns/strategies/result.rb +22 -0
  60. data/lib/booth/credentials/create.rb +28 -0
  61. data/lib/booth/credentials/create_with_onboarding.rb +26 -0
  62. data/lib/booth/credentials/find_by_username.rb +45 -0
  63. data/lib/booth/credentials/mode.rb +69 -0
  64. data/lib/booth/credentials/modes/otp_addable.rb +23 -0
  65. data/lib/booth/credentials/modes/otp_changeable.rb +23 -0
  66. data/lib/booth/credentials/modes/otp_manageable.rb +17 -0
  67. data/lib/booth/credentials/modes/otp_removable.rb +23 -0
  68. data/lib/booth/credentials/modes/password_addable.rb +29 -0
  69. data/lib/booth/credentials/modes/password_changeable.rb +31 -0
  70. data/lib/booth/credentials/modes/password_manageable.rb +17 -0
  71. data/lib/booth/credentials/modes/password_removable.rb +24 -0
  72. data/lib/booth/credentials/modes/password_removal_requires_user_verifiable_webauth.rb +16 -0
  73. data/lib/booth/credentials/modes/webauth_addable.rb +26 -0
  74. data/lib/booth/credentials/modes/webauth_manageable.rb +16 -0
  75. data/lib/booth/credentials/modes/webauth_removable.rb +25 -0
  76. data/lib/booth/credentials/otp_authentication.rb +59 -0
  77. data/lib/booth/credentials/password_authentication.rb +72 -0
  78. data/lib/booth/credentials/webauth_challenge.rb +28 -0
  79. data/lib/booth/engine.rb +25 -0
  80. data/lib/booth/errors.rb +86 -0
  81. data/lib/booth/geolocation.rb +20 -0
  82. data/lib/booth/hooks/after_fetch.rb +54 -0
  83. data/lib/booth/hooks/before_logout.rb +29 -0
  84. data/lib/booth/hooks/serialize_from_session.rb +24 -0
  85. data/lib/booth/hooks/serialize_into_session.rb +14 -0
  86. data/lib/booth/logger.rb +41 -0
  87. data/lib/booth/logging.rb +59 -0
  88. data/lib/booth/method_object.rb +73 -0
  89. data/lib/booth/mode.rb +22 -0
  90. data/lib/booth/models/application_record.rb +7 -0
  91. data/lib/booth/models/audit.rb +24 -0
  92. data/lib/booth/models/authenticator.rb +45 -0
  93. data/lib/booth/models/concerns/modeable.rb +50 -0
  94. data/lib/booth/models/concerns/otpable.rb +37 -0
  95. data/lib/booth/models/concerns/passwordable.rb +58 -0
  96. data/lib/booth/models/contest.rb +55 -0
  97. data/lib/booth/models/contests/scopes/recently_created.rb +23 -0
  98. data/lib/booth/models/contests/scopes/recently_responded.rb +32 -0
  99. data/lib/booth/models/credential.rb +61 -0
  100. data/lib/booth/models/onboarding.rb +61 -0
  101. data/lib/booth/models/password_reset.rb +41 -0
  102. data/lib/booth/models/recovery.rb +32 -0
  103. data/lib/booth/models/registration.rb +10 -0
  104. data/lib/booth/models/session.rb +47 -0
  105. data/lib/booth/models/user_agent.rb +50 -0
  106. data/lib/booth/modes/base.rb +25 -0
  107. data/lib/booth/modes/username_and_password.rb +7 -0
  108. data/lib/booth/modes/username_and_webauth.rb +7 -0
  109. data/lib/booth/modes/username_password_and_otp.rb +7 -0
  110. data/lib/booth/modes/username_password_and_webauth.rb +7 -0
  111. data/lib/booth/onboardings/find.rb +35 -0
  112. data/lib/booth/onboardings/propagate_to_credential.rb +63 -0
  113. data/lib/booth/onboardings/step.rb +68 -0
  114. data/lib/booth/password_resets/create.rb +57 -0
  115. data/lib/booth/password_resets/find.rb +36 -0
  116. data/lib/booth/password_resets/propagate_to_credential.rb +36 -0
  117. data/lib/booth/password_resets/step.rb +18 -0
  118. data/lib/booth/recoveries/create.rb +45 -0
  119. data/lib/booth/request.rb +106 -0
  120. data/lib/booth/requests/agent.rb +14 -0
  121. data/lib/booth/requests/authentication.rb +47 -0
  122. data/lib/booth/requests/ip.rb +28 -0
  123. data/lib/booth/requests/return_path.rb +34 -0
  124. data/lib/booth/requests/session.rb +106 -0
  125. data/lib/booth/requests/storage.rb +62 -0
  126. data/lib/booth/requests/storages/login.rb +108 -0
  127. data/lib/booth/requests/storages/otp.rb +54 -0
  128. data/lib/booth/requests/storages/password.rb +49 -0
  129. data/lib/booth/requests/storages/password_reset.rb +35 -0
  130. data/lib/booth/requests/storages/recovery.rb +35 -0
  131. data/lib/booth/requests/storages/registration.rb +27 -0
  132. data/lib/booth/requests/storages/webauth.rb +38 -0
  133. data/lib/booth/requests/sudo.rb +110 -0
  134. data/lib/booth/routes/userland.rb +80 -0
  135. data/lib/booth/sessions/create_and_login.rb +46 -0
  136. data/lib/booth/sessions/historical_locations.rb +18 -0
  137. data/lib/booth/sessions/index.rb +59 -0
  138. data/lib/booth/sessions/revoke.rb +51 -0
  139. data/lib/booth/sessions/revoke_all_others.rb +43 -0
  140. data/lib/booth/sessions/to_passport.rb +51 -0
  141. data/lib/booth/syntaxes/contest_code.rb +58 -0
  142. data/lib/booth/syntaxes/email.rb +97 -0
  143. data/lib/booth/syntaxes/ip.rb +37 -0
  144. data/lib/booth/syntaxes/otp.rb +57 -0
  145. data/lib/booth/syntaxes/scope.rb +21 -0
  146. data/lib/booth/syntaxes/scope_comparison.rb +28 -0
  147. data/lib/booth/syntaxes/secret_key.rb +64 -0
  148. data/lib/booth/syntaxes/username.rb +85 -0
  149. data/lib/booth/syntaxes/uuid.rb +23 -0
  150. data/lib/booth/test/helpers.rb +63 -0
  151. data/lib/booth/test/support/assert_all_partials_were_covered.rb +63 -0
  152. data/lib/booth/test/support/assert_logged_in.rb +49 -0
  153. data/lib/booth/test/support/assert_logged_out.rb +30 -0
  154. data/lib/booth/test/support/assert_partial.rb +29 -0
  155. data/lib/booth/test/support/force_login.rb +26 -0
  156. data/lib/booth/test/support/get_session_value.rb +35 -0
  157. data/lib/booth/test/support/otp_code_from_session.rb +30 -0
  158. data/lib/booth/test/support/soft_reset_session.rb +22 -0
  159. data/lib/booth/test/userland/logins/missing_authenticators.rb +72 -0
  160. data/lib/booth/test/userland/logins/missing_onboarding.rb +35 -0
  161. data/lib/booth/test/userland/logins/username_and_password.rb +40 -0
  162. data/lib/booth/test/userland/logins/username_and_webauth.rb +75 -0
  163. data/lib/booth/test/userland/logins/username_password_and_otp.rb +45 -0
  164. data/lib/booth/test/userland/logins/username_password_and_webauth.rb +86 -0
  165. data/lib/booth/test/userland/onboardings/already_logged_in.rb +64 -0
  166. data/lib/booth/test/userland/onboardings/otp.rb +63 -0
  167. data/lib/booth/test/userland/onboardings/password.rb +49 -0
  168. data/lib/booth/test/userland/onboardings/timeout.rb +47 -0
  169. data/lib/booth/test/userland/otps/manage.rb +86 -0
  170. data/lib/booth/test/userland/password_resets/reset.rb +102 -0
  171. data/lib/booth/test/userland.rb +38 -0
  172. data/lib/booth/test/webauthn/disable.rb +17 -0
  173. data/lib/booth/test/webauthn/enable.rb +19 -0
  174. data/lib/booth/test/webauthn/virtual_authenticators/create.rb +38 -0
  175. data/lib/booth/test/webauthn/virtual_authenticators/destroy.rb +20 -0
  176. data/lib/booth/test.rb +53 -0
  177. data/lib/booth/to_struct.rb +11 -0
  178. data/lib/booth/userland/extract_flash_messages.rb +35 -0
  179. data/lib/booth/userland/logins/create.rb +28 -0
  180. data/lib/booth/userland/logins/destroy.rb +37 -0
  181. data/lib/booth/userland/logins/new.rb +70 -0
  182. data/lib/booth/userland/logins/transitions/create/choose_username.rb +41 -0
  183. data/lib/booth/userland/logins/transitions/create/enter_otp.rb +70 -0
  184. data/lib/booth/userland/logins/transitions/create/skip_remotes.rb +24 -0
  185. data/lib/booth/userland/logins/transitions/create/verify_password.rb +70 -0
  186. data/lib/booth/userland/logins/transitions/create/webauth_authentication_initiation.rb +55 -0
  187. data/lib/booth/userland/logins/transitions/create/webauth_authentication_verification.rb +80 -0
  188. data/lib/booth/userland/logins/transitions/new/already_logged_in.rb +21 -0
  189. data/lib/booth/userland/logins/transitions/new/fallible.rb +27 -0
  190. data/lib/booth/userland/logins/transitions/new/mode_first_time.rb +20 -0
  191. data/lib/booth/userland/logins/transitions/new/mode_username_and_password.rb +20 -0
  192. data/lib/booth/userland/logins/transitions/new/mode_username_and_webauth.rb +26 -0
  193. data/lib/booth/userland/logins/transitions/new/mode_username_password_and_otp.rb +24 -0
  194. data/lib/booth/userland/logins/transitions/new/mode_username_password_and_webauth.rb +24 -0
  195. data/lib/booth/userland/logins/transitions/new/no_username_chosen.rb +19 -0
  196. data/lib/booth/userland/logins/transitions/new/remote_session_available.rb +52 -0
  197. data/lib/booth/userland/logins/transitions/new/timed_out.rb +25 -0
  198. data/lib/booth/userland/onboardings/show.rb +74 -0
  199. data/lib/booth/userland/onboardings/transitions/update/choose_mode.rb +58 -0
  200. data/lib/booth/userland/onboardings/transitions/update/choose_password.rb +41 -0
  201. data/lib/booth/userland/onboardings/transitions/update/choose_webauth_nickname.rb +50 -0
  202. data/lib/booth/userland/onboardings/transitions/update/confirm_otp.rb +58 -0
  203. data/lib/booth/userland/onboardings/transitions/update/confirm_password.rb +49 -0
  204. data/lib/booth/userland/onboardings/transitions/update/register_otp.rb +31 -0
  205. data/lib/booth/userland/onboardings/transitions/update/reset_otp.rb +40 -0
  206. data/lib/booth/userland/onboardings/transitions/update/reset_password.rb +35 -0
  207. data/lib/booth/userland/onboardings/transitions/update/reset_webauth.rb +46 -0
  208. data/lib/booth/userland/onboardings/transitions/update/webauth_authentication_initiation.rb +40 -0
  209. data/lib/booth/userland/onboardings/transitions/update/webauth_authentication_verification.rb +59 -0
  210. data/lib/booth/userland/onboardings/transitions/update/webauth_registration_initiation.rb +46 -0
  211. data/lib/booth/userland/onboardings/transitions/update/webauth_registration_verification.rb +56 -0
  212. data/lib/booth/userland/onboardings/update.rb +68 -0
  213. data/lib/booth/userland/otps/destroy.rb +42 -0
  214. data/lib/booth/userland/otps/edit.rb +72 -0
  215. data/lib/booth/userland/otps/guards/manageable.rb +21 -0
  216. data/lib/booth/userland/otps/guards/sudo.rb +23 -0
  217. data/lib/booth/userland/otps/show.rb +36 -0
  218. data/lib/booth/userland/otps/sudo.rb +51 -0
  219. data/lib/booth/userland/otps/transitions/update/confirm.rb +84 -0
  220. data/lib/booth/userland/otps/transitions/update/register.rb +40 -0
  221. data/lib/booth/userland/otps/transitions/update/reset.rb +31 -0
  222. data/lib/booth/userland/otps/update.rb +34 -0
  223. data/lib/booth/userland/password_resets/create.rb +73 -0
  224. data/lib/booth/userland/password_resets/guards/logged_out.rb +21 -0
  225. data/lib/booth/userland/password_resets/new.rb +57 -0
  226. data/lib/booth/userland/password_resets/show.rb +77 -0
  227. data/lib/booth/userland/password_resets/transitions/update/choose_password.rb +48 -0
  228. data/lib/booth/userland/password_resets/transitions/update/confirm_password.rb +54 -0
  229. data/lib/booth/userland/password_resets/transitions/update/reset_password.rb +29 -0
  230. data/lib/booth/userland/password_resets/update.rb +65 -0
  231. data/lib/booth/userland/passwords/destroy.rb +41 -0
  232. data/lib/booth/userland/passwords/edit.rb +54 -0
  233. data/lib/booth/userland/passwords/guards/manageable.rb +21 -0
  234. data/lib/booth/userland/passwords/guards/removable.rb +21 -0
  235. data/lib/booth/userland/passwords/guards/sudo.rb +21 -0
  236. data/lib/booth/userland/passwords/remove.rb +34 -0
  237. data/lib/booth/userland/passwords/show.rb +32 -0
  238. data/lib/booth/userland/passwords/sudo.rb +55 -0
  239. data/lib/booth/userland/passwords/transitions/remove/step.rb +27 -0
  240. data/lib/booth/userland/passwords/transitions/update/choose_password.rb +62 -0
  241. data/lib/booth/userland/passwords/transitions/update/confirm_password.rb +82 -0
  242. data/lib/booth/userland/passwords/update.rb +33 -0
  243. data/lib/booth/userland/personal_contests/show.rb +60 -0
  244. data/lib/booth/userland/personal_contests/update.rb +37 -0
  245. data/lib/booth/userland/recoveries/create.rb +48 -0
  246. data/lib/booth/userland/recoveries/new.rb +35 -0
  247. data/lib/booth/userland/registrations/create.rb +56 -0
  248. data/lib/booth/userland/registrations/new.rb +39 -0
  249. data/lib/booth/userland/sessions/destroy_one_or_other.rb +41 -0
  250. data/lib/booth/userland/sessions/index.rb +27 -0
  251. data/lib/booth/userland/sessions/show.rb +31 -0
  252. data/lib/booth/userland/sessions/transitions/destroy/enter_password.rb +50 -0
  253. data/lib/booth/userland/sessions/transitions/destroy/enter_webauth.rb +56 -0
  254. data/lib/booth/userland/sessions/transitions/destroy/verify_password.rb +83 -0
  255. data/lib/booth/userland/sessions/transitions/destroy/webauth_authentication_initiation.rb +38 -0
  256. data/lib/booth/userland/sessions/transitions/destroy/webauth_authentication_verification.rb +61 -0
  257. data/lib/booth/userland/sessions/transitions/show/enter_webauth.rb +56 -0
  258. data/lib/booth/userland/webauths/create.rb +83 -0
  259. data/lib/booth/userland/webauths/destroy.rb +60 -0
  260. data/lib/booth/userland/webauths/guards/manageable.rb +21 -0
  261. data/lib/booth/userland/webauths/guards/sudo.rb +22 -0
  262. data/lib/booth/userland/webauths/index.rb +43 -0
  263. data/lib/booth/userland/webauths/new.rb +70 -0
  264. data/lib/booth/userland/webauths/sudo.rb +25 -0
  265. data/lib/booth/userland/webauths/transitions/create/authentication_initiation.rb +52 -0
  266. data/lib/booth/userland/webauths/transitions/create/authentication_verification.rb +64 -0
  267. data/lib/booth/userland/webauths/transitions/create/choose_nickname.rb +50 -0
  268. data/lib/booth/userland/webauths/transitions/create/registration_initiation.rb +61 -0
  269. data/lib/booth/userland/webauths/transitions/create/registration_verification.rb +68 -0
  270. data/lib/booth/userland/webauths/transitions/create/reset.rb +36 -0
  271. data/lib/booth/userland/webauths/transitions/new/step.rb +23 -0
  272. data/lib/booth/userland/webauths/transitions/sudo/authentication_initiation.rb +47 -0
  273. data/lib/booth/userland/webauths/transitions/sudo/authentication_verification.rb +34 -0
  274. data/lib/booth/userland.rb +192 -0
  275. data/lib/booth/version.rb +3 -0
  276. data/lib/booth/webauth/authentication_verification.rb +68 -0
  277. data/lib/booth/webauth/demand_user_verification.rb +29 -0
  278. data/lib/booth/webauth/options_for_create.rb +46 -0
  279. data/lib/booth/webauth/options_for_get.rb +29 -0
  280. data/lib/booth.rb +267 -0
  281. data/lib/generators/booth/migration/migration_generator.rb +25 -0
  282. data/lib/generators/booth/migration/templates/add_credential_to_users.erb +18 -0
  283. data/lib/generators/booth/migration/templates/create_booth_mode_types.erb +20 -0
  284. data/lib/generators/booth/migration/templates/create_booth_tables.erb +135 -0
  285. metadata +861 -0
@@ -0,0 +1,58 @@
1
+ module Booth
2
+ module Syntaxes
3
+ class ContestCode
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ param :input
8
+
9
+ def call
10
+ debug { "Checking contest #{input.inspect} for valid syntax..." }
11
+ check_blank.on_success { check_characters }
12
+ .on_success { check_length }
13
+ end
14
+
15
+ private
16
+
17
+ def check_blank
18
+ return Tron.success :contest_code_present if input.present?
19
+
20
+ debug { 'This contest is blank.' }
21
+ Tron.failure :blank_contest_code,
22
+ normalized_contest_code: nil,
23
+ public_message: I18n.t('booth.blank_contest_code')
24
+ end
25
+
26
+ def check_characters
27
+ return Tron.success :contest_code_consists_of_digits if input_without_spaces.match?(allowed_regexp)
28
+
29
+ debug { 'This contest contains invalid characters' }
30
+ Tron.failure :invalid_contest_code_format,
31
+ normalized_contest_code: nil,
32
+ public_message: I18n.t('booth.invalid_contest_code_format')
33
+ end
34
+
35
+ def check_length
36
+ if input_without_spaces.to_s.length == ::Booth.config.contest_digits
37
+ return Tron.success :valid_contest_code_syntax,
38
+ normalized_contest_code: input_without_spaces
39
+ end
40
+
41
+ debug { "This contest is not #{::Booth.config.contest_digits} characters long." }
42
+ Tron.failure :wrong_contest_code_length,
43
+ normalized_contest_code: nil,
44
+ public_message: I18n.t('booth.wrong_contest_code_length', digits: ::Booth.config.contest_digits)
45
+ end
46
+
47
+ # Helpers
48
+
49
+ def input_without_spaces
50
+ input.to_s.delete(' ')
51
+ end
52
+
53
+ def allowed_regexp
54
+ /\A\d+\z/
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,97 @@
1
+ module Booth
2
+ module Syntaxes
3
+ class Email
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ param :input
8
+
9
+ def call
10
+ debug { "Checking email #{input.inspect} for valid syntax..." }
11
+ check_blank.on_success { check_too_short }
12
+ .on_success { check_too_long }
13
+ .on_success { check_characters }
14
+ .on_success { check_ampersand }
15
+ end
16
+
17
+ private
18
+
19
+ def check_blank
20
+ return Tron.success :email_present if input.present?
21
+
22
+ debug { 'This email is blank.' }
23
+ Tron.failure :blank_email,
24
+ normalized_email: nil,
25
+ normalized_invalid_email: normalized_email,
26
+ public_message: I18n.t('booth.blank_email')
27
+ end
28
+
29
+ def check_too_short
30
+ return Tron.success :email_not_too_short if input.to_s.length >= min_length
31
+
32
+ debug { "This email is less than #{min_length} characters long." }
33
+ Tron.failure :email_is_too_short,
34
+ normalized_email: nil,
35
+ normalized_invalid_email: normalized_email,
36
+ public_message: I18n.t('booth.email_too_short', minimum: min_length)
37
+ end
38
+
39
+ def check_too_long
40
+ return Tron.success :email_not_too_long if input.to_s.length <= max_length
41
+
42
+ debug { "This email is more than #{max_length} characters long." }
43
+ Tron.failure :email_is_too_long,
44
+ normalized_email: nil,
45
+ normalized_invalid_email: normalized_email,
46
+ public_message: I18n.t('booth.email_too_long', maximum: max_length)
47
+ end
48
+
49
+ def check_characters
50
+ return Tron.success :all_characters_valid if input.to_s.length == normalized_email.length
51
+
52
+ debug { 'This email contains invalid characters' }
53
+ Tron.failure :invalid_email_format,
54
+ normalized_email: nil,
55
+ normalized_invalid_email: normalized_email,
56
+ public_message: I18n.t('booth.invalid_email_characters')
57
+ end
58
+
59
+ # See https://github.com/heartcombo/devise/blob/8593801130f2df94a50863b5db535c272b00efe1/lib/devise.rb#L112-L116
60
+ def check_ampersand
61
+ if input.to_s.match(/\A[^@]+@[^@]+\z/)
62
+ debug { 'This email has the correct syntax' }
63
+ return Tron.success :valid_email_syntax,
64
+ normalized_email:,
65
+ normalized_invalid_email: normalized_email
66
+ end
67
+
68
+ debug { 'This email does not contain an ampersand' }
69
+ Tron.failure :email_ampersand_invalid,
70
+ normalized_email: nil,
71
+ normalized_invalid_email: normalized_email,
72
+ public_message: I18n.t('booth.invalid_email_format')
73
+ end
74
+
75
+ # Limit size to prevent cookie overflows.
76
+ def normalized_email
77
+ input.to_s
78
+ .downcase
79
+ .delete("^#{allowed_characters}")
80
+ .to(max_length - 1)
81
+ .presence
82
+ end
83
+
84
+ def allowed_characters
85
+ ::Regexp.escape 'abcdefghijklmnopqrstuvwxyz0123456789@ßöäü_+-,.!?#$%'
86
+ end
87
+
88
+ def min_length
89
+ 3
90
+ end
91
+
92
+ def max_length
93
+ 254
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,37 @@
1
+ module Booth
2
+ module Syntaxes
3
+ class Ip
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ param :input
8
+
9
+ def call
10
+ # debug { "Checking IP address #{input.inspect} for valid syntax..." }
11
+ check_blank.on_success { check_validity }
12
+ end
13
+
14
+ private
15
+
16
+ def check_blank
17
+ return Tron.success :ip_present if input.present?
18
+
19
+ # debug { 'This IP is blank.' }
20
+ Tron.failure :blank_ip, normalized_ip: nil
21
+ end
22
+
23
+ def check_validity
24
+ return Tron.success :ip_valid, normalized_ip: ip_addr.to_s if ip_addr
25
+
26
+ # debug { 'This IP is invalid.' }
27
+ Tron.failure :invalid_ip, normalized_ip: nil
28
+ end
29
+
30
+ def ip_addr
31
+ IPAddr.new(input)
32
+ rescue ArgumentError
33
+ nil
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,57 @@
1
+ module Booth
2
+ module Syntaxes
3
+ class Otp
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ param :input
8
+
9
+ def call
10
+ check_blank.on_success { check_characters }
11
+ .on_success { check_length }
12
+ end
13
+
14
+ private
15
+
16
+ def check_blank
17
+ return Tron.success :otp_present if input.present?
18
+
19
+ debug { "OTP #{input.inspect} is blank." }
20
+ Tron.failure :blank_otp,
21
+ normalized_otp: nil,
22
+ public_message: I18n.t('booth.blank_otp')
23
+ end
24
+
25
+ def check_characters
26
+ return Tron.success :otp_consists_of_digits if input_without_spaces.match?(allowed_regexp)
27
+
28
+ debug { "OTP #{input.inspect} contains invalid characters" }
29
+ Tron.failure :invalid_otp_format,
30
+ normalized_otp: nil,
31
+ public_message: I18n.t('booth.invalid_otp_format')
32
+ end
33
+
34
+ def check_length
35
+ if input_without_spaces.to_s.length == ::Booth.config.otp_digits
36
+ return Tron.success :valid_otp_syntax,
37
+ normalized_otp: input_without_spaces
38
+ end
39
+
40
+ debug { "OTP #{input.inspect} is not #{::Booth.config.otp_digits} characters long." }
41
+ Tron.failure :wrong_otp_length,
42
+ normalized_otp: nil,
43
+ public_message: I18n.t('booth.wrong_otp_length', digits: ::Booth.config.otp_digits)
44
+ end
45
+
46
+ # Helpers
47
+
48
+ def input_without_spaces
49
+ input.to_s.delete(' ')
50
+ end
51
+
52
+ def allowed_regexp
53
+ /\A\d+\z/
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,21 @@
1
+ module Booth
2
+ module Syntaxes
3
+ class Scope
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ param :input
8
+
9
+ def call
10
+ return Tron.success(:valid_scope_syntax, normalized_scope: input.to_sym) if regexp.match(input.to_s)
11
+
12
+ raise ::Booth::Errors::InvalidScopeSyntax, input
13
+ end
14
+
15
+ # Same convention as a Ruby variable name.
16
+ def regexp
17
+ /\A[a-z]{1}[a-z0-9_]+[a-z0-9]{1}\z/
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ module Booth
2
+ module Syntaxes
3
+ class ScopeComparison
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ option :this, as: :raw_this
8
+ option :that, as: :raw_that
9
+
10
+ def call
11
+ return Tron.success(:identical_scopes) if this == that
12
+
13
+ debug { "The requested scope #{this.inspect} does not match what's on record #{that.inspect}" }
14
+ Tron.failure :mismatching_scopes, this:, that:
15
+ end
16
+
17
+ private
18
+
19
+ def this
20
+ ::Booth::Syntaxes::Scope.call(raw_this).normalized_scope
21
+ end
22
+
23
+ def that
24
+ ::Booth::Syntaxes::Scope.call(raw_that).normalized_scope
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,64 @@
1
+ module Booth
2
+ module Syntaxes
3
+ class SecretKey
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ param :input
8
+
9
+ def call
10
+ debug { "Checking secret key #{input.inspect} for valid syntax..." }
11
+ check_missing.on_success { check_blank }
12
+ .on_success { check_length }
13
+ .on_success { check_characters }
14
+ end
15
+
16
+ private
17
+
18
+ def check_missing
19
+ return Tron.success :secret_key_non_nil unless input.nil?
20
+
21
+ debug { 'This secret key is nil.' }
22
+ Tron.failure :missing_secret_key,
23
+ normalized_secret_key: nil,
24
+ public_message: I18n.t('booth.missing_secret_key')
25
+ end
26
+
27
+ def check_blank
28
+ return Tron.success :secret_key_present if input.present?
29
+
30
+ debug { 'This secret key is blank.' }
31
+ Tron.failure :blank_secret_key,
32
+ normalized_secret_key: nil,
33
+ public_message: I18n.t('booth.blank_secret_key')
34
+ end
35
+
36
+ def check_length
37
+ return Tron.success :secret_key_has_correct_length if input.to_s.length == 30
38
+
39
+ debug { 'This secret key is not 30 characters long.' }
40
+ Tron.failure :wrong_secret_key_length,
41
+ normalized_secret_key: nil,
42
+ public_message: I18n.t('booth.wrong_secret_key_length')
43
+ end
44
+
45
+ def check_characters
46
+ if input.to_s.match?(allowed_regexp)
47
+ return Tron.success :valid_secret_key_syntax,
48
+ normalized_secret_key: input.to_s
49
+ end
50
+
51
+ debug { 'This secret key contains invalid characters' }
52
+ Tron.failure :invalid_secret_key_format,
53
+ normalized_secret_key: nil,
54
+ public_message: I18n.t('booth.invalid_secret_key_format')
55
+ end
56
+
57
+ # Helpers
58
+
59
+ def allowed_regexp
60
+ /\A[1-9a-zA-Z]{30}\z/
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,85 @@
1
+ module Booth
2
+ module Syntaxes
3
+ class Username
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ param :input
8
+
9
+ def call
10
+ # debug { "Checking username #{input.inspect} for valid syntax..." }
11
+ check_blank.on_success { check_too_short }
12
+ .on_success { check_too_long }
13
+ .on_success { check_characters }
14
+ end
15
+
16
+ private
17
+
18
+ def check_blank
19
+ return Tron.success :username_present if input.present?
20
+
21
+ debug { 'This username is blank.' }
22
+ Tron.failure :blank_username,
23
+ normalized_username: nil,
24
+ normalized_invalid_username: normalized_username,
25
+ public_message: I18n.t('booth.blank_username')
26
+ end
27
+
28
+ def check_too_short
29
+ return Tron.success :username_not_too_short if input.to_s.length >= min_length
30
+
31
+ debug { "This username is less than #{min_length} characters long." }
32
+ Tron.failure :username_is_too_short,
33
+ normalized_username: nil,
34
+ normalized_invalid_username: normalized_username,
35
+ public_message: I18n.t('booth.username_too_short', minimum: min_length)
36
+ end
37
+
38
+ def check_too_long
39
+ return Tron.success :username_not_too_long if input.to_s.length <= max_length
40
+
41
+ debug { "This username is more than #{max_length} characters long." }
42
+ Tron.failure :username_is_too_long,
43
+ normalized_username: nil,
44
+ normalized_invalid_username: normalized_username,
45
+ public_message: I18n.t('booth.username_too_long', maximum: max_length)
46
+ end
47
+
48
+ def check_characters
49
+ if input.to_s.length == normalized_username.length
50
+ debug { 'This username has the correct syntax' }
51
+ return Tron.success :valid_username_syntax,
52
+ normalized_username:,
53
+ normalized_invalid_username: normalized_username
54
+ end
55
+
56
+ debug { 'This username contains invalid characters' }
57
+ Tron.failure :invalid_username_format,
58
+ normalized_username: nil,
59
+ normalized_invalid_username: normalized_username,
60
+ public_message: I18n.t('booth.invalid_username_format')
61
+ end
62
+
63
+ # Limit size to prevent cookie overflows.
64
+ def normalized_username
65
+ input.to_s
66
+ .downcase
67
+ .delete("^#{allowed_characters}")
68
+ .to(max_length - 1)
69
+ .presence
70
+ end
71
+
72
+ def allowed_characters
73
+ ::Regexp.escape 'abcdefghijklmnopqrstuvwxyz0123456789@ßöäü_+-,.!?#$%'
74
+ end
75
+
76
+ def min_length
77
+ 3
78
+ end
79
+
80
+ def max_length
81
+ 50
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,23 @@
1
+ module Booth
2
+ module Syntaxes
3
+ class Uuid
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ param :input
8
+ option :raise_if_invalid, default: -> { true }
9
+
10
+ def call
11
+ return Tron.success(:valid_uuid, uuid: input) if regexp.match(input.to_s)
12
+ raise ArgumentError, "Invalid UUID: #{input.inspect}" if raise_if_invalid
13
+
14
+ Tron.failure :invalid_uuid, uuid: nil
15
+ end
16
+
17
+ # For practical reasons we only accept downcased (i.e. normalized) UUIDs.
18
+ def regexp
19
+ /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ require 'active_support/testing/time_helpers'
2
+
3
+ require_relative 'support/assert_all_partials_were_covered'
4
+ require_relative 'support/assert_logged_in'
5
+ require_relative 'support/assert_logged_out'
6
+ require_relative 'support/assert_partial'
7
+ require_relative 'support/get_session_value'
8
+ require_relative 'support/otp_code_from_session'
9
+ require_relative 'support/soft_reset_session'
10
+ require_relative 'webauthn/disable'
11
+ require_relative 'webauthn/enable'
12
+ require_relative 'webauthn/virtual_authenticators/create'
13
+ require_relative 'webauthn/virtual_authenticators/destroy'
14
+
15
+ module Booth
16
+ module Test
17
+ module Helpers
18
+ extend ActiveSupport::Concern
19
+
20
+ included do
21
+ include ActiveSupport::Testing::TimeHelpers
22
+ end
23
+
24
+ def assert_logged_out
25
+ ::Booth::Test::Support::AssertLoggedOut.call(page:, scope:)
26
+ end
27
+
28
+ def assert_logged_in(credential:)
29
+ ::Booth::Test::Support::AssertLoggedIn.call(page:, scope:, credential:)
30
+ end
31
+
32
+ def assert_userland_partial(controller:, step:)
33
+ ::Booth::Test::Support::AssertPartial.call(page:, namespace: :userland, controller:, step:)
34
+ end
35
+
36
+ def extract_otp_secret_key_and_generate_code
37
+ secret_key = page.body.split('?secret=').last.split('&').first
38
+ raise 'Expected an OTP secret but found none' if secret_key.blank?
39
+
40
+ code = ::Booth::Models::Credential.new(otp_secret_key: secret_key).otp_code
41
+ Booth.config.logger&.debug(to_s) { "Extracted OTP secret #{secret_key} and derived the code #{code}" }
42
+ code
43
+ end
44
+
45
+ def soft_reset_session
46
+ ::Booth::Test::Support::SoftResetSession.call(page:)
47
+ end
48
+
49
+ def setup_virtual_authenticator_environment
50
+ # For some reason, the Chrome Virtual Authenticator Environment often leaks from one test to the next.
51
+ # All kinds of errors in Webauth only cause one single generic `NotAllowedError` for privacy reasons
52
+ # So it's impossible to debug the actual cause. I just found out after many hours that disabling first, works.
53
+ ::Booth::Test::Webauthn::Disable.call devtools: page.driver.browser.devtools
54
+ ::Booth::Test::Webauthn::Enable.call devtools: page.driver.browser.devtools
55
+ end
56
+
57
+ def create_virtual_authenticator(has_user_verification: true)
58
+ setup_virtual_authenticator_environment
59
+ ::Booth::Test::Webauthn::VirtualAuthenticators::Create.call(page:, has_user_verification:)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,63 @@
1
+ module Booth
2
+ module Test
3
+ module Support
4
+ class AssertAllPartialsWereCovered
5
+ include ::Booth::MethodObject
6
+
7
+ def call
8
+ return if missing_partials.empty?
9
+
10
+ raise "Expected these partials to be covered: #{missing_partials}"
11
+ end
12
+
13
+ private
14
+
15
+ def missing_partials
16
+ (expected_partials - covered_partials)
17
+ end
18
+
19
+ def expected_partials # rubocop:disable Metrics/MethodLength
20
+ %w[
21
+ userland/login/enter_otp
22
+ userland/login/enter_password
23
+ userland/login/enter_username
24
+ userland/login/enter_webauth
25
+ userland/login/needs_onboarding
26
+ userland/login/no_authenticators
27
+ userland/login/remote_session_available
28
+ userland/onboarding/already_logged_in
29
+ userland/onboarding/choose_password
30
+ userland/onboarding/choose_webauth_nickname
31
+ userland/onboarding/completed
32
+ userland/onboarding/confirm_otp
33
+ userland/onboarding/confirm_password
34
+ userland/onboarding/confirm_webauth
35
+ userland/onboarding/register_otp
36
+ userland/onboarding/register_webauth
37
+ userland/onboarding/timed_out
38
+ userland/otp/add
39
+ userland/otp/confirm
40
+ userland/otp/register
41
+ userland/otp/show
42
+ userland/otp/successfully_changed
43
+ userland/otp/sudo
44
+ userland/password_reset/check_your_mail
45
+ userland/password_reset/choose_password
46
+ userland/password_reset/completed
47
+ userland/password_reset/confirm_password
48
+ userland/password_reset/logout_first
49
+ userland/password_reset/new
50
+ userland/password_reset/revoked
51
+ userland/password_reset/throttled
52
+ userland/password_reset/timed_out
53
+ userland/password_reset/wrong_user_logged_in
54
+ ]
55
+ end
56
+
57
+ def covered_partials
58
+ Booth::Test::Support::AssertPartial.asserted_partials.to_a
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,49 @@
1
+ module Booth
2
+ module Test
3
+ module Support
4
+ class AssertLoggedIn
5
+ class AssertionFailedError < StandardError; end
6
+
7
+ include ::Booth::MethodObject
8
+ include ::Booth::Logging
9
+
10
+ option :page
11
+ option :scope
12
+ option :credential
13
+
14
+ def call
15
+ tries ||= 0
16
+ ::Capybara::Lockstep.synchronize
17
+
18
+ active_sessions.each do |session|
19
+ browser_session_id = ::Booth::Test::Support::GetSessionValue.call(page:, key:)
20
+ return true if browser_session_id == session.id.to_s
21
+ end
22
+
23
+ raise AssertionFailedError, "Expected Credential `#{credential.id}` to be logged in with a session of: #{active_sessions.map(&:id)}"
24
+
25
+ # With the Webauth pingpong it sometimes takes a little longer.
26
+ # And Capybara Lockstep doesn't seem to be able to detect that.
27
+ rescue AssertionFailedError
28
+ if (tries += 1) < 3
29
+ debug { 'Trying again...' }
30
+ sleep 1
31
+ retry
32
+ end
33
+
34
+ raise
35
+ end
36
+
37
+ private
38
+
39
+ def active_sessions
40
+ credential.sessions.active_scope.sorted_scope
41
+ end
42
+
43
+ def key
44
+ "warden.user.#{scope}.key"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,30 @@
1
+ module Booth
2
+ module Test
3
+ module Support
4
+ class AssertLoggedOut
5
+ include ::Booth::MethodObject
6
+
7
+ option :page
8
+ option :scope
9
+
10
+ def call
11
+ ::Capybara::Lockstep.synchronize
12
+
13
+ return unless logged_in?
14
+
15
+ raise 'Expected nobody to logged in, but somebody is logged in.'
16
+ end
17
+
18
+ private
19
+
20
+ def logged_in?
21
+ ::Booth::Test::Support::GetSessionValue.call(page:, key:)
22
+ end
23
+
24
+ def key
25
+ "warden.user.#{scope}.key"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end