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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/masks/server/scim_endpoint.rb +2 -2
- data/app/controllers/masks/server/account_exports_controller.rb +2 -2
- data/app/controllers/masks/server/addresses_controller.rb +2 -3
- data/app/controllers/masks/server/authenticator_apps_controller.rb +2 -3
- data/app/controllers/masks/server/authorize_controller.rb +2 -2
- data/app/controllers/masks/server/device_authorizations_controller.rb +2 -2
- data/app/controllers/masks/server/device_verifications_controller.rb +2 -2
- data/app/controllers/masks/server/enrollments_controller.rb +4 -4
- data/app/controllers/masks/server/forward_auth_controller.rb +2 -2
- data/app/controllers/masks/server/introspections_controller.rb +2 -2
- data/app/controllers/masks/server/logins_controller.rb +10 -12
- data/app/controllers/masks/server/organization_members_controller.rb +2 -2
- data/app/controllers/masks/server/passwords_controller.rb +2 -3
- data/app/controllers/masks/server/pushed_authorizations_controller.rb +2 -2
- data/app/controllers/masks/server/received_signals_controller.rb +2 -2
- data/app/controllers/masks/server/registrations_controller.rb +2 -2
- data/app/controllers/masks/server/revocations_controller.rb +2 -2
- data/app/controllers/masks/server/saml_controller.rb +2 -2
- data/app/controllers/masks/server/sign_in_approvals_controller.rb +2 -3
- data/app/controllers/masks/server/tokens_controller.rb +2 -2
- data/app/controllers/masks/server/verifications_controller.rb +2 -3
- data/app/graphql/masks/server/manage/types/health_check_type.rb +1 -1
- data/app/ldap/masks/server/ldap/gate.rb +14 -16
- data/app/models/masks/server/app_passwords.rb +7 -13
- data/app/models/masks/server/client_metadata_document.rb +4 -8
- data/app/models/masks/server/confirmation_code.rb +2 -2
- data/app/models/masks/server/declared.rb +3 -1
- data/app/models/masks/server/event.rb +2 -2
- data/app/models/masks/server/health.rb +70 -3
- data/app/models/masks/server/saml_identity/farewells.rb +1 -0
- data/app/models/masks/server/tenant.rb +48 -4
- data/app/models/masks/server/throttle.rb +165 -0
- data/config/locales/de/manage/health.yml +9 -2
- data/config/locales/en/manage/health.yml +9 -2
- data/config/locales/es/manage/health.yml +9 -2
- data/config/locales/fr/manage/health.yml +9 -2
- data/db/migrate/20261031100000_default_key_rotation_to_ninety_days.rb +5 -0
- data/lib/generators/masks/server/install/install_generator.rb +19 -0
- data/lib/masks/server/configuration.rb +14 -8
- data/lib/masks/server/engine.rb +2 -1
- data/lib/masks/server/forwarding.rb +26 -3
- data/lib/masks/server/tenancy/middleware.rb +4 -7
- data/lib/masks/server/tenant_isolation.rb +12 -0
- data/version.txt +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c268c36afc7aa404d7cb904ef65cb1f67db51d6b84bdb3a5f83d5aae7d84bf48
|
|
4
|
+
data.tar.gz: '09285b0606e241c323c82f16555328830421c7d97338d9aa32f38055e6972353'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
11
|
+
rate_limit **Throttle.limits(:scim),
|
|
12
12
|
by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
|
|
13
|
-
with:
|
|
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
|
|
4
|
+
rate_limit **Throttle.limits(:export), only: :create,
|
|
5
5
|
by: -> { [ current_tenant.id, current_actor&.id || request.remote_ip ].join(":") },
|
|
6
|
-
with:
|
|
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
|
|
5
|
-
within: 3.minutes,
|
|
4
|
+
rate_limit **Throttle.limits(:account_attempt),
|
|
6
5
|
by: -> { [ current_tenant.id, current_actor&.id ].join(":") },
|
|
7
|
-
with:
|
|
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
|
|
5
|
-
within: 3.minutes,
|
|
4
|
+
rate_limit **Throttle.limits(:account_attempt),
|
|
6
5
|
by: -> { [ current_tenant.id, current_actor&.id ].join(":") },
|
|
7
|
-
with:
|
|
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
|
|
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:
|
|
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
|
|
7
|
+
rate_limit **Throttle.limits(:device_authorization),
|
|
8
8
|
by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
|
|
9
|
-
with:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
7
|
+
rate_limit **Throttle.limits(:forward_auth), only: %i[start callback],
|
|
8
8
|
by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
|
|
9
|
-
with:
|
|
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
|
|
8
|
+
rate_limit **Throttle.limits(:introspection),
|
|
9
9
|
by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
|
|
10
|
-
with:
|
|
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
|
|
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:
|
|
13
|
+
with: Throttle.refused(:attempt) { too_many("too-many-attempts") }
|
|
15
14
|
|
|
16
|
-
rate_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:
|
|
17
|
+
with: Throttle.refused(:account_attempt) { too_many("too-many-attempts-for-account") }
|
|
20
18
|
|
|
21
|
-
rate_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:
|
|
22
|
+
with: Throttle.refused(:recovery) { too_many("too-many-attempts") }
|
|
26
23
|
|
|
27
|
-
rate_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:
|
|
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
|
|
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:
|
|
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
|
|
5
|
-
within: 3.minutes,
|
|
4
|
+
rate_limit **Throttle.limits(:account_attempt),
|
|
6
5
|
by: -> { [ current_tenant.id, current_actor&.id ].join(":") },
|
|
7
|
-
with:
|
|
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
|
|
6
|
+
rate_limit **Throttle.limits(:pushed_authorization),
|
|
7
7
|
by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
|
|
8
|
-
with:
|
|
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
|
|
6
|
+
rate_limit **Throttle.limits(:signal), only: :create,
|
|
7
7
|
by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
|
|
8
|
-
with:
|
|
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
|
|
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
|
|
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
|
|
8
|
+
rate_limit **Throttle.limits(:saml), only: %i[sso initiate slo],
|
|
9
9
|
by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
|
|
10
|
-
with:
|
|
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
|
|
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:
|
|
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
|
|
12
|
+
rate_limit **Throttle.limits(:token),
|
|
13
13
|
by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
|
|
14
|
-
with:
|
|
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
|
|
5
|
-
within: 15.minutes,
|
|
4
|
+
rate_limit **Throttle.limits(:recovery),
|
|
6
5
|
by: -> { [ current_tenant.id, current_actor&.id ].join(":") },
|
|
7
|
-
with:
|
|
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
|
|
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
|
|
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
|
|
97
|
-
|
|
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!(
|
|
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!(
|
|
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
|
-
|
|
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
|
|
180
|
+
"ldap:#{@ip_address}"
|
|
180
181
|
end
|
|
181
182
|
|
|
182
183
|
def account_key(tenant, uid)
|
|
183
|
-
"ldap
|
|
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!(
|
|
191
|
-
|
|
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
|
|
27
|
-
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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 { |
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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 =
|
|
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 >=
|
|
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
|
-
|
|
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,
|
|
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
|