masks-server 0.8.0 → 0.10.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 +4 -3
- data/app/controllers/concerns/masks/server/resource_token.rb +12 -3
- data/app/controllers/masks/server/addresses_controller.rb +6 -1
- data/app/controllers/masks/server/application_controller.rb +36 -6
- data/app/controllers/masks/server/authorize_controller.rb +24 -19
- data/app/controllers/masks/server/device_verifications_controller.rb +1 -1
- data/app/controllers/masks/server/logins_controller.rb +3 -1
- data/app/controllers/masks/server/notifications_controller.rb +11 -1
- data/app/controllers/masks/server/profiles_controller.rb +5 -0
- data/app/controllers/masks/server/registrations_controller.rb +9 -1
- data/app/controllers/masks/server/saml_controller.rb +1 -7
- data/app/controllers/masks/server/scim/metadata_controller.rb +1 -1
- data/app/controllers/masks/server/tokens_controller.rb +9 -1
- data/app/graphql/masks/server/manage/mutations/add_app.rb +1 -1
- data/app/graphql/masks/server/manage/mutations/create_actor.rb +1 -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 +2 -0
- data/app/graphql/masks/server/manage/types/query_type.rb +7 -0
- data/app/helpers/masks/server/account_helper.rb +1 -1
- data/app/jobs/masks/server/cleanup_job.rb +1 -0
- 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 +28 -5
- data/app/models/masks/server/client.rb +61 -6
- data/app/models/masks/server/client_keys.rb +12 -1
- data/app/models/masks/server/declared.rb +2 -1
- data/app/models/masks/server/event.rb +1 -0
- data/app/models/masks/server/handshake.rb +12 -6
- data/app/models/masks/server/login.rb +3 -6
- data/app/models/masks/server/notifications.rb +8 -0
- data/app/models/masks/server/pending_handshake.rb +2 -1
- 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 +6 -5
- data/app/policies/masks/server/exchange_policy.rb +1 -1
- data/app/policies/masks/server/grant_checks.rb +1 -1
- data/app/views/masks/server/account/_info.html.erb +13 -5
- data/app/views/masks/server/account/_organizations.html.erb +4 -4
- data/app/views/masks/server/account/index.html.erb +1 -1
- data/app/views/masks/server/application/_actor.html.erb +27 -0
- data/app/views/masks/server/authorize/error.html.erb +2 -2
- data/app/views/masks/server/handshakes/refused.html.erb +1 -1
- data/app/views/masks/server/handshakes/show.html.erb +1 -1
- data/app/views/masks/server/logins/_choose_organization.html.erb +1 -1
- data/app/views/masks/server/logins/_consent.html.erb +1 -1
- data/app/views/masks/server/sessions/confirm.html.erb +1 -1
- data/config/locales/de/account.yml +6 -2
- data/config/locales/de/actors.yml +2 -0
- data/config/locales/de/application.yml +1 -1
- data/config/locales/de/authorize.yml +2 -0
- data/config/locales/de/events.yml +7 -5
- data/config/locales/de/flashes.yml +1 -0
- data/config/locales/de/logins.yml +1 -0
- data/config/locales/de/manage/client.yml +3 -0
- data/config/locales/de/manage/groups.yml +1 -1
- data/config/locales/de/manage/ldap.yml +1 -0
- data/config/locales/de/manage/organization.yml +1 -1
- data/config/locales/de/notifications.yml +1 -0
- data/config/locales/en/account.yml +6 -2
- data/config/locales/en/actors.yml +2 -0
- data/config/locales/en/application.yml +1 -1
- data/config/locales/en/authorize.yml +2 -0
- data/config/locales/en/events.yml +7 -5
- 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/groups.yml +1 -1
- data/config/locales/en/manage/ldap.yml +1 -0
- data/config/locales/en/manage/organization.yml +1 -1
- data/config/locales/en/notifications.yml +1 -0
- data/config/locales/es/account.yml +6 -2
- data/config/locales/es/actors.yml +2 -0
- data/config/locales/es/application.yml +1 -1
- data/config/locales/es/authorize.yml +2 -0
- data/config/locales/es/events.yml +7 -5
- data/config/locales/es/flashes.yml +1 -0
- data/config/locales/es/logins.yml +1 -0
- data/config/locales/es/manage/client.yml +3 -0
- data/config/locales/es/manage/groups.yml +1 -1
- data/config/locales/es/manage/ldap.yml +1 -0
- data/config/locales/es/manage/organization.yml +1 -1
- data/config/locales/es/notifications.yml +1 -0
- data/config/locales/fr/account.yml +6 -2
- data/config/locales/fr/actors.yml +2 -0
- data/config/locales/fr/application.yml +1 -1
- data/config/locales/fr/authorize.yml +2 -0
- data/config/locales/fr/events.yml +7 -5
- data/config/locales/fr/flashes.yml +1 -0
- data/config/locales/fr/logins.yml +1 -0
- data/config/locales/fr/manage/client.yml +3 -0
- data/config/locales/fr/manage/groups.yml +1 -1
- data/config/locales/fr/manage/ldap.yml +1 -0
- data/config/locales/fr/manage/organization.yml +1 -1
- data/config/locales/fr/notifications.yml +1 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20261013010000_add_handshake_return_to_to_clients.rb +5 -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/lib/generators/masks/server/install/install_generator.rb +2 -2
- data/lib/masks/server/browser_access.rb +35 -0
- data/lib/masks/server/configuration.rb +11 -3
- data/lib/masks/server/engine.rb +1 -0
- data/lib/masks/server/schema_reference.rb +17 -4
- data/lib/masks/server/tenancy/middleware.rb +9 -1
- data/lib/masks/server.rb +1 -0
- data/public/masks-assets/.vite/manifest.json +7 -7
- data/public/masks-assets/assets/app-D7W0gAyT.css +2 -0
- data/public/masks-assets/assets/{initial-D99AzFOd.js → initial-C1GpCiEh.js} +2 -2
- data/public/masks-assets/assets/{initial-D99AzFOd.js.map → initial-C1GpCiEh.js.map} +1 -1
- data/public/masks-assets/assets/login-C_6igRZ3.js +4 -0
- data/public/masks-assets/assets/login-C_6igRZ3.js.map +1 -0
- data/public/masks-assets/assets/manage-DR9qQiBU.js +569 -0
- data/public/masks-assets/assets/manage-DR9qQiBU.js.map +1 -0
- data/version.txt +1 -1
- metadata +14 -9
- data/app/views/masks/server/application/_person.html.erb +0 -27
- data/public/masks-assets/assets/app-DoQZrM_J.css +0 -2
- data/public/masks-assets/assets/login-CNs1UpxJ.js +0 -4
- data/public/masks-assets/assets/login-CNs1UpxJ.js.map +0 -1
- data/public/masks-assets/assets/manage-isLCrWiU.js +0 -567
- data/public/masks-assets/assets/manage-isLCrWiU.js.map +0 -1
|
@@ -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
|
|
|
@@ -190,14 +195,14 @@ module Masks
|
|
|
190
195
|
holds?(Scopes::MANAGE)
|
|
191
196
|
end
|
|
192
197
|
|
|
193
|
-
def
|
|
198
|
+
def card_json
|
|
194
199
|
style = Avatars.held?(self) ? Avatars::PHOTO : Avatars::FALLBACK
|
|
195
200
|
|
|
196
201
|
{
|
|
197
202
|
"name" => display_name,
|
|
198
203
|
"details" => display_details,
|
|
199
|
-
"note" => (I18n.t("application.
|
|
200
|
-
"role" => (I18n.t("application.
|
|
204
|
+
"note" => (I18n.t("application.actor.unconfirmed") if email_unconfirmed?),
|
|
205
|
+
"role" => (I18n.t("application.actor.manager") if manages?),
|
|
201
206
|
"avatar" => Avatars.url(self, style, subject: uuid, size: 128)
|
|
202
207
|
}
|
|
203
208
|
end
|
|
@@ -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?
|
|
@@ -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
|
|
|
@@ -123,7 +125,7 @@ module Masks
|
|
|
123
125
|
{
|
|
124
126
|
name: handshake.name,
|
|
125
127
|
redirect_uris: handshake.redirect_uris,
|
|
126
|
-
post_logout_redirect_uris: [ handshake.
|
|
128
|
+
post_logout_redirect_uris: [ handshake.post_logout_redirect_uri ].compact,
|
|
127
129
|
resources: [ handshake.resource ],
|
|
128
130
|
allowed_scopes: Scopes.join(handshake.scopes),
|
|
129
131
|
grant_types: handshake.grant_types,
|
|
@@ -131,6 +133,7 @@ module Masks
|
|
|
131
133
|
token_endpoint_auth_method: handshake.auth_method,
|
|
132
134
|
backchannel_logout_uri: handshake.backchannel_logout_uri,
|
|
133
135
|
client_uri: handshake.origin,
|
|
136
|
+
handshake_return_to: handshake.return_to,
|
|
134
137
|
dynamic: false
|
|
135
138
|
}
|
|
136
139
|
end
|
|
@@ -169,8 +172,7 @@ module Masks
|
|
|
169
172
|
name: held[:name] || name,
|
|
170
173
|
grant_types: grant_types,
|
|
171
174
|
response_types: held[:response_types].presence || response_types_for(grant_types),
|
|
172
|
-
allowed_scopes: Scopes.join(
|
|
173
|
-
Current.tenant&.dynamic_client_ceiling || DEFAULT_SCOPES)),
|
|
175
|
+
allowed_scopes: Scopes.join(declared_scopes(metadata)),
|
|
174
176
|
dynamic: true
|
|
175
177
|
)
|
|
176
178
|
end
|
|
@@ -206,6 +208,12 @@ module Masks
|
|
|
206
208
|
).issue_credentials!
|
|
207
209
|
end
|
|
208
210
|
|
|
211
|
+
def declared_scopes(metadata)
|
|
212
|
+
declared = metadata.to_h.stringify_keys["scope"]
|
|
213
|
+
|
|
214
|
+
Scopes.list(declared).empty? ? [] : bounded(declared)
|
|
215
|
+
end
|
|
216
|
+
|
|
209
217
|
def bounded(requested)
|
|
210
218
|
reserved = Scopes.reserved(requested) + Scopes.list(requested).select { |s| Scopes.prefix?(s) }
|
|
211
219
|
|
|
@@ -371,6 +379,11 @@ module Masks
|
|
|
371
379
|
|
|
372
380
|
def authenticate_secret(candidate)
|
|
373
381
|
return true if public?
|
|
382
|
+
|
|
383
|
+
secret_matches?(candidate) && !secret_expired?
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def secret_matches?(candidate)
|
|
374
387
|
return false unless secret?
|
|
375
388
|
return false if secret_digest.blank? || candidate.blank?
|
|
376
389
|
|
|
@@ -379,8 +392,12 @@ module Masks
|
|
|
379
392
|
false
|
|
380
393
|
end
|
|
381
394
|
|
|
395
|
+
def secret_expired?
|
|
396
|
+
secret_expires_at.present? && secret_expires_at.past?
|
|
397
|
+
end
|
|
398
|
+
|
|
382
399
|
def redirect_uri?(candidate)
|
|
383
|
-
redirect_uris.include?(candidate.to_s)
|
|
400
|
+
redirect_uris.include?(candidate.to_s) || redirect_uris.any? { |registered| same_loopback?(registered, candidate.to_s) }
|
|
384
401
|
end
|
|
385
402
|
|
|
386
403
|
def grants?(grant_type)
|
|
@@ -424,7 +441,11 @@ module Masks
|
|
|
424
441
|
end
|
|
425
442
|
|
|
426
443
|
def scope_list
|
|
427
|
-
Scopes.union(required_scopes,
|
|
444
|
+
Scopes.union(required_scopes, offered_scopes)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def follows_registration?
|
|
448
|
+
dynamic? && Scopes.list(allowed_scopes).empty?
|
|
428
449
|
end
|
|
429
450
|
|
|
430
451
|
def access_lifetime
|
|
@@ -435,6 +456,10 @@ module Masks
|
|
|
435
456
|
refresh_token_lifetime&.seconds || RefreshToken.lifetime
|
|
436
457
|
end
|
|
437
458
|
|
|
459
|
+
def refresh_absolute_lifetime
|
|
460
|
+
refresh_token_absolute_lifetime&.seconds || RefreshToken.absolute_lifetime
|
|
461
|
+
end
|
|
462
|
+
|
|
438
463
|
def admits?(actor)
|
|
439
464
|
allowed_groups.empty? || actor.group_labels.intersect?(allowed_groups)
|
|
440
465
|
end
|
|
@@ -442,7 +467,19 @@ module Masks
|
|
|
442
467
|
def permitted_scopes(requested)
|
|
443
468
|
return scope_list if Scopes.list(requested).empty?
|
|
444
469
|
|
|
445
|
-
Scopes.union(required_scopes,
|
|
470
|
+
return Scopes.union(required_scopes, unreserved(requested)) if open_to_anything?
|
|
471
|
+
|
|
472
|
+
Scopes.union(required_scopes, Scopes.granted(requested, offered_scopes))
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def refused_scopes(requested)
|
|
476
|
+
Scopes.list(requested) - permitted_scopes(requested)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def offered_scopes
|
|
480
|
+
return Scopes.list(allowed_scopes) unless follows_registration?
|
|
481
|
+
|
|
482
|
+
Current.tenant&.dynamic_client_ceiling || DEFAULT_SCOPES
|
|
446
483
|
end
|
|
447
484
|
|
|
448
485
|
def metadata
|
|
@@ -544,6 +581,24 @@ module Masks
|
|
|
544
581
|
self.class.loopback?(uri)
|
|
545
582
|
end
|
|
546
583
|
|
|
584
|
+
def open_to_anything?
|
|
585
|
+
follows_registration? && Current.tenant&.dynamic_registration == Tenant::REGISTRATION_ANYTHING
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def unreserved(requested)
|
|
589
|
+
Scopes.list(requested).reject { |scope| Scopes.prefix?(scope) || Scopes.reserved(scope).any? }
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def same_loopback?(registered, candidate)
|
|
593
|
+
held = URI.parse(registered)
|
|
594
|
+
asked = URI.parse(candidate)
|
|
595
|
+
|
|
596
|
+
[ held, asked ].all? { |uri| uri.instance_of?(URI::HTTP) && LOOPBACK.include?(uri.hostname) && uri.userinfo.nil? && uri.fragment.nil? } &&
|
|
597
|
+
held.hostname == asked.hostname && held.path == asked.path && held.query == asked.query
|
|
598
|
+
rescue URI::InvalidURIError
|
|
599
|
+
false
|
|
600
|
+
end
|
|
601
|
+
|
|
547
602
|
def grant_types_are_known
|
|
548
603
|
unknown = grant_types - GRANT_TYPES
|
|
549
604
|
errors.add(:grant_types, "not supported: #{unknown.join(', ')}") if unknown.any?
|
|
@@ -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
|
|
|
@@ -4,7 +4,7 @@ module Masks
|
|
|
4
4
|
GRANT_TYPES = %w[authorization_code refresh_token].freeze
|
|
5
5
|
|
|
6
6
|
attr_reader :name, :redirect_uris, :resource, :scopes, :return_to, :state, :auth_method,
|
|
7
|
-
:backchannel_logout_uri
|
|
7
|
+
:backchannel_logout_uri, :post_logout_redirect_uri
|
|
8
8
|
|
|
9
9
|
class << self
|
|
10
10
|
def from_request(request)
|
|
@@ -19,7 +19,8 @@ module Masks
|
|
|
19
19
|
return_to: params["return_to"],
|
|
20
20
|
state: params["state"],
|
|
21
21
|
auth_method: params["token_endpoint_auth_method"],
|
|
22
|
-
backchannel_logout_uri: params["backchannel_logout_uri"]
|
|
22
|
+
backchannel_logout_uri: params["backchannel_logout_uri"],
|
|
23
|
+
post_logout_redirect_uri: params["post_logout_redirect_uri"]
|
|
23
24
|
)
|
|
24
25
|
end
|
|
25
26
|
|
|
@@ -36,7 +37,8 @@ module Masks
|
|
|
36
37
|
return_to: row.redirect_uri,
|
|
37
38
|
state: held["state"],
|
|
38
39
|
auth_method: held["auth_method"],
|
|
39
|
-
backchannel_logout_uri: held["backchannel_logout_uri"]
|
|
40
|
+
backchannel_logout_uri: held["backchannel_logout_uri"],
|
|
41
|
+
post_logout_redirect_uri: held["post_logout_redirect_uri"]
|
|
40
42
|
)
|
|
41
43
|
end
|
|
42
44
|
end
|
|
@@ -54,7 +56,8 @@ module Masks
|
|
|
54
56
|
"state" => state,
|
|
55
57
|
"redirect_uris" => redirect_uris.sort,
|
|
56
58
|
"auth_method" => auth_method,
|
|
57
|
-
"backchannel_logout_uri" => backchannel_logout_uri
|
|
59
|
+
"backchannel_logout_uri" => backchannel_logout_uri,
|
|
60
|
+
"post_logout_redirect_uri" => post_logout_redirect_uri
|
|
58
61
|
}.compact
|
|
59
62
|
end
|
|
60
63
|
|
|
@@ -73,12 +76,14 @@ module Masks
|
|
|
73
76
|
|
|
74
77
|
pairs << [ "state", state ] if state
|
|
75
78
|
pairs << [ "backchannel_logout_uri", backchannel_logout_uri ] if backchannel_logout_uri
|
|
79
|
+
pairs << [ "post_logout_redirect_uri", post_logout_redirect_uri ] if post_logout_redirect_uri
|
|
76
80
|
redirect_uris.each { |uri| pairs << [ "redirect_uris", uri ] }
|
|
77
81
|
pairs
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
def initialize(name: nil, redirect_uris: nil, resource: nil, scopes: nil,
|
|
81
|
-
return_to: nil, state: nil, auth_method: nil, backchannel_logout_uri: nil
|
|
85
|
+
return_to: nil, state: nil, auth_method: nil, backchannel_logout_uri: nil,
|
|
86
|
+
post_logout_redirect_uri: nil)
|
|
82
87
|
@name = name.to_s.strip.presence || "An application"
|
|
83
88
|
@redirect_uris = Array(redirect_uris).map(&:to_s).reject(&:empty?).uniq
|
|
84
89
|
@resource = resource.to_s
|
|
@@ -87,6 +92,7 @@ module Masks
|
|
|
87
92
|
@state = state.presence
|
|
88
93
|
@auth_method = auth_method.to_s.presence || Client::DEFAULT_AUTH_METHOD
|
|
89
94
|
@backchannel_logout_uri = backchannel_logout_uri.to_s.strip.presence
|
|
95
|
+
@post_logout_redirect_uri = post_logout_redirect_uri.to_s.strip.presence
|
|
90
96
|
end
|
|
91
97
|
|
|
92
98
|
def origin
|
|
@@ -134,7 +140,7 @@ module Masks
|
|
|
134
140
|
return "token_endpoint_auth_method must be one of #{Client::AUTH_METHODS.join(', ')}"
|
|
135
141
|
end
|
|
136
142
|
|
|
137
|
-
elsewhere = ([ return_to ] + redirect_uris + Array(backchannel_logout_uri))
|
|
143
|
+
elsewhere = ([ return_to ] + redirect_uris + Array(backchannel_logout_uri) + Array(post_logout_redirect_uri))
|
|
138
144
|
.reject { |uri| origin_of(uri) == origin }
|
|
139
145
|
return "everything must share the origin #{origin}: #{elsewhere.join(', ')}" if elsewhere.any?
|
|
140
146
|
|
|
@@ -416,8 +416,7 @@ module Masks
|
|
|
416
416
|
"rid" => rid,
|
|
417
417
|
"docs" => ::Rails.configuration.masks.docs_url,
|
|
418
418
|
"actor" => actor && { "nickname" => actor.nickname, "name" => actor.name,
|
|
419
|
-
"identifier" => actor.identifier },
|
|
420
|
-
"person" => person_json,
|
|
419
|
+
"identifier" => actor.identifier, "card" => card_json },
|
|
421
420
|
"client" => client_json,
|
|
422
421
|
"tenant" => tenant && {
|
|
423
422
|
"name" => tenant.name, "logo" => tenant.logo_url
|
|
@@ -429,10 +428,8 @@ module Masks
|
|
|
429
428
|
answering.reduce(base) { |json, state| state.enabled? ? json.merge(state.as_json) : json }
|
|
430
429
|
end
|
|
431
430
|
|
|
432
|
-
def
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
actor.person_json
|
|
431
|
+
def card_json
|
|
432
|
+
actor.card_json if authenticated_at
|
|
436
433
|
end
|
|
437
434
|
|
|
438
435
|
def forget_vanished_actor!
|
|
@@ -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?
|
|
@@ -19,7 +19,8 @@ module Masks
|
|
|
19
19
|
"redirect_uris" => handshake.redirect_uris,
|
|
20
20
|
"state" => handshake.state,
|
|
21
21
|
"auth_method" => handshake.auth_method,
|
|
22
|
-
"backchannel_logout_uri" => handshake.backchannel_logout_uri
|
|
22
|
+
"backchannel_logout_uri" => handshake.backchannel_logout_uri,
|
|
23
|
+
"post_logout_redirect_uri" => handshake.post_logout_redirect_uri
|
|
23
24
|
}.compact
|
|
24
25
|
)
|
|
25
26
|
end
|
|
@@ -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
|
|
@@ -23,7 +23,7 @@ module Masks
|
|
|
23
23
|
|
|
24
24
|
class << self
|
|
25
25
|
def start!(actor:, device: nil, user_agent: nil, ip_address: nil, amr: [], origin: nil, policy: nil,
|
|
26
|
-
first_factor: {})
|
|
26
|
+
first_factor: {}, authenticated_at: nil)
|
|
27
27
|
secret = SecureRandom.urlsafe_base64(48)
|
|
28
28
|
lifetime = policy&.session_lifetime&.seconds || LIFETIME
|
|
29
29
|
now = Time.current
|
|
@@ -36,7 +36,7 @@ module Masks
|
|
|
36
36
|
user_agent: user_agent,
|
|
37
37
|
ip_address: ip_address,
|
|
38
38
|
origin: origin.presence || Current.origin,
|
|
39
|
-
authenticated_at: now,
|
|
39
|
+
authenticated_at: authenticated_at || now,
|
|
40
40
|
last_seen_at: now,
|
|
41
41
|
amr: Array(amr),
|
|
42
42
|
first_factor: first_factor["factor"],
|
|
@@ -71,10 +71,11 @@ module Masks
|
|
|
71
71
|
{ "factor" => first_factor, "provider_id" => provider_id }.compact
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
def first_factored!(held)
|
|
75
|
-
|
|
74
|
+
def first_factored!(held, at: nil)
|
|
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?
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
save! if changed?
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
def bound?
|
|
@@ -85,7 +85,7 @@ module Masks
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def scopes_stay_within_the_client
|
|
88
|
-
beyond =
|
|
88
|
+
beyond = client.refused_scopes(requested_scopes)
|
|
89
89
|
|
|
90
90
|
deny!("invalid_scope", "this client may not hold #{beyond.join(', ')}") if beyond.any?
|
|
91
91
|
end
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<div class="entries">
|
|
5
5
|
<details class="entry" id="profile">
|
|
6
6
|
<summary>
|
|
7
|
-
<span class="entry-icon"><%= account_icon(:
|
|
7
|
+
<span class="entry-icon"><%= account_icon(:actor) %></span>
|
|
8
8
|
<span class="entry-label"><%= account_text("profile.heading") %></span>
|
|
9
9
|
<span class="entry-state"><%= actor.name.presence || actor.nickname %></span>
|
|
10
10
|
<span class="entry-mark" aria-hidden="true"></span>
|
|
@@ -46,7 +46,11 @@
|
|
|
46
46
|
</summary>
|
|
47
47
|
|
|
48
48
|
<div class="entry-body">
|
|
49
|
-
<p class="aside"><%= account_text("notifications.lede") %></p>
|
|
49
|
+
<p class="aside"><%= account_text("notifications.lede") %> <%= account_text("notifications.kept") %></p>
|
|
50
|
+
|
|
51
|
+
<% unless fresh %>
|
|
52
|
+
<p class="aside"><%= account_text("notifications.again") %></p>
|
|
53
|
+
<% end %>
|
|
50
54
|
|
|
51
55
|
<% unless Masks::Server::Notifications.mailable?(actor) %>
|
|
52
56
|
<p class="aside aside-bad"><%= account_text("notifications.silent") %></p>
|
|
@@ -63,8 +67,12 @@
|
|
|
63
67
|
<% actions.each do |action| %>
|
|
64
68
|
<label class="switch">
|
|
65
69
|
<span><%= t("events.actions.#{action}") %></span>
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
<% if Masks::Server::Notifications.always?(action) %>
|
|
71
|
+
<span class="switch-kept"><%= account_text("notifications.always") %></span>
|
|
72
|
+
<% else %>
|
|
73
|
+
<%= check_box_tag "notifications[]", action, actor.notified?(action),
|
|
74
|
+
id: "notify-#{action.tr('.', '-')}" %>
|
|
75
|
+
<% end %>
|
|
68
76
|
</label>
|
|
69
77
|
<% end %>
|
|
70
78
|
</div>
|
|
@@ -72,7 +80,7 @@
|
|
|
72
80
|
<% end %>
|
|
73
81
|
|
|
74
82
|
<div class="action-row">
|
|
75
|
-
<button type="submit" class="action action-sm"><%= account_text("notifications.submit") %></button>
|
|
83
|
+
<button type="submit" class="action action-sm"><%= account_text(fresh ? "notifications.submit" : "notifications.submit_again") %></button>
|
|
76
84
|
</div>
|
|
77
85
|
<% end %>
|
|
78
86
|
</div>
|