masks-server 0.11.0 → 0.13.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.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/masks/server/scim_endpoint.rb +2 -2
  3. data/app/controllers/masks/server/account_controller.rb +3 -0
  4. data/app/controllers/masks/server/account_exports_controller.rb +2 -2
  5. data/app/controllers/masks/server/addresses_controller.rb +2 -3
  6. data/app/controllers/masks/server/app_passwords_controller.rb +41 -0
  7. data/app/controllers/masks/server/authenticator_apps_controller.rb +2 -3
  8. data/app/controllers/masks/server/authorize_controller.rb +33 -2
  9. data/app/controllers/masks/server/device_authorizations_controller.rb +2 -2
  10. data/app/controllers/masks/server/device_verifications_controller.rb +2 -2
  11. data/app/controllers/masks/server/enrollments_controller.rb +4 -4
  12. data/app/controllers/masks/server/forward_auth_controller.rb +106 -14
  13. data/app/controllers/masks/server/introspections_controller.rb +2 -2
  14. data/app/controllers/masks/server/logins_controller.rb +14 -15
  15. data/app/controllers/masks/server/organization_access_controller.rb +39 -0
  16. data/app/controllers/masks/server/organization_members_controller.rb +2 -2
  17. data/app/controllers/masks/server/passwords_controller.rb +2 -3
  18. data/app/controllers/masks/server/pushed_authorizations_controller.rb +2 -2
  19. data/app/controllers/masks/server/received_signals_controller.rb +2 -2
  20. data/app/controllers/masks/server/registrations_controller.rb +2 -2
  21. data/app/controllers/masks/server/revocations_controller.rb +2 -2
  22. data/app/controllers/masks/server/saml_controller.rb +2 -2
  23. data/app/controllers/masks/server/sign_in_approvals_controller.rb +2 -3
  24. data/app/controllers/masks/server/tokens_controller.rb +2 -2
  25. data/app/controllers/masks/server/verifications_controller.rb +2 -3
  26. data/app/graphql/masks/server/manage/mutations/base_mutation.rb +7 -0
  27. data/app/graphql/masks/server/manage/mutations/create_group.rb +6 -4
  28. data/app/graphql/masks/server/manage/mutations/decline_access.rb +27 -0
  29. data/app/graphql/masks/server/manage/mutations/grant_access.rb +29 -0
  30. data/app/graphql/masks/server/manage/mutations/group_source.rb +34 -0
  31. data/app/graphql/masks/server/manage/mutations/issue_ldap_credential.rb +5 -2
  32. data/app/graphql/masks/server/manage/mutations/refresh_group.rb +21 -0
  33. data/app/graphql/masks/server/manage/mutations/remove_group_member.rb +2 -0
  34. data/app/graphql/masks/server/manage/mutations/run_health_checks.rb +19 -0
  35. data/app/graphql/masks/server/manage/mutations/update_client.rb +6 -0
  36. data/app/graphql/masks/server/manage/mutations/update_group.rb +7 -4
  37. data/app/graphql/masks/server/manage/mutations/update_tenant.rb +14 -1
  38. data/app/graphql/masks/server/manage/types/access_request_type.rb +21 -0
  39. data/app/graphql/masks/server/manage/types/actor_type.rb +6 -0
  40. data/app/graphql/masks/server/manage/types/client_type.rb +12 -0
  41. data/app/graphql/masks/server/manage/types/group_condition_input.rb +16 -0
  42. data/app/graphql/masks/server/manage/types/group_condition_type.rb +29 -0
  43. data/app/graphql/masks/server/manage/types/group_member_type.rb +16 -0
  44. data/app/graphql/masks/server/manage/types/group_type.rb +17 -1
  45. data/app/graphql/masks/server/manage/types/health_check_type.rb +18 -0
  46. data/app/graphql/masks/server/manage/types/ldap_credential_type.rb +1 -0
  47. data/app/graphql/masks/server/manage/types/mutation_type.rb +4 -0
  48. data/app/graphql/masks/server/manage/types/query_type.rb +24 -0
  49. data/app/graphql/masks/server/manage/types/release_type.rb +16 -0
  50. data/app/graphql/masks/server/manage/types/tenant_type.rb +6 -1
  51. data/app/helpers/masks/server/account_helper.rb +4 -0
  52. data/app/jobs/masks/server/cleanup_job.rb +1 -0
  53. data/app/jobs/masks/server/health_check_job.rb +21 -0
  54. data/app/jobs/masks/server/refresh_groups_job.rb +18 -0
  55. data/app/jobs/masks/server/release_check_job.rb +21 -0
  56. data/app/ldap/masks/server/ldap/connection.rb +3 -2
  57. data/app/ldap/masks/server/ldap/gate.rb +44 -19
  58. data/app/logins/masks/server/login_states/second_factor.rb +1 -1
  59. data/app/mailers/masks/server/actor_mailer.rb +31 -0
  60. data/app/models/masks/server/access_request.rb +103 -0
  61. data/app/models/masks/server/actor.rb +6 -0
  62. data/app/models/masks/server/app_password.rb +91 -0
  63. data/app/models/masks/server/app_passwords.rb +76 -0
  64. data/app/models/masks/server/client.rb +15 -0
  65. data/app/models/masks/server/client_metadata_document.rb +4 -8
  66. data/app/models/masks/server/confirmation_code.rb +2 -2
  67. data/app/models/masks/server/connection.rb +7 -0
  68. data/app/models/masks/server/declared.rb +13 -3
  69. data/app/models/masks/server/email_verification.rb +2 -2
  70. data/app/models/masks/server/event.rb +13 -3
  71. data/app/models/masks/server/federation/protocol.rb +1 -1
  72. data/app/models/masks/server/federation/saml.rb +1 -1
  73. data/app/models/masks/server/forward_auth_rules.rb +162 -0
  74. data/app/models/masks/server/group.rb +207 -4
  75. data/app/models/masks/server/group_member.rb +7 -0
  76. data/app/models/masks/server/group_rule.rb +63 -0
  77. data/app/models/masks/server/health.rb +310 -0
  78. data/app/models/masks/server/health_check.rb +27 -0
  79. data/app/models/masks/server/invitation.rb +2 -2
  80. data/app/models/masks/server/ldap_credential.rb +2 -2
  81. data/app/models/masks/server/login.rb +28 -2
  82. data/app/models/masks/server/mail_previews.rb +5 -0
  83. data/app/models/masks/server/mail_template.rb +2 -0
  84. data/app/models/masks/server/membership.rb +10 -3
  85. data/app/models/masks/server/notifications.rb +3 -1
  86. data/app/models/masks/server/password_reset.rb +2 -2
  87. data/app/models/masks/server/provider.rb +1 -0
  88. data/app/models/masks/server/queue.rb +60 -0
  89. data/app/models/masks/server/release.rb +74 -0
  90. data/app/models/masks/server/saml_identity/farewells.rb +1 -0
  91. data/app/models/masks/server/session.rb +8 -0
  92. data/app/models/masks/server/tenant.rb +87 -6
  93. data/app/models/masks/server/throttle.rb +165 -0
  94. data/app/models/masks/server/token.rb +2 -1
  95. data/app/views/masks/server/account/_app_passwords.html.erb +83 -0
  96. data/app/views/masks/server/account/_organizations.html.erb +33 -0
  97. data/app/views/masks/server/account/index.html.erb +5 -0
  98. data/app/views/masks/server/actor_mailer/access_declined.html.erb +5 -0
  99. data/app/views/masks/server/actor_mailer/access_declined.text.erb +5 -0
  100. data/app/views/masks/server/actor_mailer/access_granted.html.erb +7 -0
  101. data/app/views/masks/server/actor_mailer/access_granted.text.erb +7 -0
  102. data/app/views/masks/server/authorize/error.html.erb +25 -0
  103. data/config/locales/de/access_requests.yml +43 -0
  104. data/config/locales/de/account/app_passwords.yml +42 -0
  105. data/config/locales/de/events.yml +5 -0
  106. data/config/locales/de/flashes.yml +1 -0
  107. data/config/locales/de/group_sources.yml +5 -0
  108. data/config/locales/de/manage/access_requests.yml +12 -0
  109. data/config/locales/de/manage/app_passwords.yml +16 -0
  110. data/config/locales/de/manage/forward_auth_rules.yml +29 -0
  111. data/config/locales/de/manage/group_sources.yml +44 -0
  112. data/config/locales/de/manage/health.yml +85 -0
  113. data/config/locales/de/manage/links.yml +20 -0
  114. data/config/locales/en/access_requests.yml +43 -0
  115. data/config/locales/en/account/app_passwords.yml +42 -0
  116. data/config/locales/en/events.yml +5 -0
  117. data/config/locales/en/flashes.yml +1 -0
  118. data/config/locales/en/group_sources.yml +5 -0
  119. data/config/locales/en/manage/access_requests.yml +12 -0
  120. data/config/locales/en/manage/app_passwords.yml +16 -0
  121. data/config/locales/en/manage/forward_auth_rules.yml +29 -0
  122. data/config/locales/en/manage/group_sources.yml +44 -0
  123. data/config/locales/en/manage/health.yml +85 -0
  124. data/config/locales/en/manage/links.yml +20 -0
  125. data/config/locales/es/access_requests.yml +43 -0
  126. data/config/locales/es/account/app_passwords.yml +42 -0
  127. data/config/locales/es/events.yml +5 -0
  128. data/config/locales/es/flashes.yml +1 -0
  129. data/config/locales/es/group_sources.yml +5 -0
  130. data/config/locales/es/manage/access_requests.yml +12 -0
  131. data/config/locales/es/manage/app_passwords.yml +16 -0
  132. data/config/locales/es/manage/forward_auth_rules.yml +29 -0
  133. data/config/locales/es/manage/group_sources.yml +44 -0
  134. data/config/locales/es/manage/health.yml +85 -0
  135. data/config/locales/es/manage/links.yml +20 -0
  136. data/config/locales/fr/access_requests.yml +43 -0
  137. data/config/locales/fr/account/app_passwords.yml +42 -0
  138. data/config/locales/fr/events.yml +5 -0
  139. data/config/locales/fr/flashes.yml +1 -0
  140. data/config/locales/fr/group_sources.yml +5 -0
  141. data/config/locales/fr/manage/access_requests.yml +12 -0
  142. data/config/locales/fr/manage/app_passwords.yml +16 -0
  143. data/config/locales/fr/manage/forward_auth_rules.yml +29 -0
  144. data/config/locales/fr/manage/group_sources.yml +44 -0
  145. data/config/locales/fr/manage/health.yml +85 -0
  146. data/config/locales/fr/manage/links.yml +20 -0
  147. data/config/routes.rb +5 -0
  148. data/db/migrate/20261020100000_create_access_requests.rb +28 -0
  149. data/db/migrate/20261020110000_add_app_passwords_to_clients.rb +5 -0
  150. data/db/migrate/20261020120000_add_forward_auth_rules_to_clients.rb +6 -0
  151. data/db/migrate/20261020130000_add_sources_to_groups.rb +15 -0
  152. data/db/migrate/20261020140000_create_health_checks.rb +25 -0
  153. data/db/migrate/20261020150000_add_link_lifetimes_to_tenants.rb +8 -0
  154. data/db/migrate/20261021100000_add_pinned_origin_to_tenants.rb +5 -0
  155. data/db/migrate/20261031100000_default_key_rotation_to_ninety_days.rb +5 -0
  156. data/lib/generators/masks/server/install/install_generator.rb +22 -2
  157. data/lib/masks/server/configuration.rb +16 -19
  158. data/lib/masks/server/engine.rb +2 -1
  159. data/lib/masks/server/forwarding.rb +26 -3
  160. data/lib/masks/server/schema_reference.rb +12 -0
  161. data/lib/masks/server/tenancy/middleware.rb +21 -13
  162. data/lib/masks/server/tenant_isolation.rb +12 -0
  163. data/public/masks-assets/.vite/manifest.json +13 -13
  164. data/public/masks-assets/assets/{account-D46qDQCo.js → account-h7I6VUPR.js} +2 -2
  165. data/public/masks-assets/assets/{account-D46qDQCo.js.map → account-h7I6VUPR.js.map} +1 -1
  166. data/public/masks-assets/assets/{app-D7W0gAyT.css → app-BXOScMjL.css} +1 -1
  167. data/public/masks-assets/assets/{handshake-DIFZPSAK.js → handshake-YQpRNMIs.js} +2 -2
  168. data/public/masks-assets/assets/{handshake-DIFZPSAK.js.map → handshake-YQpRNMIs.js.map} +1 -1
  169. data/public/masks-assets/assets/initial-Cn-UuAsy.js +3 -0
  170. data/public/masks-assets/assets/{initial-C1GpCiEh.js.map → initial-Cn-UuAsy.js.map} +1 -1
  171. data/public/masks-assets/assets/{login-C_6igRZ3.js → login-C21_jb6L.js} +3 -3
  172. data/public/masks-assets/assets/{login-C_6igRZ3.js.map → login-C21_jb6L.js.map} +1 -1
  173. data/public/masks-assets/assets/{manage-D4MzaDvM.js → manage-DpVxe5sv.js} +80 -51
  174. data/public/masks-assets/assets/manage-DpVxe5sv.js.map +1 -0
  175. data/public/masks-assets/assets/passkey-5UYPVD3c.js +2 -0
  176. data/public/masks-assets/assets/{passkey-DIj_5LCY.js.map → passkey-5UYPVD3c.js.map} +1 -1
  177. data/version.txt +1 -1
  178. metadata +89 -14
  179. data/public/masks-assets/assets/initial-C1GpCiEh.js +0 -3
  180. data/public/masks-assets/assets/manage-D4MzaDvM.js.map +0 -1
  181. data/public/masks-assets/assets/passkey-DIj_5LCY.js +0 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 608910b5ec386933423f04a3596c4b5d454b38e956449ca66d762d2cd79e3e82
