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.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/masks/server/client_authentication.rb +3 -2
  3. data/app/controllers/masks/server/addresses_controller.rb +6 -1
  4. data/app/controllers/masks/server/application_controller.rb +29 -1
  5. data/app/controllers/masks/server/authorize_controller.rb +23 -18
  6. data/app/controllers/masks/server/device_verifications_controller.rb +1 -1
  7. data/app/controllers/masks/server/discovery_controller.rb +36 -0
  8. data/app/controllers/masks/server/enrollments_controller.rb +17 -0
  9. data/app/controllers/masks/server/logins_controller.rb +2 -1
  10. data/app/controllers/masks/server/notifications_controller.rb +11 -1
  11. data/app/controllers/masks/server/organization_members_controller.rb +3 -1
  12. data/app/controllers/masks/server/profiles_controller.rb +5 -0
  13. data/app/controllers/masks/server/saml_controller.rb +1 -7
  14. data/app/controllers/masks/server/tokens_controller.rb +8 -0
  15. data/app/graphql/masks/server/manage/mutations/create_actor.rb +1 -0
  16. data/app/graphql/masks/server/manage/mutations/unenroll_client.rb +28 -0
  17. data/app/graphql/masks/server/manage/mutations/update_actor.rb +1 -0
  18. data/app/graphql/masks/server/manage/mutations/update_client.rb +4 -0
  19. data/app/graphql/masks/server/manage/types/client_type.rb +8 -0
  20. data/app/graphql/masks/server/manage/types/mutation_type.rb +1 -0
  21. data/app/graphql/masks/server/manage/types/query_type.rb +7 -0
  22. data/app/jobs/masks/server/application_job.rb +28 -0
  23. data/app/jobs/masks/server/check_domain_claims_job.rb +1 -3
  24. data/app/jobs/masks/server/cleanup_job.rb +5 -6
  25. data/app/jobs/masks/server/idle_accounts_job.rb +1 -3
  26. data/app/jobs/masks/server/prune_queue_job.rb +19 -0
  27. data/app/jobs/masks/server/refresh_saml_metadata_job.rb +1 -3
  28. data/app/jobs/masks/server/rotate_signing_keys_job.rb +1 -3
  29. data/app/jobs/masks/server/text_job.rb +12 -1
  30. data/app/ldap/masks/server/ldap/listener.rb +16 -3
  31. data/app/logins/masks/server/login_states/confirmation.rb +9 -3
  32. data/app/logins/masks/server/login_states/signup.rb +1 -0
  33. data/app/models/masks/server/actor.rb +25 -2
  34. data/app/models/masks/server/app_preset.rb +102 -0
  35. data/app/models/masks/server/boot_task.rb +41 -0
  36. data/app/models/masks/server/client.rb +15 -0
  37. data/app/models/masks/server/client_keys.rb +12 -1
  38. data/app/models/masks/server/declared.rb +2 -1
  39. data/app/models/masks/server/enrollment_token.rb +1 -1
  40. data/app/models/masks/server/event.rb +6 -1
  41. data/app/models/masks/server/job_failures.rb +25 -0
  42. data/app/models/masks/server/notifications.rb +8 -0
  43. data/app/models/masks/server/pending_request.rb +8 -0
  44. data/app/models/masks/server/refresh_token.rb +8 -0
  45. data/app/models/masks/server/released_nickname.rb +26 -0
  46. data/app/models/masks/server/scim/filter.rb +45 -5
  47. data/app/models/masks/server/scim/user.rb +1 -0
  48. data/app/models/masks/server/session.rb +3 -4
  49. data/app/views/masks/server/account/_info.html.erb +12 -4
  50. data/app/views/masks/server/account/index.html.erb +1 -1
  51. data/app/views/masks/server/authorize/error.html.erb +2 -2
  52. data/config/locales/de/account.yml +4 -0
  53. data/config/locales/de/actors.yml +2 -0
  54. data/config/locales/de/authorize.yml +2 -0
  55. data/config/locales/de/events.yml +6 -0
  56. data/config/locales/de/flashes.yml +1 -0
  57. data/config/locales/de/logins.yml +1 -0
  58. data/config/locales/de/manage/client.yml +4 -1
  59. data/config/locales/de/manage/ldap.yml +1 -0
  60. data/config/locales/de/manage/machines.yml +13 -0
  61. data/config/locales/de/manage/namespaces.yml +1 -1
  62. data/config/locales/de/notifications.yml +1 -0
  63. data/config/locales/en/account.yml +4 -0
  64. data/config/locales/en/actors.yml +2 -0
  65. data/config/locales/en/authorize.yml +2 -0
  66. data/config/locales/en/events.yml +6 -0
  67. data/config/locales/en/flashes.yml +1 -0
  68. data/config/locales/en/logins.yml +1 -0
  69. data/config/locales/en/manage/client.yml +3 -0
  70. data/config/locales/en/manage/ldap.yml +1 -0
  71. data/config/locales/en/manage/machines.yml +13 -0
  72. data/config/locales/en/notifications.yml +1 -0
  73. data/config/locales/es/account.yml +4 -0
  74. data/config/locales/es/actors.yml +2 -0
  75. data/config/locales/es/authorize.yml +2 -0
  76. data/config/locales/es/events.yml +6 -0
  77. data/config/locales/es/flashes.yml +1 -0
  78. data/config/locales/es/handshakes.yml +1 -1
  79. data/config/locales/es/logins.yml +1 -0
  80. data/config/locales/es/manage/client.yml +3 -0
  81. data/config/locales/es/manage/ldap.yml +1 -0
  82. data/config/locales/es/manage/machines.yml +13 -0
  83. data/config/locales/es/notifications.yml +1 -0
  84. data/config/locales/fr/account.yml +4 -0
  85. data/config/locales/fr/actors.yml +2 -0
  86. data/config/locales/fr/authorize.yml +2 -0
  87. data/config/locales/fr/events.yml +6 -0
  88. data/config/locales/fr/flashes.yml +1 -0
  89. data/config/locales/fr/handshakes.yml +1 -1
  90. data/config/locales/fr/logins.yml +1 -0
  91. data/config/locales/fr/manage/client.yml +4 -1
  92. data/config/locales/fr/manage/clients.yml +1 -1
  93. data/config/locales/fr/manage/ldap.yml +1 -0
  94. data/config/locales/fr/manage/machines.yml +13 -0
  95. data/config/locales/fr/manage/providers.yml +1 -1
  96. data/config/locales/fr/notifications.yml +1 -0
  97. data/config/routes.rb +2 -0
  98. data/db/migrate/20261018100000_create_released_nicknames.rb +22 -0
  99. data/db/migrate/20261018110000_add_refresh_token_absolute_lifetime_to_clients.rb +5 -0
  100. data/db/migrate/20261019140000_add_indexes_for_cleanup_and_metrics.rb +10 -0
  101. data/lib/masks/server/configuration.rb +12 -0
  102. data/lib/masks/server/tenancy/middleware.rb +9 -1
  103. data/lib/tasks/masks.rake +31 -7
  104. data/public/masks-assets/.vite/manifest.json +2 -2
  105. data/public/masks-assets/assets/{app-B27oPUll.css → app-D7W0gAyT.css} +1 -1
  106. data/public/masks-assets/assets/manage-D4MzaDvM.js +576 -0
  107. data/public/masks-assets/assets/manage-D4MzaDvM.js.map +1 -0
  108. data/version.txt +1 -1
  109. metadata +16 -4
  110. data/public/masks-assets/assets/manage-Dbfiy3Mj.js +0 -567
  111. 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: 36e33f34e755bbbb4470a5c55b920624f2042678b87c810ce614e86f9638a4df
