masks-server 0.12.0 → 0.14.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/manage_controller.rb +1 -0
- 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/scim/metadata_controller.rb +1 -1
- 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/app_preset.rb +1 -1
- data/app/models/masks/server/client.rb +24 -0
- 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/app/views/masks/server/handshakes/show.html.erb +2 -8
- data/app/views/masks/server/logins/_consent.html.erb +43 -44
- data/app/views/masks/server/logins/_enrol.html.erb +1 -1
- data/app/views/masks/server/logins/_setup_configure.html.erb +2 -6
- data/app/views/masks/server/logins/_signup.html.erb +2 -9
- data/app/views/masks/server/logins/_signup_credentials.html.erb +1 -2
- data/app/views/masks/server/logins/_signup_head.html.erb +1 -11
- data/config/locales/de/account.yml +4 -4
- data/config/locales/de/handshakes.yml +6 -6
- data/config/locales/de/logins.yml +44 -30
- data/config/locales/de/manage/health.yml +9 -2
- data/config/locales/en/account.yml +4 -4
- data/config/locales/en/handshakes.yml +6 -6
- data/config/locales/en/logins.yml +44 -30
- data/config/locales/en/manage/health.yml +9 -2
- data/config/locales/es/account.yml +4 -4
- data/config/locales/es/handshakes.yml +6 -6
- data/config/locales/es/logins.yml +42 -28
- data/config/locales/es/manage/health.yml +9 -2
- data/config/locales/fr/account.yml +4 -4
- data/config/locales/fr/handshakes.yml +6 -6
- data/config/locales/fr/logins.yml +43 -29
- 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/schema_reference.rb +1 -1
- data/lib/masks/server/tenancy/middleware.rb +4 -7
- data/lib/masks/server/tenant_isolation.rb +12 -0
- data/public/masks-assets/.vite/manifest.json +3 -3
- data/public/masks-assets/assets/app-Cfbt-A5V.css +2 -0
- data/public/masks-assets/assets/login-BDggujiZ.js +6 -0
- data/public/masks-assets/assets/login-BDggujiZ.js.map +1 -0
- data/public/masks-assets/assets/{manage-DpVxe5sv.js → manage-VgOqKj7N.js} +2 -2
- data/public/masks-assets/assets/manage-VgOqKj7N.js.map +1 -0
- data/version.txt +1 -1
- metadata +8 -6
- data/public/masks-assets/assets/app-BXOScMjL.css +0 -2
- data/public/masks-assets/assets/login-C21_jb6L.js +0 -4
- data/public/masks-assets/assets/login-C21_jb6L.js.map +0 -1
- data/public/masks-assets/assets/manage-DpVxe5sv.js.map +0 -1
|
@@ -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
|
|
@@ -10,11 +10,19 @@ module Masks
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
SERVER = %w[jobs worker queue ldap metrics].freeze
|
|
13
|
-
TENANT = %w[mail origin domains keys].freeze
|
|
13
|
+
TENANT = %w[mail origin domains keys proxy].freeze
|
|
14
14
|
NAMES = (SERVER + TENANT).freeze
|
|
15
15
|
|
|
16
16
|
FAILING_KEY = "masks:health:failing".freeze
|
|
17
17
|
ORIGIN_PREFIX = "masks:health:origin:".freeze
|
|
18
|
+
PROXY_KEY = "masks:health:proxy".freeze
|
|
19
|
+
PROXY_ADDRESSES = 5
|
|
20
|
+
PROXY_LOG = {
|
|
21
|
+
"untrusted" => "proxy: forwarded headers arrived from %{address}, which MASKS_TRUSTED_PROXIES does not list, so they were ignored",
|
|
22
|
+
"plain" => "proxy: %{address} forwards requests as plain http; have it send X-Forwarded-Proto: https"
|
|
23
|
+
}.freeze
|
|
24
|
+
ONE_ADDRESS_FLOOR = 20
|
|
25
|
+
LOOPBACK = /\A(?:(?:.+\.)?localhost|127(?:\.\d{1,3}){3}|\[::1\])\z/i
|
|
18
26
|
KEEP = 1.day
|
|
19
27
|
NOTE_EVERY = 10.minutes
|
|
20
28
|
SMTP_TIMEOUT = 5
|
|
@@ -37,7 +45,7 @@ module Masks
|
|
|
37
45
|
end
|
|
38
46
|
|
|
39
47
|
def tenant_results
|
|
40
|
-
[ mail_result, origin_result, domains_result, keys_result ]
|
|
48
|
+
[ mail_result, origin_result, domains_result, keys_result, proxy_result ]
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
def store!(results)
|
|
@@ -203,6 +211,65 @@ module Masks
|
|
|
203
211
|
"#{ORIGIN_PREFIX}#{tenant.id}"
|
|
204
212
|
end
|
|
205
213
|
|
|
214
|
+
def note_proxy!(kind, address)
|
|
215
|
+
return if address.blank? || !PROXY_LOG.key?(kind)
|
|
216
|
+
|
|
217
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
218
|
+
key = "proxy:#{kind}:#{address}"
|
|
219
|
+
last = noted[key]
|
|
220
|
+
|
|
221
|
+
return if last && now - last < NOTE_EVERY
|
|
222
|
+
|
|
223
|
+
noted[key] = now
|
|
224
|
+
::Rails.logger.warn(format(PROXY_LOG.fetch(kind), address: address))
|
|
225
|
+
|
|
226
|
+
held = ::Rails.cache.read(PROXY_KEY) || {}
|
|
227
|
+
seen = (held[kind] ||= {})
|
|
228
|
+
seen[address] = Time.current.utc.iso8601 if seen.key?(address) || seen.size < PROXY_ADDRESSES
|
|
229
|
+
::Rails.cache.write(PROXY_KEY, held, expires_in: KEEP)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def forget_proxy!
|
|
233
|
+
noted.keys.grep(/\Aproxy:/).each { |key| noted.delete(key) }
|
|
234
|
+
::Rails.cache.delete(PROXY_KEY)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def proxy_result
|
|
238
|
+
held = ::Rails.cache.read(PROXY_KEY) || {}
|
|
239
|
+
untrusted = held["untrusted"].to_h.keys.sort
|
|
240
|
+
plain = held["plain"].to_h.keys.sort
|
|
241
|
+
|
|
242
|
+
return warning("proxy", "untrusted", addresses: untrusted) if untrusted.any?
|
|
243
|
+
return warning("proxy", "plain", addresses: plain) if plain.any?
|
|
244
|
+
|
|
245
|
+
address, count = sole_private_address
|
|
246
|
+
|
|
247
|
+
return warning("proxy", "one", address: address, count: count) if address
|
|
248
|
+
|
|
249
|
+
ok("proxy", "quiet")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def sole_private_address
|
|
253
|
+
origin = Current.tenant&.public_origin
|
|
254
|
+
|
|
255
|
+
return if origin.nil? || URI.parse(origin).host.to_s.match?(LOOPBACK)
|
|
256
|
+
|
|
257
|
+
recent = Event.where(created_at: 1.hour.ago..).where.not(ip_address: [ nil, "" ])
|
|
258
|
+
addresses = recent.distinct.limit(2).pluck(:ip_address)
|
|
259
|
+
|
|
260
|
+
return unless addresses.size == 1
|
|
261
|
+
|
|
262
|
+
count = recent.count
|
|
263
|
+
|
|
264
|
+
return if count < ONE_ADDRESS_FLOOR
|
|
265
|
+
|
|
266
|
+
address = IPAddr.new(addresses.first)
|
|
267
|
+
|
|
268
|
+
[ addresses.first, count ] if address.private? || address.loopback? || address.link_local?
|
|
269
|
+
rescue IPAddr::InvalidAddressError, URI::InvalidURIError
|
|
270
|
+
nil
|
|
271
|
+
end
|
|
272
|
+
|
|
206
273
|
def domains_result
|
|
207
274
|
return ok("domains", "none") unless DomainClaim.exists?
|
|
208
275
|
|
|
@@ -221,7 +288,7 @@ module Masks
|
|
|
221
288
|
days = ((Time.current - active.activated_at) / 1.day).floor
|
|
222
289
|
period = Current.tenant.key_rotation_days
|
|
223
290
|
|
|
224
|
-
return
|
|
291
|
+
return warning("keys", "manual", days: days) if period.nil?
|
|
225
292
|
return warning("keys", "stale", days: days, period: period) if days > period
|
|
226
293
|
|
|
227
294
|
ok("keys", "fresh", days: days, period: period)
|
|
@@ -11,6 +11,7 @@ module Masks
|
|
|
11
11
|
held = ::Rails.cache.read(key)
|
|
12
12
|
|
|
13
13
|
return nil unless held.is_a?(Hash) && held["client_id"] == client.client_id
|
|
14
|
+
return nil unless Replay.first?("saml-farewell", request_id, within: client.id, expires_in: WAIT)
|
|
14
15
|
|
|
15
16
|
::Rails.cache.delete(key)
|
|
16
17
|
new(held["steps"], held["finish"])
|
|
@@ -31,6 +31,21 @@ module Masks
|
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
class Pooled < StandardError
|
|
35
|
+
def initialize(message = nil)
|
|
36
|
+
super(
|
|
37
|
+
message ||
|
|
38
|
+
"this server reaches Postgres through a pooler that hands out a different server " \
|
|
39
|
+
"connection for each transaction. The tenant masks sets on one connection is gone from " \
|
|
40
|
+
"the next, so a query would see no tenant's rows, or another tenant's. Connect masks to " \
|
|
41
|
+
"Postgres directly, or run the pooler in session mode."
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
POOL_PROBE = "masks.pool_probe".freeze
|
|
47
|
+
POOL_PROBE_ROUNDS = 3
|
|
48
|
+
|
|
34
49
|
encrypts :pairwise_salt, :setup_token, key_provider: Server.tenant_key_provider
|
|
35
50
|
|
|
36
51
|
attribute :uuid, default: -> { SecureRandom.uuid }
|
|
@@ -348,10 +363,28 @@ module Masks
|
|
|
348
363
|
|
|
349
364
|
raise Exposed, held&.fetch("rolname", nil) unless held && held["bypasses"] == false
|
|
350
365
|
raise Owner, held["rolname"] if held["owns"] && guards_ownership?
|
|
366
|
+
raise Pooled if pooled?
|
|
351
367
|
|
|
352
368
|
@isolated = true
|
|
353
369
|
end
|
|
354
370
|
|
|
371
|
+
def pooled?
|
|
372
|
+
return false if connection_pool.size < 2
|
|
373
|
+
|
|
374
|
+
POOL_PROBE_ROUNDS.times do
|
|
375
|
+
mine = SecureRandom.hex(8)
|
|
376
|
+
connection.exec_query("SELECT set_config($1, $2, false)", "probe", [ POOL_PROBE, mine ])
|
|
377
|
+
|
|
378
|
+
return false unless touched_another_connection?
|
|
379
|
+
|
|
380
|
+
return true unless connection.select_value("SELECT current_setting('#{POOL_PROBE}', true)") == mine
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
false
|
|
384
|
+
rescue ActiveRecord::ConnectionTimeoutError
|
|
385
|
+
false
|
|
386
|
+
end
|
|
387
|
+
|
|
355
388
|
def role_privileges
|
|
356
389
|
connection.select_one(<<~SQL)
|
|
357
390
|
SELECT rolname, rolsuper OR rolbypassrls AS bypasses,
|
|
@@ -383,23 +416,34 @@ module Masks
|
|
|
383
416
|
end
|
|
384
417
|
|
|
385
418
|
def clear!
|
|
386
|
-
enter(nil)
|
|
419
|
+
enter(nil, for_transaction: false)
|
|
387
420
|
end
|
|
388
421
|
|
|
389
422
|
private
|
|
390
423
|
|
|
391
|
-
def enter(tenant)
|
|
424
|
+
def enter(tenant, for_transaction: connection.transaction_open?)
|
|
392
425
|
Current.tenant = tenant
|
|
393
426
|
|
|
394
427
|
connection.exec_query(
|
|
395
|
-
"SELECT set_config($1, $2,
|
|
396
|
-
[ TenantIsolation::SETTING, tenant&.id.to_s ]
|
|
428
|
+
"SELECT set_config($1, $2, $3)", "tenant",
|
|
429
|
+
[ TenantIsolation::SETTING, tenant&.id.to_s, for_transaction ]
|
|
397
430
|
)
|
|
398
431
|
|
|
399
432
|
connection.clear_query_cache
|
|
400
433
|
rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::ConnectionFailed
|
|
401
434
|
nil
|
|
402
435
|
end
|
|
436
|
+
|
|
437
|
+
def touched_another_connection?
|
|
438
|
+
other = connection_pool.checkout
|
|
439
|
+
|
|
440
|
+
return false if other.equal?(connection)
|
|
441
|
+
|
|
442
|
+
other.exec_query("SELECT set_config($1, $2, false)", "probe", [ POOL_PROBE, "another connection" ])
|
|
443
|
+
true
|
|
444
|
+
ensure
|
|
445
|
+
connection_pool.checkin(other) if other
|
|
446
|
+
end
|
|
403
447
|
end
|
|
404
448
|
|
|
405
449
|
def signing_key
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
module Throttle
|
|
4
|
+
class Invalid < StandardError; end
|
|
5
|
+
|
|
6
|
+
Budget = Struct.new(:name, :to, :within, :about, :variable, keyword_init: true) do
|
|
7
|
+
def key
|
|
8
|
+
name.to_s
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def limits
|
|
12
|
+
{ to: to, within: within }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def reference
|
|
16
|
+
"| `#{variable}` | `#{key}` | #{to} in #{within.inspect} | #{about} |"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
DEFAULTS = [
|
|
21
|
+
Budget.new(name: :attempt, to: 10, within: 3.minutes, variable: "MASKS_ATTEMPT_LIMIT",
|
|
22
|
+
about: "Sign-in attempts from one IP address. Includes sign-in through a provider, failed LDAP binds, and app passwords."),
|
|
23
|
+
Budget.new(name: :account_attempt, to: 5, within: 3.minutes, variable: "MASKS_ACCOUNT_ATTEMPT_LIMIT",
|
|
24
|
+
about: "Sign-in attempts against one account, from any IP address. Includes failed LDAP binds and app passwords. " \
|
|
25
|
+
"Also how often a signed-in account changes its password, email address, or phone number, adds an " \
|
|
26
|
+
"authenticator app, or approves a sign-in on another device."),
|
|
27
|
+
Budget.new(name: :recovery, to: 5, within: 15.minutes, variable: "MASKS_RECOVERY_LIMIT",
|
|
28
|
+
about: "Codes and links sent by email or text message. Counted by IP address during sign-in, and by account for " \
|
|
29
|
+
"every address or phone number a code goes to, including a phone added or changed during sign-in."),
|
|
30
|
+
Budget.new(name: :registration, to: 10, within: 10.minutes, variable: "MASKS_REGISTRATION_LIMIT",
|
|
31
|
+
about: "Dynamic client registrations from one IP address, and new client metadata documents."),
|
|
32
|
+
Budget.new(name: :device_code, to: 30, within: 3.minutes, variable: "MASKS_DEVICE_CODE_LIMIT",
|
|
33
|
+
about: "Device user codes entered from one IP address."),
|
|
34
|
+
Budget.new(name: :claim, to: 3, within: 1.hour, variable: "MASKS_CLAIM_LIMIT",
|
|
35
|
+
about: "Tenants claimed from one IP address, when a subdomain template claims a tenant on its first visit. " \
|
|
36
|
+
"Counted across the whole server."),
|
|
37
|
+
Budget.new(name: :claim_ceiling, to: 30, within: 1.hour, variable: "MASKS_CLAIM_CEILING",
|
|
38
|
+
about: "Tenants claimed from all IP addresses together, under the same template. Counted across the whole server."),
|
|
39
|
+
Budget.new(name: :token, to: 60, within: 1.minute, variable: "MASKS_TOKEN_LIMIT", about: "Token requests from one IP address."),
|
|
40
|
+
Budget.new(name: :pushed_authorization, to: 60, within: 1.minute, variable: "MASKS_PUSHED_AUTHORIZATION_LIMIT", about: "Pushed authorization requests from one IP address."),
|
|
41
|
+
Budget.new(name: :device_authorization, to: 60, within: 1.minute, variable: "MASKS_DEVICE_AUTHORIZATION_LIMIT", about: "Device authorization requests from one IP address."),
|
|
42
|
+
Budget.new(name: :introspection, to: 120, within: 1.minute, variable: "MASKS_INTROSPECTION_LIMIT", about: "Token introspections from one IP address."),
|
|
43
|
+
Budget.new(name: :revocation, to: 120, within: 1.minute, variable: "MASKS_REVOCATION_LIMIT", about: "Token revocations from one IP address."),
|
|
44
|
+
Budget.new(name: :saml, to: 60, within: 1.minute, variable: "MASKS_SAML_LIMIT", about: "SAML sign-in and logout requests from one IP address."),
|
|
45
|
+
Budget.new(name: :forward_auth, to: 30, within: 1.minute, variable: "MASKS_FORWARD_AUTH_LIMIT", about: "Forward auth sign-ins started or finished from one IP address."),
|
|
46
|
+
Budget.new(name: :enrollment, to: 30, within: 1.minute, variable: "MASKS_ENROLLMENT_LIMIT", about: "Enrollment tokens redeemed from one IP address."),
|
|
47
|
+
Budget.new(name: :app_enrollment, to: 60, within: 1.hour, variable: "MASKS_APP_ENROLLMENT_LIMIT", about: "Enrollment tokens one app mints."),
|
|
48
|
+
Budget.new(name: :invitation, to: 30, within: 1.minute, variable: "MASKS_INVITATION_LIMIT", about: "Organization invitations one account sends or sends again."),
|
|
49
|
+
Budget.new(name: :export, to: 5, within: 1.hour, variable: "MASKS_EXPORT_LIMIT", about: "Account exports one account asks for."),
|
|
50
|
+
Budget.new(name: :access_request, to: 1, within: 1.hour, variable: "MASKS_ACCESS_REQUEST_LIMIT", about: "Times one account asks for access to one app."),
|
|
51
|
+
Budget.new(name: :signal, to: 600, within: 1.minute, variable: "MASKS_SIGNAL_LIMIT", about: "Security events received from one IP address."),
|
|
52
|
+
Budget.new(name: :scim, to: 600, within: 1.minute, variable: "MASKS_SCIM_LIMIT", about: "SCIM provisioning requests from one IP address.")
|
|
53
|
+
].freeze
|
|
54
|
+
|
|
55
|
+
NAMES = DEFAULTS.map(&:key).freeze
|
|
56
|
+
|
|
57
|
+
module_function
|
|
58
|
+
|
|
59
|
+
def budgets
|
|
60
|
+
@budgets ||= DEFAULTS.to_h { |budget| [ budget.name, budget.dup.tap { |held| held.to = limit_for(budget) } ] }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def budget(name)
|
|
64
|
+
budgets.fetch(name.to_sym) { raise Invalid, "no limit is called #{name}. The limits are #{NAMES.join(', ')}." }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def limit(name)
|
|
68
|
+
budget(name).to
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def limits(name)
|
|
72
|
+
budget(name).limits
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def refused(name, &block)
|
|
76
|
+
held = budget(name)
|
|
77
|
+
|
|
78
|
+
proc { Throttle.hit!(held.name); instance_exec(&block) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def key(name, by, scope: nil, named: nil)
|
|
82
|
+
held = budget(name)
|
|
83
|
+
|
|
84
|
+
[ "rate-limit", scope || "masks/server/#{held.key}", named, by ].compact.join(":")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def count!(name, by, scope: nil, named: nil)
|
|
88
|
+
::Rails.cache.increment(key(name, by, scope: scope, named: named), 1, expires_in: budget(name).within)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def exceeded?(name, by, scope: nil, named: nil, times: 1)
|
|
92
|
+
::Rails.cache.read(key(name, by, scope: scope, named: named)).to_i >= budget(name).to * times
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def crowded?(name, by, scope: nil, named: nil, times: 1)
|
|
96
|
+
held = budget(name)
|
|
97
|
+
|
|
98
|
+
::Rails.cache.increment(key(name, by, scope: scope, named: named), 1, expires_in: held.within).to_i > held.to * times
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def hit!(name, actor: nil, client: nil, address: Current.ip_address, **details)
|
|
102
|
+
held = budget(name)
|
|
103
|
+
|
|
104
|
+
::Rails.logger.info("masks: the #{held.key} limit of #{held.to} in #{held.within.inspect} was reached" \
|
|
105
|
+
"#{address.present? ? " from #{address}" : ''}")
|
|
106
|
+
|
|
107
|
+
return if Current.tenant.nil?
|
|
108
|
+
return unless ::Rails.cache.write(key(name, "reached:#{address}"), true, unless_exist: true, expires_in: held.within)
|
|
109
|
+
|
|
110
|
+
Event.record!(Event::LIMIT_REACHED, actor: actor, by: nil, client: client, limit: held.key, **details)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def reference
|
|
114
|
+
DEFAULTS.map(&:reference)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def reset!
|
|
118
|
+
@budgets = nil
|
|
119
|
+
@declared = nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def limit_for(budget)
|
|
123
|
+
from_env(budget) || declared.fetch(budget.key, nil) || budget.to
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def from_env(budget)
|
|
127
|
+
value = ENV[budget.variable]
|
|
128
|
+
|
|
129
|
+
return nil if value.blank?
|
|
130
|
+
|
|
131
|
+
whole!(value, budget.variable)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def declared
|
|
135
|
+
@declared ||= read_declared(::Rails.configuration.masks.config_path)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def read_declared(path)
|
|
139
|
+
return {} if path.blank?
|
|
140
|
+
|
|
141
|
+
document = YAML.safe_load_file(path, aliases: false) || {}
|
|
142
|
+
limits = document.is_a?(Hash) ? document.fetch("limits", nil) || {} : {}
|
|
143
|
+
|
|
144
|
+
raise Invalid, "limits in #{path} must map each limit's name to a whole number" unless limits.is_a?(Hash)
|
|
145
|
+
|
|
146
|
+
unknown = limits.keys.map(&:to_s) - NAMES
|
|
147
|
+
raise Invalid, "#{path} sets limits called #{unknown.join(', ')}, which do not exist. The limits are #{NAMES.join(', ')}." if unknown.any?
|
|
148
|
+
|
|
149
|
+
limits.to_h { |name, value| [ name.to_s, whole!(value, "limits.#{name} in #{path}") ] }
|
|
150
|
+
rescue Errno::ENOENT, Psych::Exception
|
|
151
|
+
{}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def whole!(value, where)
|
|
155
|
+
count = Integer(value.to_s, 10)
|
|
156
|
+
|
|
157
|
+
raise Invalid, "#{where} is #{value.inspect}. A limit is a whole number, 1 or more." if count < 1
|
|
158
|
+
|
|
159
|
+
count
|
|
160
|
+
rescue ArgumentError, TypeError
|
|
161
|
+
raise Invalid, "#{where} is #{value.inspect}. A limit is a whole number, 1 or more."
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -12,21 +12,15 @@
|
|
|
12
12
|
<% hot, rest = @scopes.to_a.partition { |scope, _| heeded_scope?(scope) } %>
|
|
13
13
|
<% after_sign_out = @handshake.try(:post_logout_redirect_uri) %>
|
|
14
14
|
|
|
15
|
-
<div class="auth-
|
|
15
|
+
<div class="auth-solo">
|
|
16
16
|
<span class="auth-mark auth-mark-client" aria-hidden="true"><%= name.to_s.first&.upcase %></span>
|
|
17
|
-
<span class="auth-wire"></span>
|
|
18
|
-
<%= tenant_mark(Masks::Server::Current.tenant) %>
|
|
19
17
|
</div>
|
|
20
18
|
|
|
21
19
|
<div class="prompt-head">
|
|
22
20
|
<h1 class="prompt-title"><%= t(@existing ? ".title_again" : ".title", name: name, tenant: tenant) %></h1>
|
|
23
21
|
|
|
24
|
-
<p class="handshake-origin">
|
|
25
|
-
<span class="handshake-origin-label"><%= t(".runs_at") %></span>
|
|
26
|
-
<span class="handshake-origin-value"><%= @handshake.origin %></span>
|
|
27
|
-
</p>
|
|
28
|
-
|
|
29
22
|
<p class="prompt-lede">
|
|
23
|
+
<%= t(".runs_at", name: name, origin: @handshake.origin) %>
|
|
30
24
|
<%= t(@existing ? ".lede_again_html" : ".lede_html", name: name, tenant: tenant) %>
|
|
31
25
|
</p>
|
|
32
26
|
</div>
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
<% state = login.state("consent") %>
|
|
2
2
|
<% client = login.client_json || {} %>
|
|
3
3
|
<% hot, rest = ranked_scopes(Masks::Server::ResourceMetadata.describe(state.audience, state.scopes)) %>
|
|
4
|
+
<% all = hot + rest %>
|
|
4
5
|
|
|
5
|
-
<div class="auth-
|
|
6
|
+
<div class="auth-solo">
|
|
6
7
|
<% if client["logo"] %>
|
|
7
8
|
<%= image_tag client["logo"], alt: "", class: "auth-mark auth-mark-client auth-mark-logo", referrerpolicy: "no-referrer" %>
|
|
8
9
|
<% else %>
|
|
9
10
|
<span class="auth-mark auth-mark-client" aria-hidden="true"><%= client["name"].to_s.strip.first&.upcase %></span>
|
|
10
11
|
<% end %>
|
|
11
|
-
<span class="auth-wire"></span>
|
|
12
|
-
<%= tenant_mark(Masks::Server::Current.tenant) %>
|
|
13
12
|
</div>
|
|
14
13
|
|
|
15
14
|
<%= render "masks/server/logins/head", title: t(".title", client: login.client&.name) %>
|
|
@@ -20,48 +19,30 @@
|
|
|
20
19
|
<div class="ledger-row">
|
|
21
20
|
<span class="ledger-label"><%= t(".access") %></span>
|
|
22
21
|
|
|
23
|
-
<% if
|
|
22
|
+
<% if all.empty? %>
|
|
24
23
|
<p class="empty"><%= t(".no_access") %></p>
|
|
24
|
+
<% else %>
|
|
25
|
+
<ul class="plain-list">
|
|
26
|
+
<% all.each do |_, description| %>
|
|
27
|
+
<li><%= description %></li>
|
|
28
|
+
<% end %>
|
|
29
|
+
</ul>
|
|
25
30
|
<% end %>
|
|
26
|
-
|
|
27
|
-
<ul class="grant-scopes">
|
|
28
|
-
<% hot.each do |scope, description| %>
|
|
29
|
-
<li class="grant-scope grant-scope-hot">
|
|
30
|
-
<span><%= description %></span>
|
|
31
|
-
<span class="chip-key"><%= scope %></span>
|
|
32
|
-
</li>
|
|
33
|
-
<% end %>
|
|
34
|
-
|
|
35
|
-
<% if hot.any? && rest.any? %>
|
|
36
|
-
<li class="grant-scope-split"></li>
|
|
37
|
-
<% end %>
|
|
38
|
-
|
|
39
|
-
<% rest.each do |scope, description| %>
|
|
40
|
-
<li class="grant-scope">
|
|
41
|
-
<span><%= description %></span>
|
|
42
|
-
<span class="chip-key"><%= scope %></span>
|
|
43
|
-
</li>
|
|
44
|
-
<% end %>
|
|
45
|
-
</ul>
|
|
46
31
|
</div>
|
|
47
32
|
|
|
48
33
|
<% if state.audience.any? %>
|
|
49
34
|
<div class="ledger-row">
|
|
50
|
-
<span class="
|
|
51
|
-
<% state.audience.each do |resource| %>
|
|
52
|
-
<span class="ledger-value aside-mono"><%= resource %></span>
|
|
53
|
-
<% end %>
|
|
35
|
+
<span class="aside"><%= t(".audience", resources: state.audience.join(", ")) %></span>
|
|
54
36
|
</div>
|
|
55
37
|
<% end %>
|
|
56
38
|
|
|
57
39
|
<% if (details = state.details&.described).present? %>
|
|
58
40
|
<div class="ledger-row">
|
|
59
41
|
<span class="ledger-label"><%= t(".details") %></span>
|
|
60
|
-
<ul class="
|
|
42
|
+
<ul class="plain-list">
|
|
61
43
|
<% details.each do |detail| %>
|
|
62
|
-
<li
|
|
63
|
-
|
|
64
|
-
<span class="chip-key"><%= detail["type"] %></span>
|
|
44
|
+
<li>
|
|
45
|
+
<%= detail["label"] %>
|
|
65
46
|
<% if detail["fields"].any? %>
|
|
66
47
|
<dl class="grant-fields">
|
|
67
48
|
<% detail["fields"].each do |name, value| %>
|
|
@@ -75,23 +56,41 @@
|
|
|
75
56
|
</ul>
|
|
76
57
|
</div>
|
|
77
58
|
<% end %>
|
|
59
|
+
</div>
|
|
78
60
|
|
|
79
|
-
|
|
61
|
+
<% links = { "site" => t(".site", client: client["name"]), "terms" => t(".terms"), "privacy" => t(".privacy") }.select { |key, _| client[key] } %>
|
|
62
|
+
<details class="disclosure handshake-more">
|
|
63
|
+
<summary class="textlink"><%= t(".more") %></summary>
|
|
64
|
+
|
|
65
|
+
<div class="slab">
|
|
80
66
|
<div class="ledger-row">
|
|
81
|
-
<span class="ledger-label"><%= t(".
|
|
82
|
-
<
|
|
67
|
+
<span class="ledger-label"><%= t(".scopes") %></span>
|
|
68
|
+
<ul class="grant-scopes">
|
|
69
|
+
<% all.each do |scope, description| %>
|
|
70
|
+
<li class="grant-scope">
|
|
71
|
+
<span><%= description %></span>
|
|
72
|
+
<span class="chip-key"><%= scope %></span>
|
|
73
|
+
</li>
|
|
74
|
+
<% end %>
|
|
75
|
+
</ul>
|
|
83
76
|
</div>
|
|
84
|
-
<% end %>
|
|
85
|
-
</div>
|
|
86
77
|
|
|
87
|
-
<%
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
78
|
+
<% if client["returnsTo"] %>
|
|
79
|
+
<div class="ledger-row">
|
|
80
|
+
<span class="ledger-label"><%= t(".returns_to") %></span>
|
|
81
|
+
<span class="ledger-value aside-mono"><%= client["returnsTo"] %></span>
|
|
82
|
+
</div>
|
|
92
83
|
<% end %>
|
|
93
|
-
|
|
94
|
-
<%
|
|
84
|
+
|
|
85
|
+
<% if links.any? %>
|
|
86
|
+
<p class="aside client-links">
|
|
87
|
+
<% links.each do |key, label| %>
|
|
88
|
+
<%= link_to label, client[key], class: "textlink", rel: "noopener noreferrer", target: "_blank" %>
|
|
89
|
+
<% end %>
|
|
90
|
+
</p>
|
|
91
|
+
<% end %>
|
|
92
|
+
</div>
|
|
93
|
+
</details>
|
|
95
94
|
|
|
96
95
|
<div class="action-row">
|
|
97
96
|
<%= form_with url: login_path, method: :post, id: "consent", class: "action-grow" do |form| %>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<div class="flow<%= " setup" if signing_up %>">
|
|
9
9
|
<% if signing_up %>
|
|
10
|
-
<%= render "masks/server/logins/signup_head", login: login, at: 2
|
|
10
|
+
<%= render "masks/server/logins/signup_head", login: login, at: 2 %>
|
|
11
11
|
|
|
12
12
|
<p class="aside"><%= enrolment["required"] ? t(".lede") : t(".lede_optional") %></p>
|
|
13
13
|
<% else %>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<% configure = login.state("configure").as_json["configure"] %>
|
|
2
2
|
|
|
3
3
|
<div class="setup flow">
|
|
4
|
-
<%= render "masks/server/logins/signup_head", login: login, at: 3
|
|
4
|
+
<%= render "masks/server/logins/signup_head", login: login, at: 3 %>
|
|
5
5
|
|
|
6
6
|
<%= form_with url: login_path, method: :post, id: "setup-configure", class: "flow" do |form| %>
|
|
7
7
|
<%= hidden_field_tag :rid, login.rid %>
|
|
@@ -12,11 +12,6 @@
|
|
|
12
12
|
<%= form.text_field :called, value: configure["called"], class: "control" %>
|
|
13
13
|
<span class="field-hint"><%= t(".called_hint") %></span>
|
|
14
14
|
</label>
|
|
15
|
-
|
|
16
|
-
<div class="ledger-row">
|
|
17
|
-
<span class="ledger-label"><%= t("logins.signing_up.origin") %></span>
|
|
18
|
-
<span class="ledger-value aside-mono"><%= Masks::Server::Current.origin %></span>
|
|
19
|
-
</div>
|
|
20
15
|
</div>
|
|
21
16
|
|
|
22
17
|
<button type="submit" name="event" value="setup-configure" class="action">
|
|
@@ -25,6 +20,7 @@
|
|
|
25
20
|
<% end %>
|
|
26
21
|
|
|
27
22
|
<p class="aside">
|
|
23
|
+
<%= t("logins.signing_up.origin", origin: Masks::Server::Current.origin) %>
|
|
28
24
|
<%= t("logins.signing_up.once") %>
|
|
29
25
|
<%= link_to t("logins.signing_up.docs"), Rails.configuration.masks.docs_url,
|
|
30
26
|
class: "textlink", rel: "noopener", target: "_blank" %>
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
<% done = ->(field) { " ledger-step-done" if value.(field).present? } %>
|
|
5
5
|
|
|
6
6
|
<div class="setup flow">
|
|
7
|
-
<%= render "masks/server/logins/signup_head", login: login, at: 1
|
|
8
|
-
mark: value.("nickname").presence || value.("email") %>
|
|
7
|
+
<%= render "masks/server/logins/signup_head", login: login, at: 1 %>
|
|
9
8
|
|
|
10
9
|
<%= form_with url: login_path, method: :post, id: "signup", class: "flow" do |form| %>
|
|
11
10
|
<%= hidden_field_tag :rid, login.rid %>
|
|
@@ -51,13 +50,6 @@
|
|
|
51
50
|
<%= render "masks/server/logins/hint", login: login, field: "phone" %>
|
|
52
51
|
</label>
|
|
53
52
|
<% end %>
|
|
54
|
-
|
|
55
|
-
<% if login.signing_up["firstRun"] %>
|
|
56
|
-
<div class="ledger-row">
|
|
57
|
-
<span class="ledger-label"><%= t("logins.signing_up.origin") %></span>
|
|
58
|
-
<span class="ledger-value aside-mono"><%= Masks::Server::Current.origin %></span>
|
|
59
|
-
</div>
|
|
60
|
-
<% end %>
|
|
61
53
|
</div>
|
|
62
54
|
|
|
63
55
|
<% if signup["work"] %>
|
|
@@ -71,6 +63,7 @@
|
|
|
71
63
|
|
|
72
64
|
<% if login.signing_up["firstRun"] %>
|
|
73
65
|
<p class="aside">
|
|
66
|
+
<%= t("logins.signing_up.origin", origin: Masks::Server::Current.origin) %>
|
|
74
67
|
<%= t("logins.signing_up.once") %>
|
|
75
68
|
<%= link_to t("logins.signing_up.docs"), Rails.configuration.masks.docs_url,
|
|
76
69
|
class: "textlink", rel: "noopener", target: "_blank" %>
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
<% rows = %w[name nickname email phone].filter_map { |field| [ field, signup[field] ] if signup[field].present? } %>
|
|
3
3
|
|
|
4
4
|
<div class="setup flow">
|
|
5
|
-
<%= render "masks/server/logins/signup_head", login: login, at: 2
|
|
6
|
-
mark: signup["nickname"].presence || signup["email"] %>
|
|
5
|
+
<%= render "masks/server/logins/signup_head", login: login, at: 2 %>
|
|
7
6
|
|
|
8
7
|
<%= form_with url: login_path, method: :post, id: "signup-credentials", class: "flow" do |form| %>
|
|
9
8
|
<%= hidden_field_tag :rid, login.rid %>
|
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
<% tenant = Masks::Server::Current.tenant&.name.to_s %>
|
|
2
2
|
<% signing_up = login.signing_up %>
|
|
3
3
|
|
|
4
|
-
<div class="auth-pair">
|
|
5
|
-
<span class="auth-mark auth-mark-client<%= " auth-mark-pending" if mark.blank? %>" aria-hidden="true"><%= mark.to_s.first&.upcase %></span>
|
|
6
|
-
<span class="auth-wire"></span>
|
|
7
|
-
<% if signing_up["firstRun"] %>
|
|
8
|
-
<img src="/masks-public/icon.svg" alt="" class="auth-mark auth-mark-rose">
|
|
9
|
-
<% else %>
|
|
10
|
-
<%= tenant_mark(Masks::Server::Current.tenant) %>
|
|
11
|
-
<% end %>
|
|
12
|
-
</div>
|
|
13
|
-
|
|
14
4
|
<% if signing_up["firstRun"] %>
|
|
15
5
|
<%= render "masks/server/logins/head", cap: t("logins.signing_up.setup_cap"), title: t("logins.signing_up.setup_title"), name: tenant %>
|
|
16
6
|
<% else %>
|
|
17
|
-
<%= render "masks/server/logins/head", title: t("logins.signing_up.signup_title")
|
|
7
|
+
<%= render "masks/server/logins/head", title: t("logins.signing_up.signup_title") %>
|
|
18
8
|
<% end %>
|
|
19
9
|
|
|
20
10
|
<%= render "masks/server/logins/steps", steps: signing_up["steps"], at: local_assigns[:at] || signing_up["steps"].length %>
|