4
- data.tar.gz: f70b574939800fdcb3407025402e39663e15f8a9c71e2dff473685147e7cf693
3
+ metadata.gz: c268c36afc7aa404d7cb904ef65cb1f67db51d6b84bdb3a5f83d5aae7d84bf48
4
+ data.tar.gz: '09285b0606e241c323c82f16555328830421c7d97338d9aa32f38055e6972353'
5
5
  SHA512:
6
- metadata.gz: a061d6d81ac514d825d3963c466e99272d224e0250e964e8a057a8e794a7cd1f26955cd8e1cb5753eb418f8c91e5dd2d9b31b98c8bba4eb97d11d7112ce0a40b
7
- data.tar.gz: c537d6afb5fc139d5e83fd92143f0eb28588f9ec1992e607209f89c5c9f59dfff3502dc0f1196a39c2a19fcec67910458494324a70e739ced310ea73f69168cb
6
+ metadata.gz: a253ac4a9e52258b001f1a7424a189728979a5363255d1d9c4d306e70385ea2eecdd8f93e2859603f6959e43786f90be43921d5ed7bc523fa81477f06384f507
7
+ data.tar.gz: 844b3eb9927070dfddbea67dbbb17992772e84348c35ffdc9feba33fd1b079f718e9d986b8f9da591f77498a54a8404c7240a6dd5ad22d320a04500ec57641f6
@@ -8,9 +8,9 @@ module Masks
8
8
 
