masks-server 0.9.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 +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/logins_controller.rb +2 -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/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/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/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 +25 -2
- 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/event.rb +1 -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 +2 -0
- 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/ldap.yml +1 -0
- 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 +2 -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/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 +2 -0
- 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/ldap.yml +1 -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 +2 -0
- 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/ldap.yml +1 -0
- data/config/locales/fr/notifications.yml +1 -0
- data/config/routes.rb +1 -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/masks/server/configuration.rb +8 -0
- data/lib/masks/server/tenancy/middleware.rb +9 -1
- 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-DR9qQiBU.js +569 -0
- data/public/masks-assets/assets/manage-DR9qQiBU.js.map +1 -0
- data/version.txt +1 -1
- metadata +7 -4
- data/public/masks-assets/assets/manage-Dbfiy3Mj.js +0 -567
- data/public/masks-assets/assets/manage-Dbfiy3Mj.js.map +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 544dc01d0600507eb713667f0d8cf57275b4b7d957b603e3085f9e197e7e63c6
|
|
4
|
+
data.tar.gz: 70f664c959b40e98e9a273582a45c0dba352e5186430a72c26e3a8dd87db978b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5c6177ca713658c3003f33cea0b8902c6810266271f4d4e6d0215d8a291b6da13a47add90dca3b65df3f9614f6161dd5627853c3e9038ddafffcc0963ce27d4
|
|
7
|
+
data.tar.gz: 872c0d7ce2bd4090f885fdbce3eb82d8459ff73840ac89d3bd9cd2088ae334dc1efb2c34fe7bc17a2f413492d07c15c74a69f73b771f98847a4a69ae10416228
|
|
@@ -16,9 +16,10 @@ module Masks
|
|
|
16
16
|
client_refused!("no client is registered with that client_id") if client.nil?
|
|
17
17
|
client_refused!("this client authenticates with a signed assertion, not a secret") if client.asserts?
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
return client if client.authenticate_secret(secret)
|
|
20
20
|
|
|
21
|
-
client
|
|
21
|
+
client_refused!("this client's secret expired on #{client.secret_expires_at.iso8601}, and a manager rotates it") if client.secret_expired? && client.secret_matches?(secret)
|
|
22
|
+
client_refused!
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def asserted_client(assertion)
|
|
@@ -8,6 +8,7 @@ module Masks
|
|
|
8
8
|
only: :update
|
|
9
9
|
|
|
10
10
|
before_action :require_actor
|
|
11
|
+
before_action :known_channel
|
|
11
12
|
before_action -> { reauthenticated!(anchor, t("addresses.again")) }
|
|
12
13
|
before_action :refuse_directed
|
|
13
14
|
|
|
@@ -55,7 +56,11 @@ module Masks
|
|
|
55
56
|
private
|
|
56
57
|
|
|
57
58
|
def channel
|
|
58
|
-
params[:channel].to_s
|
|
59
|
+
@channel ||= Addresses::CHANNELS.keys.find { |known| known == params[:channel].to_s }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def known_channel
|
|
63
|
+
head :not_found if channel.nil?
|
|
59
64
|
end
|
|
60
65
|
|
|
61
66
|
def anchor
|
|
@@ -264,6 +264,34 @@ module Masks
|
|
|
264
264
|
)
|
|
265
265
|
end
|
|
266
266
|
|
|
267
|
+
def answerable?(pending)
|
|
268
|
+
authorization = pending.authorization
|
|
269
|
+
|
|
270
|
+
authorization.client&.approved? || ours?(authorization.redirect_uri)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def refuse_on_masks(pending, refusal)
|
|
274
|
+
pending.refuse!(refusal.error, refusal.description)
|
|
275
|
+
PendingRequest.claim(rid_for(pending))
|
|
276
|
+
|
|
277
|
+
authorize_refused_path(rid: rid_for(pending))
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def refused_request(rid)
|
|
281
|
+
return nil if rid.blank? || !tracked(REQUESTS).value?(rid)
|
|
282
|
+
|
|
283
|
+
PendingRequest.spent(rid)&.then { |held| held if held.consumed? }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def ours?(target)
|
|
287
|
+
here = URI.parse(issuer.url)
|
|
288
|
+
there = URI.parse(target.to_s)
|
|
289
|
+
|
|
290
|
+
there.scheme == here.scheme && there.host == here.host && there.port == here.port
|
|
291
|
+
rescue URI::InvalidURIError
|
|
292
|
+
false
|
|
293
|
+
end
|
|
294
|
+
|
|
267
295
|
def authorize_url_for(pending)
|
|
268
296
|
"#{authorize_path}?#{URI.encode_www_form(pending.authorization.query_pairs)}"
|
|
269
297
|
end
|
|
@@ -303,7 +331,7 @@ module Masks
|
|
|
303
331
|
|
|
304
332
|
def settle!(login)
|
|
305
333
|
hold(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by,
|
|
306
|
-
authenticated_at: login.
|
|
334
|
+
authenticated_at: login.authenticated_at)
|
|
307
335
|
forget_login
|
|
308
336
|
end
|
|
309
337
|
|
|
@@ -45,6 +45,7 @@ module Masks
|
|
|
45
45
|
|
|
46
46
|
login = advance(pending)
|
|
47
47
|
|
|
48
|
+
return redirect_to(refuse_on_masks(pending, login.refusal)) if login.refused? && !answerable?(pending)
|
|
48
49
|
return answer(deny(pending, login.refusal.error, login.refusal.description)) if login.refused?
|
|
49
50
|
return interaction_required(pending) if login.prompted? && pending.silent?
|
|
50
51
|
return redirect_to(login.redirect_to, allow_other_host: true) if login.redirect_to.present?
|
|
@@ -53,6 +54,15 @@ module Masks
|
|
|
53
54
|
complete(pending, attempt, login)
|
|
54
55
|
end
|
|
55
56
|
|
|
57
|
+
def refused
|
|
58
|
+
refusal = refused_request(params[:rid])&.refusal
|
|
59
|
+
|
|
60
|
+
return show_error("invalid_request", "that sign-in request has expired", :bad_request) if refusal.nil?
|
|
61
|
+
|
|
62
|
+
@refused = true
|
|
63
|
+
show_error(refusal["error"], refusal["description"], :forbidden)
|
|
64
|
+
end
|
|
65
|
+
|
|
56
66
|
private
|
|
57
67
|
|
|
58
68
|
def validate(authorization)
|
|
@@ -66,6 +76,7 @@ module Masks
|
|
|
66
76
|
req.verify_redirect_uri!(client.redirect_uri?(req.redirect_uri) ? [ req.redirect_uri.to_s ] : client.redirect_uris)
|
|
67
77
|
req.verified_redirect_uri = with_issuer(req.verified_redirect_uri)
|
|
68
78
|
res.redirect_uri = req.verified_redirect_uri
|
|
79
|
+
req.verified_redirect_uri = nil unless hears_errors?(client, req.redirect_uri)
|
|
69
80
|
|
|
70
81
|
permit(req) { authorization.validate! }
|
|
71
82
|
end
|
|
@@ -97,7 +108,7 @@ module Masks
|
|
|
97
108
|
actor: current_actor,
|
|
98
109
|
device: current_device,
|
|
99
110
|
session: current_session,
|
|
100
|
-
authenticated_at:
|
|
111
|
+
authenticated_at: current_session&.authenticated_at,
|
|
101
112
|
amr: login.amr.presence || current_session&.amr,
|
|
102
113
|
organization: login.state("organization-choice").selected
|
|
103
114
|
)
|
|
@@ -121,21 +132,20 @@ module Masks
|
|
|
121
132
|
client = Client.authenticating(params[:client_id])
|
|
122
133
|
target = params[:redirect_uri].to_s
|
|
123
134
|
|
|
124
|
-
if target.present? && (client
|
|
135
|
+
if target.present? && hears_errors?(client, target)
|
|
125
136
|
return redirect_to refusal_uri(target, error), allow_other_host: true
|
|
126
137
|
end
|
|
127
138
|
|
|
128
|
-
|
|
129
|
-
|
|
139
|
+
show_error(error.error.to_s, error.description, error.status)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def show_error(code, description, status)
|
|
143
|
+
@error_code = code
|
|
144
|
+
@error_description = description
|
|
130
145
|
|
|
131
146
|
respond_to do |format|
|
|
132
|
-
format.html { render :error, status:
|
|
133
|
-
format.any
|
|
134
|
-
render json: {
|
|
135
|
-
"error" => @error_code,
|
|
136
|
-
"error_description" => @error_description
|
|
137
|
-
}, status: error.status
|
|
138
|
-
end
|
|
147
|
+
format.html { render :error, status: status }
|
|
148
|
+
format.any { render json: { "error" => code, "error_description" => description }, status: status }
|
|
139
149
|
end
|
|
140
150
|
end
|
|
141
151
|
|
|
@@ -163,13 +173,8 @@ module Masks
|
|
|
163
173
|
Rack::OAuth2::Server::Authorize::BadRequest.new(error, description)
|
|
164
174
|
end
|
|
165
175
|
|
|
166
|
-
def
|
|
167
|
-
|
|
168
|
-
there = URI.parse(target.to_s)
|
|
169
|
-
|
|
170
|
-
there.scheme == here.scheme && there.host == here.host && there.port == here.port
|
|
171
|
-
rescue URI::InvalidURIError
|
|
172
|
-
false
|
|
176
|
+
def hears_errors?(client, target)
|
|
177
|
+
ours?(target) || (client.present? && client.approved? && client.redirect_uri?(target))
|
|
173
178
|
end
|
|
174
179
|
|
|
175
180
|
def refusal_uri(target, error)
|
|
@@ -59,7 +59,7 @@ module Masks
|
|
|
59
59
|
actor: current_actor,
|
|
60
60
|
session: current_session,
|
|
61
61
|
device: current_device,
|
|
62
|
-
authenticated_at:
|
|
62
|
+
authenticated_at: current_session&.authenticated_at,
|
|
63
63
|
amr: login.amr.presence || current_session&.amr,
|
|
64
64
|
organization: login.state("organization-choice").selected
|
|
65
65
|
)
|
|
@@ -152,7 +152,7 @@ module Masks
|
|
|
152
152
|
|
|
153
153
|
if login.touched?(:first_factor)
|
|
154
154
|
sign_in(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by,
|
|
155
|
-
authenticated_at: login.
|
|
155
|
+
authenticated_at: login.authenticated_at)
|
|
156
156
|
elsif login.signed_in? && login.touched?(:second_factor)
|
|
157
157
|
current_session.update!(amr: Array(current_session.amr) | login.amr)
|
|
158
158
|
end
|
|
@@ -191,6 +191,7 @@ module Masks
|
|
|
191
191
|
return refuse_device(pending) if login.refused? && pending&.device?
|
|
192
192
|
return refuse_saml(pending) if login.refused? && pending&.saml?
|
|
193
193
|
return saml_resume_path(rid: rid_for(pending)) if pending&.saml?
|
|
194
|
+
return refuse_on_masks(pending, login.refusal) if login.refused? && pending && !answerable?(pending)
|
|
194
195
|
return deny(pending, login.refusal.error, login.refusal.description) if login.refused? && pending
|
|
195
196
|
return device_url_for(pending) if pending&.device?
|
|
196
197
|
return authorize_url_for(pending) if pending
|
|
@@ -2,9 +2,19 @@ module Masks
|
|
|
2
2
|
module Server
|
|
3
3
|
class NotificationsController < ApplicationController
|
|
4
4
|
before_action :require_actor
|
|
5
|
+
before_action -> { reauthenticated!("notifications", t("notifications.again")) }
|
|
5
6
|
|
|
6
7
|
def update
|
|
8
|
+
was = current_actor.muted_notifications
|
|
7
9
|
current_actor.update!(notifications: wanted)
|
|
10
|
+
now = current_actor.muted_notifications
|
|
11
|
+
|
|
12
|
+
muted = now - was
|
|
13
|
+
unmuted = was - now
|
|
14
|
+
|
|
15
|
+
if muted.any? || unmuted.any?
|
|
16
|
+
Event.record!(Event::NOTIFICATIONS_CHANGED, actor: current_actor, muted: muted, unmuted: unmuted)
|
|
17
|
+
end
|
|
8
18
|
|
|
9
19
|
redirect_to "#{root_path}#notifications", notice: t("notifications.saved")
|
|
10
20
|
end
|
|
@@ -12,7 +22,7 @@ module Masks
|
|
|
12
22
|
private
|
|
13
23
|
|
|
14
24
|
def wanted
|
|
15
|
-
Array(params[:notifications]).map(&:to_s) & Notifications::
|
|
25
|
+
Array(params[:notifications]).map(&:to_s) & Notifications::MUTABLE
|
|
16
26
|
end
|
|
17
27
|
|
|
18
28
|
def require_actor
|
|
@@ -5,6 +5,7 @@ module Masks
|
|
|
5
5
|
|
|
6
6
|
def update
|
|
7
7
|
return refuse(t("profiles.directed")) if current_actor.directed?
|
|
8
|
+
return unless kept_nickname? || reauthenticated!("profile", t("profiles.again"))
|
|
8
9
|
|
|
9
10
|
current_actor.assign_attributes(profile)
|
|
10
11
|
changed = current_actor.changed
|
|
@@ -22,6 +23,10 @@ module Masks
|
|
|
22
23
|
params.permit(:name, :nickname).to_h.transform_values { |value| value.strip.presence }
|
|
23
24
|
end
|
|
24
25
|
|
|
26
|
+
def kept_nickname?
|
|
27
|
+
!profile.key?("nickname") || profile["nickname"] == current_actor.nickname
|
|
28
|
+
end
|
|
29
|
+
|
|
25
30
|
def require_actor
|
|
26
31
|
redirect_to login_path unless current_actor
|
|
27
32
|
end
|
|
@@ -139,7 +139,7 @@ module Masks
|
|
|
139
139
|
assertion = response_for(pending)
|
|
140
140
|
encoded = assertion.success(
|
|
141
141
|
actor: current_actor, session: current_session, name_id_format: saml["name_id_format"], scopes: scopes,
|
|
142
|
-
authenticated_at:
|
|
142
|
+
authenticated_at: current_session&.authenticated_at,
|
|
143
143
|
amr: login.amr.presence || current_session&.amr
|
|
144
144
|
)
|
|
145
145
|
|
|
@@ -174,12 +174,6 @@ module Masks
|
|
|
174
174
|
)
|
|
175
175
|
end
|
|
176
176
|
|
|
177
|
-
def refused_request(rid)
|
|
178
|
-
return nil if rid.blank? || !tracked(REQUESTS).value?(rid)
|
|
179
|
-
|
|
180
|
-
PendingRequest.spent(rid)&.then { |held| held if held.consumed? }
|
|
181
|
-
end
|
|
182
|
-
|
|
183
177
|
def post(pending, encoded)
|
|
184
178
|
post_saml(pending.client, pending.redirect_uri, "SAMLResponse", encoded, relay_state: pending.held("saml")["relay_state"])
|
|
185
179
|
end
|
|
@@ -124,6 +124,7 @@ module Masks
|
|
|
124
124
|
|
|
125
125
|
req.invalid_grant!("that refresh token was issued to another client") if token.client_id != client.id
|
|
126
126
|
req.invalid_grant!("the session that refresh token was issued in has ended") if outlived?(token)
|
|
127
|
+
exhausted!(req, token, client)
|
|
127
128
|
member!(req, token)
|
|
128
129
|
consented!(req, token, client)
|
|
129
130
|
Current.organization = token.organization
|
|
@@ -180,6 +181,13 @@ module Masks
|
|
|
180
181
|
held.present? && held.bounded? && !held.live?
|
|
181
182
|
end
|
|
182
183
|
|
|
184
|
+
def exhausted!(req, token, client)
|
|
185
|
+
return unless token.exhausted?(client.refresh_absolute_lifetime)
|
|
186
|
+
|
|
187
|
+
token.revoke_family!
|
|
188
|
+
req.invalid_grant!("the sign-in behind that refresh token is older than #{client.name} allows")
|
|
189
|
+
end
|
|
190
|
+
|
|
183
191
|
def replayed!(spent, client)
|
|
184
192
|
return if spent.nil? || !spent.consumed?
|
|
185
193
|
|
|
@@ -29,6 +29,7 @@ module Masks
|
|
|
29
29
|
needs!(:security, "changing someone's email address") if changing_email
|
|
30
30
|
previous = actor.email if changing_email && actor.email_verified_at.present?
|
|
31
31
|
|
|
32
|
+
actor.trusted_rename = true
|
|
32
33
|
actor.assign_attributes(attributes)
|
|
33
34
|
actor.email_verified_at = nil if changing_email
|
|
34
35
|
actor.phone_verified_at = nil if attributes.key?(:phone) && actor.phone_changed?
|
|
@@ -36,6 +36,9 @@ module Masks
|
|
|
36
36
|
description: "Seconds an access token lasts, from 60 to a day. Zero goes back to an hour."
|
|
37
37
|
argument :refresh_token_lifetime, Integer, required: false,
|
|
38
38
|
description: "Seconds a refresh token lasts unused, from 300 to 400 days. Zero goes back to 30 days."
|
|
39
|
+
argument :refresh_token_absolute_lifetime, Integer, required: false,
|
|
40
|
+
description: "Seconds from a sign-in until its refresh tokens stop, however often they are used, " \
|
|
41
|
+
"from 300 to 400 days. Zero goes back to 90 days."
|
|
39
42
|
argument :allowed_groups, [ String ], required: false,
|
|
40
43
|
description: "Group keys, or organization roles as key:role, whose people may sign in to this client. Empty lets everyone in."
|
|
41
44
|
argument :sign_in_policy, ID, required: false
|
|
@@ -68,6 +71,7 @@ module Masks
|
|
|
68
71
|
attributes[:consent_lifetime] = nil if attributes[:consent_lifetime]&.zero?
|
|
69
72
|
attributes[:access_token_lifetime] = nil if attributes[:access_token_lifetime]&.zero?
|
|
70
73
|
attributes[:refresh_token_lifetime] = nil if attributes[:refresh_token_lifetime]&.zero?
|
|
74
|
+
attributes[:refresh_token_absolute_lifetime] = nil if attributes[:refresh_token_absolute_lifetime]&.zero?
|
|
71
75
|
client.assign_attributes(attributes)
|
|
72
76
|
client.response_types = client.default_response_types if attributes.key?(:grant_types)
|
|
73
77
|
client.required_scopes = Scopes.join(required_scopes) unless required_scopes.nil?
|
|
@@ -16,6 +16,8 @@ module Masks
|
|
|
16
16
|
description: "Seconds an access token lasts. Null is an hour."
|
|
17
17
|
field :refresh_token_lifetime, Integer,
|
|
18
18
|
description: "Seconds a refresh token lasts unused. Each refresh starts it over. Null is 30 days."
|
|
19
|
+
field :refresh_token_absolute_lifetime, Integer,
|
|
20
|
+
description: "Seconds from a sign-in until its refresh tokens stop, however often they are used. Null is 90 days."
|
|
19
21
|
field :allowed_groups, [ String ], null: false,
|
|
20
22
|
description: "Group keys, or organization roles as key:role, whose people may sign in. Empty lets everyone in."
|
|
21
23
|
field :namespaces, [ "Masks::Server::Manage::Types::NamespaceType" ], null: false
|
|
@@ -139,6 +139,9 @@ module Masks
|
|
|
139
139
|
field :ldap_base_dn, String, null: false, description: "The base DN of this tenant's LDAP directory."
|
|
140
140
|
field :ldap_port, Integer, null: false, description: "The port masks serves LDAP on."
|
|
141
141
|
field :ldap_tls, Boolean, null: false, description: "Whether that port speaks LDAPS."
|
|
142
|
+
field :ldap_local_only, Boolean, null: false,
|
|
143
|
+
description: "Whether the directory answers only on the server's own machine, " \
|
|
144
|
+
"since it has no certificate and MASKS_LDAP_PLAINTEXT is not set."
|
|
142
145
|
field :saml_metadata_url, String, null: false
|
|
143
146
|
|
|
144
147
|
field :minimum_password, Integer, null: false
|
|
@@ -236,6 +239,10 @@ module Masks
|
|
|
236
239
|
::Rails.configuration.masks.ldap_cert.present?
|
|
237
240
|
end
|
|
238
241
|
|
|
242
|
+
def ldap_local_only
|
|
243
|
+
Ldap::Listener.local_only?
|
|
244
|
+
end
|
|
245
|
+
|
|
239
246
|
def ldap_base_dn
|
|
240
247
|
Ldap::Directory.base_for(Current.tenant).to_s
|
|
241
248
|
end
|
|
@@ -33,6 +33,7 @@ module Masks
|
|
|
33
33
|
Event.where(created_at: ...tenant.event_retention.ago).in_batches(of: BATCH).delete_all
|
|
34
34
|
Members.purge_lapsed!
|
|
35
35
|
Replay.where(expires_at: ...Time.current).in_batches(of: BATCH).delete_all
|
|
36
|
+
ReleasedNickname.lapsed.in_batches(of: BATCH).delete_all
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def sweep_tokens(cutoff)
|
|
@@ -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?
|
|
@@ -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
|
|
|
@@ -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,
|