masks-server 0.10.0 → 0.12.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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/masks/server/account_controller.rb +3 -0
  3. data/app/controllers/masks/server/app_passwords_controller.rb +41 -0
  4. data/app/controllers/masks/server/authorize_controller.rb +33 -2
  5. data/app/controllers/masks/server/discovery_controller.rb +36 -0
  6. data/app/controllers/masks/server/enrollments_controller.rb +17 -0
  7. data/app/controllers/masks/server/forward_auth_controller.rb +104 -12
  8. data/app/controllers/masks/server/logins_controller.rb +4 -3
  9. data/app/controllers/masks/server/organization_access_controller.rb +39 -0
  10. data/app/controllers/masks/server/organization_members_controller.rb +3 -1
  11. data/app/graphql/masks/server/manage/mutations/base_mutation.rb +7 -0
  12. data/app/graphql/masks/server/manage/mutations/create_group.rb +6 -4
  13. data/app/graphql/masks/server/manage/mutations/decline_access.rb +27 -0
  14. data/app/graphql/masks/server/manage/mutations/grant_access.rb +29 -0
  15. data/app/graphql/masks/server/manage/mutations/group_source.rb +34 -0
  16. data/app/graphql/masks/server/manage/mutations/issue_ldap_credential.rb +5 -2
  17. data/app/graphql/masks/server/manage/mutations/refresh_group.rb +21 -0
  18. data/app/graphql/masks/server/manage/mutations/remove_group_member.rb +2 -0
  19. data/app/graphql/masks/server/manage/mutations/run_health_checks.rb +19 -0
  20. data/app/graphql/masks/server/manage/mutations/unenroll_client.rb +28 -0
  21. data/app/graphql/masks/server/manage/mutations/update_client.rb +6 -0
  22. data/app/graphql/masks/server/manage/mutations/update_group.rb +7 -4
  23. data/app/graphql/masks/server/manage/mutations/update_tenant.rb +14 -1
  24. data/app/graphql/masks/server/manage/types/access_request_type.rb +21 -0
  25. data/app/graphql/masks/server/manage/types/actor_type.rb +6 -0
  26. data/app/graphql/masks/server/manage/types/client_type.rb +18 -0
  27. data/app/graphql/masks/server/manage/types/group_condition_input.rb +16 -0
  28. data/app/graphql/masks/server/manage/types/group_condition_type.rb +29 -0
  29. data/app/graphql/masks/server/manage/types/group_member_type.rb +16 -0
  30. data/app/graphql/masks/server/manage/types/group_type.rb +17 -1
  31. data/app/graphql/masks/server/manage/types/health_check_type.rb +18 -0
  32. data/app/graphql/masks/server/manage/types/ldap_credential_type.rb +1 -0
  33. data/app/graphql/masks/server/manage/types/mutation_type.rb +5 -0
  34. data/app/graphql/masks/server/manage/types/query_type.rb +24 -0
  35. data/app/graphql/masks/server/manage/types/release_type.rb +16 -0
  36. data/app/graphql/masks/server/manage/types/tenant_type.rb +6 -1
  37. data/app/helpers/masks/server/account_helper.rb +4 -0
  38. data/app/jobs/masks/server/application_job.rb +28 -0
  39. data/app/jobs/masks/server/check_domain_claims_job.rb +1 -3
  40. data/app/jobs/masks/server/cleanup_job.rb +5 -6
  41. data/app/jobs/masks/server/health_check_job.rb +21 -0
  42. data/app/jobs/masks/server/idle_accounts_job.rb +1 -3
  43. data/app/jobs/masks/server/prune_queue_job.rb +19 -0
  44. data/app/jobs/masks/server/refresh_groups_job.rb +18 -0
  45. data/app/jobs/masks/server/refresh_saml_metadata_job.rb +1 -3
  46. data/app/jobs/masks/server/release_check_job.rb +21 -0
  47. data/app/jobs/masks/server/rotate_signing_keys_job.rb +1 -3
  48. data/app/jobs/masks/server/text_job.rb +12 -1
  49. data/app/ldap/masks/server/ldap/connection.rb +3 -2
  50. data/app/ldap/masks/server/ldap/gate.rb +31 -4
  51. data/app/logins/masks/server/login_states/second_factor.rb +1 -1
  52. data/app/mailers/masks/server/actor_mailer.rb +31 -0
  53. data/app/models/masks/server/access_request.rb +103 -0
  54. data/app/models/masks/server/actor.rb +6 -0
  55. data/app/models/masks/server/app_password.rb +91 -0
  56. data/app/models/masks/server/app_passwords.rb +82 -0
  57. data/app/models/masks/server/app_preset.rb +102 -0
  58. data/app/models/masks/server/boot_task.rb +41 -0
  59. data/app/models/masks/server/client.rb +15 -0
  60. data/app/models/masks/server/connection.rb +7 -0
  61. data/app/models/masks/server/declared.rb +10 -2
  62. data/app/models/masks/server/email_verification.rb +2 -2
  63. data/app/models/masks/server/enrollment_token.rb +1 -1
  64. data/app/models/masks/server/event.rb +15 -1
  65. data/app/models/masks/server/federation/protocol.rb +1 -1
  66. data/app/models/masks/server/federation/saml.rb +1 -1
  67. data/app/models/masks/server/forward_auth_rules.rb +162 -0
  68. data/app/models/masks/server/group.rb +207 -4
  69. data/app/models/masks/server/group_member.rb +7 -0
  70. data/app/models/masks/server/group_rule.rb +63 -0
  71. data/app/models/masks/server/health.rb +243 -0
  72. data/app/models/masks/server/health_check.rb +27 -0
  73. data/app/models/masks/server/invitation.rb +2 -2
  74. data/app/models/masks/server/job_failures.rb +25 -0
  75. data/app/models/masks/server/ldap_credential.rb +2 -2
  76. data/app/models/masks/server/login.rb +28 -2
  77. data/app/models/masks/server/mail_previews.rb +5 -0
  78. data/app/models/masks/server/mail_template.rb +2 -0
  79. data/app/models/masks/server/membership.rb +10 -3
  80. data/app/models/masks/server/notifications.rb +3 -1
  81. data/app/models/masks/server/password_reset.rb +2 -2
  82. data/app/models/masks/server/provider.rb +1 -0
  83. data/app/models/masks/server/queue.rb +60 -0
  84. data/app/models/masks/server/release.rb +74 -0
  85. data/app/models/masks/server/session.rb +8 -0
  86. data/app/models/masks/server/tenant.rb +39 -2
  87. data/app/models/masks/server/token.rb +2 -1
  88. data/app/views/masks/server/account/_app_passwords.html.erb +83 -0
  89. data/app/views/masks/server/account/_organizations.html.erb +33 -0
  90. data/app/views/masks/server/account/index.html.erb +5 -0
  91. data/app/views/masks/server/actor_mailer/access_declined.html.erb +5 -0
  92. data/app/views/masks/server/actor_mailer/access_declined.text.erb +5 -0
  93. data/app/views/masks/server/actor_mailer/access_granted.html.erb +7 -0
  94. data/app/views/masks/server/actor_mailer/access_granted.text.erb +7 -0
  95. data/app/views/masks/server/authorize/error.html.erb +25 -0
  96. data/config/locales/de/access_requests.yml +43 -0
  97. data/config/locales/de/account/app_passwords.yml +42 -0
  98. data/config/locales/de/events.yml +9 -0
  99. data/config/locales/de/flashes.yml +1 -0
  100. data/config/locales/de/group_sources.yml +5 -0
  101. data/config/locales/de/manage/access_requests.yml +12 -0
  102. data/config/locales/de/manage/app_passwords.yml +16 -0
  103. data/config/locales/de/manage/client.yml +1 -1
  104. data/config/locales/de/manage/forward_auth_rules.yml +29 -0
  105. data/config/locales/de/manage/group_sources.yml +44 -0
  106. data/config/locales/de/manage/health.yml +78 -0
  107. data/config/locales/de/manage/links.yml +20 -0
  108. data/config/locales/de/manage/machines.yml +13 -0
  109. data/config/locales/de/manage/namespaces.yml +1 -1
  110. data/config/locales/en/access_requests.yml +43 -0
  111. data/config/locales/en/account/app_passwords.yml +42 -0
  112. data/config/locales/en/events.yml +9 -0
  113. data/config/locales/en/flashes.yml +1 -0
  114. data/config/locales/en/group_sources.yml +5 -0
  115. data/config/locales/en/manage/access_requests.yml +12 -0
  116. data/config/locales/en/manage/app_passwords.yml +16 -0
  117. data/config/locales/en/manage/forward_auth_rules.yml +29 -0
  118. data/config/locales/en/manage/group_sources.yml +44 -0
  119. data/config/locales/en/manage/health.yml +78 -0
  120. data/config/locales/en/manage/links.yml +20 -0
  121. data/config/locales/en/manage/machines.yml +13 -0
  122. data/config/locales/es/access_requests.yml +43 -0
  123. data/config/locales/es/account/app_passwords.yml +42 -0
  124. data/config/locales/es/events.yml +9 -0
  125. data/config/locales/es/flashes.yml +1 -0
  126. data/config/locales/es/group_sources.yml +5 -0
  127. data/config/locales/es/handshakes.yml +1 -1
  128. data/config/locales/es/manage/access_requests.yml +12 -0
  129. data/config/locales/es/manage/app_passwords.yml +16 -0
  130. data/config/locales/es/manage/forward_auth_rules.yml +29 -0
  131. data/config/locales/es/manage/group_sources.yml +44 -0
  132. data/config/locales/es/manage/health.yml +78 -0
  133. data/config/locales/es/manage/links.yml +20 -0
  134. data/config/locales/es/manage/machines.yml +13 -0
  135. data/config/locales/fr/access_requests.yml +43 -0
  136. data/config/locales/fr/account/app_passwords.yml +42 -0
  137. data/config/locales/fr/events.yml +9 -0
  138. data/config/locales/fr/flashes.yml +1 -0
  139. data/config/locales/fr/group_sources.yml +5 -0
  140. data/config/locales/fr/handshakes.yml +1 -1
  141. data/config/locales/fr/manage/access_requests.yml +12 -0
  142. data/config/locales/fr/manage/app_passwords.yml +16 -0
  143. data/config/locales/fr/manage/client.yml +1 -1
  144. data/config/locales/fr/manage/clients.yml +1 -1
  145. data/config/locales/fr/manage/forward_auth_rules.yml +29 -0
  146. data/config/locales/fr/manage/group_sources.yml +44 -0
  147. data/config/locales/fr/manage/health.yml +78 -0
  148. data/config/locales/fr/manage/links.yml +20 -0
  149. data/config/locales/fr/manage/machines.yml +13 -0
  150. data/config/locales/fr/manage/providers.yml +1 -1
  151. data/config/routes.rb +6 -0
  152. data/db/migrate/20261019140000_add_indexes_for_cleanup_and_metrics.rb +10 -0
  153. data/db/migrate/20261020100000_create_access_requests.rb +28 -0
  154. data/db/migrate/20261020110000_add_app_passwords_to_clients.rb +5 -0
  155. data/db/migrate/20261020120000_add_forward_auth_rules_to_clients.rb +6 -0
  156. data/db/migrate/20261020130000_add_sources_to_groups.rb +15 -0
  157. data/db/migrate/20261020140000_create_health_checks.rb +25 -0
  158. data/db/migrate/20261020150000_add_link_lifetimes_to_tenants.rb +8 -0
  159. data/db/migrate/20261021100000_add_pinned_origin_to_tenants.rb +5 -0
  160. data/lib/generators/masks/server/install/install_generator.rb +3 -2
  161. data/lib/masks/server/configuration.rb +6 -11
  162. data/lib/masks/server/schema_reference.rb +12 -0
  163. data/lib/masks/server/tenancy/middleware.rb +17 -6
  164. data/lib/tasks/masks.rake +31 -7
  165. data/public/masks-assets/.vite/manifest.json +13 -13
  166. data/public/masks-assets/assets/{account-D46qDQCo.js → account-h7I6VUPR.js} +2 -2
  167. data/public/masks-assets/assets/{account-D46qDQCo.js.map → account-h7I6VUPR.js.map} +1 -1
  168. data/public/masks-assets/assets/{app-D7W0gAyT.css → app-BXOScMjL.css} +1 -1
  169. data/public/masks-assets/assets/{handshake-DIFZPSAK.js → handshake-YQpRNMIs.js} +2 -2
  170. data/public/masks-assets/assets/{handshake-DIFZPSAK.js.map → handshake-YQpRNMIs.js.map} +1 -1
  171. data/public/masks-assets/assets/initial-Cn-UuAsy.js +3 -0
  172. data/public/masks-assets/assets/{initial-C1GpCiEh.js.map → initial-Cn-UuAsy.js.map} +1 -1
  173. data/public/masks-assets/assets/{login-C_6igRZ3.js → login-C21_jb6L.js} +3 -3
  174. data/public/masks-assets/assets/{login-C_6igRZ3.js.map → login-C21_jb6L.js.map} +1 -1
  175. data/public/masks-assets/assets/{manage-DR9qQiBU.js → manage-DpVxe5sv.js} +86 -50
  176. data/public/masks-assets/assets/manage-DpVxe5sv.js.map +1 -0
  177. data/public/masks-assets/assets/passkey-5UYPVD3c.js +2 -0
  178. data/public/masks-assets/assets/{passkey-DIj_5LCY.js.map → passkey-5UYPVD3c.js.map} +1 -1
  179. data/version.txt +1 -1
  180. metadata +96 -14
  181. data/public/masks-assets/assets/initial-C1GpCiEh.js +0 -3
  182. data/public/masks-assets/assets/manage-DR9qQiBU.js.map +0 -1
  183. data/public/masks-assets/assets/passkey-DIj_5LCY.js +0 -2