9
9
  skip_forgery_protection
10
10
 
11
- rate_limit to: 600, within: 1.minute,
11
+ rate_limit **Throttle.limits(:scim),
12
12
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
13
- with: -> { scim_error(Scim::Error.new(:too_many_requests, "too many provisioning requests from this address")) }
13
+ with: Throttle.refused(:scim) { scim_error(Scim::Error.new(:too_many_requests, "too many provisioning requests from this address")) }
14
14
 
15
15
  rescue_from Scim::Error, with: :scim_error
16
16
  before_action :require_provisioner
@@ -9,6 +9,9 @@ module Masks
9
9
  return redirect_to login_path if @actor.nil?
10
10
 
11
11
  @apps = Apps.held_by(@actor)
12
+ @app_passwords = AppPassword.live.where(actor: @actor).includes(:client).newest_first.to_a
13
+ @app_password_clients = AppPassword.offered(@actor)
14
+ @app_password = flash[:app_password]
12
15
  @launchable = Apps.launchable(@actor)
13
16
  @invitations = @actor.memberships.outstanding.joins(:organization).merge(Organization.active)
14
17
  .includes(:organization, :invited_by).order("organizations.name").to_a
@@ -1,9 +1,9 @@
1
1
  module Masks
2
2
  module Server
3
3
  class AccountExportsController < ApplicationController
