masks-server 0.10.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/masks/server/discovery_controller.rb +36 -0
  3. data/app/controllers/masks/server/enrollments_controller.rb +17 -0
  4. data/app/controllers/masks/server/organization_members_controller.rb +3 -1
  5. data/app/graphql/masks/server/manage/mutations/unenroll_client.rb +28 -0
  6. data/app/graphql/masks/server/manage/types/client_type.rb +6 -0
  7. data/app/graphql/masks/server/manage/types/mutation_type.rb +1 -0
  8. data/app/jobs/masks/server/application_job.rb +28 -0
  9. data/app/jobs/masks/server/check_domain_claims_job.rb +1 -3
  10. data/app/jobs/masks/server/cleanup_job.rb +4 -6
  11. data/app/jobs/masks/server/idle_accounts_job.rb +1 -3
  12. data/app/jobs/masks/server/prune_queue_job.rb +19 -0
  13. data/app/jobs/masks/server/refresh_saml_metadata_job.rb +1 -3
  14. data/app/jobs/masks/server/rotate_signing_keys_job.rb +1 -3
  15. data/app/jobs/masks/server/text_job.rb +12 -1
  16. data/app/models/masks/server/app_preset.rb +102 -0
  17. data/app/models/masks/server/boot_task.rb +41 -0
  18. data/app/models/masks/server/enrollment_token.rb +1 -1
  19. data/app/models/masks/server/event.rb +5 -1
  20. data/app/models/masks/server/job_failures.rb +25 -0
  21. data/config/locales/de/events.yml +4 -0
  22. data/config/locales/de/manage/client.yml +1 -1
  23. data/config/locales/de/manage/machines.yml +13 -0
  24. data/config/locales/de/manage/namespaces.yml +1 -1
  25. data/config/locales/en/events.yml +4 -0
  26. data/config/locales/en/manage/machines.yml +13 -0
  27. data/config/locales/es/events.yml +4 -0
  28. data/config/locales/es/handshakes.yml +1 -1
  29. data/config/locales/es/manage/machines.yml +13 -0
  30. data/config/locales/fr/events.yml +4 -0
  31. data/config/locales/fr/handshakes.yml +1 -1
  32. data/config/locales/fr/manage/client.yml +1 -1
  33. data/config/locales/fr/manage/clients.yml +1 -1
  34. data/config/locales/fr/manage/machines.yml +13 -0
  35. data/config/locales/fr/manage/providers.yml +1 -1
  36. data/config/routes.rb +1 -0
  37. data/db/migrate/20261019140000_add_indexes_for_cleanup_and_metrics.rb +10 -0
  38. data/lib/masks/server/configuration.rb +4 -0
  39. data/lib/tasks/masks.rake +31 -7
  40. data/public/masks-assets/.vite/manifest.json +1 -1
  41. data/public/masks-assets/assets/{manage-DR9qQiBU.js → manage-D4MzaDvM.js} +33 -26
  42. data/public/masks-assets/assets/manage-D4MzaDvM.js.map +1 -0
  43. data/version.txt +1 -1
  44. metadata +12 -3
  45. data/public/masks-assets/assets/manage-DR9qQiBU.js.map +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 544dc01d0600507eb713667f0d8cf57275b4b7d957b603e3085f9e197e7e63c6
4
- data.tar.gz: 70f664c959b40e98e9a273582a45c0dba352e5186430a72c26e3a8dd87db978b
3
+ metadata.gz: 608910b5ec386933423f04a3596c4b5d454b38e956449ca66d762d2cd79e3e82
4
+ data.tar.gz: f70b574939800fdcb3407025402e39663e15f8a9c71e2dff473685147e7cf693
5
5
  SHA512:
6
- metadata.gz: a5c6177ca713658c3003f33cea0b8902c6810266271f4d4e6d0215d8a291b6da13a47add90dca3b65df3f9614f6161dd5627853c3e9038ddafffcc0963ce27d4
7
- data.tar.gz: 872c0d7ce2bd4090f885fdbce3eb82d8459ff73840ac89d3bd9cd2088ae334dc1efb2c34fe7bc17a2f413492d07c15c74a69f73b771f98847a4a69ae10416228
6
+ metadata.gz: a061d6d81ac514d825d3963c466e99272d224e0250e964e8a057a8e794a7cd1f26955cd8e1cb5753eb418f8c91e5dd2d9b31b98c8bba4eb97d11d7112ce0a40b
7
+ data.tar.gz: c537d6afb5fc139d5e83fd92143f0eb28588f9ec1992e607209f89c5c9f59dfff3502dc0f1196a39c2a19fcec67910458494324a70e739ced310ea73f69168cb
@@ -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",
@@ -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
 
