masks-server 0.12.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 (46) 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_exports_controller.rb +2 -2
  4. data/app/controllers/masks/server/addresses_controller.rb +2 -3
  5. data/app/controllers/masks/server/authenticator_apps_controller.rb +2 -3
  6. data/app/controllers/masks/server/authorize_controller.rb +2 -2
  7. data/app/controllers/masks/server/device_authorizations_controller.rb +2 -2
  8. data/app/controllers/masks/server/device_verifications_controller.rb +2 -2
  9. data/app/controllers/masks/server/enrollments_controller.rb +4 -4
  10. data/app/controllers/masks/server/forward_auth_controller.rb +2 -2
  11. data/app/controllers/masks/server/introspections_controller.rb +2 -2
  12. data/app/controllers/masks/server/logins_controller.rb +10 -12
  13. data/app/controllers/masks/server/organization_members_controller.rb +2 -2
  14. data/app/controllers/masks/server/passwords_controller.rb +2 -3
  15. data/app/controllers/masks/server/pushed_authorizations_controller.rb +2 -2
  16. data/app/controllers/masks/server/received_signals_controller.rb +2 -2
  17. data/app/controllers/masks/server/registrations_controller.rb +2 -2
  18. data/app/controllers/masks/server/revocations_controller.rb +2 -2
  19. data/app/controllers/masks/server/saml_controller.rb +2 -2
  20. data/app/controllers/masks/server/sign_in_approvals_controller.rb +2 -3
  21. data/app/controllers/masks/server/tokens_controller.rb +2 -2
  22. data/app/controllers/masks/server/verifications_controller.rb +2 -3
  23. data/app/graphql/masks/server/manage/types/health_check_type.rb +1 -1
  24. data/app/ldap/masks/server/ldap/gate.rb +14 -16
  25. data/app/models/masks/server/app_passwords.rb +7 -13
  26. data/app/models/masks/server/client_metadata_document.rb +4 -8
  27. data/app/models/masks/server/confirmation_code.rb +2 -2
  28. data/app/models/masks/server/declared.rb +3 -1
  29. data/app/models/masks/server/event.rb +2 -2
  30. data/app/models/masks/server/health.rb +70 -3
  31. data/app/models/masks/server/saml_identity/farewells.rb +1 -0
  32. data/app/models/masks/server/tenant.rb +48 -4
  33. data/app/models/masks/server/throttle.rb +165 -0
  34. data/config/locales/de/manage/health.yml +9 -2
  35. data/config/locales/en/manage/health.yml +9 -2
  36. data/config/locales/es/manage/health.yml +9 -2
  37. data/config/locales/fr/manage/health.yml +9 -2
  38. data/db/migrate/20261031100000_default_key_rotation_to_ninety_days.rb +5 -0
  39. data/lib/generators/masks/server/install/install_generator.rb +19 -0
  40. data/lib/masks/server/configuration.rb +14 -8
  41. data/lib/masks/server/engine.rb +2 -1
  42. data/lib/masks/server/forwarding.rb +26 -3
  43. data/lib/masks/server/tenancy/middleware.rb +4 -7
  44. data/lib/masks/server/tenant_isolation.rb +12 -0
  45. data/version.txt +1 -1
  46. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74cc1568b822c061657b6625a809deb750f5503e66fa8d0f390c7a596004953e
4
- data.tar.gz: 8bdb0e298ee2c901c71f654215fd0b958d36cd6e69befa231313dd2f98a5d249
3
+ metadata.gz: c268c36afc7aa404d7cb904ef65cb1f67db51d6b84bdb3a5f83d5aae7d84bf48
4
+ data.tar.gz: '09285b0606e241c323c82f16555328830421c7d97338d9aa32f38055e6972353'
5
5
  SHA512:
6
- metadata.gz: e0aa16a41f23221d2274bf66aac45ba8a1c20c901526cfd79905d14a229b3614921dba96c90a4ad296f69a210c8aee6d6aa11453d063d8a07cd9b1b724f13feb
7
- data.tar.gz: 4c36c8378c7d459c4c4280227d36ec6f91234d3c2db47f857b8165312af340958789373497de7aa244f8c13ceb09a33415e2de40af99eed39331d1c3161b0838
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
@@ -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
@@ -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,9 +9,9 @@ module Masks
9
9
  refuse(bad_request(:invalid_request_object, refusal.message))
10
10
  end
11
11
 
12
- rate_limit to: 1, within: 1.hour, only: :ask,
12
+ rate_limit **Throttle.limits(:access_request), only: :ask,
13
13
  by: -> { [ current_tenant.id, current_actor&.id, refused_request(params[:rid])&.client_id ].join(":") },