4
- rate_limit to: 5, within: 1.hour, only: :create,
4
+ rate_limit **Throttle.limits(:export), only: :create,
5
5
  by: -> { [ current_tenant.id, current_actor&.id || request.remote_ip ].join(":") },
6
- with: -> { redirect_to root_path(anchor: "export"), alert: t("account_export.throttled") }
6
+ with: Throttle.refused(:export) { redirect_to root_path(anchor: "export"), alert: t("account_export.throttled") }
7
7
 
8
8
  def create
9
9
  actor = current_actor
@@ -1,10 +1,9 @@
1
1
  module Masks
2
2
  module Server
3
3
  class AddressesController < ApplicationController
4
- rate_limit to: ::Rails.configuration.masks.account_attempt_limit,
5
- within: 3.minutes,
4
+ rate_limit **Throttle.limits(:account_attempt),
6
5
  by: -> { [ current_tenant.id, current_actor&.id ].join(":") },
7
- with: -> { refuse(t("addresses.too_many_attempts")) },
6
+ with: Throttle.refused(:account_attempt) { refuse(t("addresses.too_many_attempts")) },
8
7
  only: :update
9
8
 
10
9
  before_action :require_actor
@@ -0,0 +1,41 @@
1
+ module Masks
2
+ module Server
3
+ class AppPasswordsController < ApplicationController
4
+ ANCHOR = "app-passwords".freeze
5
+
6
+ before_action :require_actor
7
+
8
+ def create
9
+ return unless reauthenticated!(ANCHOR, t("app_passwords.again"))
10
+
11
+ client = Client.active.find_by(client_id: params[:client_id].to_s)
12
+ held = AppPassword.issue!(actor: current_actor, client: client, name: params[:name])
13
+
14
+ Event.record!(Event::APP_PASSWORD_CREATED, actor: current_actor, client: client, name: held.name)
15
+
16
+ flash[:app_password] = { "name" => held.name, "app" => client.name, "secret" => held.secret }
17
+ redirect_to root_path(anchor: ANCHOR)
18
+ rescue AppPassword::Refused => e
19
+ redirect_to root_path(anchor: ANCHOR), alert: t("app_passwords.refused.#{e.message}", most: AppPassword::MOST)
20
+ end
21
+
22
+ def destroy
23
+ held = AppPassword.live.find_by(id: params[:id].to_s[/\A\d+\z/], actor: current_actor)
24
+
25
+ return redirect_to root_path(anchor: ANCHOR), alert: t("app_passwords.unknown") if held.nil?
26
+
27
+ held.revoke!
28
+
29
+ Event.record!(Event::APP_PASSWORD_REVOKED, actor: current_actor, client: held.client, name: held.name)
30
+
31
+ redirect_to root_path(anchor: ANCHOR), notice: t("app_passwords.revoked", name: held.name)
32
+ end
33
+
34
+ private
35
+
36
+ def require_actor
37
+ redirect_to login_path unless current_actor
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,10 +1,9 @@
1
1
  module Masks
2
2
  module Server
3
3
  class AuthenticatorAppsController < ApplicationController
4
- rate_limit to: ::Rails.configuration.masks.account_attempt_limit,
5
- within: 3.minutes,
4
+ rate_limit **Throttle.limits(:account_attempt),
6
5
  by: -> { [ current_tenant.id, current_actor&.id ].join(":") },
7
- with: -> { refuse(t("authenticator_apps.too_many_attempts")) },
6
+ with: Throttle.refused(:account_attempt) { refuse(t("authenticator_apps.too_many_attempts")) },
8
7
  only: :create
9
8
 
10
9
  before_action :require_actor
@@ -9,6 +9,10 @@ module Masks
9
9
  refuse(bad_request(:invalid_request_object, refusal.message))
10
10
  end
11
11
 
12
+ rate_limit **Throttle.limits(:access_request), only: :ask,
13
+ by: -> { [ current_tenant.id, current_actor&.id, refused_request(params[:rid])&.client_id ].join(":") },
14
+ with: Throttle.refused(:access_request) { redirect_to authorize_refused_path(rid: params[:rid]), alert: t("authorize.error.slow_down") }
15
+
12
16
  def show
13
17
  authorization = Authorization.from_request(request)
14
18
 
@@ -45,7 +49,7 @@ module Masks
45
49
 
46
50
  login = advance(pending)
47
51
 
48
- return redirect_to(refuse_on_masks(pending, login.refusal)) if login.refused? && !answerable?(pending)
52
+ return redirect_to(refuse_on_masks(pending, login.refusal)) if login.refused? && (!answerable?(pending) || askable?(pending, login))
49
53
  return answer(deny(pending, login.refusal.error, login.refusal.description)) if login.refused?
50
54
  return interaction_required(pending) if login.prompted? && pending.silent?
51
55
  return redirect_to(login.redirect_to, allow_other_host: true) if login.redirect_to.present?
@@ -55,14 +59,31 @@ module Masks
55
59
  end
56
60
 
57
61
  def refused
58
- refusal = refused_request(params[:rid])&.refusal
62
+ pending = refused_request(params[:rid])
63
+ refusal = pending&.refusal
59
64
 
60
65
  return show_error("invalid_request", "that sign-in request has expired", :bad_request) if refusal.nil?
61
66
 
62
67
  @refused = true