@@ -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
@@ -32,6 +32,8 @@ module Masks
32
32
  argument :saml_slo_url, String, required: false,
33
33
  description: "The application's single logout URL, which receives masks' logout messages over HTTP-POST. Empty stops single logout."
34
34
  argument :consent_required, Boolean, required: false
35
+ argument :app_passwords, Boolean, required: false,
36
+ description: "Whether people may make an app password for this client. An app password signs in over LDAP and forward auth without a second factor, for this client alone."
35
37
  argument :access_token_lifetime, Integer, required: false,
36
38
  description: "Seconds an access token lasts, from 60 to a day. Zero goes back to an hour."
37
39
  argument :refresh_token_lifetime, Integer, required: false,
@@ -41,6 +43,10 @@ module Masks
41
43
  "from 300 to 400 days. Zero goes back to 90 days."
42
44
  argument :allowed_groups, [ String ], required: false,
43
45
  description: "Group keys, or organization roles as key:role, whose people may sign in to this client. Empty lets everyone in."
46
+ argument :forward_auth_rules, GraphQL::Types::JSON, required: false,
47
+ description: "The ordered rules forward auth follows, for an app behind a proxy only. Each rule has `paths` " \
48
+ "(absolute, with * for one segment and ** for any depth), `methods`, `networks` (addresses or ranges), " \
49
+ "and `groups` it covers, and an `outcome` of open, one_factor, two_factor, token, or deny. Empty removes them all."
44
50
  argument :sign_in_policy, ID, required: false