14
- with: -> { redirect_to authorize_refused_path(rid: params[:rid]), alert: t("authorize.error.slow_down") }
14
+ with: Throttle.refused(:access_request) { redirect_to authorize_refused_path(rid: params[:rid]), alert: t("authorize.error.slow_down") }
15
15
 
16
16
  def show
17
17
  authorization = Authorization.from_request(request)
@@ -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)
@@ -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
@@ -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)
@@ -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)
@@ -1,10 +1,9 @@
1
1
  module Masks
2
2
  module Server
3
3
  class VerificationsController < ApplicationController
4
- rate_limit to: ::Rails.configuration.masks.recovery_limit,
5
- within: 15.minutes,
4
+ rate_limit **Throttle.limits(:recovery),
6
5
  by: -> { [ current_tenant.id, current_actor&.id ].join(":") },
7
- with: -> { redirect_to root_path, alert: t("verifications.too_many_requests") }
6
+ with: Throttle.refused(:recovery) { redirect_to root_path, alert: t("verifications.too_many_requests") }
8
7
 
9
8
  before_action :require_actor
10
9
 
@@ -5,7 +5,7 @@ module Masks
5
5
  class HealthCheckType < BaseObject
6
6
  description "The latest result of one health check. Checks run every 10 minutes."
7
7
 
8
- field :name, ID, null: false, description: "jobs, worker, queue, ldap, metrics, mail, origin, domains, or keys."
8
+ field :name, ID, null: false, description: "jobs, worker, queue, ldap, metrics, mail, origin, domains, keys, or proxy."
9
9
  field :status, String, null: false, description: "ok, warning, or failing."
10
10
  field :summary, String, null: false, description: "A short word for what the check found, such as none, stale, or silent."
11
11
  field :details, GraphQL::Types::JSON, null: false, description: "The figures and names behind the summary."
@@ -2,7 +2,6 @@ module Masks
2
2
  module Server
3
3
  module Ldap
4
4
  class Gate
5
- WINDOW = 3.minutes
6
5
  VIA = "ldap".freeze
7
6
 
8
7
  Bound = Data.define(:kind, :tenant, :dn, :subject, :client_id) do
@@ -30,7 +29,7 @@ module Masks
30
29
  password = password.to_s
31
30
  return anonymous if name.blank? && password.empty?
32
31
  return refused(Protocol::UNWILLING_TO_PERFORM, "unauthenticated binds are refused") if password.empty?
33
- return throttled if failures(ip_key) >= ::Rails.configuration.masks.attempt_limit
32
+ return throttled(:attempt) if Throttle.exceeded?(:attempt, ip_key)
34
33
 
35
34
  dn = parse(name)
36
35
  tenant = dn && Directory.tenant_for(dn)
@@ -93,8 +92,8 @@ module Masks
93
92
  def person(tenant, directory, uid, password)
94
93
  account = account_key(tenant, uid)
95
94
 
96
- if failures(account) >= ::Rails.configuration.masks.account_attempt_limit
97
- Event.record!(Event::LOGIN_THROTTLED, actor: Actor.locate(uid), by: nil, identifier: uid, via: VIA)
95
+ if Throttle.exceeded?(:account_attempt, account)
96
+ Throttle.hit!(:account_attempt, actor: Actor.locate(uid), address: @ip_address, identifier: uid, via: VIA)
98
97
  return throttled
99
98
  end
100
99
 
@@ -102,7 +101,7 @@ module Masks
102
101
  return app_password(tenant, directory, uid, password, account) if actor.nil? && app_password?(password)
103
102
 
104
103
  if actor.nil?
105
- failed!(ip_key, account)
104
+ failed!(account)
106
105
  return refuse(uid, "password")
107
106
  end
108
107
 
@@ -125,7 +124,7 @@ module Masks
125
124
  return throttled if outcome.throttled?
126
125
  return bound(tenant, directory, outcome.actor, factor: AppPasswords::FACTOR) if outcome.actor
127
126
 
128
- failed!(ip_key, account) if outcome.wrong?
127
+ failed!(account) if outcome.wrong?
129
128
  invalid
130
129
  end
131
130
 
@@ -159,7 +158,7 @@ module Masks
159
158
 
160
159
  def decoy(password)
161
160
  BCrypt::Password.new(Actor.decoy_digest) == password
162
- failed!(ip_key)
161
+ Throttle.count!(:attempt, ip_key)
163
162
  invalid
164
163
  end
165
164
 