68
+ @asking = asking_for(pending)
69
+ @waiting = @asking && AccessRequest.open.find_by(actor: current_actor, client: @asking)
70
+ @return_to = answerable?(pending) && pending.authorization.redirect_with(
71
+ issuer: issuer, error: refusal["error"], error_description: refusal["description"]
72
+ )
63
73
  show_error(refusal["error"], refusal["description"], :forbidden)
64
74
  end
65
75
 
76
+ def ask
77
+ pending = refused_request(params[:rid])
78
+ asking = pending && asking_for(pending)
79
+
80
+ return show_error("invalid_request", "that sign-in request has expired", :bad_request) if asking.nil?
81
+
82
+ AccessRequest.ask!(actor: current_actor, client: asking)
83
+
84
+ redirect_to authorize_refused_path(rid: params[:rid]), notice: t("authorize.error.asked")
85
+ end
86
+
66
87
  private
67
88
 
68
89
  def validate(authorization)
@@ -149,6 +170,16 @@ module Masks
149
170
  end
150
171
  end
151
172
 
173
+ def askable?(pending, login)
174
+ !pending.silent? && login.actor.present? && asking_for(pending, login.actor).present?
175
+ end
176
+
177
+ def asking_for(pending, actor = current_actor)
178
+ client = pending.client
179
+
180
+ client if actor && client && client.allowed_groups.any? && !client.admits?(actor)
181
+ end
182
+
152
183
  def admitted?(authorization)
153
184
  tracked(REQUESTS).key?(authorization.fingerprint)
154
185
  end
@@ -4,9 +4,9 @@ module Masks
4
4
  include RackOAuth2Endpoint
5
5
  include TokenPresented
6
6
 
7
- rate_limit to: 60, within: 1.minute,
7
+ rate_limit **Throttle.limits(:device_authorization),
8
8
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
9
- with: -> { slow_down }
9
+ with: Throttle.refused(:device_authorization) { slow_down }
10
10
 
11
11
  def create
12
12
  client = authenticate_client!
@@ -6,10 +6,10 @@ module Masks
6
6
 
7
7
  before_action :establish_device
8
8
 
9
- rate_limit to: ::Rails.configuration.masks.device_code_limit, within: 3.minutes,
9
+ rate_limit **Throttle.limits(:device_code),
10
10
  if: -> { params[:user_code].present? },
11
11
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
12
- with: -> { asking(UNKNOWN, status: :too_many_requests) }
12
+ with: Throttle.refused(:device_code) { asking(UNKNOWN, status: :too_many_requests) }
13
13
 
14
14
  def show
15
15
  return asking(params[:refused].presence) if params[:user_code].blank?
@@ -3,18 +3,18 @@ module Masks
3
3
  class EnrollmentsController < ApplicationController
4
4
  include TokenPresented
5
5
 