45
51
  argument :client_uri, String, required: false
46
52
  argument :logo_uri, String, required: false
@@ -3,19 +3,22 @@ module Masks
3
3
  module Manage
4
4
  module Mutations
5
5
  class UpdateGroup < BaseMutation
6
+ include GroupSource
7
+
6
8
  requires :security
7
9
 
8
10
  argument :key, String
9
- argument :name, String
11
+ argument :name, String, required: false
10
12
 
11
13
  field :group, Types::GroupType, null: false
12
14
 
13
- def resolve(key:, name:)
15
+ def resolve(key:, name: nil, **source)
14
16
  group = group!(key)
15
- group.name = name
17
+ group.name = name unless name.nil?
18
+ fill_source(group, **source)
16
19
  save!(group)
17
20
 
18
- { group: group }
21
+ { group: group.reload }
19
22
  end
20
23
  end
21
24
  end
@@ -30,14 +30,27 @@ module Masks
30
30
  argument :key_rotation_days, Integer, required: false,
31
31
  description: "Days between scheduled signing key rotations, from 7 to 365. Zero leaves rotation to a manager."
32
32
 
33
+ argument :invitation_lifetime, Integer, required: false,
34
+ description: "Seconds an invitation to an account stays valid, from an hour to 30 days. Zero goes back to 7 days."
35
+ argument :organization_invitation_lifetime, Integer, required: false,
36
+ description: "Seconds an invitation to an organization stays open after it was last sent, from an hour to 30 days. Zero goes back to 14 days."
37
+ argument :password_reset_lifetime, Integer, required: false,
38
+ description: "Seconds a password reset link stays valid, from 5 minutes to a day. Zero goes back to 30 minutes."
39
+ argument :email_verification_lifetime, Integer, required: false,
40
+ description: "Seconds an email confirmation link stays valid, from an hour to 14 days. Zero goes back to 2 days."
41
+
33
42
  field :tenant, Types::TenantType, null: false