@@ -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
@@ -63,6 +63,8 @@ module Masks
63
63
  description: "The app that enrolled this client as one of its machines. Archiving that app archives this client."
64
64
  field :enrolled, [ ClientType ], null: false,
65
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."
66
68
  field :archived_at, GraphQL::Types::ISO8601DateTime
67
69
  field :secret_expires_at, GraphQL::Types::ISO8601DateTime
68
70
  field :created_at, GraphQL::Types::ISO8601DateTime, null: false
@@ -85,6 +87,10 @@ module Masks
85
87
  object.enrolled.active.order(:name)
86
88
  end
87
89
 
90
+ def last_seen_at
91
+ Masks::Server::Token.where(kind: TokenType::GRANTS, client_id: object.id).maximum(:created_at)
92
+ end
93
+
88
94
  def tokens(limit: nil)
89
95
  Masks::Server::Token.where(kind: TokenType::GRANTS, client_id: object.id)
90
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
@@ -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
@@ -37,13 +37,11 @@ module Masks
37
37
  end
38
38
 
39
39
  def sweep_tokens(cutoff)
40
- loop do
41
- roots = Token.group("COALESCE(root_id, id)")
42
- .having("MAX(LEAST(consumed_at, expires_at)) < ?", cutoff)
43
- .limit(BATCH).pluck(Arel.sql("COALESCE(root_id, id)"))
44
- 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)
45
43
 
46
- Token.families(roots).delete_all
44
+ Token.families(ids - kept).delete_all
47
45
  end
48
46
 
49
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
@@ -3,7 +3,11 @@ module Masks
3
3
  class TextJob < ApplicationJob
4
4
  queue_as :mailers
5
5
 
6
- retry_on Sender::Failed, wait: :polynomially_longer, attempts: 3
6
+ ATTEMPTS = 3
7
+
8
+ retry_on Sender::Failed, wait: :polynomially_longer, attempts: ATTEMPTS do |job, error|
9
+ job.gave_up!(error)
10
+ end
7
11
 
8
12
  def perform(to, body)
9
13
  sender = Current.tenant&.sms_sender
@@ -12,6 +16,13 @@ module Masks
12
16
 
13
17
  sender.deliver(to: to, body: body)
14
18
  end
19
+
20
+ def gave_up!(error)
21
+ Tenant.switch(held_tenant) do
22
+ Event.record!(Event::TEXT_UNDELIVERED, actor: nil, by: nil, device: nil, ip_address: nil, user_agent: nil,
23
+ said: error.message)
24
+ end
25
+ end
15
26
  end
16
27
  end
17
28
  end
@@ -182,6 +182,108 @@ module Masks
182
182
  [ "OAUTH_PROVIDER_NAME", "{tenant}" ], [ "ENABLE_OAUTH_SIGNUP", "true" ],
183
183
  [ "ENABLE_OAUTH_ROLE_MANAGEMENT", "true" ], [ "OAUTH_ROLES_CLAIM", "groups" ], [ "OAUTH_ADMIN_ROLES", "admins" ]
184
184
  ]
