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
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)
|
|
@@ -44,7 +45,7 @@ module Masks
|
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def assertion_audiences
|
|
47
|
-
[ issuer.url, "#{issuer.url}/token", "#{issuer.url}#{request.
|
|
48
|
+
[ issuer.url, "#{issuer.url}/token", "#{issuer.url}#{request.path_info}" ].uniq
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
def presented_credentials
|
|
@@ -19,7 +19,7 @@ module Masks
|
|
|
19
19
|
answer = handler.call(request.env)
|
|
20
20
|
|
|
21
21
|
return render_rack(answer) unless answer.first == 200
|
|
22
|
-
return
|
|
22
|
+
return ask_for_token if token.nil?
|
|
23
23
|
|
|
24
24
|
yield token
|
|
25
25
|
end
|
|
@@ -50,8 +50,11 @@ module Masks
|
|
|
50
50
|
|
|
51
51
|
def decode(bearer)
|
|
52
52
|
claims_in_token(bearer.access_token)
|
|
53
|
+
rescue JWT::ExpiredSignature
|
|
54
|
+
bearer.invalid_token!("that token has expired")
|
|
53
55
|
rescue JWT::DecodeError => e
|
|
54
|
-
|
|
56
|
+
::Rails.logger.info("masks: an access token was refused: #{e.message}")
|
|
57
|
+
bearer.invalid_token!("that token is not one this server signed")
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
def claims_in_token(secret)
|
|
@@ -96,7 +99,7 @@ module Masks
|
|
|
96
99
|
end
|
|
97
100
|
|
|
98
101
|
def proof_for(secret = nil)
|
|
99
|
-
Proof.read!(request, url: "#{issuer.url}#{request.
|
|
102
|
+
Proof.read!(request, url: "#{issuer.url}#{request.path_info}", access_token: secret)
|
|
100
103
|
end
|
|
101
104
|
|
|
102
105
|
def held_token(secret)
|
|
@@ -118,6 +121,12 @@ module Masks
|
|
|
118
121
|
nil
|
|
119
122
|
end
|
|
120
123
|
|
|
124
|
+
def ask_for_token
|
|
125
|
+
response.headers["WWW-Authenticate"] = %(Bearer realm="#{issuer.url}")
|
|
126
|
+
|
|
127
|
+
head :unauthorized
|
|
128
|
+
end
|
|
129
|
+
|
|
121
130
|
def refuse_token(description)
|
|
122
131
|
render_rack(
|
|
123
132
|
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
|
|
@@ -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
|
|
@@ -143,7 +143,7 @@ module Masks
|
|
|
143
143
|
Current.device = device
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
-
def sign_in(actor, amr: [], policy: nil, first_factor: {})
|
|
146
|
+
def sign_in(actor, amr: [], policy: nil, first_factor: {}, authenticated_at: nil)
|
|
147
147
|
carried = session.to_hash.slice(REQUESTS, HANDSHAKES, "login", RETURN_TO)
|
|
148
148
|
reset_session
|
|
149
149
|
carried.each { |key, value| session[key] = value }
|
|
@@ -157,7 +157,8 @@ module Masks
|
|
|
157
157
|
ip_address: request.remote_ip,
|
|
158
158
|
amr: amr,
|
|
159
159
|
policy: policy,
|
|
160
|
-
first_factor: first_factor
|
|
160
|
+
first_factor: first_factor,
|
|
161
|
+
authenticated_at: authenticated_at
|
|
161
162
|
)
|
|
162
163
|
|
|
163
164
|
cookies.encrypted[:masks_session] = {
|
|
@@ -263,6 +264,34 @@ module Masks
|
|
|
263
264
|
)
|
|
264
265
|
end
|
|
265
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
|
+
|
|
266
295
|
def authorize_url_for(pending)
|
|
267
296
|
"#{authorize_path}?#{URI.encode_www_form(pending.authorization.query_pairs)}"
|
|
268
297
|
end
|
|
@@ -301,17 +330,18 @@ module Masks
|
|
|
301
330
|
end
|
|
302
331
|
|
|
303
332
|
def settle!(login)
|
|
304
|
-
hold(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by
|
|
333
|
+
hold(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by,
|
|
334
|
+
authenticated_at: login.authenticated_at)
|
|
305
335
|
forget_login
|
|
306
336
|
end
|
|
307
337
|
|
|
308
|
-
def hold(actor, amr: [], policy: nil, first_factor: {})
|
|
338
|
+
def hold(actor, amr: [], policy: nil, first_factor: {}, authenticated_at: nil)
|
|
309
339
|
return if actor.nil?
|
|
310
|
-
return current_session.first_factored!(first_factor) if current_session&.actor_id == actor.id
|
|
340
|
+
return current_session.first_factored!(first_factor, at: authenticated_at) if current_session&.actor_id == actor.id
|
|
311
341
|
|
|
312
342
|
sign_out if current_session
|
|
313
343
|
|
|
314
|
-
sign_in(actor, amr: amr, policy: policy, first_factor: first_factor)
|
|
344
|
+
sign_in(actor, amr: amr, policy: policy, first_factor: first_factor, authenticated_at: authenticated_at)
|
|
315
345
|
end
|
|
316
346
|
|
|
317
347
|
def policy_denied(denial)
|
|
@@ -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)
|
|
@@ -63,9 +73,10 @@ module Masks
|
|
|
63
73
|
req.bad_request!(:invalid_client, unknown_client(req.client_id)) if client.nil?
|
|
64
74
|
req.invalid_request!("redirect_uri is required") if req.redirect_uri.blank?
|
|
65
75
|
|
|
66
|
-
req.verify_redirect_uri!(client.redirect_uris)
|
|
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
|
)
|
|
@@ -151,7 +151,8 @@ module Masks
|
|
|
151
151
|
return unless login.actor
|
|
152
152
|
|
|
153
153
|
if login.touched?(:first_factor)
|
|
154
|
-
sign_in(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by
|
|
154
|
+
sign_in(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by,
|
|
155
|
+
authenticated_at: login.authenticated_at)
|
|
155
156
|
elsif login.signed_in? && login.touched?(:second_factor)
|
|
156
157
|
current_session.update!(amr: Array(current_session.amr) | login.amr)
|
|
157
158
|
end
|
|
@@ -190,6 +191,7 @@ module Masks
|
|
|
190
191
|
return refuse_device(pending) if login.refused? && pending&.device?
|
|
191
192
|
return refuse_saml(pending) if login.refused? && pending&.saml?
|
|
192
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)
|
|
193
195
|
return deny(pending, login.refusal.error, login.refusal.description) if login.refused? && pending
|
|
194
196
|
return device_url_for(pending) if pending&.device?
|
|
195
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
|
|
@@ -133,6 +133,12 @@ module Masks
|
|
|
133
133
|
}, status: :bad_request
|
|
134
134
|
end
|
|
135
135
|
|
|
136
|
+
def ask_for_token
|
|
137
|
+
response.headers["WWW-Authenticate"] = %(Bearer realm="#{issuer.url}")
|
|
138
|
+
|
|
139
|
+
head :unauthorized
|
|
140
|
+
end
|
|
141
|
+
|
|
136
142
|
def issued(client)
|
|
137
143
|
client.metadata.merge(
|
|
138
144
|
"client_secret" => client.secret,
|
|
@@ -155,7 +161,9 @@ module Masks
|
|
|
155
161
|
end
|
|
156
162
|
|
|
157
163
|
def unauthorized(description)
|
|
158
|
-
|
|
164
|
+
return ask_for_token if bearer.blank?
|
|
165
|
+
|
|
166
|
+
response.headers["WWW-Authenticate"] = %(Bearer realm="#{issuer.url}", error="invalid_token")
|
|
159
167
|
|
|
160
168
|
render json: {
|
|
161
169
|
"error" => "invalid_token",
|
|
@@ -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
|
|
@@ -7,7 +7,7 @@ module Masks
|
|
|
7
7
|
def service_provider_config
|
|
8
8
|
scim({
|
|
9
9
|
"schemas" => [ Scim::SERVICE_PROVIDER_CONFIG ],
|
|
10
|
-
"documentationUri" => "#{::Rails.configuration.masks.docs_url}/
|
|
10
|
+
"documentationUri" => "#{::Rails.configuration.masks.docs_url}/guides/sso-saml/#provisioning",
|
|
11
11
|
"patch" => { "supported" => true },
|
|
12
12
|
"bulk" => { "supported" => false, "maxOperations" => 0, "maxPayloadSize" => 0 },
|
|
13
13
|
"filter" => { "supported" => true, "maxResults" => Scim::MAX_RESULTS },
|
|
@@ -42,7 +42,7 @@ module Masks
|
|
|
42
42
|
return @proof if defined?(@proof)
|
|
43
43
|
|
|
44
44
|
@proof = if Proof.presented?(request)
|
|
45
|
-
Proof.read!(request, url: "#{issuer.url}#{request.
|
|
45
|
+
Proof.read!(request, url: "#{issuer.url}#{request.path_info}")
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -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
|
|
|
@@ -7,7 +7,7 @@ module Masks
|
|
|
7
7
|
|
|
8
8
|
argument :preset, ID
|
|
9
9
|
argument :base_url, String, description: "Where the app is served, such as https://grafana.example.com."
|
|
10
|
-
argument :name, String, required: false, description: "What people see.
|
|
10
|
+
argument :name, String, required: false, description: "What people see."
|
|
11
11
|
argument :slug, String, required: false,
|
|
12
12
|
description: "The name some apps put in their redirect URI, such as Gitea's authentication source."
|
|
13
13
|
|
|
@@ -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
|
|
@@ -15,7 +15,7 @@ module Masks
|
|
|
15
15
|
close: '<path d="M6.5 6.5l7 7M13.5 6.5l-7 7"/>',
|
|
16
16
|
check: '<path d="m5.5 10.5 3 3 6-7"/>',
|
|
17
17
|
organization: '<rect x="3.5" y="7" width="13" height="9.5" rx="1.5"/><path d="M7.5 7V4.5h5V7"/><path d="M3.5 11h13"/>',
|
|
18
|
-
|
|
18
|
+
actor: '<circle cx="10" cy="7" r="3.25"/><path d="M4 16.5c.8-3 3.2-4.5 6-4.5s5.2 1.5 6 4.5"/>',
|
|
19
19
|
authenticator: '<rect x="3" y="5.5" width="14" height="9" rx="2"/><path d="M6.5 10h.01M10 10h.01M13.5 10h.01"/>',
|
|
20
20
|
backup_codes: '<path d="M5.5 3.5h9v13h-9z"/><path d="M8 7h4M8 10h4M8 13h4"/>',
|
|
21
21
|
manage: '<path d="M10 2.5 3.5 5v5c0 3.4 2.7 6.2 6.5 7.5 3.8-1.3 6.5-4.1 6.5-7.5V5L10 2.5Z"/>'
|
|
@@ -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
|
|