34
43
 
35
44
  def resolve(name: nil, dynamic_client_scopes: nil, dynamic_registration: nil,
36
45
  named_by: nil, browsers_only: nil, blocked_agents: nil, sign_in_policy: nil,
37
46
  suspend_after: nil, delete_after: nil, event_retention_days: nil, risky_networks: nil,
38
- accent_color: nil, key_rotation_days: nil, ldap_enabled: nil, ldap_password_only: nil)
47
+ accent_color: nil, key_rotation_days: nil, ldap_enabled: nil, ldap_password_only: nil, **link_lifetimes)
39
48
  tenant = Current.tenant
40
49
 
50
+ link_lifetimes.compact.each do |name, seconds|
51
+ tenant[name] = seconds.zero? ? Masks::Server::Tenant.default_link_lifetime(name) : seconds
52
+ end
53
+
41
54
  tenant.name = name unless name.nil?
42
55
  tenant.accent_color = accent_color unless accent_color.nil?
43
56
  tenant.key_rotation_days = key_rotation_days.zero? ? nil : key_rotation_days unless key_rotation_days.nil?
@@ -0,0 +1,21 @@
1
+ module Masks
2
+ module Server
3
+ module Manage
4
+ module Types
5
+ class AccessRequestType < BaseObject
6
+ description "An actor turned away from a client who asked for access. Granting it puts them in one of the groups the client allows."
7
+
8
+ field :id, ID, null: false
9
+ field :actor, ActorType, null: false
10
+ field :client, ClientType, null: false
11
+ field :label, String, description: "The group key, or organization role as key:role, the actor was put in. Null until granted."
12
+ field :decided_by, ActorType
13
+ field :granted_at, GraphQL::Types::ISO8601DateTime
14
+ field :declined_at, GraphQL::Types::ISO8601DateTime
15
+ field :open, Boolean, null: false, method: :open?, description: "True while nobody has granted or declined it."
16
+ field :created_at, GraphQL::Types::ISO8601DateTime, null: false
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -32,6 +32,8 @@ module Masks
32
32
  field :tokens, [ "Masks::Server::Manage::Types::TokenType" ], null: false
