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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/masks/server/scim_endpoint.rb +2 -2
- data/app/controllers/masks/server/account_controller.rb +3 -0
- 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/app_passwords_controller.rb +41 -0
- data/app/controllers/masks/server/authenticator_apps_controller.rb +2 -3
- data/app/controllers/masks/server/authorize_controller.rb +33 -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 +106 -14
- data/app/controllers/masks/server/introspections_controller.rb +2 -2
- data/app/controllers/masks/server/logins_controller.rb +14 -15
- data/app/controllers/masks/server/organization_access_controller.rb +39 -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/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/mutations/base_mutation.rb +7 -0
- data/app/graphql/masks/server/manage/mutations/create_group.rb +6 -4
- data/app/graphql/masks/server/manage/mutations/decline_access.rb +27 -0
- data/app/graphql/masks/server/manage/mutations/grant_access.rb +29 -0
- data/app/graphql/masks/server/manage/mutations/group_source.rb +34 -0
- data/app/graphql/masks/server/manage/mutations/issue_ldap_credential.rb +5 -2
- data/app/graphql/masks/server/manage/mutations/refresh_group.rb +21 -0
- data/app/graphql/masks/server/manage/mutations/remove_group_member.rb +2 -0
- data/app/graphql/masks/server/manage/mutations/run_health_checks.rb +19 -0
- data/app/graphql/masks/server/manage/mutations/update_client.rb +6 -0
- data/app/graphql/masks/server/manage/mutations/update_group.rb +7 -4
- data/app/graphql/masks/server/manage/mutations/update_tenant.rb +14 -1
- data/app/graphql/masks/server/manage/types/access_request_type.rb +21 -0
- data/app/graphql/masks/server/manage/types/actor_type.rb +6 -0
- data/app/graphql/masks/server/manage/types/client_type.rb +12 -0
- data/app/graphql/masks/server/manage/types/group_condition_input.rb +16 -0
- data/app/graphql/masks/server/manage/types/group_condition_type.rb +29 -0
- data/app/graphql/masks/server/manage/types/group_member_type.rb +16 -0
- data/app/graphql/masks/server/manage/types/group_type.rb +17 -1
- data/app/graphql/masks/server/manage/types/health_check_type.rb +18 -0
- data/app/graphql/masks/server/manage/types/ldap_credential_type.rb +1 -0
- data/app/graphql/masks/server/manage/types/mutation_type.rb +4 -0
- data/app/graphql/masks/server/manage/types/query_type.rb +24 -0
- data/app/graphql/masks/server/manage/types/release_type.rb +16 -0
- data/app/graphql/masks/server/manage/types/tenant_type.rb +6 -1
- data/app/helpers/masks/server/account_helper.rb +4 -0
- data/app/jobs/masks/server/cleanup_job.rb +1 -0
- data/app/jobs/masks/server/health_check_job.rb +21 -0
- data/app/jobs/masks/server/refresh_groups_job.rb +18 -0
- data/app/jobs/masks/server/release_check_job.rb +21 -0
- data/app/ldap/masks/server/ldap/connection.rb +3 -2
- data/app/ldap/masks/server/ldap/gate.rb +44 -19
- data/app/logins/masks/server/login_states/second_factor.rb +1 -1
- data/app/mailers/masks/server/actor_mailer.rb +31 -0
- data/app/models/masks/server/access_request.rb +103 -0
- data/app/models/masks/server/actor.rb +6 -0
- data/app/models/masks/server/app_password.rb +91 -0
- data/app/models/masks/server/app_passwords.rb +76 -0
- data/app/models/masks/server/client.rb +15 -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/connection.rb +7 -0
- data/app/models/masks/server/declared.rb +13 -3
- data/app/models/masks/server/email_verification.rb +2 -2
- data/app/models/masks/server/event.rb +13 -3
- data/app/models/masks/server/federation/protocol.rb +1 -1
- data/app/models/masks/server/federation/saml.rb +1 -1
- data/app/models/masks/server/forward_auth_rules.rb +162 -0
- data/app/models/masks/server/group.rb +207 -4
- data/app/models/masks/server/group_member.rb +7 -0
- data/app/models/masks/server/group_rule.rb +63 -0
- data/app/models/masks/server/health.rb +310 -0
- data/app/models/masks/server/health_check.rb +27 -0
- data/app/models/masks/server/invitation.rb +2 -2
- data/app/models/masks/server/ldap_credential.rb +2 -2
- data/app/models/masks/server/login.rb +28 -2
- data/app/models/masks/server/mail_previews.rb +5 -0
- data/app/models/masks/server/mail_template.rb +2 -0
- data/app/models/masks/server/membership.rb +10 -3
- data/app/models/masks/server/notifications.rb +3 -1
- data/app/models/masks/server/password_reset.rb +2 -2
- data/app/models/masks/server/provider.rb +1 -0
- data/app/models/masks/server/queue.rb +60 -0
- data/app/models/masks/server/release.rb +74 -0
- data/app/models/masks/server/saml_identity/farewells.rb +1 -0
- data/app/models/masks/server/session.rb +8 -0
- data/app/models/masks/server/tenant.rb +87 -6
- data/app/models/masks/server/throttle.rb +165 -0
- data/app/models/masks/server/token.rb +2 -1
- data/app/views/masks/server/account/_app_passwords.html.erb +83 -0
- data/app/views/masks/server/account/_organizations.html.erb +33 -0
- data/app/views/masks/server/account/index.html.erb +5 -0
- data/app/views/masks/server/actor_mailer/access_declined.html.erb +5 -0
- data/app/views/masks/server/actor_mailer/access_declined.text.erb +5 -0
- data/app/views/masks/server/actor_mailer/access_granted.html.erb +7 -0
- data/app/views/masks/server/actor_mailer/access_granted.text.erb +7 -0
- data/app/views/masks/server/authorize/error.html.erb +25 -0
- data/config/locales/de/access_requests.yml +43 -0
- data/config/locales/de/account/app_passwords.yml +42 -0
- data/config/locales/de/events.yml +5 -0
- data/config/locales/de/flashes.yml +1 -0
- data/config/locales/de/group_sources.yml +5 -0
- data/config/locales/de/manage/access_requests.yml +12 -0
- data/config/locales/de/manage/app_passwords.yml +16 -0
- data/config/locales/de/manage/forward_auth_rules.yml +29 -0
- data/config/locales/de/manage/group_sources.yml +44 -0
- data/config/locales/de/manage/health.yml +85 -0
- data/config/locales/de/manage/links.yml +20 -0
- data/config/locales/en/access_requests.yml +43 -0
- data/config/locales/en/account/app_passwords.yml +42 -0
- data/config/locales/en/events.yml +5 -0
- data/config/locales/en/flashes.yml +1 -0
- data/config/locales/en/group_sources.yml +5 -0
- data/config/locales/en/manage/access_requests.yml +12 -0
- data/config/locales/en/manage/app_passwords.yml +16 -0
- data/config/locales/en/manage/forward_auth_rules.yml +29 -0
- data/config/locales/en/manage/group_sources.yml +44 -0
- data/config/locales/en/manage/health.yml +85 -0
- data/config/locales/en/manage/links.yml +20 -0
- data/config/locales/es/access_requests.yml +43 -0
- data/config/locales/es/account/app_passwords.yml +42 -0
- data/config/locales/es/events.yml +5 -0
- data/config/locales/es/flashes.yml +1 -0
- data/config/locales/es/group_sources.yml +5 -0
- data/config/locales/es/manage/access_requests.yml +12 -0
- data/config/locales/es/manage/app_passwords.yml +16 -0
- data/config/locales/es/manage/forward_auth_rules.yml +29 -0
- data/config/locales/es/manage/group_sources.yml +44 -0
- data/config/locales/es/manage/health.yml +85 -0
- data/config/locales/es/manage/links.yml +20 -0
- data/config/locales/fr/access_requests.yml +43 -0
- data/config/locales/fr/account/app_passwords.yml +42 -0
- data/config/locales/fr/events.yml +5 -0
- data/config/locales/fr/flashes.yml +1 -0
- data/config/locales/fr/group_sources.yml +5 -0
- data/config/locales/fr/manage/access_requests.yml +12 -0
- data/config/locales/fr/manage/app_passwords.yml +16 -0
- data/config/locales/fr/manage/forward_auth_rules.yml +29 -0
- data/config/locales/fr/manage/group_sources.yml +44 -0
- data/config/locales/fr/manage/health.yml +85 -0
- data/config/locales/fr/manage/links.yml +20 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20261020100000_create_access_requests.rb +28 -0
- data/db/migrate/20261020110000_add_app_passwords_to_clients.rb +5 -0
- data/db/migrate/20261020120000_add_forward_auth_rules_to_clients.rb +6 -0
- data/db/migrate/20261020130000_add_sources_to_groups.rb +15 -0
- data/db/migrate/20261020140000_create_health_checks.rb +25 -0
- data/db/migrate/20261020150000_add_link_lifetimes_to_tenants.rb +8 -0
- data/db/migrate/20261021100000_add_pinned_origin_to_tenants.rb +5 -0
- data/db/migrate/20261031100000_default_key_rotation_to_ninety_days.rb +5 -0
- data/lib/generators/masks/server/install/install_generator.rb +22 -2
- data/lib/masks/server/configuration.rb +16 -19
- data/lib/masks/server/engine.rb +2 -1
- data/lib/masks/server/forwarding.rb +26 -3
- data/lib/masks/server/schema_reference.rb +12 -0
- data/lib/masks/server/tenancy/middleware.rb +21 -13
- data/lib/masks/server/tenant_isolation.rb +12 -0
- data/public/masks-assets/.vite/manifest.json +13 -13
- data/public/masks-assets/assets/{account-D46qDQCo.js → account-h7I6VUPR.js} +2 -2
- data/public/masks-assets/assets/{account-D46qDQCo.js.map → account-h7I6VUPR.js.map} +1 -1
- data/public/masks-assets/assets/{app-D7W0gAyT.css → app-BXOScMjL.css} +1 -1
- data/public/masks-assets/assets/{handshake-DIFZPSAK.js → handshake-YQpRNMIs.js} +2 -2
- data/public/masks-assets/assets/{handshake-DIFZPSAK.js.map → handshake-YQpRNMIs.js.map} +1 -1
- data/public/masks-assets/assets/initial-Cn-UuAsy.js +3 -0
- data/public/masks-assets/assets/{initial-C1GpCiEh.js.map → initial-Cn-UuAsy.js.map} +1 -1
- data/public/masks-assets/assets/{login-C_6igRZ3.js → login-C21_jb6L.js} +3 -3
- data/public/masks-assets/assets/{login-C_6igRZ3.js.map → login-C21_jb6L.js.map} +1 -1
- data/public/masks-assets/assets/{manage-D4MzaDvM.js → manage-DpVxe5sv.js} +80 -51
- data/public/masks-assets/assets/manage-DpVxe5sv.js.map +1 -0
- data/public/masks-assets/assets/passkey-5UYPVD3c.js +2 -0
- data/public/masks-assets/assets/{passkey-DIj_5LCY.js.map → passkey-5UYPVD3c.js.map} +1 -1
- data/version.txt +1 -1
- metadata +89 -14
- data/public/masks-assets/assets/initial-C1GpCiEh.js +0 -3
- data/public/masks-assets/assets/manage-D4MzaDvM.js.map +0 -1
- data/public/masks-assets/assets/passkey-DIj_5LCY.js +0 -2
|
@@ -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 }
|
|
@@ -64,6 +79,12 @@ module Masks
|
|
|
64
79
|
|
|
65
80
|
IDLE_DAYS = (60..3650)
|
|
66
81
|
RETENTION_DAYS = (30..2555)
|
|
82
|
+
LINK_LIFETIMES = {
|
|
83
|
+
invitation_lifetime: (1.hour.to_i..30.days.to_i),
|
|
84
|
+
organization_invitation_lifetime: (1.hour.to_i..30.days.to_i),
|
|
85
|
+
password_reset_lifetime: (5.minutes.to_i..1.day.to_i),
|
|
86
|
+
email_verification_lifetime: (1.hour.to_i..14.days.to_i)
|
|
87
|
+
}.freeze
|
|
67
88
|
|
|
68
89
|
validates :named_by, inclusion: { in: NAMES }, allow_nil: true
|
|
69
90
|
validates :accent_color, format: { with: ACCENT }, allow_nil: true
|
|
@@ -71,6 +92,18 @@ module Masks
|
|
|
71
92
|
validates :dynamic_registration, inclusion: { in: REGISTRATIONS }, allow_nil: true
|
|
72
93
|
validates :suspend_after, :delete_after, numericality: { only_integer: true, in: IDLE_DAYS }, allow_nil: true
|
|
73
94
|
validates :event_retention_days, numericality: { only_integer: true, in: RETENTION_DAYS }, allow_nil: true
|
|
95
|
+
def self.spoken(seconds)
|
|
96
|
+
unit, length = { "day" => 1.day, "hour" => 1.hour, "minute" => 1.minute }.find { |_, held| (seconds % held).zero? }
|
|
97
|
+
count = seconds / length
|
|
98
|
+
|
|
99
|
+
"#{count} #{unit.pluralize(count)}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
LINK_LIFETIMES.each do |name, range|
|
|
103
|
+
shortest, longest = [ range.begin, range.end ].map { |seconds| spoken(seconds) }
|
|
104
|
+
|
|
105
|
+
validates name, numericality: { only_integer: true, in: range, message: "must be from #{shortest} to #{longest}" }
|
|
106
|
+
end
|
|
74
107
|
validates :delete_after, comparison: { greater_than: :suspend_after, message: "must be longer than suspend after" },
|
|
75
108
|
if: -> { suspend_after && delete_after }
|
|
76
109
|
|
|
@@ -104,8 +137,20 @@ module Masks
|
|
|
104
137
|
event_retention_days&.days || Event::RETENTION
|
|
105
138
|
end
|
|
106
139
|
|
|
140
|
+
def self.link_lifetime(name, tenant = Current.tenant)
|
|
141
|
+
(tenant ? tenant[name] : column_defaults.fetch(name.to_s)).seconds
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.default_link_lifetime(name)
|
|
145
|
+
column_defaults.fetch(name.to_s)
|
|
146
|
+
end
|
|
147
|
+
|
|
107
148
|
def public_origin
|
|
108
|
-
custom_origin || templated_origin
|
|
149
|
+
custom_origin || templated_origin || held_origin
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def held_origin
|
|
153
|
+
pinned_origin if self.class.pins_origin?
|
|
109
154
|
end
|
|
110
155
|
|
|
111
156
|
def templated_origin
|
|
@@ -246,6 +291,10 @@ module Masks
|
|
|
246
291
|
serving(host) || named(host)
|
|
247
292
|
end
|
|
248
293
|
|
|
294
|
+
def pins_origin?
|
|
295
|
+
::Rails.configuration.masks.public_origin_template.nil? && ::Rails.configuration.masks.pin_origin
|
|
296
|
+
end
|
|
297
|
+
|
|
249
298
|
def served_domain
|
|
250
299
|
template = ::Rails.configuration.masks.public_origin_template
|
|
251
300
|
|
|
@@ -314,10 +363,28 @@ module Masks
|
|
|
314
363
|
|
|
315
364
|
raise Exposed, held&.fetch("rolname", nil) unless held && held["bypasses"] == false
|
|
316
365
|
raise Owner, held["rolname"] if held["owns"] && guards_ownership?
|
|
366
|
+
raise Pooled if pooled?
|
|
317
367
|
|
|
318
368
|
@isolated = true
|
|
319
369
|
end
|
|
320
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
|
+
|
|
321
388
|
def role_privileges
|
|
322
389
|
connection.select_one(<<~SQL)
|
|
323
390
|
SELECT rolname, rolsuper OR rolbypassrls AS bypasses,
|
|
@@ -349,23 +416,34 @@ module Masks
|
|
|
349
416
|
end
|
|
350
417
|
|
|
351
418
|
def clear!
|
|
352
|
-
enter(nil)
|
|
419
|
+
enter(nil, for_transaction: false)
|
|
353
420
|
end
|
|
354
421
|
|
|
355
422
|
private
|
|
356
423
|
|
|
357
|
-
def enter(tenant)
|
|
424
|
+
def enter(tenant, for_transaction: connection.transaction_open?)
|
|
358
425
|
Current.tenant = tenant
|
|
359
426
|
|
|
360
427
|
connection.exec_query(
|
|
361
|
-
"SELECT set_config($1, $2,
|
|
362
|
-
[ TenantIsolation::SETTING, tenant&.id.to_s ]
|
|
428
|
+
"SELECT set_config($1, $2, $3)", "tenant",
|
|
429
|
+
[ TenantIsolation::SETTING, tenant&.id.to_s, for_transaction ]
|
|
363
430
|
)
|
|
364
431
|
|
|
365
432
|
connection.clear_query_cache
|
|
366
433
|
rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::ConnectionFailed
|
|
367
434
|
nil
|
|
368
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
|
|
369
447
|
end
|
|
370
448
|
|
|
371
449
|
def signing_key
|
|
@@ -403,7 +481,10 @@ module Masks
|
|
|
403
481
|
end
|
|
404
482
|
|
|
405
483
|
def set_up!
|
|
406
|
-
|
|
484
|
+
self.setup_token = nil
|
|
485
|
+
self.pinned_origin ||= Current.origin.presence if self.class.pins_origin?
|
|
486
|
+
|
|
487
|
+
save! if changed?
|
|
407
488
|
end
|
|
408
489
|
|
|
409
490
|
def setup_announcement
|
|
@@ -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
|
|
@@ -22,7 +22,8 @@ module Masks
|
|
|
22
22
|
"pushed" => "PushedRequest",
|
|
23
23
|
"approval" => "SignInApproval",
|
|
24
24
|
"forward_code" => "ForwardCode",
|
|
25
|
-
"forward_session" => "ForwardSession"
|
|
25
|
+
"forward_session" => "ForwardSession",
|
|
26
|
+
"app_password" => "AppPassword"
|
|
26
27
|
}.freeze
|
|
27
28
|
|
|
28
29
|
self.inheritance_column = "kind"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<section class="group" id="app-passwords">
|
|
2
|
+
<h2><%= account_text("app_passwords.heading") %></h2>
|
|
3
|
+
|
|
4
|
+
<div class="entries">
|
|
5
|
+
<details class="entry" <%= "open" if app_password %>>
|
|
6
|
+
<summary>
|
|
7
|
+
<span class="entry-icon"><%= account_icon(:password) %></span>
|
|
8
|
+
<span class="entry-label"><%= account_text("app_passwords.entry") %></span>
|
|
9
|
+
<span class="entry-state">
|
|
10
|
+
<%= app_passwords.any? ? app_passwords.size : account_text("app_passwords.state.none") %>
|
|
11
|
+
</span>
|
|
12
|
+
<span class="entry-mark" aria-hidden="true"></span>
|
|
13
|
+
</summary>
|
|
14
|
+
|
|
15
|
+
<div class="entry-body">
|
|
16
|
+
<p class="aside"><%= account_text("app_passwords.lede") %></p>
|
|
17
|
+
|
|
18
|
+
<% if app_password %>
|
|
19
|
+
<p class="aside aside-bad" role="alert">
|
|
20
|
+
<%= account_text("app_passwords.shown_once", name: app_password["name"], app: app_password["app"]) %>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<ol class="codes">
|
|
24
|
+
<li><code><%= app_password["secret"] %></code></li>
|
|
25
|
+
</ol>
|
|
26
|
+
<% end %>
|
|
27
|
+
|
|
28
|
+
<% if app_passwords.any? %>
|
|
29
|
+
<div class="items">
|
|
30
|
+
<% app_passwords.each do |held| %>
|
|
31
|
+
<div class="item">
|
|
32
|
+
<span class="item-name">
|
|
33
|
+
<span><%= held.name %></span>
|
|
34
|
+
<span class="tag"><%= held.client.name %></span>
|
|
35
|
+
</span>
|
|
36
|
+
|
|
37
|
+
<span class="item-meta">
|
|
38
|
+
<%= account_text("app_passwords.made_ago", age: time_ago_in_words(held.created_at)) %>
|
|
39
|
+
·
|
|
40
|
+
<% if held.used_at %>
|
|
41
|
+
<%= account_text("app_passwords.used_ago", age: time_ago_in_words(held.used_at)) %>
|
|
42
|
+
<% else %>
|
|
43
|
+
<%= account_text("app_passwords.never_used") %>
|
|
44
|
+
<% end %>
|
|
45
|
+
</span>
|
|
46
|
+
|
|
47
|
+
<%= button_to account_text("app_passwords.revoke"),
|
|
48
|
+
account_app_password_path(held.id), method: :delete,
|
|
49
|
+
class: "action action-quiet action-sm", form_class: "item-act",
|
|
50
|
+
form: { data: { turbo_confirm: account_text("app_passwords.confirm_revoke", name: held.name) } } %>
|
|
51
|
+
</div>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
|
|
56
|
+
<% if offered.any? %>
|
|
57
|
+
<%= form_with url: account_app_passwords_path, method: :post, class: "member-add" do |form| %>
|
|
58
|
+
<label class="field member-add-email">
|
|
59
|
+
<span class="field-label"><%= account_text("app_passwords.name") %></span>
|
|
60
|
+
<%= form.text_field :name, required: true, autocomplete: "off", class: "control", id: "app-password-name",
|
|
61
|
+
maxlength: Masks::Server::AppPassword::NAME_LIMIT,
|
|
62
|
+
placeholder: account_text("app_passwords.name_placeholder") %>
|
|
63
|
+
</label>
|
|
64
|
+
|
|
65
|
+
<label class="field member-add-role">
|
|
66
|
+
<span class="field-label"><%= account_text("app_passwords.app") %></span>
|
|
67
|
+
<%= form.select :client_id, offered.map { |client| [ client.name, client.client_id ] }, {}, class: "control",
|
|
68
|
+
id: "app-password-client" %>
|
|
69
|
+
</label>
|
|
70
|
+
|
|
71
|
+
<button type="submit" class="action action-sm member-add-go">
|
|
72
|
+
<%= account_text(fresh ? "app_passwords.make" : "app_passwords.make_again") %>
|
|
73
|
+
</button>
|
|
74
|
+
|
|
75
|
+
<span class="field-hint member-add-hint"><%= account_text("app_passwords.hint") %></span>
|
|
76
|
+
<% end %>
|
|
77
|
+
<% else %>
|
|
78
|
+
<p class="aside"><%= account_text("app_passwords.none_offered") %></p>
|
|
79
|
+
<% end %>
|
|
80
|
+
</div>
|
|
81
|
+
</details>
|
|
82
|
+
</div>
|
|
83
|
+
</section>
|
|
@@ -135,6 +135,39 @@
|
|
|
135
135
|
<% end %>
|
|
136
136
|
</div>
|
|
137
137
|
|
|
138
|
+
<% asked = account_access_requests(organization) %>
|
|
139
|
+
<% if asked.any? %>
|
|
140
|
+
<p class="aside"><strong><%= account_text("access.heading") %></strong> <%= account_text("access.lede", organization: organization.name) %></p>
|
|
141
|
+
|
|
142
|
+
<div class="items" id="access-requests-<%= organization.key %>">
|
|
143
|
+
<% asked.each do |request| %>
|
|
144
|
+
<% roles = request.organization_roles.filter_map { |key, role| role if key == organization.key } %>
|
|
145
|
+
<div class="item item-member access-request" id="access-request-<%= request.id %>">
|
|
146
|
+
<span class="item-name"><%= request.actor.identifier %></span>
|
|
147
|
+
<span class="item-meta">
|
|
148
|
+
<%= account_text("access.asked", app: request.client.name, age: time_ago_in_words(request.created_at)) %>
|
|
149
|
+
</span>
|
|
150
|
+
|
|
151
|
+
<div class="item-act item-acts">
|
|
152
|
+
<%= form_with url: account_organization_access_path(organization.key, request.id), method: :post,
|
|
153
|
+
class: "item-acts" do |form| %>
|
|
154
|
+
<%= form.select :role, roles, {}, class: "control control-sm",
|
|
155
|
+
id: "access-role-#{organization.key}-#{request.id}",
|
|
156
|
+
aria: { label: account_text("access.role_of", name: request.actor.identifier) } %>
|
|
157
|
+
<button type="submit" class="action action-sm"><%= account_text("access.grant") %></button>
|
|
158
|
+
<% end %>
|
|
159
|
+
|
|
160
|
+
<%= button_to account_text("access.decline"),
|
|
161
|
+
account_organization_access_path(organization.key, request.id), method: :delete,
|
|
162
|
+
class: "action action-quiet action-sm",
|
|
163
|
+
form: { data: { turbo_confirm: account_text("access.confirm_decline", identifier: request.actor.identifier,
|
|
164
|
+
app: request.client.name) } } %>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<% end %>
|
|
168
|
+
</div>
|
|
169
|
+
<% end %>
|
|
170
|
+
|
|
138
171
|
<%= form_with url: account_organization_members_path(organization.key), method: :post, class: "member-add" do |form| %>
|
|
139
172
|
<label class="field member-add-email">
|
|
140
173
|
<span class="field-label"><%= account_text("organizations.add_email") %></span>
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
|
|
32
32
|
<%= render "masks/server/account/apps", apps: @apps, connections: @connections, linkable: @linkable %>
|
|
33
33
|
|
|
34
|
+
<% if @app_passwords.any? || @app_password_clients.any? %>
|
|
35
|
+
<%= render "masks/server/account/app_passwords", app_passwords: @app_passwords, offered: @app_password_clients,
|
|
36
|
+
app_password: @app_password, fresh: @fresh %>
|
|
37
|
+
<% end %>
|
|
38
|
+
|
|
34
39
|
<%= render "masks/server/account/info", actor: @actor, directed: @directed, fresh: @fresh %>
|
|
35
40
|
|
|
36
41
|
<%= render "masks/server/account/activity", events: @events %>
|
|
@@ -9,4 +9,29 @@
|
|
|
9
9
|
<p class="said"><%= @error_code %> — <%= @error_description %></p>
|
|
10
10
|
|
|
11
11
|
<p class="prompt-lede"><%= @refused ? t(".nothing_shared") : t(".contact") %></p>
|
|
12
|
+
|
|
13
|
+
<% if @asking %>
|
|
14
|
+
<div id="ask-access" class="flow" data-waiting="<%= @waiting.present? %>">
|
|
15
|
+
<% if flash[:alert].present? %>
|
|
16
|
+
<p class="note note-bad" role="alert"><%= flash[:alert] %></p>
|
|
17
|
+
<% elsif flash[:notice].present? %>
|
|
18
|
+
<p class="note note-plain" role="status"><%= flash[:notice] %></p>
|
|
19
|
+
<% end %>
|
|
20
|
+
|
|
21
|
+
<% if @waiting %>
|
|
22
|
+
<p class="prompt-lede"><%= t(".waiting", app: @asking.name) %></p>
|
|
23
|
+
<% else %>
|
|
24
|
+
<p class="prompt-lede"><%= t(".asking", app: @asking.name) %></p>
|
|
25
|
+
|
|
26
|
+
<%= form_with url: authorize_access_request_path, method: :post do |form| %>
|
|
27
|
+
<%= form.hidden_field :rid, value: params[:rid] %>
|
|
28
|
+
<button type="submit" class="action"><%= t(".ask") %></button>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% end %>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<% if @return_to %>
|
|
35
|
+
<a class="action action-quiet" id="back-to-app" href="<%= @return_to %>"><%= t(".back", app: @asking&.name || t(".the_app")) %></a>
|
|
36
|
+
<% end %>
|
|
12
37
|
</div>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
de:
|
|
2
|
+
authorize:
|
|
3
|
+
error:
|
|
4
|
+
asking: "Ihr Konto ist in keiner Gruppe, die %{app} nutzen darf. Sie können um Zugang bitten. Die Betreiber von %{app} entscheiden, und Sie erhalten in jedem Fall eine E-Mail."
|
|
5
|
+
ask: "Um Zugang bitten"
|
|
6
|
+
asked: "Ihre Anfrage wurde gesendet. Sie erhalten eine E-Mail, sobald sie entschieden ist."
|
|
7
|
+
waiting: "Ihre Anfrage, %{app} zu nutzen, wartet. Sie erhalten eine E-Mail, sobald sie entschieden ist."
|
|
8
|
+
slow_down: "Sie haben vor weniger als einer Stunde um Zugang zu dieser App gebeten. Warten Sie auf die Antwort."
|
|
9
|
+
back: "Zurück zu %{app}"
|
|
10
|
+
the_app: "der App"
|
|
11
|
+
|
|
12
|
+
account:
|
|
13
|
+
index:
|
|
14
|
+
access:
|
|
15
|
+
heading: "Um Zugang gebeten"
|
|
16
|
+
lede: "Diese Personen wurden von einer App abgewiesen, die Mitglieder von %{organization} zulässt. Mit Genehmigen werden sie Mitglied mit der Rolle, die Sie wählen."
|
|
17
|
+
asked: "Hat vor %{age} gebeten, %{app} zu nutzen"
|
|
18
|
+
role_of: "Rolle für %{name}"
|
|
19
|
+
grant: "Genehmigen"
|
|
20
|
+
decline: "Ablehnen"
|
|
21
|
+
confirm_decline: "Die Anfrage von %{identifier}, %{app} zu nutzen, ablehnen? Die Person erhält eine E-Mail und kann in einer Stunde erneut bitten."
|
|
22
|
+
|
|
23
|
+
organization_access:
|
|
24
|
+
granted: "%{identifier} ist jetzt %{role} von %{organization} und kann %{app} nutzen."
|
|
25
|
+
declined: "Die Anfrage von %{identifier}, %{app} zu nutzen, ist abgelehnt."
|
|
26
|
+
unknown: "Diese Anfrage wartet nicht auf %{organization}."
|
|
27
|
+
|
|
28
|
+
actor_mailer:
|
|
29
|
+
access_granted:
|
|
30
|
+
subject: "Sie können %{app} jetzt nutzen"
|
|
31
|
+
lede: "Ihre Anfrage, %{app} bei %{tenant} zu nutzen, wurde genehmigt. Melden Sie sich erneut bei %{app} an, und Sie werden eingelassen."
|
|
32
|
+
action: "%{app} öffnen"
|
|
33
|
+
access_declined:
|
|
34
|
+
subject: "Ihre Anfrage, %{app} zu nutzen, wurde abgelehnt"
|
|
35
|
+
lede: "Jemand, der %{app} bei %{tenant} betreibt, hat Ihre Anfrage um Zugang abgelehnt."
|
|
36
|
+
again: "Wenn Sie das für einen Fehler halten, fragen Sie die Person direkt. In einer Stunde können Sie erneut bitten."
|
|
37
|
+
|
|
38
|
+
events:
|
|
39
|
+
actions:
|
|
40
|
+
access:
|
|
41
|
+
requested: "Um Zugang zu einer App gebeten"
|
|
42
|
+
granted: "Zugang zu einer App genehmigt"
|
|
43
|
+
declined: "Zugang zu einer App abgelehnt"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
de:
|
|
2
|
+
account:
|
|
3
|
+
index:
|
|
4
|
+
app_passwords:
|
|
5
|
+
heading: "App-Passwörter"
|
|
6
|
+
entry: "App-Passwörter"
|
|
7
|
+
state:
|
|
8
|
+
none: "Keine"
|
|
9
|
+
lede: "Mit einem App-Passwort meldet sich eine einzelne App, etwa ein Medienserver oder ein NAS, ohne Ihren zweiten Faktor als Sie an. Es gilt nur für die App, für die es erstellt wurde."
|
|
10
|
+
shown_once: "Das ist das App-Passwort für %{name}. Es wird nur einmal angezeigt. Geben Sie es jetzt in %{app} ein."
|
|
11
|
+
made_ago: "erstellt vor %{age}"
|
|
12
|
+
used_ago: "zuletzt verwendet vor %{age}"
|
|
13
|
+
never_used: "nie verwendet"
|
|
14
|
+
revoke: "Widerrufen"
|
|
15
|
+
confirm_revoke: "%{name} widerrufen? Die App kann sich damit sofort nicht mehr anmelden."
|
|
16
|
+
name: "Name"
|
|
17
|
+
name_placeholder: "Jellyfin auf dem Fernseher"
|
|
18
|
+
app: "App"
|
|
19
|
+
make: "App-Passwort erstellen"
|
|
20
|
+
make_again: "Zum Erstellen erneut anmelden"
|
|
21
|
+
hint: "Ihr eigenes Passwort und Ihre Passkeys bleiben unverändert. Das Widerrufen eines App-Passworts ändert sonst nichts."
|
|
22
|
+
none_offered: "Noch keine App akzeptiert App-Passwörter."
|
|
23
|
+
app_passwords:
|
|
24
|
+
again: "Melden Sie sich erneut an, um ein App-Passwort zu erstellen. Ihre letzte Anmeldung liegt eine Weile zurück."
|
|
25
|
+
unknown: "Dieses Konto hat kein solches App-Passwort."
|
|
26
|
+
revoked: "%{name} ist widerrufen."
|
|
27
|
+
refused:
|
|
28
|
+
name: "Geben Sie dem App-Passwort einen Namen mit höchstens 64 Zeichen."
|
|
29
|
+
app: "Diese App akzeptiert keine App-Passwörter."
|
|
30
|
+
too_many: "Sie haben bereits %{most} App-Passwörter. Widerrufen Sie zuerst eines."
|
|
31
|
+
events:
|
|
32
|
+
actions:
|
|
33
|
+
app_password:
|
|
34
|
+
created: "App-Passwort erstellt"
|
|
35
|
+
used: "Mit einem App-Passwort angemeldet"
|
|
36
|
+
revoked: "App-Passwort widerrufen"
|
|
37
|
+
actor_mailer:
|
|
38
|
+
notification:
|
|
39
|
+
said:
|
|
40
|
+
app_password:
|
|
41
|
+
created: "Für Ihr %{tenant}-Konto wurde ein App-Passwort erstellt. Damit meldet sich eine App ohne Ihren zweiten Faktor an."
|
|
42
|
+
revoked: "Ein App-Passwort Ihres %{tenant}-Kontos wurde widerrufen."
|
|
@@ -159,6 +159,11 @@ de:
|
|
|
159
159
|
updated: "E-Mail-Vorlage geändert"
|
|
160
160
|
job:
|
|
161
161
|
failed: "Hintergrundjob für diesen Mandanten fehlgeschlagen"
|
|
162
|
+
health:
|
|
163
|
+
failing: "Zustandsprüfung fehlgeschlagen"
|
|
164
|
+
recovered: "Zustandsprüfung wieder in Ordnung"
|
|
165
|
+
release:
|
|
166
|
+
available: "Neue Version verfügbar"
|
|
162
167
|
stream:
|
|
163
168
|
created: "Event-Stream hinzugefügt"
|
|
164
169
|
updated: "Event-Stream geändert"
|