6
- rate_limit to: 30, within: 1.minute,
6
+ rate_limit **Throttle.limits(:enrollment),
7
7
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
8
- with: -> {
8
+ with: Throttle.refused(:enrollment) {
9
9
  render json: {
10
10
  "error" => "too_many_requests",
11
11
  "error_description" => "too many enrollment tokens asked for from this address"
12
12
  }, status: :too_many_requests
13
13
  }
14
14
 
15
- rate_limit to: 60, within: 1.hour, only: :create, name: "app",
15
+ rate_limit **Throttle.limits(:app_enrollment), only: :create, name: "app",
16
16
  by: -> { [ current_tenant.id, claimed_client_id ].join(":") },
17
- with: -> {
17
+ with: Throttle.refused(:app_enrollment) {
18
18
  render json: {
19
19
  "error" => "too_many_requests",
20
20
  "error_description" => "too many enrollment tokens asked for by this app"
@@ -4,9 +4,9 @@ module Masks
4
4
  skip_forgery_protection
5
5
  before_action -> { request.session_options[:skip] = true }, only: %i[check callback]
6
6
 
7
- rate_limit to: 30, within: 1.minute, only: %i[start callback],
7
+ rate_limit **Throttle.limits(:forward_auth), only: %i[start callback],
8
8
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
9
- with: -> { head :too_many_requests }
9
+ with: Throttle.refused(:forward_auth) { head :too_many_requests }
10
10
 
11
11
  def check
12
12
  target = Forwarded.target(request)
@@ -15,13 +15,21 @@ module Masks
15
15
 
16
16
  client = Client.forwarding_to(target)
17
17
  return head(:forbidden) if client.nil?
18
+ return basic(client, target) if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request)
18
19
 
19
20
  held = ForwardSession.held(cookies[ForwardSession::COOKIE], client)
20
- return admit(held.actor) if held
21
-
22
- return head(:unauthorized) if params[:status] == "401"
23
-
24
- redirect_to forward_auth_start_url(rd: target.to_s), allow_other_host: true
21
+ decided = client.forward_rules.decide(path: target.path, method: forwarded_method, address: request.remote_ip,
22
+ actor: held&.actor)
23
+
24
+ case decided.outcome
25
+ when ForwardAuthRules::OPEN then admit(held&.actor)
26
+ when ForwardAuthRules::DENY then head(:forbidden)
27
+ when ForwardAuthRules::ONE_FACTOR then held ? admit(held.actor) : ask_to_sign_in(target)
28
+ when ForwardAuthRules::TWO_FACTOR then held&.session&.multi_factored? ? admit(held.actor) : ask_to_sign_in(target)
29
+ else
30
+ complete = held if held&.session&.complete?
31
+ decided.outcome == ForwardAuthRules::TOKEN ? admit_presented(client, complete) : answer(complete, target)
32
+ end
25
33
  end
26
34
 
27
35
  def start
@@ -29,8 +37,20 @@ module Masks
29
37
  client = Client.forwarding_to(target)
30
38
 
31
39
  return render(plain: t("forward_auth.unknown"), status: :not_found) if client.nil?
32
- return redirect_to(login_path(return_to: request.fullpath)) if current_actor.nil?
33
- return render(plain: t("forward_auth.not_admitted", app: client.name), status: :forbidden) unless client.admits?(current_actor)
40
+
41
+ decided = client.forward_rules.decide(path: target.path, method: "GET", address: request.remote_ip, actor: current_actor)
42
+
43
+ return render(plain: t("forward_auth.denied", app: client.name), status: :forbidden) if decided.outcome == ForwardAuthRules::DENY
44
+ return send_to_login(decided) if current_actor.nil?
45
+
46
+ unless client.admits?(current_actor)
47
+ return redirect_to(target.to_s, allow_other_host: true) if decided.outcome == ForwardAuthRules::OPEN
48
+
49
+ return render(plain: t("forward_auth.not_admitted", app: client.name), status: :forbidden)
50
+ end
51
+
52
+ return step_up if decided.outcome == ForwardAuthRules::TWO_FACTOR && !current_session.multi_factored?
53
+ return step_up if [ ForwardAuthRules::ONE_FACTOR, ForwardAuthRules::OPEN ].exclude?(decided.outcome) && !current_session.complete?
34
54
 
35
55
  code = ForwardCode.mint!(actor: current_actor, client: client, session: current_session,
36
56
  payload: { "return_to" => target.to_s })
@@ -67,15 +87,87 @@ module Masks
67
87
  redirect_to code.return_to, allow_other_host: true
68
88
  end
69
89
 
70
- def admit(actor)
71
- response.headers["Remote-User"] = actor.nickname.presence || actor.identifier
72
- response.headers["Remote-Email"] = actor.email if actor.email_verified_at.present?
73
- response.headers["Remote-Name"] = actor.name if actor.name.present?
74
- response.headers["Remote-Groups"] = actor.group_labels.join(",")
90
+ def basic(client, target)
91
+ identifier, secret = ActionController::HttpAuthentication::Basic.user_name_and_password(request)
92
+ outcome = AppPasswords.authenticate(client: client, identifier: identifier, secret: secret, via: "forward_auth")
93
+
94
+ return head(:too_many_requests) if outcome.throttled?
95
+ return head(:unauthorized) if outcome.actor.nil?
96
+
97
+ decided = client.forward_rules.decide(path: target.path, method: forwarded_method, address: request.remote_ip,
98
+ actor: outcome.actor)
99
+ return head(:forbidden) if decided.outcome == ForwardAuthRules::DENY
100
+
101
+ admit(outcome.actor)
102
+ end
103
+
104
+ def answer(held, target)
105
+ held ? admit(held.actor) : ask_to_sign_in(target)
106
+ end
107
+
108
+ def ask_to_sign_in(target)
109
+ return head(:unauthorized) if params[:status] == "401"
110
+
111
+ redirect_to forward_auth_start_url(rd: target.to_s), allow_other_host: true
112
+ end
113
+
114
+ def send_to_login(decided)
115
+ Login.new(store: login_store).relax! if decided.outcome == ForwardAuthRules::ONE_FACTOR
116
+
117
+ redirect_to login_path(return_to: request.fullpath)
118
+ end
119
+
120
+ def step_up
121
+ Login.new(store: login_store).step_up!
122
+
123
+ redirect_to login_path(return_to: request.fullpath)
124
+ end
125
+
126
+ def admit_presented(client, held)
127
+ return admit(held.actor) if held
128
+
129
+ token = presented_token(client)
130
+ return admit(token.actor, machine: token.client) if token
131
+
132
+ response.headers["WWW-Authenticate"] = %(Bearer realm="#{client.forward_origin}")
133
+ head :unauthorized
134
+ end
135
+
136
+ def presented_token(client)
137
+ scheme, secret = request.authorization.to_s.split(" ", 2)
138
+ return nil unless scheme.to_s.casecmp?("Bearer") && secret.present?
139
+
140
+ claims = AccessToken.decode(secret.strip, issuer: issuer)
141
+ token = AccessToken.live.includes(:actor, :client).find_by(digest: claims["jti"])
142
+
143
+ return nil if token.nil? || token.bound? || token.client.nil?
144
+ return nil unless token.client_id == client.id || Array(token.audience).include?(client.forward_origin)
145
+ return nil if token.actor && (token.actor.suspended? || !client.admits?(token.actor))
146
+
147
+ token
148
+ rescue JWT::DecodeError
149
+ nil
150
+ end
151
+
152
+ def admit(actor, machine: nil)
153
+ if actor
154
+ response.headers["Remote-User"] = actor.nickname.presence || actor.identifier
155
+ response.headers["Remote-Email"] = actor.email if actor.email_verified_at.present?
156
+ response.headers["Remote-Name"] = actor.name if actor.name.present?
157
+ response.headers["Remote-Groups"] = actor.group_labels.join(",")
158
+ elsif machine
159
+ response.headers["Remote-User"] = machine.client_id
160
+ response.headers["Remote-Name"] = machine.name
161
+ response.headers["Remote-Groups"] = ""
162
+ end
75
163
 
76
164
  head :ok
77
165
  end
78
166
 
167
+ def forwarded_method
168
+ request.headers["X-Forwarded-Method"].presence || request.headers["X-Original-Method"].presence || "GET"
169
+ end
170
+
79
171
  def return_to
80
172
  raw = request.query_string[/(?:\A|&)rd=(.*)\z/m, 1].to_s
81
173
 
@@ -5,9 +5,9 @@ module Masks
5
5
 
6
6
  INACTIVE = { "active" => false }.freeze
7
7
 
8
- rate_limit to: 120, within: 1.minute,
8
+ rate_limit **Throttle.limits(:introspection),
9
9
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
10
- with: -> { render json: INACTIVE, status: :too_many_requests }
10
+ with: Throttle.refused(:introspection) { render json: INACTIVE, status: :too_many_requests }
11
11
 
12
12
  def create
13
13
  client = authenticate_client!
@@ -8,26 +8,22 @@ module Masks
8
8
 
9
9
  skip_forgery_protection
10
10
 
11
- rate_limit to: ::Rails.configuration.masks.attempt_limit,
12
- within: 3.minutes, only: :update, if: -> { verifying? },
11
+ rate_limit **Throttle.limits(:attempt), only: :update, if: -> { verifying? },
13
12
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
14
- with: -> { too_many("too-many-attempts") }
13
+ with: Throttle.refused(:attempt) { too_many("too-many-attempts") }
15
14
 
16
- rate_limit to: ::Rails.configuration.masks.account_attempt_limit,
17
- within: 3.minutes, only: :update, name: "identifier", if: -> { verifying? },
15
+ rate_limit **Throttle.limits(:account_attempt), only: :update, name: "identifier", if: -> { verifying? },
18
16
  by: -> { [ current_tenant.id, attempted_account ].join(":") },
19
- with: -> { too_many("too-many-attempts-for-account") }
17
+ with: Throttle.refused(:account_attempt) { too_many("too-many-attempts-for-account") }
20
18
 
21
- rate_limit to: ::Rails.configuration.masks.recovery_limit,
22
- within: 15.minutes, only: :update, name: "recovery",
19
+ rate_limit **Throttle.limits(:recovery), only: :update, name: "recovery",
23
20
  if: -> { Login.limit_for(params[:event]) == :sending },
24
21
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
25
- with: -> { too_many("too-many-attempts") }
22
+ with: Throttle.refused(:recovery) { too_many("too-many-attempts") }
26
23
 
27
- rate_limit to: ::Rails.configuration.masks.attempt_limit,
28
- within: 3.minutes, only: %i[provider posted_provider], name: "provider",
24
+ rate_limit **Throttle.limits(:attempt), only: %i[provider posted_provider], name: "provider",
29
25
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
30
- with: -> { too_many("too-many-attempts") }
26
+ with: Throttle.refused(:attempt) { too_many("too-many-attempts") }
31
27
 
32
28
  before_action :refuse_blocked_agent
33
29
  before_action :verify_authenticity_token, except: :posted_provider
@@ -35,9 +31,9 @@ module Masks
35
31
 
36
32
  def show
37
33
  hold_return_to
38
- return redirect_to after_login_path if current_actor && pending.nil?
39
-
40
34
  @login = run
35
+ return redirect_to after_login_path if current_actor && pending.nil? && !@login.stepping_up?
36
+
41
37
  @login.carry!(flash[:warnings]) if flash[:warnings].present?
42
38
 
43
39
  redirect_to after_login_path if @login.settled?
@@ -108,6 +104,8 @@ module Masks
108
104
  end
109
105
 
110
106
  def take_posted(handle)
107
+ return {} unless Replay.first?("provider-callback", handle, expires_in: POSTED_WINDOW)
108
+
111
109
  key = posted_key(handle)
112
110
  held = ::Rails.cache.read(key)
113
111
  ::Rails.cache.delete(key)
@@ -153,8 +151,9 @@ module Masks
153
151
  if login.touched?(:first_factor)
154
152
  sign_in(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by,
155
153
  authenticated_at: login.authenticated_at)
154
+ current_session.update!(second_factor_skipped: true) if login.skipped_second_factor?
156
155
  elsif login.signed_in? && login.touched?(:second_factor)
157
- current_session.update!(amr: Array(current_session.amr) | login.amr)
156
+ current_session.update!(amr: Array(current_session.amr) | login.amr, second_factor_skipped: false)
158
157
  end
159
158
 
160
159
  forget_login
@@ -0,0 +1,39 @@
1
+ module Masks
2
+ module Server
3
+ class OrganizationAccessController < OrganizationMembersController
4
+ def create
5
+ own!
6
+
7
+ asked = asked!
8
+ role = params[:role].to_s
9
+
10
+ asked.grant!(label: "#{@organization.key}:#{role}", by: current_actor, journey: Journey.account(current_actor))
11
+
12
+ back(notice: t("organization_access.granted", identifier: asked.actor.identifier, app: asked.client.name,
13
+ role: role, organization: @organization.name))
14
+ rescue AccessRequest::Refused => e
15
+ back(alert: e.message)
16
+ end
17
+
18
+ def destroy
19
+ own!
20
+
21
+ asked = asked!
22
+ asked.decline!(by: current_actor, journey: Journey.account(current_actor))
23
+
24
+ back(notice: t("organization_access.declined", identifier: asked.actor.identifier, app: asked.client.name))
25
+ rescue AccessRequest::Refused => e
26
+ back(alert: e.message)
27
+ end
28
+
29
+ private
30
+
31
+ def asked!
32
+ id = params[:id].to_s[/\A\d+\z/]
33
+
34
+ (id && AccessRequest.for_organization(@organization).includes(:actor, :client).find_by(id: id)) ||
35
+ raise(AccessRequest::Refused, t("organization_access.unknown", organization: @organization.name))
36
+ end
37
+ end
38
+ end
39
+ end
@@ -7,9 +7,9 @@ module Masks
7
7
  back(alert: t("organization_members.owners_only", organization: @organization.name))
8
8
  end
9
9
 
10
- rate_limit to: 30, within: 1.minute, only: %i[create resend],
10
+ rate_limit **Throttle.limits(:invitation), only: %i[create resend],
11
11
  by: -> { [ current_tenant.id, current_actor&.id || request.remote_ip ].join(":") },
12
- with: -> { back(alert: t("organization_members.slow_down")) }
12
+ with: Throttle.refused(:invitation) { back(alert: t("organization_members.slow_down")) }
13
13
 
14
14
  before_action :require_actor
15
15
  before_action :require_membership
@@ -1,10 +1,9 @@
1
1
  module Masks
2
2
  module Server
3
3
  class PasswordsController < ApplicationController
4
- rate_limit to: ::Rails.configuration.masks.account_attempt_limit,
5
- within: 3.minutes,
4
+ rate_limit **Throttle.limits(:account_attempt),
6
5
  by: -> { [ current_tenant.id, current_actor&.id ].join(":") },
7
- with: -> { refuse(t("passwords.too_many_attempts")) }
6
+ with: Throttle.refused(:account_attempt) { refuse(t("passwords.too_many_attempts")) }
8
7
 
9
8
  before_action :require_actor
10
9
 
@@ -3,9 +3,9 @@ module Masks
3
3
  class PushedAuthorizationsController < ApplicationController
4
4
  include TokenPresented
5
5
 
6
- rate_limit to: 60, within: 1.minute,
6
+ rate_limit **Throttle.limits(:pushed_authorization),
7
7
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
8
- with: -> { slow_down }
8
+ with: Throttle.refused(:pushed_authorization) { slow_down }
9
9
 
10
10
  def create
11
11
  client = authenticate_client!
@@ -3,9 +3,9 @@ module Masks
3
3
  class ReceivedSignalsController < ApplicationController
4
4
  skip_forgery_protection
5
5
 
6
- rate_limit to: 600, within: 1.minute, only: :create,
6
+ rate_limit **Throttle.limits(:signal), only: :create,
7
7
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
8
- with: -> { render json: { "err" => "invalid_request", "description" => "too many events from this address" }, status: :too_many_requests }
8
+ with: Throttle.refused(:signal) { render json: { "err" => "invalid_request", "description" => "too many events from this address" }, status: :too_many_requests }
9
9
 
10
10
  def create
11
11
  ReceivedSignal.new(request.raw_post, issuer: issuer).receive!
@@ -3,9 +3,9 @@ module Masks
3
3
  class RegistrationsController < ApplicationController
4
4
  skip_forgery_protection
5
5
 
6
- rate_limit to: ::Rails.configuration.masks.registration_limit, within: 10.minutes, only: :create,
6
+ rate_limit **Throttle.limits(:registration), only: :create,
7
7
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
8
- with: -> {
8
+ with: Throttle.refused(:registration) {
9
9
  render json: {
10
10
  "error" => "too_many_requests",
11
11
  "error_description" => "too many registrations from this address"
@@ -3,9 +3,9 @@ module Masks
3
3
  class RevocationsController < ApplicationController
4
4
  include TokenPresented
5
5
 
6
- rate_limit to: 120, within: 1.minute,
6
+ rate_limit **Throttle.limits(:revocation),
7
7
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
8
- with: -> {
8
+ with: Throttle.refused(:revocation) {
9
9
  render json: {
10
10
  "error" => "slow_down",
11
11
  "error_description" => "too many revocation requests from this address"
@@ -5,9 +5,9 @@ module Masks
5
5
 
6
6
  skip_forgery_protection only: %i[sso slo]
7
7
 
8
- rate_limit to: 60, within: 1.minute, only: %i[sso initiate slo],
8
+ rate_limit **Throttle.limits(:saml), only: %i[sso initiate slo],
9
9
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
10
- with: -> { refuse("too many sign-in requests from this address", status: :too_many_requests) }
10
+ with: Throttle.refused(:saml) { refuse("too many sign-in requests from this address", status: :too_many_requests) }
11
11
 
12
12
  rescue_from SamlIdentity::Refused do |refusal|
13
13
  refuse(refusal.message)
@@ -5,11 +5,10 @@ module Masks
5
5
 
6
6
  before_action :require_trusted_device
7
7
 
8
- rate_limit to: ::Rails.configuration.masks.account_attempt_limit,
9
- within: 3.minutes,
8
+ rate_limit **Throttle.limits(:account_attempt),
10
9
  only: :create,
11
10
  by: -> { [ current_tenant.id, current_actor&.id ].join(":") },
12
- with: -> { refuse(t("sign_in_approvals.too_many_attempts")) }
11
+ with: Throttle.refused(:account_attempt) { refuse(t("sign_in_approvals.too_many_attempts")) }
13
12
 
14
13
  def create
15
14
  approval = SignInApproval.matching(actor: current_actor, code: params[:code], from: current_device)
@@ -9,9 +9,9 @@ module Masks
9
9
 
10
10
  skip_forgery_protection
11
11
 
12
- rate_limit to: 60, within: 1.minute,
12
+ rate_limit **Throttle.limits(:token),
13
13
  by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
14
- with: -> { slow_down }
14
+ with: Throttle.refused(:token) { slow_down }
15
15
 
16
16
  def create
17
17
  return unsupported_grant_type unless Client::GRANT_TYPES.include?(params[:grant_type].to_s)