33
33
  field :memberships, [ "Masks::Server::Manage::Types::MembershipType" ], null: false
34
34
  field :groups, [ "Masks::Server::Manage::Types::GroupType" ], null: false
35
+ field :group_memberships, [ "Masks::Server::Manage::Types::GroupMemberType" ], null: false,
36
+ description: "Each group the person is in, with what put them there."
35
37
  field :group_labels, [ String ], null: false,
36
38
  description: "What the groups claim carries: each group's key, and each organization role as key:role."
37
39
 
@@ -130,6 +132,10 @@ module Masks
130
132
  object.memberships.includes(:organization).joins(:organization).order("organizations.name")
131
133
  end
132
134
 
135
+ def group_memberships
136
+ object.group_members.includes(:group).joins(:group).order("groups.name")
137
+ end
138
+
133
139
  def tokens
134
140
  Masks::Server::Token.where(kind: TokenType::GRANTS, actor_id: object.id)
135
141
  .live
@@ -20,6 +20,10 @@ module Masks
20
20
  description: "Seconds from a sign-in until its refresh tokens stop, however often they are used. Null is 90 days."
21
21
  field :allowed_groups, [ String ], null: false,
22
22
  description: "Group keys, or organization roles as key:role, whose people may sign in. Empty lets everyone in."
23
+ field :forward_auth_rules, GraphQL::Types::JSON, null: false,
24
+ description: "The ordered rules forward auth follows for an app behind a proxy. Each has `paths`, `methods`, " \
25
+ "`networks`, and `groups` it covers, and an `outcome` of open, one_factor, two_factor, token, or deny. " \
26
+ "The first rule that covers a request decides it. With no rule, a signed-in person who may sign in is let through."
23
27
  field :namespaces, [ "Masks::Server::Manage::Types::NamespaceType" ], null: false
24
28
  field :token_endpoint_auth_method, String, null: false
25
29
  field :subject_type, String, null: false
@@ -55,6 +59,8 @@ module Masks
55
59
  description: "Where masks posts a signed LogoutRequest when a person who signed in to this SAML application signs out, and its LogoutResponse when the application asks."
56
60
  field :require_signed_request_object, Boolean, null: false
57
61
  field :consent_required, Boolean, null: false
62
+ field :app_passwords, Boolean, null: false,
63
+ description: "Whether people may make an app password for this client, which LDAP and forward auth accept in place of their password and second factor."
58
64
  field :sign_in_policy, SignInPolicyType
59
65
  field :dynamic, Boolean, null: false
60
66
  field :approved_at, GraphQL::Types::ISO8601DateTime
@@ -63,6 +69,10 @@ module Masks
63
69
  description: "The app that enrolled this client as one of its machines. Archiving that app archives this client."
64
70
  field :enrolled, [ ClientType ], null: false,
65
71
  description: "The machines this app enrolled that are not archived."
72
+ field :last_seen_at, GraphQL::Types::ISO8601DateTime,
73
+ description: "When this client last got a token. Null if it never has."
74
+ field :access_requests, [ "Masks::Server::Manage::Types::AccessRequestType" ], null: false,
75
+ description: "Actors turned away from this client who asked for access and are waiting."
66
76
  field :archived_at, GraphQL::Types::ISO8601DateTime
67
77
  field :secret_expires_at, GraphQL::Types::ISO8601DateTime
68
78
  field :created_at, GraphQL::Types::ISO8601DateTime, null: false