@@ -167,7 +166,9 @@ module Masks
167
166
  refused(Protocol::INVALID_CREDENTIALS, "invalid credentials")
168
167
  end
169
168
 
170
- def throttled
169
+ def throttled(limit = nil)
170
+ Throttle.hit!(limit, address: @ip_address) if limit
171
+
171
172
  refused(Protocol::UNWILLING_TO_PERFORM, "too many attempts; wait a few minutes")
172
173
  end
173
174
 
@@ -176,19 +177,16 @@ module Masks
176
177
  end
177
178
 
178
179
  def ip_key
179
- "ldap-attempts:ip:#{@ip_address}"
180
+ "ldap:#{@ip_address}"
180
181
  end
181
182
 
182
183
  def account_key(tenant, uid)
183
- "ldap-attempts:#{tenant.id}:#{Digest::SHA256.hexdigest(uid.to_s.strip.downcase)}"
184
- end
185
-
186
- def failures(key)
187
- ::Rails.cache.read(key).to_i
184
+ "ldap:#{tenant.id}:#{Digest::SHA256.hexdigest(uid.to_s.strip.downcase)}"
188
185
  end
189
186
 
190
- def failed!(*keys)
191
- keys.each { |key| ::Rails.cache.increment(key, 1, expires_in: WINDOW) }
187
+ def failed!(account)
188
+ Throttle.count!(:attempt, ip_key)
189
+ Throttle.count!(:account_attempt, account)
192
190
  end
193
191
  end
194
192
  end
@@ -3,7 +3,6 @@ module Masks
3
3
  module AppPasswords
4
4
  FACTOR = "app_password".freeze
5
5
  THROTTLED = "throttled".freeze
6
- WINDOW = 3.minutes
7
6
  SCOPE = "masks/server/logins".freeze
8
7
 
9
8
  Outcome = Data.define(:actor, :refusal) do
@@ -21,10 +20,11 @@ module Masks
21
20
  return refused(nil, identifier, "factor-not-offered", via, client) unless client&.app_passwords? && !client.archived?
22
21
 
23
22
  actor = Actor.locate(identifier)
24
- keys = [ account_key(tenant, actor, identifier), address_key(tenant, ip_address) ]
23
+ keys = { account_attempt: [ account_key(tenant, actor, identifier), "identifier" ], attempt: [ address_key(tenant, ip_address), nil ] }
24
+ exceeded = keys.find { |limit, (by, named)| Throttle.exceeded?(limit, by, scope: SCOPE, named: named) }&.first
25
25
 
26
- if throttled?(keys)
27
- Event.record!(Event::LOGIN_THROTTLED, actor: actor, by: nil, client: client, identifier: identifier, via: via)
26
+ if exceeded
27
+ Throttle.hit!(exceeded, actor: actor, client: client, address: ip_address, identifier: identifier, via: via)
28
28
  return Outcome.new(actor: nil, refusal: THROTTLED)
29
29
  end
30
30
 
@@ -60,21 +60,15 @@ module Masks
60
60
  end
61
61
 
62
62
  def account_key(tenant, actor, identifier)
63
- [ "rate-limit", SCOPE, "identifier", tenant.id, actor&.id || identifier.to_s.strip.downcase ].join(":")
63
+ [ tenant.id, actor&.id || identifier.to_s.strip.downcase ].join(":")
64
64
  end
65
65
 
66
66
  def address_key(tenant, ip_address)
67
- [ "rate-limit", SCOPE, tenant.id, ip_address ].join(":")
68
- end
69
-
70
- def throttled?(keys)
71
- limits = [ ::Rails.configuration.masks.account_attempt_limit, ::Rails.configuration.masks.attempt_limit ]
72
-
73
- keys.zip(limits).any? { |key, limit| ::Rails.cache.read(key).to_i >= limit }
67
+ [ tenant.id, ip_address ].join(":")
74
68
  end
75
69
 
76
70
  def failed!(keys)
77
- keys.each { |key| ::Rails.cache.increment(key, 1, expires_in: WINDOW) }
71
+ keys.each { |limit, (by, named)| Throttle.count!(limit, by, scope: SCOPE, named: named) }
78
72
  end
79
73
  end
80
74
  end
@@ -12,7 +12,6 @@ module Masks
12
12
  LONGEST = 24.hours
13
13
  REFUSAL_LIFETIME = 5.minutes
14
14
  NAME_LIMIT = 100
15
- WINDOW = 10.minutes
16
15
  TENANT_FACTOR = 10
17
16
  AUTH_METHODS = %w[none private_key_jwt].freeze