4
- data.tar.gz: 2bf178807ba578015922b44145ad0eb28454698adb877ac51b5059be57a35ca3
3
+ metadata.gz: 608910b5ec386933423f04a3596c4b5d454b38e956449ca66d762d2cd79e3e82
4
+ data.tar.gz: f70b574939800fdcb3407025402e39663e15f8a9c71e2dff473685147e7cf693
5
5
  SHA512:
6
- metadata.gz: 3d32dd6592c56c0132971c1f322ca90b2a31df6832c0b9fe33d5c41c7d5d7aab443b4c6b2fec0a9d3ac0635b22b6d86bbda1c451bd791fd3794e4776c1edd1c8
7
- data.tar.gz: 59f5658c277590947cf74f3b8e57fe2d9d7b01f1a160bdc3bdbc3593f4aba9b9395b08255b866e54029f48aff6d9b7b1384daf9ab1b21b95f7468678a800fbcb
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
- client_refused! unless client.authenticate_secret(secret)
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.factored_at(:first_factor))
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: login.authenticated_at || current_session&.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&.redirect_uri?(target) || ours?(target))
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
- @error_code = error.error.to_s
129
- @error_description = error.description
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: error.status }
133
- format.any do
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 ours?(target)
167
- here = URI.parse(issuer.url)
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: login.authenticated_at || current_session&.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.factored_at(:first_factor))
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::MAILED
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
- @organization.memberships.includes(:actor).find_by(id: params[:id].to_s) ||
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: login.authenticated_at || current_session&.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
 
@@ -18,6 +18,7 @@ module Masks
18
18
  granting!(scopes)
19
19
 
20
20
  actor = Actor.new(
21
+ trusted_rename: true,
21
22
  nickname: nickname,
22
23
  email: email,
23
24
  scopes: Scopes.join(Scopes.list(scopes).presence || Scopes::STANDARD)
@@ -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
@@ -5,9 +5,7 @@ module Masks
5
5
  across_tenants!
6
6
 
7
7
  def perform
8
- Tenant.active.find_each do |tenant|
9
- Tenant.switch(tenant) { check }
10
- end
8
+ each_tenant { check }
11
9
  end
12
10
 
13
11
  private
@@ -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
- loop do
40
- roots = Token.group("COALESCE(root_id, id)")
41
- .having("MAX(LEAST(consumed_at, expires_at)) < ?", cutoff)
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(roots).delete_all
44
+ Token.families(ids - kept).delete_all
46
45
  end
47
46
 
48
47
  Token.where.not(kind: RefreshToken.sti_name).ended_before(cutoff)
@@ -5,9 +5,7 @@ module Masks
5
5
  across_tenants!
6
6
 
7
7
  def perform
8
- Tenant.active.idling.find_each do |tenant|
9
- Tenant.switch(tenant) { IdleAccounts.sweep(tenant) }
10
- end
8
+ each_tenant(Tenant.active.idling) { |tenant| IdleAccounts.sweep(tenant) }
11
9
  end
12
10
  end
13
11
  end
@@ -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
@@ -5,9 +5,7 @@ module Masks
5
5
  across_tenants!
6
6
 
7
7
  def perform
8
- Tenant.active.find_each do |tenant|
9
- Tenant.switch(tenant) { refresh }
10
- end
8
+ each_tenant { refresh }
11
9
  end
12
10
 
13
11
  private
@@ -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).find_each do |tenant|
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