@@ -85,6 +95,14 @@ module Masks
85
95
  object.enrolled.active.order(:name)
86
96
  end
87
97
 
98
+ def access_requests
99
+ object.access_requests.open.includes(:actor).order(:created_at)
100
+ end
101
+
102
+ def last_seen_at
103
+ Masks::Server::Token.where(kind: TokenType::GRANTS, client_id: object.id).maximum(:created_at)
104
+ end
105
+
88
106
  def tokens(limit: nil)
89
107
  Masks::Server::Token.where(kind: TokenType::GRANTS, client_id: object.id)
90
108
  .live
@@ -0,0 +1,16 @@
1
+ module Masks
2
+ module Server
3
+ module Manage
4
+ module Types
5
+ class GroupConditionInput < GraphQL::Schema::InputObject
6
+ description "One condition of a group's rule."
7
+
8
+ argument :kind, String, description: "email_domain or profile."
9
+ argument :field, String, required: false,
10
+ description: "For profile: name, given_name, family_name, middle_name, nickname, gender, zoneinfo, or locale."
11
+ argument :value, String, description: "The domain a verified email address ends with, or the value the profile field equals."
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ module Masks
2
+ module Server
3
+ module Manage
4
+ module Types
5
+ class GroupConditionType < BaseObject
6
+ description "One condition of a group's rule. Every condition must hold for a person to be in the group."
7
+
8
+ field :kind, String, null: false, description: "email_domain or profile."
9
+ field :field, String, description: "The profile field a profile condition reads."
10
+ field :value, String, null: false,
11
+ description: "The domain a verified email address ends with, or the value the profile field equals. " \
12
+ "A profile condition matches only people whose profile a directory sets."
13
+
14
+ def kind
15
+ object["kind"]
16
+ end
17
+
18
+ def field
19
+ object["field"]
20
+ end
21
+
22
+ def value
23
+ object["value"]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ module Masks
2
+ module Server
3
+ module Manage
4
+ module Types
5
+ class GroupMemberType < BaseObject
6
+ description "A person's place in a group, and what put them there."
7
+
8
+ field :group, GroupType, null: false
9
+ field :actor, ActorType, null: false
10
+ field :sourced_by, String, description: "rule, provider, or group when the group's source put them in. Null when a manager added them by hand."
11
+ field :created_at, GraphQL::Types::ISO8601DateTime, null: false
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -3,14 +3,26 @@ module Masks
3
3
  module Manage
4
4
  module Types
5
5
  class GroupType < BaseObject
6
- description "A named set of people in the tenant. The groups claim lists the key of each group a person is in."
6
+ description "A named set of people in the tenant. The groups claim lists the key of each group a person is in. " \
7
+ "A group fills by hand, from a rule, from a provider, or from other groups."
7
8
 
8
9
  field :key, String, null: false
9
10
  field :name, String, null: false
11
+ field :source, String, null: false, description: "manual, rule, provider, or group."
12
+ field :rule, [ GroupConditionType ], null: false, description: "The conditions a rule group checks. Empty unless the source is rule."
13
+ field :provider_key, String, description: "The provider a provider group reads at sign-in."
14
+ field :upstream_group, String, description: "The group name the provider lists for people who belong."
15
+ field :parent_labels, [ String ], null: false, description: "The group keys and organization roles, as key:role, a nested group holds everyone from."
16
+ field :refreshed_at, GraphQL::Types::ISO8601DateTime, description: "When the source last filled the group."
10
17
  field :member_count, Integer, null: false
11
18
  field :members, [ ActorType ], null: false
19
+ field :memberships, [ GroupMemberType ], null: false, description: "Each member with what put them in the group."
12
20
  field :created_at, GraphQL::Types::ISO8601DateTime, null: false
13
21
 
22
+ def provider_key
23
+ object.provider&.key
24
+ end
25
+
14
26
  def member_count
15
27
  object.members.count
16
28
  end
@@ -18,6 +30,10 @@ module Masks
18
30
  def members
19
31
  object.actors.order(:nickname)
20
32
  end
33
+
34
+ def memberships
35
+ object.members.includes(:actor).sort_by { |member| member.actor.nickname.to_s }
36
+ end
21
37
  end
22
38
  end
23
39
  end
