masks-server 0.9.0 → 0.11.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/client_authentication.rb +3 -2
- data/app/controllers/masks/server/addresses_controller.rb +6 -1
- data/app/controllers/masks/server/application_controller.rb +29 -1
- data/app/controllers/masks/server/authorize_controller.rb +23 -18
- data/app/controllers/masks/server/device_verifications_controller.rb +1 -1
- data/app/controllers/masks/server/discovery_controller.rb +36 -0
- data/app/controllers/masks/server/enrollments_controller.rb +17 -0
- data/app/controllers/masks/server/logins_controller.rb +2 -1
- data/app/controllers/masks/server/notifications_controller.rb +11 -1
- data/app/controllers/masks/server/organization_members_controller.rb +3 -1
- data/app/controllers/masks/server/profiles_controller.rb +5 -0
- data/app/controllers/masks/server/saml_controller.rb +1 -7
- data/app/controllers/masks/server/tokens_controller.rb +8 -0
- data/app/graphql/masks/server/manage/mutations/create_actor.rb +1 -0
- data/app/graphql/masks/server/manage/mutations/unenroll_client.rb +28 -0
- data/app/graphql/masks/server/manage/mutations/update_actor.rb +1 -0
- data/app/graphql/masks/server/manage/mutations/update_client.rb +4 -0
- data/app/graphql/masks/server/manage/types/client_type.rb +8 -0
- data/app/graphql/masks/server/manage/types/mutation_type.rb +1 -0
- data/app/graphql/masks/server/manage/types/query_type.rb +7 -0
- data/app/jobs/masks/server/application_job.rb +28 -0
- data/app/jobs/masks/server/check_domain_claims_job.rb +1 -3
- data/app/jobs/masks/server/cleanup_job.rb +5 -6
- data/app/jobs/masks/server/idle_accounts_job.rb +1 -3
- data/app/jobs/masks/server/prune_queue_job.rb +19 -0
- data/app/jobs/masks/server/refresh_saml_metadata_job.rb +1 -3
- data/app/jobs/masks/server/rotate_signing_keys_job.rb +1 -3
- data/app/jobs/masks/server/text_job.rb +12 -1
- data/app/ldap/masks/server/ldap/listener.rb +16 -3
- data/app/logins/masks/server/login_states/confirmation.rb +9 -3
- data/app/logins/masks/server/login_states/signup.rb +1 -0
- data/app/models/masks/server/actor.rb +25 -2
- data/app/models/masks/server/app_preset.rb +102 -0
- data/app/models/masks/server/boot_task.rb +41 -0
- data/app/models/masks/server/client.rb +15 -0
- data/app/models/masks/server/client_keys.rb +12 -1
- data/app/models/masks/server/declared.rb +2 -1
- data/app/models/masks/server/enrollment_token.rb +1 -1
- data/app/models/masks/server/event.rb +6 -1
- data/app/models/masks/server/job_failures.rb +25 -0
- data/app/models/masks/server/notifications.rb +8 -0
- data/app/models/masks/server/pending_request.rb +8 -0
- data/app/models/masks/server/refresh_token.rb +8 -0
- data/app/models/masks/server/released_nickname.rb +26 -0
- data/app/models/masks/server/scim/filter.rb +45 -5
- data/app/models/masks/server/scim/user.rb +1 -0
- data/app/models/masks/server/session.rb +3 -4
- data/app/views/masks/server/account/_info.html.erb +12 -4
- data/app/views/masks/server/account/index.html.erb +1 -1
- data/app/views/masks/server/authorize/error.html.erb +2 -2
- data/config/locales/de/account.yml +4 -0
- data/config/locales/de/actors.yml +2 -0
- data/config/locales/de/authorize.yml +2 -0
- data/config/locales/de/events.yml +6 -0
- data/config/locales/de/flashes.yml +1 -0
- data/config/locales/de/logins.yml +1 -0
- data/config/locales/de/manage/client.yml +4 -1
- data/config/locales/de/manage/ldap.yml +1 -0
- data/config/locales/de/manage/machines.yml +13 -0
- data/config/locales/de/manage/namespaces.yml +1 -1
- data/config/locales/de/notifications.yml +1 -0
- data/config/locales/en/account.yml +4 -0
- data/config/locales/en/actors.yml +2 -0
- data/config/locales/en/authorize.yml +2 -0
- data/config/locales/en/events.yml +6 -0
- data/config/locales/en/flashes.yml +1 -0
- data/config/locales/en/logins.yml +1 -0
- data/config/locales/en/manage/client.yml +3 -0
- data/config/locales/en/manage/ldap.yml +1 -0
- data/config/locales/en/manage/machines.yml +13 -0
- data/config/locales/en/notifications.yml +1 -0
- data/config/locales/es/account.yml +4 -0
- data/config/locales/es/actors.yml +2 -0
- data/config/locales/es/authorize.yml +2 -0
- data/config/locales/es/events.yml +6 -0
- data/config/locales/es/flashes.yml +1 -0
- data/config/locales/es/handshakes.yml +1 -1
- data/config/locales/es/logins.yml +1 -0
- data/config/locales/es/manage/client.yml +3 -0
- data/config/locales/es/manage/ldap.yml +1 -0
- data/config/locales/es/manage/machines.yml +13 -0
- data/config/locales/es/notifications.yml +1 -0
- data/config/locales/fr/account.yml +4 -0
- data/config/locales/fr/actors.yml +2 -0
- data/config/locales/fr/authorize.yml +2 -0
- data/config/locales/fr/events.yml +6 -0
- data/config/locales/fr/flashes.yml +1 -0
- data/config/locales/fr/handshakes.yml +1 -1
- data/config/locales/fr/logins.yml +1 -0
- data/config/locales/fr/manage/client.yml +4 -1
- data/config/locales/fr/manage/clients.yml +1 -1
- data/config/locales/fr/manage/ldap.yml +1 -0
- data/config/locales/fr/manage/machines.yml +13 -0
- data/config/locales/fr/manage/providers.yml +1 -1
- data/config/locales/fr/notifications.yml +1 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20261018100000_create_released_nicknames.rb +22 -0
- data/db/migrate/20261018110000_add_refresh_token_absolute_lifetime_to_clients.rb +5 -0
- data/db/migrate/20261019140000_add_indexes_for_cleanup_and_metrics.rb +10 -0
- data/lib/masks/server/configuration.rb +12 -0
- data/lib/masks/server/tenancy/middleware.rb +9 -1
- data/lib/tasks/masks.rake +31 -7
- data/public/masks-assets/.vite/manifest.json +2 -2
- data/public/masks-assets/assets/{app-B27oPUll.css → app-D7W0gAyT.css} +1 -1
- data/public/masks-assets/assets/manage-D4MzaDvM.js +576 -0
- data/public/masks-assets/assets/manage-D4MzaDvM.js.map +1 -0
- data/version.txt +1 -1
- metadata +16 -4
- data/public/masks-assets/assets/manage-Dbfiy3Mj.js +0 -567
- data/public/masks-assets/assets/manage-Dbfiy3Mj.js.map +0 -1
|
@@ -3,7 +3,11 @@ module Masks
|
|
|
3
3
|
class TextJob < ApplicationJob
|
|
4
4
|
queue_as :mailers
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
ATTEMPTS = 3
|
|
7
|
+
|
|
8
|
+
retry_on Sender::Failed, wait: :polynomially_longer, attempts: ATTEMPTS do |job, error|
|
|
9
|
+
job.gave_up!(error)
|
|
10
|
+
end
|
|
7
11
|
|
|
8
12
|
def perform(to, body)
|
|
9
13
|
sender = Current.tenant&.sms_sender
|
|
@@ -12,6 +16,13 @@ module Masks
|
|
|
12
16
|
|
|
13
17
|
sender.deliver(to: to, body: body)
|
|
14
18
|
end
|
|
19
|
+
|
|
20
|
+
def gave_up!(error)
|
|
21
|
+
Tenant.switch(held_tenant) do
|
|
22
|
+
Event.record!(Event::TEXT_UNDELIVERED, actor: nil, by: nil, device: nil, ip_address: nil, user_agent: nil,
|
|
23
|
+
said: error.message)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
15
26
|
end
|
|
16
27
|
end
|
|
17
28
|
end
|
|
@@ -6,11 +6,24 @@ module Masks
|
|
|
6
6
|
IDLE = 5.minutes
|
|
7
7
|
HANDSHAKE = 10.seconds
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
EVERY_ADDRESS = "0.0.0.0".freeze
|
|
10
|
+
THIS_MACHINE = "127.0.0.1".freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :host, :port
|
|
10
13
|
|
|
11
14
|
class << self
|
|
12
15
|
def from_configuration(config = ::Rails.configuration.masks)
|
|
13
|
-
|
|
16
|
+
tls = tls(config.ldap_cert, config.ldap_key)
|
|
17
|
+
|
|
18
|
+
new(host: host_for(tls: tls, plaintext: config.ldap_plaintext), port: config.ldap_port, tls: tls)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def host_for(tls:, plaintext:)
|
|
22
|
+
tls || plaintext ? EVERY_ADDRESS : THIS_MACHINE
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def local_only?(config = ::Rails.configuration.masks)
|
|
26
|
+
config.ldap_cert.blank? && !config.ldap_plaintext
|
|
14
27
|
end
|
|
15
28
|
|
|
16
29
|
def tls(cert, key)
|
|
@@ -40,7 +53,7 @@ module Masks
|
|
|
40
53
|
def start
|
|
41
54
|
@server = TCPServer.new(@host, @wanted_port)
|
|
42
55
|
@port = @server.local_address.ip_port
|
|
43
|
-
@logger.info("masks ldap: listening on #{@host}:#{@port}#{' with TLS' if @tls}")
|
|
56
|
+
@logger.info("masks ldap: listening on #{@host}:#{@port}#{' with TLS' if @tls}#{' on this machine only' if @host == THIS_MACHINE}")
|
|
44
57
|
@acceptor = Thread.new { accept_loop }
|
|
45
58
|
self
|
|
46
59
|
end
|
|
@@ -23,11 +23,11 @@ module Masks
|
|
|
23
23
|
resend
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
handles "confirm:add-phone" do
|
|
26
|
+
handles "confirm:add-phone", limit: :sending do
|
|
27
27
|
add_phone
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
handles "confirm:change-phone" do
|
|
30
|
+
handles "confirm:change-phone", limit: :sending do
|
|
31
31
|
change_phone
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -129,9 +129,15 @@ module Masks
|
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
def send_code(channel)
|
|
132
|
+
@tokens&.delete(channel)
|
|
133
|
+
|
|
134
|
+
if ConfirmationCode.crowded?(actor: actor, channel: channel)
|
|
135
|
+
login.store[HELD] = held.merge("#{channel}_sent" => Time.current.to_i)
|
|
136
|
+
return warn!(channel == ConfirmationCode::PHONE ? "too-many-texts" : "too-many-codes")
|
|
137
|
+
end
|
|
138
|
+
|
|
132
139
|
token = Confirmations.send_code(actor, channel, journey: Masks::Server::Journey.sign_in(login))
|
|
133
140
|
|
|
134
|
-
@tokens&.delete(channel)
|
|
135
141
|
login.store[HELD] = held.merge(channel => token.id, "#{channel}_sent" => Time.current.to_i)
|
|
136
142
|
end
|
|
137
143
|
|
|
@@ -33,6 +33,9 @@ module Masks
|
|
|
33
33
|
has_many :groups, through: :group_members
|
|
34
34
|
|
|
35
35
|
before_destroy :sign_out_everywhere!, prepend: true
|
|
36
|
+
before_destroy :release_nickname
|
|
37
|
+
|
|
38
|
+
attr_accessor :trusted_rename
|
|
36
39
|
|
|
37
40
|
validates :nickname,
|
|
38
41
|
uniqueness: { scope: :tenant_id, case_sensitive: false },
|
|
@@ -52,8 +55,10 @@ module Masks
|
|
|
52
55
|
allow_blank: true
|
|
53
56
|
|
|
54
57
|
validate :named
|
|
58
|
+
validate :nickname_free, if: -> { nickname.present? && will_save_change_to_nickname? && !trusted_rename }
|
|
55
59
|
|
|
56
60
|
before_save :activate_once_a_password_exists
|
|
61
|
+
after_save :release_previous_nickname, if: :saved_change_to_nickname?
|
|
57
62
|
before_save :forget_code_factors_for_new_addresses
|
|
58
63
|
after_update :forget_mailed_links, if: :saved_change_to_email?
|
|
59
64
|
|
|
@@ -282,11 +287,16 @@ module Masks
|
|
|
282
287
|
|
|
283
288
|
held.find_each(&:revoke!)
|
|
284
289
|
RefreshToken.where(actor_id: id).live.find_each(&:revoke!)
|
|
290
|
+
|
|
291
|
+
issued = AccessToken.where(actor_id: id).live
|
|
292
|
+
issued = issued.where.not(session_id: keeping.id) if keeping
|
|
293
|
+
issued.update_all(consumed_at: Time.current, updated_at: Time.current)
|
|
294
|
+
|
|
285
295
|
DeviceFactor.forget!(actor: self)
|
|
286
296
|
end
|
|
287
297
|
|
|
288
298
|
def notified?(action)
|
|
289
|
-
Notifications.mailed?(action) && !muted_notifications.include?(action.to_s)
|
|
299
|
+
Notifications.mailed?(action) && (Notifications.always?(action) || !muted_notifications.include?(action.to_s))
|
|
290
300
|
end
|
|
291
301
|
|
|
292
302
|
def notifications
|
|
@@ -294,7 +304,7 @@ module Masks
|
|
|
294
304
|
end
|
|
295
305
|
|
|
296
306
|
def notifications=(wanted)
|
|
297
|
-
self.muted_notifications = Notifications::
|
|
307
|
+
self.muted_notifications = Notifications::MUTABLE - Array(wanted).map(&:to_s)
|
|
298
308
|
end
|
|
299
309
|
|
|
300
310
|
def otp?
|
|
@@ -508,6 +518,19 @@ module Masks
|
|
|
508
518
|
end
|
|
509
519
|
end
|
|
510
520
|
|
|
521
|
+
def nickname_free
|
|
522
|
+
errors.add(:nickname, :held) if ReleasedNickname.held_from?(nickname, self)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def release_previous_nickname
|
|
526
|
+
ReleasedNickname.where(nickname: nickname.to_s.downcase, actor_id: id).delete_all if nickname.present?
|
|
527
|
+
ReleasedNickname.release!(nickname_before_last_save, actor: self)
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def release_nickname
|
|
531
|
+
ReleasedNickname.release!(nickname, actor: self)
|
|
532
|
+
end
|
|
533
|
+
|
|
511
534
|
def manages_by_both
|
|
512
535
|
errors.add(:nickname, :blank) if nickname.blank?
|
|
513
536
|
errors.add(:email, :blank) if email.blank?
|
|
@@ -182,6 +182,108 @@ module Masks
|
|
|
182
182
|
[ "OAUTH_PROVIDER_NAME", "{tenant}" ], [ "ENABLE_OAUTH_SIGNUP", "true" ],
|
|
183
183
|
[ "ENABLE_OAUTH_ROLE_MANAGEMENT", "true" ], [ "OAUTH_ROLES_CLAIM", "groups" ], [ "OAUTH_ADMIN_ROLES", "admins" ]
|
|
184
184
|
]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
key: "tailscale", name: "Tailscale",
|
|
188
|
+
redirects: [ "https://login.tailscale.com/a/oauth_response" ],
|
|
189
|
+
scopes: %w[openid profile email],
|
|
190
|
+
guide: "https://tailscale.com/kb/1240/sso-custom-oidc",
|
|
191
|
+
settings: [
|
|
192
|
+
[ "WebFinger domain", "{masks_host}" ], [ "Issuer URL", "{issuer}" ],
|
|
193
|
+
[ "Client ID", "{client_id}" ], [ "Client secret", "{client_secret}" ]
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
key: "headscale", name: "Headscale",
|
|
198
|
+
redirects: [ "{base}/oidc/callback" ],
|
|
199
|
+
scopes: %w[openid profile email groups],
|
|
200
|
+
guide: "https://headscale.net/stable/ref/oidc/",
|
|
201
|
+
settings: [
|
|
202
|
+
[ "oidc.issuer", "{issuer}" ], [ "oidc.client_id", "{client_id}" ], [ "oidc.client_secret", "{client_secret}" ],
|
|
203
|
+
[ "oidc.scope", "[openid, profile, email, groups]" ], [ "oidc.allowed_groups", "[admins]" ]
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
key: "home-assistant", name: "Home Assistant (OIDC integration)",
|
|
208
|
+
redirects: [ "{base}/auth/oidc/callback" ],
|
|
209
|
+
scopes: %w[openid profile email groups],
|
|
210
|
+
guide: "https://github.com/christiaangoossens/hass-oidc-auth",
|
|
211
|
+
settings: [
|
|
212
|
+
[ "auth_oidc.discovery_url", "{discovery}" ], [ "auth_oidc.client_id", "{client_id}" ],
|
|
213
|
+
[ "auth_oidc.client_secret", "{client_secret}" ], [ "auth_oidc.display_name", "{tenant}" ],
|
|
214
|
+
[ "auth_oidc.roles.admin", "admins" ]
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
key: "synapse", name: "Matrix (Synapse)", asks: { "name" => "masks" },
|
|
219
|
+
redirects: [ "{base}/_synapse/client/oidc/callback" ],
|
|
220
|
+
scopes: %w[openid profile email],
|
|
221
|
+
guide: "https://element-hq.github.io/synapse/latest/openid.html",
|
|
222
|
+
settings: [
|
|
223
|
+
[ "oidc_providers", "- idp_id: {name}\n idp_name: {tenant}\n issuer: {issuer}\n client_id: {client_id}\n" \
|
|
224
|
+
" client_secret: {client_secret}\n scopes: [openid, profile, email]\n" \
|
|
225
|
+
" user_mapping_provider:\n config:\n localpart_template: \"{{ user.preferred_username }}\"\n" \
|
|
226
|
+
" display_name_template: \"{{ user.name }}\"\n email_template: \"{{ user.email }}\"" ]
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
key: "bookstack", name: "BookStack",
|
|
231
|
+
redirects: [ "{base}/oidc/callback" ],
|
|
232
|
+
scopes: %w[openid profile email groups],
|
|
233
|
+
guide: "https://www.bookstackapp.com/docs/admin/oidc-auth/",
|
|
234
|
+
settings: [
|
|
235
|
+
[ "AUTH_METHOD", "oidc" ], [ "OIDC_NAME", "{tenant}" ], [ "OIDC_ISSUER", "{issuer}" ], [ "OIDC_ISSUER_DISCOVER", "true" ],
|
|
236
|
+
[ "OIDC_CLIENT_ID", "{client_id}" ], [ "OIDC_CLIENT_SECRET", "{client_secret}" ],
|
|
237
|
+
[ "OIDC_DISPLAY_NAME_CLAIMS", "name" ], [ "OIDC_ADDITIONAL_SCOPES", "groups" ],
|
|
238
|
+
[ "OIDC_USER_TO_GROUPS", "true" ], [ "OIDC_GROUPS_CLAIM", "groups" ], [ "OIDC_REMOVE_FROM_GROUPS", "true" ]
|
|
239
|
+
]
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
key: "wikijs", name: "Wiki.js", asks: { "name" => "masks" },
|
|
243
|
+
redirects: [ "{base}/login/{name}/callback" ],
|
|
244
|
+
scopes: %w[openid profile email groups],
|
|
245
|
+
guide: "https://docs.requarks.io/auth/oidc",
|
|
246
|
+
settings: [
|
|
247
|
+
[ "Client ID", "{client_id}" ], [ "Client Secret", "{client_secret}" ],
|
|
248
|
+
[ "Authorization Endpoint URL", "{authorize}" ], [ "Token Endpoint URL", "{token}" ],
|
|
249
|
+
[ "User Info Endpoint URL", "{userinfo}" ], [ "Issuer", "{issuer}" ], [ "Logout URL", "{logout}" ],
|
|
250
|
+
[ "Email Claim", "email" ], [ "Display Name Claim", "name" ], [ "Groups Claim", "groups" ],
|
|
251
|
+
[ "Callback URL", "{base}/login/{name}/callback" ]
|
|
252
|
+
]
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
key: "minio", name: "MinIO",
|
|
256
|
+
redirects: [ "{base}/oauth_callback" ],
|
|
257
|
+
scopes: %w[openid profile email groups],
|
|
258
|
+
guide: "https://min.io/docs/minio/linux/operations/external-iam/configure-openid-external-identity-management.html",
|
|
259
|
+
settings: [
|
|
260
|
+
[ "MINIO_IDENTITY_OPENID_CONFIG_URL", "{discovery}" ], [ "MINIO_IDENTITY_OPENID_CLIENT_ID", "{client_id}" ],
|
|
261
|
+
[ "MINIO_IDENTITY_OPENID_CLIENT_SECRET", "{client_secret}" ], [ "MINIO_IDENTITY_OPENID_SCOPES", "openid,profile,email,groups" ],
|
|
262
|
+
[ "MINIO_IDENTITY_OPENID_CLAIM_NAME", "groups" ], [ "MINIO_IDENTITY_OPENID_REDIRECT_URI", "{base}/oauth_callback" ],
|
|
263
|
+
[ "MINIO_IDENTITY_OPENID_DISPLAY_NAME", "{tenant}" ]
|
|
264
|
+
]
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
key: "argocd", name: "Argo CD",
|
|
268
|
+
redirects: [ "{base}/auth/callback", "{base}/api/dex/callback" ],
|
|
269
|
+
scopes: %w[openid profile email groups],
|
|
270
|
+
guide: "https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/#existing-oidc-provider",
|
|
271
|
+
settings: [
|
|
272
|
+
[ "argocd-cm oidc.config", "name: {tenant}\nissuer: {issuer}\nclientID: {client_id}\nclientSecret: {client_secret}\n" \
|
|
273
|
+
"requestedScopes: [openid, profile, email, groups]" ],
|
|
274
|
+
[ "argocd-rbac-cm policy.csv", "g, admins, role:admin" ], [ "argocd-rbac-cm scopes", "[groups]" ]
|
|
275
|
+
]
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
key: "synology", name: "Synology DSM",
|
|
279
|
+
redirects: [ "{base}", "{base}/" ],
|
|
280
|
+
scopes: %w[openid profile email],
|
|
281
|
+
guide: "https://kb.synology.com/en-global/DSM/help/DSM/AdminCenter/connection_sso_client",
|
|
282
|
+
settings: [
|
|
283
|
+
[ "Profile", "OIDC" ], [ "Name", "{tenant}" ], [ "Well Known URL", "{discovery}" ],
|
|
284
|
+
[ "Client ID", "{client_id}" ], [ "Client secret", "{client_secret}" ],
|
|
285
|
+
[ "Scope", "openid profile email" ], [ "Username claim", "preferred_username" ], [ "Redirect URI", "{base}" ]
|
|
286
|
+
]
|
|
185
287
|
}
|
|
186
288
|
].map(&:freeze).freeze
|
|
187
289
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "socket"
|
|
2
|
+
require "zlib"
|
|
3
|
+
|
|
4
|
+
module Masks
|
|
5
|
+
module Server
|
|
6
|
+
class BootTask
|
|
7
|
+
WINDOW = 10.minutes
|
|
8
|
+
|
|
9
|
+
Outcome = Struct.new(:name, :host, :value, :done) do
|
|
10
|
+
def done? = done
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def once(name, fingerprint: nil)
|
|
15
|
+
key = "masks:boot:#{name}:#{Digest::SHA256.hexdigest(fingerprint.to_s)[0, 16]}"
|
|
16
|
+
host = Socket.gethostname
|
|
17
|
+
outcome = nil
|
|
18
|
+
|
|
19
|
+
ActiveRecord::Base.transaction do
|
|
20
|
+
ActiveRecord::Base.connection.execute("SELECT pg_advisory_xact_lock(#{lock_id(name)})")
|
|
21
|
+
done_by = ::Rails.cache.read(key)
|
|
22
|
+
|
|
23
|
+
if done_by
|
|
24
|
+
outcome = Outcome.new(name, done_by, nil, false)
|
|
25
|
+
else
|
|
26
|
+
value = yield
|
|
27
|
+
::Rails.cache.write(key, host, expires_in: WINDOW)
|
|
28
|
+
outcome = Outcome.new(name, host, value, true)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
outcome
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def lock_id(name)
|
|
36
|
+
Zlib.crc32("masks:boot:#{name}")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -82,6 +82,8 @@ module Masks
|
|
|
82
82
|
validate :resources_are_named_only_when_approved
|
|
83
83
|
validates :access_token_lifetime, numericality: { only_integer: true, in: 60..1.day.to_i }, allow_nil: true
|
|
84
84
|
validates :refresh_token_lifetime, numericality: { only_integer: true, in: 5.minutes.to_i..400.days.to_i }, allow_nil: true
|
|
85
|
+
validates :refresh_token_absolute_lifetime, numericality: { only_integer: true, in: 5.minutes.to_i..400.days.to_i },
|
|
86
|
+
allow_nil: true
|
|
85
87
|
validates :consent_lifetime, numericality: { only_integer: true, greater_than_or_equal_to: 300,
|
|
86
88
|
less_than_or_equal_to: Consent::LONGEST.to_i }, allow_nil: true
|
|
87
89
|
|
|
@@ -377,6 +379,11 @@ module Masks
|
|
|
377
379
|
|
|
378
380
|
def authenticate_secret(candidate)
|
|
379
381
|
return true if public?
|
|
382
|
+
|
|
383
|
+
secret_matches?(candidate) && !secret_expired?
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def secret_matches?(candidate)
|
|
380
387
|
return false unless secret?
|
|
381
388
|
return false if secret_digest.blank? || candidate.blank?
|
|
382
389
|
|
|
@@ -385,6 +392,10 @@ module Masks
|
|
|
385
392
|
false
|
|
386
393
|
end
|
|
387
394
|
|
|
395
|
+
def secret_expired?
|
|
396
|
+
secret_expires_at.present? && secret_expires_at.past?
|
|
397
|
+
end
|
|
398
|
+
|
|
388
399
|
def redirect_uri?(candidate)
|
|
389
400
|
redirect_uris.include?(candidate.to_s) || redirect_uris.any? { |registered| same_loopback?(registered, candidate.to_s) }
|
|
390
401
|
end
|
|
@@ -445,6 +456,10 @@ module Masks
|
|
|
445
456
|
refresh_token_lifetime&.seconds || RefreshToken.lifetime
|
|
446
457
|
end
|
|
447
458
|
|
|
459
|
+
def refresh_absolute_lifetime
|
|
460
|
+
refresh_token_absolute_lifetime&.seconds || RefreshToken.absolute_lifetime
|
|
461
|
+
end
|
|
462
|
+
|
|
448
463
|
def admits?(actor)
|
|
449
464
|
allowed_groups.empty? || actor.group_labels.intersect?(allowed_groups)
|
|
450
465
|
end
|
|
@@ -78,7 +78,7 @@ module Masks
|
|
|
78
78
|
raise Refused, "#{algorithm.presence || 'that'} is not an algorithm #{subject} may be signed with" unless ALGORITHMS.include?(algorithm)
|
|
79
79
|
|
|
80
80
|
verified = attempt(token, header, algorithm, fresh: false)
|
|
81
|
-
verified ||= attempt(token, header, algorithm, fresh: true) if remote? && unknown_kid?(header)
|
|
81
|
+
verified ||= attempt(token, header, algorithm, fresh: true) if remote? && unknown_kid?(header) && refetch_open?
|
|
82
82
|
|
|
83
83
|
verified || raise(Refused, "#{subject} was not signed by a key this client registered")
|
|
84
84
|
rescue JWT::DecodeError
|
|
@@ -127,9 +127,20 @@ module Masks
|
|
|
127
127
|
kid ? held.select { |jwk| jwk["kid"] == kid } : held
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
def refetch_open?
|
|
131
|
+
return false if ::Rails.cache.exist?(refetched_key)
|
|
132
|
+
|
|
133
|
+
::Rails.cache.write(refetched_key, Time.current.to_i, expires_in: LIFETIME)
|
|
134
|
+
true
|
|
135
|
+
end
|
|
136
|
+
|
|
130
137
|
def cache_key
|
|
131
138
|
"client-jwks:#{client.tenant_id}:#{client.id}:#{Digest::SHA256.hexdigest(client.jwks_uri.to_s)}"
|
|
132
139
|
end
|
|
140
|
+
|
|
141
|
+
def refetched_key
|
|
142
|
+
"#{cache_key}:refetched"
|
|
143
|
+
end
|
|
133
144
|
end
|
|
134
145
|
end
|
|
135
146
|
end
|
|
@@ -5,7 +5,7 @@ module Masks
|
|
|
5
5
|
|
|
6
6
|
TENANT_KEYS = %w[name accent_color groups apps].freeze
|
|
7
7
|
APP_KEYS = %w[preset url slug name client_id client_secret_env redirect_uris scopes allowed_groups
|
|
8
|
-
access_token_lifetime refresh_token_lifetime].freeze
|
|
8
|
+
access_token_lifetime refresh_token_lifetime refresh_token_absolute_lifetime].freeze
|
|
9
9
|
|
|
10
10
|
def self.apply!(path = ::Rails.configuration.masks.config_path)
|
|
11
11
|
return [] if path.blank?
|
|
@@ -92,6 +92,7 @@ module Masks
|
|
|
92
92
|
allowed_groups: Array(declared["allowed_groups"]),
|
|
93
93
|
access_token_lifetime: declared["access_token_lifetime"],
|
|
94
94
|
refresh_token_lifetime: declared["refresh_token_lifetime"],
|
|
95
|
+
refresh_token_absolute_lifetime: declared["refresh_token_absolute_lifetime"],
|
|
95
96
|
consent_required: false
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -18,6 +18,7 @@ module Masks
|
|
|
18
18
|
|
|
19
19
|
ACCOUNT_CREATED = "account.created".freeze
|
|
20
20
|
ACCOUNT_EXPORTED = "account.exported".freeze
|
|
21
|
+
NOTIFICATIONS_CHANGED = "notifications.changed".freeze
|
|
21
22
|
INVITATION_SENT = "invitation.sent".freeze
|
|
22
23
|
INVITATION_ACCEPTED = "invitation.accepted".freeze
|
|
23
24
|
|
|
@@ -145,6 +146,7 @@ module Masks
|
|
|
145
146
|
SIGNAL_STREAM_UPDATED = "signal_stream.updated".freeze
|
|
146
147
|
SIGNAL_STREAM_DELETED = "signal_stream.deleted".freeze
|
|
147
148
|
SIGNAL_UNDELIVERED = "signal.undelivered".freeze
|
|
149
|
+
TEXT_UNDELIVERED = "text.undelivered".freeze
|
|
148
150
|
SIGNAL_RECEIVED = "signal.received".freeze
|
|
149
151
|
|
|
150
152
|
ORGANIZATION_CREATED = "organization.created".freeze
|
|
@@ -166,10 +168,13 @@ module Masks
|
|
|
166
168
|
CUSTOM_DOMAIN_SERVED = "custom_domain.served".freeze
|
|
167
169
|
CUSTOM_DOMAIN_STOPPED = "custom_domain.stopped".freeze
|
|
168
170
|
|
|
171
|
+
JOB_FAILED = "job.failed".freeze
|
|
172
|
+
|
|
169
173
|
GRAVE = [
|
|
170
174
|
LOGIN_REFUSED, LOGIN_THROTTLED, LOGOUT_UNDELIVERED, REFRESH_REUSED, STREAM_FAILED,
|
|
171
175
|
DEVICE_BLOCKED, ACTOR_DELETED, AUTHENTICATOR_DISABLED, CONNECTION_REFUSED, DELEGATION_REFUSED,
|
|
172
|
-
EXCHANGE_REFUSED, SIGN_IN_RISKY, SIGNAL_UNDELIVERED, SIGNAL_RECEIVED, MEMBERSHIP_ROLE_KEPT,
|
|
176
|
+
EXCHANGE_REFUSED, SIGN_IN_RISKY, SIGNAL_UNDELIVERED, TEXT_UNDELIVERED, SIGNAL_RECEIVED, MEMBERSHIP_ROLE_KEPT,
|
|
177
|
+
ORGANIZATION_OWNERLESS, JOB_FAILED
|
|
173
178
|
].freeze
|
|
174
179
|
|
|
175
180
|
ACTIONS = constants(false).filter_map do |name|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
module JobFailures
|
|
4
|
+
KEEP = 2.days
|
|
5
|
+
PREFIX = "masks:jobs:failed_tenants:".freeze
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def record(job, subdomains)
|
|
10
|
+
::Rails.cache.write("#{PREFIX}#{job}", subdomains.sort, expires_in: KEEP)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def last(job)
|
|
14
|
+
::Rails.cache.read("#{PREFIX}#{job}") || []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def named(jobs)
|
|
18
|
+
jobs.map do |job|
|
|
19
|
+
tenants = last(job)
|
|
20
|
+
tenants.empty? ? job : "#{job} (#{tenants.join(', ')})"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -51,6 +51,10 @@ module Masks
|
|
|
51
51
|
|
|
52
52
|
MAILED = GROUPS.values.flatten.freeze
|
|
53
53
|
|
|
54
|
+
ALWAYS = [ Event::PASSWORD_CHANGED, Event::PASSWORD_RESET_COMPLETED, Event::SIGN_IN_RISKY ].freeze
|
|
55
|
+
|
|
56
|
+
MUTABLE = (MAILED - ALWAYS).freeze
|
|
57
|
+
|
|
54
58
|
ONCE_PER_DEVICE = [ Event::SESSION_STARTED ].freeze
|
|
55
59
|
|
|
56
60
|
class << self
|
|
@@ -58,6 +62,10 @@ module Masks
|
|
|
58
62
|
MAILED.include?(action.to_s)
|
|
59
63
|
end
|
|
60
64
|
|
|
65
|
+
def always?(action)
|
|
66
|
+
ALWAYS.include?(action.to_s)
|
|
67
|
+
end
|
|
68
|
+
|
|
61
69
|
def raised(event)
|
|
62
70
|
return false unless mailed?(event.action) || WARNS_PREVIOUS.include?(event.action)
|
|
63
71
|
return false unless event.actor_id && ActorMailer.deliverable?
|
|
@@ -39,6 +39,14 @@ module Masks
|
|
|
39
39
|
held("max_age")
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
def refuse!(error, description)
|
|
43
|
+
update!(payload: (payload || {}).merge("refusal" => { "error" => error, "description" => description }))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def refusal
|
|
47
|
+
held("refusal")
|
|
48
|
+
end
|
|
49
|
+
|
|
42
50
|
def issue_code!(actor:, device: nil, session: nil, authenticated_at: nil, amr: nil, organization: nil)
|
|
43
51
|
AuthorizationCode.mint!(
|
|
44
52
|
organization: organization,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
class ReleasedNickname < ApplicationRecord
|
|
4
|
+
include TenantScoped
|
|
5
|
+
|
|
6
|
+
HOLD = 30.days
|
|
7
|
+
|
|
8
|
+
belongs_to :actor, optional: true
|
|
9
|
+
|
|
10
|
+
scope :holding, ->(nickname) { where(nickname: nickname.to_s.downcase).where("released_at > ?", HOLD.ago) }
|
|
11
|
+
scope :lapsed, -> { where(released_at: ...HOLD.ago) }
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def release!(nickname, actor:)
|
|
15
|
+
return if nickname.blank?
|
|
16
|
+
|
|
17
|
+
find_or_initialize_by(nickname: nickname.downcase).update!(actor: actor, released_at: Time.current)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def held_from?(nickname, actor)
|
|
21
|
+
holding(nickname).where.not(actor_id: actor.id).or(holding(nickname).where(actor_id: nil)).exists?
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
require "strscan"
|
|
2
|
+
|
|
1
3
|
module Masks
|
|
2
4
|
module Server
|
|
3
5
|
module Scim
|
|
4
6
|
class Filter
|
|
5
|
-
|
|
7
|
+
LONGEST = 512
|
|
8
|
+
TOKEN = /"(?:[^"\\]|\\.)*"|(?:[^\s"\[]|\[[^\]]*\])+/
|
|
9
|
+
PATH = /\A[A-Za-z][\w.:\[\]" ]*\z/
|
|
10
|
+
VALUE = /\A(?:"(?:[^"\\]|\\.)*"|true|false|null|[\d.]+)\z/
|
|
11
|
+
OPERATORS = %w[eq ne co sw ew pr gt ge lt le].freeze
|
|
6
12
|
ATTRIBUTES = {
|
|
7
13
|
"id" => "actors.uuid",
|
|
8
14
|
"username" => :user_name,
|
|
@@ -30,7 +36,9 @@ module Masks
|
|
|
30
36
|
end
|
|
31
37
|
|
|
32
38
|
def apply(relation)
|
|
33
|
-
|
|
39
|
+
raise Error.new(:bad_request, "a filter is at most #{LONGEST} characters", scim_type: "invalidFilter") if @expression.length > LONGEST
|
|
40
|
+
|
|
41
|
+
clauses = clauses_of(tokens)
|
|
34
42
|
|
|
35
43
|
raise Error.new(:bad_request, "at most four comparisons joined by and are understood", scim_type: "invalidFilter") if clauses.size > 4
|
|
36
44
|
|
|
@@ -39,12 +47,44 @@ module Masks
|
|
|
39
47
|
|
|
40
48
|
private
|
|
41
49
|
|
|
50
|
+
def tokens
|
|
51
|
+
scanner = StringScanner.new(@expression)
|
|
52
|
+
held = []
|
|
53
|
+
|
|
54
|
+
until scanner.eos?
|
|
55
|
+
next if scanner.skip(/\s+/)
|
|
56
|
+
|
|
57
|
+
token = scanner.scan(TOKEN)
|
|
58
|
+
|
|
59
|
+
raise unreadable(@expression) if token.nil?
|
|
60
|
+
|
|
61
|
+
held << token
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
held
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def clauses_of(tokens)
|
|
68
|
+
clauses = [ [] ]
|
|
69
|
+
|
|
70
|
+
tokens.each do |token|
|
|
71
|
+
token.casecmp?("and") ? clauses << [] : clauses.last << token
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
clauses
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def unreadable(clause)
|
|
78
|
+
Error.new(:bad_request, "#{clause.strip} is not a filter this server reads", scim_type: "invalidFilter")
|
|
79
|
+
end
|
|
80
|
+
|
|
42
81
|
def compare(relation, clause)
|
|
43
|
-
|
|
82
|
+
path, operator, raw = clause
|
|
44
83
|
|
|
45
|
-
raise
|
|
84
|
+
raise unreadable(clause.join(" ")) unless clause.size.between?(2, 3)
|
|
85
|
+
raise unreadable(clause.join(" ")) unless PATH.match?(path) && OPERATORS.include?(operator.downcase)
|
|
86
|
+
raise unreadable(clause.join(" ")) unless raw.nil? || VALUE.match?(raw)
|
|
46
87
|
|
|
47
|
-
path, operator, raw = match.captures
|
|
48
88
|
key = normalized(path)
|
|
49
89
|
column = @columns[key]
|
|
50
90
|
operator = operator.downcase
|
|
@@ -72,11 +72,10 @@ module Masks
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def first_factored!(held, at: nil)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
changes.merge!(first_factor: held["factor"], provider_id: held["provider_id"]) if held["factor"].present? && held != first_factored_by
|
|
75
|
+
self.authenticated_at = at if at && at > authenticated_at
|
|
76
|
+
assign_attributes(first_factor: held["factor"], provider_id: held["provider_id"]) if held["factor"].present?
|
|
78
77
|
|
|
79
|
-
|
|
78
|
+
save! if changed?
|
|
80
79
|
end
|
|
81
80
|
|
|
82
81
|
def bound?
|