185
+ },
186
+ {
187
+ key: "tailscale", name: "Tailscale",
188
+ redirects: [ "https://login.tailscale.com/a/oauth_response" ],
189
+ scopes: %w[openid profile email],
190
+ guide: "https://tailscale.com/kb/1240/sso-custom-oidc",
191
+ settings: [
192
+ [ "WebFinger domain", "{masks_host}" ], [ "Issuer URL", "{issuer}" ],
193
+ [ "Client ID", "{client_id}" ], [ "Client secret", "{client_secret}" ]
194
+ ]
195
+ },
196
+ {
197
+ key: "headscale", name: "Headscale",
198
+ redirects: [ "{base}/oidc/callback" ],
199
+ scopes: %w[openid profile email groups],
200
+ guide: "https://headscale.net/stable/ref/oidc/",
201
+ settings: [
202
+ [ "oidc.issuer", "{issuer}" ], [ "oidc.client_id", "{client_id}" ], [ "oidc.client_secret", "{client_secret}" ],
203
+ [ "oidc.scope", "[openid, profile, email, groups]" ], [ "oidc.allowed_groups", "[admins]" ]
204
+ ]
205
+ },
206
+ {
207
+ key: "home-assistant", name: "Home Assistant (OIDC integration)",
208
+ redirects: [ "{base}/auth/oidc/callback" ],
209
+ scopes: %w[openid profile email groups],
210
+ guide: "https://github.com/christiaangoossens/hass-oidc-auth",
211
+ settings: [
212
+ [ "auth_oidc.discovery_url", "{discovery}" ], [ "auth_oidc.client_id", "{client_id}" ],
213
+ [ "auth_oidc.client_secret", "{client_secret}" ], [ "auth_oidc.display_name", "{tenant}" ],
214
+ [ "auth_oidc.roles.admin", "admins" ]
215
+ ]
216
+ },
217
+ {
218
+ key: "synapse", name: "Matrix (Synapse)", asks: { "name" => "masks" },
219
+ redirects: [ "{base}/_synapse/client/oidc/callback" ],
220
+ scopes: %w[openid profile email],
221
+ guide: "https://element-hq.github.io/synapse/latest/openid.html",
222
+ settings: [
223
+ [ "oidc_providers", "- idp_id: {name}\n idp_name: {tenant}\n issuer: {issuer}\n client_id: {client_id}\n" \
224
+ " client_secret: {client_secret}\n scopes: [openid, profile, email]\n" \
225
+ " user_mapping_provider:\n config:\n localpart_template: \"{{ user.preferred_username }}\"\n" \
226
+ " display_name_template: \"{{ user.name }}\"\n email_template: \"{{ user.email }}\"" ]
227
+ ]
228
+ },
229
+ {
230
+ key: "bookstack", name: "BookStack",
231
+ redirects: [ "{base}/oidc/callback" ],
232
+ scopes: %w[openid profile email groups],
233
+ guide: "https://www.bookstackapp.com/docs/admin/oidc-auth/",
234
+ settings: [
235
+ [ "AUTH_METHOD", "oidc" ], [ "OIDC_NAME", "{tenant}" ], [ "OIDC_ISSUER", "{issuer}" ], [ "OIDC_ISSUER_DISCOVER", "true" ],
236
+ [ "OIDC_CLIENT_ID", "{client_id}" ], [ "OIDC_CLIENT_SECRET", "{client_secret}" ],
237
+ [ "OIDC_DISPLAY_NAME_CLAIMS", "name" ], [ "OIDC_ADDITIONAL_SCOPES", "groups" ],
238
+ [ "OIDC_USER_TO_GROUPS", "true" ], [ "OIDC_GROUPS_CLAIM", "groups" ], [ "OIDC_REMOVE_FROM_GROUPS", "true" ]
239
+ ]
240
+ },
241
+ {
242
+ key: "wikijs", name: "Wiki.js", asks: { "name" => "masks" },
243
+ redirects: [ "{base}/login/{name}/callback" ],
244
+ scopes: %w[openid profile email groups],
245
+ guide: "https://docs.requarks.io/auth/oidc",
246
+ settings: [
247
+ [ "Client ID", "{client_id}" ], [ "Client Secret", "{client_secret}" ],
248
+ [ "Authorization Endpoint URL", "{authorize}" ], [ "Token Endpoint URL", "{token}" ],
249
+ [ "User Info Endpoint URL", "{userinfo}" ], [ "Issuer", "{issuer}" ], [ "Logout URL", "{logout}" ],
250
+ [ "Email Claim", "email" ], [ "Display Name Claim", "name" ], [ "Groups Claim", "groups" ],
251
+ [ "Callback URL", "{base}/login/{name}/callback" ]
252
+ ]
253
+ },
254
+ {
255
+ key: "minio", name: "MinIO",
256
+ redirects: [ "{base}/oauth_callback" ],
257
+ scopes: %w[openid profile email groups],
258
+ guide: "https://min.io/docs/minio/linux/operations/external-iam/configure-openid-external-identity-management.html",
259
+ settings: [
260
+ [ "MINIO_IDENTITY_OPENID_CONFIG_URL", "{discovery}" ], [ "MINIO_IDENTITY_OPENID_CLIENT_ID", "{client_id}" ],
261
+ [ "MINIO_IDENTITY_OPENID_CLIENT_SECRET", "{client_secret}" ], [ "MINIO_IDENTITY_OPENID_SCOPES", "openid,profile,email,groups" ],
262
+ [ "MINIO_IDENTITY_OPENID_CLAIM_NAME", "groups" ], [ "MINIO_IDENTITY_OPENID_REDIRECT_URI", "{base}/oauth_callback" ],
263
+ [ "MINIO_IDENTITY_OPENID_DISPLAY_NAME", "{tenant}" ]
264
+ ]
265
+ },
266
+ {
267
+ key: "argocd", name: "Argo CD",
268
+ redirects: [ "{base}/auth/callback", "{base}/api/dex/callback" ],
269
+ scopes: %w[openid profile email groups],
270
+ guide: "https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/#existing-oidc-provider",
271
+ settings: [
272
+ [ "argocd-cm oidc.config", "name: {tenant}\nissuer: {issuer}\nclientID: {client_id}\nclientSecret: {client_secret}\n" \
273
+ "requestedScopes: [openid, profile, email, groups]" ],
274
+ [ "argocd-rbac-cm policy.csv", "g, admins, role:admin" ], [ "argocd-rbac-cm scopes", "[groups]" ]
275
+ ]
276
+ },
277
+ {
278
+ key: "synology", name: "Synology DSM",
279
+ redirects: [ "{base}", "{base}/" ],
280
+ scopes: %w[openid profile email],
281
+ guide: "https://kb.synology.com/en-global/DSM/help/DSM/AdminCenter/connection_sso_client",
282
+ settings: [
283
+ [ "Profile", "OIDC" ], [ "Name", "{tenant}" ], [ "Well Known URL", "{discovery}" ],
284
+ [ "Client ID", "{client_id}" ], [ "Client secret", "{client_secret}" ],
285
+ [ "Scope", "openid profile email" ], [ "Username claim", "preferred_username" ], [ "Redirect URI", "{base}" ]
286
+ ]
185
287
  }