@@ -0,0 +1,18 @@
1
+ module Masks
2
+ module Server
3
+ module Manage
4
+ module Types
5
+ class HealthCheckType < BaseObject
6
+ description "The latest result of one health check. Checks run every 10 minutes."
7
+
8
+ field :name, ID, null: false, description: "jobs, worker, queue, ldap, metrics, mail, origin, domains, or keys."
9
+ field :status, String, null: false, description: "ok, warning, or failing."
10
+ field :summary, String, null: false, description: "A short word for what the check found, such as none, stale, or silent."
11
+ field :details, GraphQL::Types::JSON, null: false, description: "The figures and names behind the summary."
12
+ field :checked_at, GraphQL::Types::ISO8601DateTime, null: false
13
+ field :changed_at, GraphQL::Types::ISO8601DateTime, null: false, description: "When the status last changed."
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -7,6 +7,7 @@ module Masks
7
7
  field :label, String, null: false
8
8
  field :bind_dn, String, null: false, description: "The DN an app binds as, under ou=services in this tenant's directory."
9
9
  field :issued_by, ActorType
10
+ field :client, ClientType, description: "The client whose app passwords bind through this credential, when one was chosen."
10
11
  field :used_at, GraphQL::Types::ISO8601DateTime, description: "When an app last bound with it, to within five minutes."
11
12
  field :created_at, GraphQL::Types::ISO8601DateTime, null: false
12
13
 
@@ -28,6 +28,9 @@ module Masks
28
28
  field :delete_group, mutation: Mutations::DeleteGroup
29
29
  field :add_group_member, mutation: Mutations::AddGroupMember
30
30
  field :remove_group_member, mutation: Mutations::RemoveGroupMember
31
+ field :grant_access, mutation: Mutations::GrantAccess
32
+ field :decline_access, mutation: Mutations::DeclineAccess
33
+ field :refresh_group, mutation: Mutations::RefreshGroup
31
34
  field :disable_code_factor, mutation: Mutations::DisableCodeFactor
32
35
  field :revoke_passkey, mutation: Mutations::RevokePasskey
33
36
  field :upload_avatar, mutation: Mutations::UploadAvatar
@@ -41,6 +44,7 @@ module Masks
41
44
  field :update_client, mutation: Mutations::UpdateClient
42
45
  field :rotate_client_secret, mutation: Mutations::RotateClientSecret
43
46
  field :archive_client, mutation: Mutations::ArchiveClient
47
+ field :unenroll_client, mutation: Mutations::UnenrollClient
44
48
  field :restore_client, mutation: Mutations::RestoreClient
45
49
  field :release_namespace, mutation: Mutations::ReleaseNamespace
46
50
  field :discover_provider, mutation: Mutations::DiscoverProvider
@@ -96,6 +100,7 @@ module Masks
96
100
  field :activate_signing_key, mutation: Mutations::ActivateSigningKey
97
101
  field :discard_signing_key, mutation: Mutations::DiscardSigningKey
98
102
  field :rotate_signing_key, mutation: Mutations::RotateSigningKey
103
+ field :run_health_checks, mutation: Mutations::RunHealthChecks
99
104
  end
100
105
  end
101
106
  end
@@ -191,6 +191,13 @@ module Masks
191
191
 
192
192
  field :event_actions, [ EventActionType ], null: false
193
193
 
194
+ field :health, [ HealthCheckType ], null: false,
195
+ description: "The latest result of each health check for this tenant and the server it runs on. " \
196
+ "Empty until the first scheduled run."
197
+ field :release, ReleaseType,
198
+ description: "The newest release when it is newer than this server. Null while the server is current, " \
199
+ "or while MASKS_RELEASE_CHECK is off."
200
+
194
201
  LIMIT = 50
195
202
  CEILING = 200
196
203
  SPAN = 30
@@ -464,6 +471,23 @@ module Masks
464
471
  Masks::Server::Event::ACTIONS.sort
465
472
  end
466
473
 
474
+ def health
475
+ Masks::Server::HealthCheck.by_name
476
+ end
477
+
478
+ def release
479
+ found = Masks::Server::Release.available
480
+
481
+ return nil if found.nil?
482
+
483
+ {
484
+ version: found["version"],
485
+ url: found["url"],
486
+ operator_step: found["operator_step"] == true,
487
+ checked_at: Time.iso8601(found["checked_at"])
488
+ }
489
+ end
490
+
467
491
  private
468
492
 
