masks-server 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/concerns/masks/server/client_authentication.rb +3 -2
- data/app/controllers/masks/server/addresses_controller.rb +6 -1
- data/app/controllers/masks/server/application_controller.rb +29 -1
- data/app/controllers/masks/server/authorize_controller.rb +23 -18
- data/app/controllers/masks/server/device_verifications_controller.rb +1 -1
- data/app/controllers/masks/server/discovery_controller.rb +36 -0
- data/app/controllers/masks/server/enrollments_controller.rb +17 -0
- data/app/controllers/masks/server/logins_controller.rb +2 -1
- data/app/controllers/masks/server/notifications_controller.rb +11 -1
- data/app/controllers/masks/server/organization_members_controller.rb +3 -1
- data/app/controllers/masks/server/profiles_controller.rb +5 -0
- data/app/controllers/masks/server/saml_controller.rb +1 -7
- data/app/controllers/masks/server/tokens_controller.rb +8 -0
- data/app/graphql/masks/server/manage/mutations/create_actor.rb +1 -0
- data/app/graphql/masks/server/manage/mutations/unenroll_client.rb +28 -0
- data/app/graphql/masks/server/manage/mutations/update_actor.rb +1 -0
- data/app/graphql/masks/server/manage/mutations/update_client.rb +4 -0
- data/app/graphql/masks/server/manage/types/client_type.rb +8 -0
- data/app/graphql/masks/server/manage/types/mutation_type.rb +1 -0
- data/app/graphql/masks/server/manage/types/query_type.rb +7 -0
- data/app/jobs/masks/server/application_job.rb +28 -0
- data/app/jobs/masks/server/check_domain_claims_job.rb +1 -3
- data/app/jobs/masks/server/cleanup_job.rb +5 -6
- data/app/jobs/masks/server/idle_accounts_job.rb +1 -3
- data/app/jobs/masks/server/prune_queue_job.rb +19 -0
- data/app/jobs/masks/server/refresh_saml_metadata_job.rb +1 -3
- data/app/jobs/masks/server/rotate_signing_keys_job.rb +1 -3
- data/app/jobs/masks/server/text_job.rb +12 -1
- data/app/ldap/masks/server/ldap/listener.rb +16 -3
- data/app/logins/masks/server/login_states/confirmation.rb +9 -3
- data/app/logins/masks/server/login_states/signup.rb +1 -0
- data/app/models/masks/server/actor.rb +25 -2
- data/app/models/masks/server/app_preset.rb +102 -0
- data/app/models/masks/server/boot_task.rb +41 -0
- data/app/models/masks/server/client.rb +15 -0
- data/app/models/masks/server/client_keys.rb +12 -1
- data/app/models/masks/server/declared.rb +2 -1
- data/app/models/masks/server/enrollment_token.rb +1 -1
- data/app/models/masks/server/event.rb +6 -1
- data/app/models/masks/server/job_failures.rb +25 -0
- data/app/models/masks/server/notifications.rb +8 -0
- data/app/models/masks/server/pending_request.rb +8 -0
- data/app/models/masks/server/refresh_token.rb +8 -0
- data/app/models/masks/server/released_nickname.rb +26 -0
- data/app/models/masks/server/scim/filter.rb +45 -5
- data/app/models/masks/server/scim/user.rb +1 -0
- data/app/models/masks/server/session.rb +3 -4
- data/app/views/masks/server/account/_info.html.erb +12 -4
- data/app/views/masks/server/account/index.html.erb +1 -1
- data/app/views/masks/server/authorize/error.html.erb +2 -2
- data/config/locales/de/account.yml +4 -0
- data/config/locales/de/actors.yml +2 -0
- data/config/locales/de/authorize.yml +2 -0
- data/config/locales/de/events.yml +6 -0
- data/config/locales/de/flashes.yml +1 -0
- data/config/locales/de/logins.yml +1 -0
- data/config/locales/de/manage/client.yml +4 -1
- data/config/locales/de/manage/ldap.yml +1 -0
- data/config/locales/de/manage/machines.yml +13 -0
- data/config/locales/de/manage/namespaces.yml +1 -1
- data/config/locales/de/notifications.yml +1 -0
- data/config/locales/en/account.yml +4 -0
- data/config/locales/en/actors.yml +2 -0
- data/config/locales/en/authorize.yml +2 -0
- data/config/locales/en/events.yml +6 -0
- data/config/locales/en/flashes.yml +1 -0
- data/config/locales/en/logins.yml +1 -0
- data/config/locales/en/manage/client.yml +3 -0
- data/config/locales/en/manage/ldap.yml +1 -0
- data/config/locales/en/manage/machines.yml +13 -0
- data/config/locales/en/notifications.yml +1 -0
- data/config/locales/es/account.yml +4 -0
- data/config/locales/es/actors.yml +2 -0
- data/config/locales/es/authorize.yml +2 -0
- data/config/locales/es/events.yml +6 -0
- data/config/locales/es/flashes.yml +1 -0
- data/config/locales/es/handshakes.yml +1 -1
- data/config/locales/es/logins.yml +1 -0
- data/config/locales/es/manage/client.yml +3 -0
- data/config/locales/es/manage/ldap.yml +1 -0
- data/config/locales/es/manage/machines.yml +13 -0
- data/config/locales/es/notifications.yml +1 -0
- data/config/locales/fr/account.yml +4 -0
- data/config/locales/fr/actors.yml +2 -0
- data/config/locales/fr/authorize.yml +2 -0
- data/config/locales/fr/events.yml +6 -0
- data/config/locales/fr/flashes.yml +1 -0
- data/config/locales/fr/handshakes.yml +1 -1
- data/config/locales/fr/logins.yml +1 -0
- data/config/locales/fr/manage/client.yml +4 -1
- data/config/locales/fr/manage/clients.yml +1 -1
- data/config/locales/fr/manage/ldap.yml +1 -0
- data/config/locales/fr/manage/machines.yml +13 -0
- data/config/locales/fr/manage/providers.yml +1 -1
- data/config/locales/fr/notifications.yml +1 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20261018100000_create_released_nicknames.rb +22 -0
- data/db/migrate/20261018110000_add_refresh_token_absolute_lifetime_to_clients.rb +5 -0
- data/db/migrate/20261019140000_add_indexes_for_cleanup_and_metrics.rb +10 -0
- data/lib/masks/server/configuration.rb +12 -0
- data/lib/masks/server/tenancy/middleware.rb +9 -1
- data/lib/tasks/masks.rake +31 -7
- data/public/masks-assets/.vite/manifest.json +2 -2
- data/public/masks-assets/assets/{app-B27oPUll.css → app-D7W0gAyT.css} +1 -1
- data/public/masks-assets/assets/manage-D4MzaDvM.js +576 -0
- data/public/masks-assets/assets/manage-D4MzaDvM.js.map +1 -0
- data/version.txt +1 -1
- metadata +16 -4
- data/public/masks-assets/assets/manage-Dbfiy3Mj.js +0 -567
- data/public/masks-assets/assets/manage-Dbfiy3Mj.js.map +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 608910b5ec386933423f04a3596c4b5d454b38e956449ca66d762d2cd79e3e82
|
|
4
|
+
data.tar.gz: f70b574939800fdcb3407025402e39663e15f8a9c71e2dff473685147e7cf693
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a061d6d81ac514d825d3963c466e99272d224e0250e964e8a057a8e794a7cd1f26955cd8e1cb5753eb418f8c91e5dd2d9b31b98c8bba4eb97d11d7112ce0a40b
|
|
7
|
+
data.tar.gz: c537d6afb5fc139d5e83fd92143f0eb28588f9ec1992e607209f89c5c9f59dfff3502dc0f1196a39c2a19fcec67910458494324a70e739ced310ea73f69168cb
|
|
@@ -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
|
)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
module Masks
|
|
2
2
|
module Server
|
|
3
3
|
class DiscoveryController < ApplicationController
|
|
4
|
+
OPENID_ISSUER = "http://openid.net/specs/connect/1.0/issuer".freeze
|
|
5
|
+
LONGEST_RESOURCE = 512
|
|
6
|
+
|
|
4
7
|
def openid
|
|
5
8
|
render json: issuer.discovery
|
|
6
9
|
end
|
|
@@ -13,6 +16,39 @@ module Masks
|
|
|
13
16
|
def resource
|
|
14
17
|
render json: issuer.protected_resource
|
|
15
18
|
end
|
|
19
|
+
|
|
20
|
+
def webfinger
|
|
21
|
+
resource = params[:resource].to_s
|
|
22
|
+
host = resource_host(resource)
|
|
23
|
+
|
|
24
|
+
return head(:bad_request) if host.nil?
|
|
25
|
+
return head(:not_found) unless host.casecmp?(URI.parse(issuer.url).host)
|
|
26
|
+
|
|
27
|
+
asked = Array(params[:rel]).map(&:to_s)
|
|
28
|
+
links = [ { "rel" => OPENID_ISSUER, "href" => issuer.url } ]
|
|
29
|
+
links = links.select { |link| asked.include?(link["rel"]) } if asked.any?
|
|
30
|
+
|
|
31
|
+
response.headers["Access-Control-Allow-Origin"] = "*"
|
|
32
|
+
render json: { "subject" => resource, "links" => links }, content_type: "application/jrd+json"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def resource_host(resource)
|
|
38
|
+
return nil if resource.blank? || resource.length > LONGEST_RESOURCE
|
|
39
|
+
|
|
40
|
+
if resource.start_with?("acct:")
|
|
41
|
+
account, separator, host = resource.delete_prefix("acct:").rpartition("@")
|
|
42
|
+
|
|
43
|
+
return nil if separator.empty? || account.blank? || host.blank? || host.match?(%r{[/\\\s?#]})
|
|
44
|
+
|
|
45
|
+
host.sub(/:\d+\z/, "")
|
|
46
|
+
elsif resource.match?(%r{\Ahttps?://})
|
|
47
|
+
URI.parse(resource).host
|
|
48
|
+
end
|
|
49
|
+
rescue URI::InvalidURIError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
16
52
|
end
|
|
17
53
|
end
|
|
18
54
|
end
|
|
@@ -12,6 +12,15 @@ module Masks
|
|
|
12
12
|
}, status: :too_many_requests
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
rate_limit to: 60, within: 1.hour, only: :create, name: "app",
|
|
16
|
+
by: -> { [ current_tenant.id, claimed_client_id ].join(":") },
|
|
17
|
+
with: -> {
|
|
18
|
+
render json: {
|
|
19
|
+
"error" => "too_many_requests",
|
|
20
|
+
"error_description" => "too many enrollment tokens asked for by this app"
|
|
21
|
+
}, status: :too_many_requests
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
before_action :no_store!
|
|
16
25
|
|
|
17
26
|
def create
|
|
@@ -55,6 +64,14 @@ module Masks
|
|
|
55
64
|
client
|
|
56
65
|
end
|
|
57
66
|
|
|
67
|
+
def claimed_client_id
|
|
68
|
+
basic = ActionController::HttpAuthentication::Basic
|
|
69
|
+
|
|
70
|
+
named, = basic.user_name_and_password(request) if basic.has_basic_credentials?(request)
|
|
71
|
+
|
|
72
|
+
(named.presence || params[:client_id]).to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
58
75
|
def not_enrolled
|
|
59
76
|
render json: {
|
|
60
77
|
"error" => "invalid_request",
|
|
@@ -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
|
|
@@ -99,7 +99,9 @@ module Masks
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
def member!
|
|
102
|
-
|
|
102
|
+
membership_id = params[:id].to_s[/\A\d+\z/]
|
|
103
|
+
|
|
104
|
+
(membership_id && @organization.memberships.includes(:actor).find_by(id: membership_id)) ||
|
|
103
105
|
raise(Members::Refused, t("organization_members.not_a_member"))
|
|
104
106
|
end
|
|
105
107
|
|
|
@@ -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
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
module Manage
|
|
4
|
+
module Mutations
|
|
5
|
+
class UnenrollClient < BaseMutation
|
|
6
|
+
requires :security
|
|
7
|
+
|
|
8
|
+
argument :client_id, ID
|
|
9
|
+
|
|
10
|
+
field :client, Types::ClientType, null: false
|
|
11
|
+
|
|
12
|
+
def resolve(client_id:)
|
|
13
|
+
machine = client!(client_id)
|
|
14
|
+
|
|
15
|
+
refuse!("that client is not a machine an app enrolled, so archive it instead") unless machine.enrolled?
|
|
16
|
+
|
|
17
|
+
revoked = machine.unenroll!
|
|
18
|
+
|
|
19
|
+
audit!(Masks::Server::Event::CLIENT_UNENROLLED, client: machine, name: machine.name,
|
|
20
|
+
enrolled_by: machine.enrolled_by.client_id, revoked: revoked)
|
|
21
|
+
|
|
22
|
+
{ client: machine }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -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
|
|
@@ -61,6 +63,8 @@ module Masks
|
|
|
61
63
|
description: "The app that enrolled this client as one of its machines. Archiving that app archives this client."
|
|
62
64
|
field :enrolled, [ ClientType ], null: false,
|
|
63
65
|
description: "The machines this app enrolled that are not archived."
|
|
66
|
+
field :last_seen_at, GraphQL::Types::ISO8601DateTime,
|
|
67
|
+
description: "When this client last got a token. Null if it never has."
|
|
64
68
|
field :archived_at, GraphQL::Types::ISO8601DateTime
|
|
65
69
|
field :secret_expires_at, GraphQL::Types::ISO8601DateTime
|
|
66
70
|
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
|
|
@@ -83,6 +87,10 @@ module Masks
|
|
|
83
87
|
object.enrolled.active.order(:name)
|
|
84
88
|
end
|
|
85
89
|
|
|
90
|
+
def last_seen_at
|
|
91
|
+
Masks::Server::Token.where(kind: TokenType::GRANTS, client_id: object.id).maximum(:created_at)
|
|
92
|
+
end
|
|
93
|
+
|
|
86
94
|
def tokens(limit: nil)
|
|
87
95
|
Masks::Server::Token.where(kind: TokenType::GRANTS, client_id: object.id)
|
|
88
96
|
.live
|
|
@@ -41,6 +41,7 @@ module Masks
|
|
|
41
41
|
field :update_client, mutation: Mutations::UpdateClient
|
|
42
42
|
field :rotate_client_secret, mutation: Mutations::RotateClientSecret
|
|
43
43
|
field :archive_client, mutation: Mutations::ArchiveClient
|
|
44
|
+
field :unenroll_client, mutation: Mutations::UnenrollClient
|
|
44
45
|
field :restore_client, mutation: Mutations::RestoreClient
|
|
45
46
|
field :release_namespace, mutation: Mutations::ReleaseNamespace
|
|
46
47
|
field :discover_provider, mutation: Mutations::DiscoverProvider
|
|
@@ -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
|
|
@@ -2,6 +2,34 @@ module Masks
|
|
|
2
2
|
module Server
|
|
3
3
|
class ApplicationJob < ActiveJob::Base
|
|
4
4
|
include Tenancy::Job
|
|
5
|
+
|
|
6
|
+
class Incomplete < StandardError; end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def each_tenant(scope = Tenant.active)
|
|
11
|
+
failed = scope.find_each.filter_map do |tenant|
|
|
12
|
+
Tenant.switch(tenant) { yield tenant }
|
|
13
|
+
nil
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
report_tenant_failure(tenant, e)
|
|
16
|
+
tenant.subdomain
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
JobFailures.record(self.class.name, failed)
|
|
20
|
+
|
|
21
|
+
raise Incomplete, "#{self.class.name.demodulize} failed for #{failed.join(', ')}" if failed.any?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def report_tenant_failure(tenant, error)
|
|
25
|
+
::Rails.error.report(error, context: { tenant: tenant.subdomain, job: self.class.name })
|
|
26
|
+
|
|
27
|
+
Tenant.switch(tenant) do
|
|
28
|
+
Event.record!(Event::JOB_FAILED, by: nil, job: self.class.name.demodulize, error: error.class.name, message: error.message.to_s[0, 200])
|
|
29
|
+
end
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
::Rails.logger.warn("#{self.class.name} could not record its failure in #{tenant.subdomain}: #{e.message}")
|
|
32
|
+
end
|
|
5
33
|
end
|
|
6
34
|
end
|
|
7
35
|
end
|
|
@@ -33,16 +33,15 @@ 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)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.limit(BATCH).pluck(Arel.sql("COALESCE(root_id, id)"))
|
|
43
|
-
break if roots.empty?
|
|
40
|
+
Token.where(root_id: nil).ended_before(cutoff).in_batches(of: BATCH) do |roots|
|
|
41
|
+
ids = roots.pluck(:id)
|
|
42
|
+
kept = Token.where(root_id: ids).where("LEAST(consumed_at, expires_at) >= ?", cutoff).distinct.pluck(:root_id)
|
|
44
43
|
|
|
45
|
-
Token.families(
|
|
44
|
+
Token.families(ids - kept).delete_all
|
|
46
45
|
end
|
|
47
46
|
|
|
48
47
|
Token.where.not(kind: RefreshToken.sti_name).ended_before(cutoff)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
class PruneQueueJob < ApplicationJob
|
|
4
|
+
queue_as :maintenance
|
|
5
|
+
across_tenants!
|
|
6
|
+
|
|
7
|
+
BATCH = 500
|
|
8
|
+
|
|
9
|
+
def perform
|
|
10
|
+
return unless defined?(::SolidQueue::Job)
|
|
11
|
+
|
|
12
|
+
settings = ::Rails.configuration.masks
|
|
13
|
+
|
|
14
|
+
::SolidQueue::Job.clear_finished_in_batches(batch_size: BATCH, finished_before: settings.finished_job_retention.ago)
|
|
15
|
+
::SolidQueue::FailedExecution.where(created_at: ...settings.failed_job_retention.ago).discard_all_in_batches(batch_size: BATCH)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -7,9 +7,7 @@ module Masks
|
|
|
7
7
|
PUBLISHED_FOR = 1.day
|
|
8
8
|
|
|
9
9
|
def perform
|
|
10
|
-
Tenant.active.where.not(key_rotation_days: nil)
|
|
11
|
-
Tenant.switch(tenant) { advance(tenant) }
|
|
12
|
-
end
|
|
10
|
+
each_tenant(Tenant.active.where.not(key_rotation_days: nil)) { |tenant| advance(tenant) }
|
|
13
11
|
end
|
|
14
12
|
|
|
15
13
|
private
|