186
288
  ].map(&:freeze).freeze
187
289
 
@@ -0,0 +1,41 @@
1
+ require "socket"
2
+ require "zlib"
3
+
4
+ module Masks
5
+ module Server
6
+ class BootTask
7
+ WINDOW = 10.minutes
8
+
9
+ Outcome = Struct.new(:name, :host, :value, :done) do
10
+ def done? = done
11
+ end
12
+
13
+ class << self
14
+ def once(name, fingerprint: nil)
15
+ key = "masks:boot:#{name}:#{Digest::SHA256.hexdigest(fingerprint.to_s)[0, 16]}"
16
+ host = Socket.gethostname
17
+ outcome = nil
18
+
19
+ ActiveRecord::Base.transaction do
20
+ ActiveRecord::Base.connection.execute("SELECT pg_advisory_xact_lock(#{lock_id(name)})")
21
+ done_by = ::Rails.cache.read(key)
22
+
23
+ if done_by
24
+ outcome = Outcome.new(name, done_by, nil, false)
25
+ else
26
+ value = yield
27
+ ::Rails.cache.write(key, host, expires_in: WINDOW)
28
+ outcome = Outcome.new(name, host, value, true)
29
+ end
30
+ end
31
+
32
+ outcome
33
+ end
34
+
35
+ def lock_id(name)
36
+ Zlib.crc32("masks:boot:#{name}")
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -2,7 +2,7 @@ module Masks
2
2
  module Server
3
3
  class EnrollmentToken < Token
4
4
  SHORTEST = 1.minute
5
- LONGEST = 1.hour
5
+ LONGEST = 1.day
6
6
 
7
7
  class << self
8
8
  def lifetime
@@ -146,6 +146,7 @@ module Masks
146
146
  SIGNAL_STREAM_UPDATED = "signal_stream.updated".freeze