469
493
  def actor_scope(search: nil, activated: nil, holds: nil, pending_approval: nil, recovery_requested: nil,
@@ -0,0 +1,16 @@
1
+ module Masks
2
+ module Server
3
+ module Manage
4
+ module Types
5
+ class ReleaseType < BaseObject
6
+ description "A newer release of masks than the one this server runs."
7
+
8
+ field :version, String, null: false
9
+ field :url, String, null: false, description: "The release notes."
10
+ field :operator_step, Boolean, null: false, description: "Whether the release notes carry a line that starts with Operator:."
11
+ field :checked_at, GraphQL::Types::ISO8601DateTime, null: false
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -9,7 +9,7 @@ module Masks
9
9
  field :origins, [ String ], null: false, description: "Every origin this tenant answers on, each its own issuer."
10
10
 
11
11
  def origins
12
- [ object.templated_origin, object.custom_origin ].compact
12
+ [ object.templated_origin || object.held_origin, object.custom_origin ].compact
13
13
  end
14
14
  field :name, String, null: false
15
15
  field :named_by, String, null: false
@@ -31,6 +31,11 @@ module Masks
31
31
  def event_retention_days
32
32
  object.event_retention.in_days.round
33
33
  end
34
+ field :invitation_lifetime, Integer, null: false, description: "Seconds an invitation to an account stays valid."
35
+ field :organization_invitation_lifetime, Integer, null: false,
36
+ description: "Seconds an invitation to an organization stays open after it was last sent."
37
+ field :password_reset_lifetime, Integer, null: false, description: "Seconds a password reset link stays valid."
38
+ field :email_verification_lifetime, Integer, null: false, description: "Seconds an email confirmation link stays valid."
34
39
  field :ldap_enabled, Boolean, null: false, description: "Whether the LDAP directory answers for this tenant.", method: :ldap?
35
40
  field :ldap_password_only, Boolean, null: false,
36
41
  description: "Whether a password alone binds to LDAP for an account that would otherwise need a second factor.",
@@ -29,6 +29,10 @@ module Masks
29
29
  t("account.index.#{key}", **options)
30
30
  end
31
31
 
32
+ def account_access_requests(organization)
33
+ AccessRequest.for_organization(organization).includes(:actor, :client).order(:created_at)
34
+ end
35
+
32
36
  def account_roster(organization)
33
37
  organization.memberships.sort_by do |member|
34
38
  [ member.pending? ? 1 : 0, member.owner? ? 0 : 1, Members.label(member).to_s.downcase ]
@@ -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
@@ -34,16 +34,15 @@ module Masks
34
34
  Members.purge_lapsed!
35
35
  Replay.where(expires_at: ...Time.current).in_batches(of: BATCH).delete_all
36
36
  ReleasedNickname.lapsed.in_batches(of: BATCH).delete_all
37
+ AccessRequest.lapsed.in_batches(of: BATCH).delete_all
37
38
  end
38
39
 
39
40
  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?
41
+ Token.where(root_id: nil).ended_before(cutoff).in_batches(of: BATCH) do |roots|
42
+ ids = roots.pluck(:id)
43
+ kept = Token.where(root_id: ids).where("LEAST(consumed_at, expires_at) >= ?", cutoff).distinct.pluck(:root_id)
45
44
 
46
- Token.families(roots).delete_all
45
+ Token.families(ids - kept).delete_all
47
46
  end
48
47
 
49
48
  Token.where.not(kind: RefreshToken.sti_name).ended_before(cutoff)
@@ -0,0 +1,21 @@
1
+ module Masks
2
+ module Server
3
+ class HealthCheckJob < ApplicationJob
4
+ queue_as :maintenance
5
+ across_tenants!
6
+
7
+ def perform
8
+ server = Health.server_results
9
+ failing = Hash.new { |held, name| held[name] = [] }
10
+
11
+ begin
12
+ each_tenant do |tenant|
13
+ Health.store!(server + Health.tenant_results).select(&:failing?).each { |row| failing[row.name] << tenant.subdomain }
14
+ end
15
+ ensure
16
+ Health.remember_all!(failing)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -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
@@ -0,0 +1,18 @@
1
+ module Masks
2
+ module Server
3
+ class RefreshGroupsJob < ApplicationJob
4
+ queue_as :maintenance
5
+ across_tenants!
6
+
7
+ def perform
8
+ each_tenant { refresh }
9
+ end
10
+
11
+ private
12
+
13
+ def refresh
14
+ Group.sourced.find_each(&:refresh!)
15
+ end
16
+ end
17
+ end
18
+ 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
@@ -0,0 +1,21 @@
1
+ module Masks
2
+ module Server
3
+ class ReleaseCheckJob < ApplicationJob
4
+ queue_as :maintenance
5
+ across_tenants!
6
+
7
+ def perform
8
+ return unless ::Rails.configuration.masks.release_check
9
+
10
+ found = Release.check!
11
+
12
+ return if found.nil?
13
+
14
+ each_tenant do
15
+ Event.record!(Event::RELEASE_AVAILABLE, by: nil, version: found["version"], url: found["url"],
16
+ operator_step: found["operator_step"])
17
+ end
18
+ end
19
+ end
20
+ end
21
+ 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).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