18
17
  DEFAULT_AUTH_METHOD = "none".freeze
@@ -62,14 +61,11 @@ module Masks
62
61
  end
63
62
 
64
63
  def crowded?
65
- limit = ::Rails.configuration.masks.registration_limit
66
- key = "masks:client-metadata-documents:#{Current.tenant&.id}:#{Time.current.to_i / WINDOW.to_i}"
67
- here = ::Rails.cache.increment("#{key}:#{Current.ip_address}", 1, expires_in: WINDOW)
68
- anywhere = ::Rails.cache.increment(key, 1, expires_in: WINDOW)
64
+ tenant = Current.tenant&.id
65
+ here = Throttle.crowded?(:registration, "documents:#{tenant}:#{Current.ip_address}")
66
+ anywhere = Throttle.crowded?(:registration, "documents:#{tenant}", times: TENANT_FACTOR)
69
67
 
70
- (here.to_i > limit || anywhere.to_i > limit * TENANT_FACTOR).tap do |crowded|
71
- ::Rails.logger.info("masks: too many new client metadata documents") if crowded
72
- end
68
+ (here || anywhere).tap { |crowded| Throttle.hit!(:registration, document: true) if crowded }
73
69
  end
74
70
 
75
71
  def refusal_key(client_id)
@@ -31,7 +31,7 @@ module Masks
31
31
 
32
32
  def inbox_crowded?(address:, ip: nil)
33
33
  recent = inbox.where(created_at: WINDOW.ago..)
34
- limit = ::Rails.configuration.masks.recovery_limit
34
+ limit = Throttle.limit(:recovery)
35
35
 
36
36
  recent.where("payload->>'address' = ?", address).count >= limit ||
37
37
  (ip.present? && recent.where("payload->>'ip' = ?", ip).count >= limit * INBOX_PER_IP)
@@ -40,7 +40,7 @@ module Masks
40
40
  def crowded?(actor:, channel:)
41
41
  where(actor_id: actor.id, created_at: WINDOW.ago..)
42
42
  .where("payload->>'channel' = ?", channel)
43
- .count >= ::Rails.configuration.masks.recovery_limit
43
+ .count >= Throttle.limit(:recovery)
44
44
  end
45
45
 
46
46
  def inbox
@@ -21,7 +21,7 @@ module Masks
21
21
  def initialize(document)
22
22
  raise Invalid, "the declared configuration must be a mapping" unless document.is_a?(Hash)
23
23
 
24
- @document = document
24
+ @document = document.except("limits")
25
25
  end
26
26
 
27
27
  def apply!
@@ -33,6 +33,8 @@ module Masks
33
33
  private
34
34
 
35
35
  def tenants
36
+ return [] if @document.empty?
37
+
36
38
  if @document.key?("tenants")
37
39
  @document.fetch("tenants").map do |subdomain, declared|
38
40
  [ Tenant.active.find_by(subdomain: subdomain) || raise(Invalid, "no tenant called #{subdomain}"), declared || {} ]
@@ -14,7 +14,7 @@ module Masks
14
14
  SESSION_EXPIRED = "session.expired".freeze
15
15
  LOGOUT_UNDELIVERED = "logout.undelivered".freeze
16
16
  LOGIN_REFUSED = "login.refused".freeze
17
- LOGIN_THROTTLED = "login.throttled".freeze
17
+ LIMIT_REACHED = "limit.reached".freeze
18
18
 
19
19
  ACCOUNT_CREATED = "account.created".freeze
20
20
  ACCOUNT_EXPORTED = "account.exported".freeze
@@ -181,7 +181,7 @@ module Masks
181
181
  RELEASE_AVAILABLE = "release.available".freeze
182
182
 
183
183
  GRAVE = [
184
- LOGIN_REFUSED, LOGIN_THROTTLED, LOGOUT_UNDELIVERED, REFRESH_REUSED, STREAM_FAILED,
184
+ LOGIN_REFUSED, LIMIT_REACHED, LOGOUT_UNDELIVERED, REFRESH_REUSED, STREAM_FAILED,
185
185
  DEVICE_BLOCKED, ACTOR_DELETED, AUTHENTICATOR_DISABLED, CONNECTION_REFUSED, DELEGATION_REFUSED,
186
186
  EXCHANGE_REFUSED, SIGN_IN_RISKY, SIGNAL_UNDELIVERED, TEXT_UNDELIVERED, SIGNAL_RECEIVED, MEMBERSHIP_ROLE_KEPT,
187
187
  ORGANIZATION_OWNERLESS, JOB_FAILED, HEALTH_FAILING