147
147
  SIGNAL_STREAM_DELETED = "signal_stream.deleted".freeze
148
148
  SIGNAL_UNDELIVERED = "signal.undelivered".freeze
149
+ TEXT_UNDELIVERED = "text.undelivered".freeze
149
150
  SIGNAL_RECEIVED = "signal.received".freeze
150
151
 
151
152
  ORGANIZATION_CREATED = "organization.created".freeze
@@ -167,10 +168,13 @@ module Masks
167
168
  CUSTOM_DOMAIN_SERVED = "custom_domain.served".freeze
168
169
  CUSTOM_DOMAIN_STOPPED = "custom_domain.stopped".freeze
169
170
 
171
+ JOB_FAILED = "job.failed".freeze
172
+
170
173
  GRAVE = [
171
174
  LOGIN_REFUSED, LOGIN_THROTTLED, LOGOUT_UNDELIVERED, REFRESH_REUSED, STREAM_FAILED,
172
175
  DEVICE_BLOCKED, ACTOR_DELETED, AUTHENTICATOR_DISABLED, CONNECTION_REFUSED, DELEGATION_REFUSED,
173
- EXCHANGE_REFUSED, SIGN_IN_RISKY, SIGNAL_UNDELIVERED, SIGNAL_RECEIVED, MEMBERSHIP_ROLE_KEPT, ORGANIZATION_OWNERLESS
176
+ EXCHANGE_REFUSED, SIGN_IN_RISKY, SIGNAL_UNDELIVERED, TEXT_UNDELIVERED, SIGNAL_RECEIVED, MEMBERSHIP_ROLE_KEPT,
177
+ ORGANIZATION_OWNERLESS, JOB_FAILED
174
178
  ].freeze
175
179
 
176
180
  ACTIONS = constants(false).filter_map do |name|
@@ -0,0 +1,25 @@
1
+ module Masks
2
+ module Server
3
+ module JobFailures
4
+ KEEP = 2.days
5
+ PREFIX = "masks:jobs:failed_tenants:".freeze
6
+
7
+ module_function
8
+
9
+ def record(job, subdomains)
10
+ ::Rails.cache.write("#{PREFIX}#{job}", subdomains.sort, expires_in: KEEP)
11
+ end
12
+
13
+ def last(job)
14
+ ::Rails.cache.read("#{PREFIX}#{job}") || []
15
+ end
16
+
17
+ def named(jobs)
18
+ jobs.map do |job|
19
+ tenants = last(job)
20
+ tenants.empty? ? job : "#{job} (#{tenants.join(', ')})"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -157,6 +157,8 @@ de:
157
157
  exported: "Aktivität exportiert"
158
158
  mail_template:
159
159
  updated: "E-Mail-Vorlage geändert"
160
+ job:
161
+ failed: "Hintergrundjob für diesen Mandanten fehlgeschlagen"
160
162
  stream:
161
163
  created: "Event-Stream hinzugefügt"
162
164
  updated: "Event-Stream geändert"
@@ -171,6 +173,8 @@ de:
171
173
  signal:
172
174
  undelivered: "Shared Signal nicht zugestellt"
173
175
  received: "Anbieter hat Abmeldung signalisiert"
176
+ text:
177
+ undelivered: "SMS nicht zugestellt"
174
178
  organization:
175
179
  created: "Organisation hinzugefügt"
176
180
  updated: "Organisation geändert"
@@ -174,7 +174,7 @@ de:
174
174
  uris:
175
175
  title: "URIs"
176
176
  acs: "Assertion Consumer Services"
177
- redirect: "Redirect"
177
+ redirect: "Weiterleitung"
178
178
  post_logout: "Redirect nach dem Logout"
179
179
  resources: "Ressourcen"
180
180
  details:
@@ -0,0 +1,13 @@
1
+ de:
2
+ manage:
3
+ machines:
4
+ title: "Maschinen"
5
+ lede: "Clients, die diese App mit einem Registrierungstoken angemeldet hat. Jeder meldet sich mit einem eigenen Schlüssel an."
6
+ empty: "Diese App hat noch keine Maschine angemeldet."
7
+ last_seen: "Zuletzt angemeldet %{day}"
8
+ never: "Nie angemeldet"
9
+ enrolled: "Angemeldet %{day}"
10
+ enrolled_by: "Eine Maschine, angemeldet von"
11
+ revoke: "Widerrufen"
12
+ revoke_confirm: "%{name} widerrufen? Sie wird archiviert, jedes Token endet, und sie kann sich nicht mehr anmelden."
13
+ revoked: "%{name} ist widerrufen und kann sich nicht mehr anmelden."
@@ -1,7 +1,7 @@
1
1
  de:
2
2
  manage:
3
3
  namespaces:
4
- title: "Namespaces"
4
+ title: "Namensräume"
5
5
  release_confirm: "%{name} freigeben? Der nächste Client, der ihn anfordert, erhält ihn, und die Scopes darunter bedeuten nicht mehr, was sie jetzt bedeuten."
6
6
  released: "%{name} kann wieder beansprucht werden."
7
7
  columns:
@@ -157,6 +157,8 @@ en:
157
157
  exported: "Activity exported"
158
158
  mail_template:
159
159
  updated: "Email template changed"
160
+ job:
161
+ failed: "Background job failed for this tenant"
160
162
  stream:
161
163
  created: "Event stream added"
162
164
  updated: "Event stream changed"
@@ -171,6 +173,8 @@ en:
171
173
  signal:
172
174
  undelivered: "Shared signal not delivered"
173
175
  received: "Provider signaled sign-out"
176
+ text:
177
+ undelivered: "Text message not delivered"
174
178
  organization:
175
179
  created: "Organization added"
176
180
  updated: "Organization changed"
@@ -0,0 +1,13 @@
1
+ en:
2
+ manage:
3
+ machines:
4
+ title: "Machines"
5
+ lede: "Clients this app enrolled with an enrollment token. Each one signs in with its own key."
6
+ empty: "This app has not enrolled a machine yet."
7
+ last_seen: "Last signed in %{day}"
8
+ never: "Never signed in"
9
+ enrolled: "Enrolled %{day}"
10
+ enrolled_by: "A machine enrolled by"
11
+ revoke: "Revoke"
12
+ revoke_confirm: "Revoke %{name}? It is archived, every token it holds ends, and it can no longer sign in."
13
+ revoked: "%{name} is revoked and can no longer sign in."
@@ -157,6 +157,8 @@ es:
157
157
  exported: "Actividad exportada"
158
158
  mail_template:
159
159
  updated: "Plantilla de correo cambiada"
160
+ job:
161
+ failed: "Tarea en segundo plano fallida para este inquilino"
160
162
  stream:
161
163
  created: "Flujo de eventos añadido"
162
164
  updated: "Flujo de eventos cambiado"
@@ -171,6 +173,8 @@ es:
171
173
  signal:
172
174
  undelivered: "Señal compartida no entregada"
173
175
  received: "El proveedor señaló un cierre de sesión"
176
+ text:
177
+ undelivered: "Mensaje de texto no entregado"
174
178
  organization:
175
179
  created: "Organización añadida"
176
180
  updated: "Organización cambiada"
@@ -26,7 +26,7 @@ es:
26
26
  area_yours: " Tú los recibes todos ahora."
27
27
  also: "%{name} también puede"
28
28
  details: "Detalles técnicos"
29
- scopes: "Scopes"
29
+ scopes: "Alcances"
30
30
  client: "Credenciales"
31
31
  credentials:
32
32
  public: "Pública. No guarda ningún secreto."
@@ -0,0 +1,13 @@
1
+ es:
2
+ manage:
3
+ machines:
4
+ title: "Máquinas"
5
+ lede: "Clientes que esta aplicación inscribió con un token de inscripción. Cada uno inicia sesión con su propia clave."
6
+ empty: "Esta aplicación aún no ha inscrito ninguna máquina."
7
+ last_seen: "Último inicio de sesión %{day}"
8
+ never: "Nunca ha iniciado sesión"
9
+ enrolled: "Inscrita %{day}"
10
+ enrolled_by: "Una máquina inscrita por"
11
+ revoke: "Revocar"
12
+ revoke_confirm: "¿Revocar %{name}? Se archiva, todos sus tokens terminan y ya no puede iniciar sesión."
13
+ revoked: "%{name} está revocada y ya no puede iniciar sesión."