masks-server 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/concerns/masks/server/scim_endpoint.rb +2 -2
- data/app/controllers/masks/server/account_controller.rb +3 -0
- data/app/controllers/masks/server/account_exports_controller.rb +2 -2
- data/app/controllers/masks/server/addresses_controller.rb +2 -3
- data/app/controllers/masks/server/app_passwords_controller.rb +41 -0
- data/app/controllers/masks/server/authenticator_apps_controller.rb +2 -3
- data/app/controllers/masks/server/authorize_controller.rb +33 -2
- data/app/controllers/masks/server/device_authorizations_controller.rb +2 -2
- data/app/controllers/masks/server/device_verifications_controller.rb +2 -2
- data/app/controllers/masks/server/enrollments_controller.rb +4 -4
- data/app/controllers/masks/server/forward_auth_controller.rb +106 -14
- data/app/controllers/masks/server/introspections_controller.rb +2 -2
- data/app/controllers/masks/server/logins_controller.rb +14 -15
- data/app/controllers/masks/server/organization_access_controller.rb +39 -0
- data/app/controllers/masks/server/organization_members_controller.rb +2 -2
- data/app/controllers/masks/server/passwords_controller.rb +2 -3
- data/app/controllers/masks/server/pushed_authorizations_controller.rb +2 -2
- data/app/controllers/masks/server/received_signals_controller.rb +2 -2
- data/app/controllers/masks/server/registrations_controller.rb +2 -2
- data/app/controllers/masks/server/revocations_controller.rb +2 -2
- data/app/controllers/masks/server/saml_controller.rb +2 -2
- data/app/controllers/masks/server/sign_in_approvals_controller.rb +2 -3
- data/app/controllers/masks/server/tokens_controller.rb +2 -2
- data/app/controllers/masks/server/verifications_controller.rb +2 -3
- data/app/graphql/masks/server/manage/mutations/base_mutation.rb +7 -0
- data/app/graphql/masks/server/manage/mutations/create_group.rb +6 -4
- data/app/graphql/masks/server/manage/mutations/decline_access.rb +27 -0
- data/app/graphql/masks/server/manage/mutations/grant_access.rb +29 -0
- data/app/graphql/masks/server/manage/mutations/group_source.rb +34 -0
- data/app/graphql/masks/server/manage/mutations/issue_ldap_credential.rb +5 -2
- data/app/graphql/masks/server/manage/mutations/refresh_group.rb +21 -0
- data/app/graphql/masks/server/manage/mutations/remove_group_member.rb +2 -0
- data/app/graphql/masks/server/manage/mutations/run_health_checks.rb +19 -0
- data/app/graphql/masks/server/manage/mutations/update_client.rb +6 -0
- data/app/graphql/masks/server/manage/mutations/update_group.rb +7 -4
- data/app/graphql/masks/server/manage/mutations/update_tenant.rb +14 -1
- data/app/graphql/masks/server/manage/types/access_request_type.rb +21 -0
- data/app/graphql/masks/server/manage/types/actor_type.rb +6 -0
- data/app/graphql/masks/server/manage/types/client_type.rb +12 -0
- data/app/graphql/masks/server/manage/types/group_condition_input.rb +16 -0
- data/app/graphql/masks/server/manage/types/group_condition_type.rb +29 -0
- data/app/graphql/masks/server/manage/types/group_member_type.rb +16 -0
- data/app/graphql/masks/server/manage/types/group_type.rb +17 -1
- data/app/graphql/masks/server/manage/types/health_check_type.rb +18 -0
- data/app/graphql/masks/server/manage/types/ldap_credential_type.rb +1 -0
- data/app/graphql/masks/server/manage/types/mutation_type.rb +4 -0
- data/app/graphql/masks/server/manage/types/query_type.rb +24 -0
- data/app/graphql/masks/server/manage/types/release_type.rb +16 -0
- data/app/graphql/masks/server/manage/types/tenant_type.rb +6 -1
- data/app/helpers/masks/server/account_helper.rb +4 -0
- data/app/jobs/masks/server/cleanup_job.rb +1 -0
- data/app/jobs/masks/server/health_check_job.rb +21 -0
- data/app/jobs/masks/server/refresh_groups_job.rb +18 -0
- data/app/jobs/masks/server/release_check_job.rb +21 -0
- data/app/ldap/masks/server/ldap/connection.rb +3 -2
- data/app/ldap/masks/server/ldap/gate.rb +44 -19
- data/app/logins/masks/server/login_states/second_factor.rb +1 -1
- data/app/mailers/masks/server/actor_mailer.rb +31 -0
- data/app/models/masks/server/access_request.rb +103 -0
- data/app/models/masks/server/actor.rb +6 -0
- data/app/models/masks/server/app_password.rb +91 -0
- data/app/models/masks/server/app_passwords.rb +76 -0
- data/app/models/masks/server/client.rb +15 -0
- data/app/models/masks/server/client_metadata_document.rb +4 -8
- data/app/models/masks/server/confirmation_code.rb +2 -2
- data/app/models/masks/server/connection.rb +7 -0
- data/app/models/masks/server/declared.rb +13 -3
- data/app/models/masks/server/email_verification.rb +2 -2
- data/app/models/masks/server/event.rb +13 -3
- data/app/models/masks/server/federation/protocol.rb +1 -1
- data/app/models/masks/server/federation/saml.rb +1 -1
- data/app/models/masks/server/forward_auth_rules.rb +162 -0
- data/app/models/masks/server/group.rb +207 -4
- data/app/models/masks/server/group_member.rb +7 -0
- data/app/models/masks/server/group_rule.rb +63 -0
- data/app/models/masks/server/health.rb +310 -0
- data/app/models/masks/server/health_check.rb +27 -0
- data/app/models/masks/server/invitation.rb +2 -2
- data/app/models/masks/server/ldap_credential.rb +2 -2
- data/app/models/masks/server/login.rb +28 -2
- data/app/models/masks/server/mail_previews.rb +5 -0
- data/app/models/masks/server/mail_template.rb +2 -0
- data/app/models/masks/server/membership.rb +10 -3
- data/app/models/masks/server/notifications.rb +3 -1
- data/app/models/masks/server/password_reset.rb +2 -2
- data/app/models/masks/server/provider.rb +1 -0
- data/app/models/masks/server/queue.rb +60 -0
- data/app/models/masks/server/release.rb +74 -0
- data/app/models/masks/server/saml_identity/farewells.rb +1 -0
- data/app/models/masks/server/session.rb +8 -0
- data/app/models/masks/server/tenant.rb +87 -6
- data/app/models/masks/server/throttle.rb +165 -0
- data/app/models/masks/server/token.rb +2 -1
- data/app/views/masks/server/account/_app_passwords.html.erb +83 -0
- data/app/views/masks/server/account/_organizations.html.erb +33 -0
- data/app/views/masks/server/account/index.html.erb +5 -0
- data/app/views/masks/server/actor_mailer/access_declined.html.erb +5 -0
- data/app/views/masks/server/actor_mailer/access_declined.text.erb +5 -0
- data/app/views/masks/server/actor_mailer/access_granted.html.erb +7 -0
- data/app/views/masks/server/actor_mailer/access_granted.text.erb +7 -0
- data/app/views/masks/server/authorize/error.html.erb +25 -0
- data/config/locales/de/access_requests.yml +43 -0
- data/config/locales/de/account/app_passwords.yml +42 -0
- data/config/locales/de/events.yml +5 -0
- data/config/locales/de/flashes.yml +1 -0
- data/config/locales/de/group_sources.yml +5 -0
- data/config/locales/de/manage/access_requests.yml +12 -0
- data/config/locales/de/manage/app_passwords.yml +16 -0
- data/config/locales/de/manage/forward_auth_rules.yml +29 -0
- data/config/locales/de/manage/group_sources.yml +44 -0
- data/config/locales/de/manage/health.yml +85 -0
- data/config/locales/de/manage/links.yml +20 -0
- data/config/locales/en/access_requests.yml +43 -0
- data/config/locales/en/account/app_passwords.yml +42 -0
- data/config/locales/en/events.yml +5 -0
- data/config/locales/en/flashes.yml +1 -0
- data/config/locales/en/group_sources.yml +5 -0
- data/config/locales/en/manage/access_requests.yml +12 -0
- data/config/locales/en/manage/app_passwords.yml +16 -0
- data/config/locales/en/manage/forward_auth_rules.yml +29 -0
- data/config/locales/en/manage/group_sources.yml +44 -0
- data/config/locales/en/manage/health.yml +85 -0
- data/config/locales/en/manage/links.yml +20 -0
- data/config/locales/es/access_requests.yml +43 -0
- data/config/locales/es/account/app_passwords.yml +42 -0
- data/config/locales/es/events.yml +5 -0
- data/config/locales/es/flashes.yml +1 -0
- data/config/locales/es/group_sources.yml +5 -0
- data/config/locales/es/manage/access_requests.yml +12 -0
- data/config/locales/es/manage/app_passwords.yml +16 -0
- data/config/locales/es/manage/forward_auth_rules.yml +29 -0
- data/config/locales/es/manage/group_sources.yml +44 -0
- data/config/locales/es/manage/health.yml +85 -0
- data/config/locales/es/manage/links.yml +20 -0
- data/config/locales/fr/access_requests.yml +43 -0
- data/config/locales/fr/account/app_passwords.yml +42 -0
- data/config/locales/fr/events.yml +5 -0
- data/config/locales/fr/flashes.yml +1 -0
- data/config/locales/fr/group_sources.yml +5 -0
- data/config/locales/fr/manage/access_requests.yml +12 -0
- data/config/locales/fr/manage/app_passwords.yml +16 -0
- data/config/locales/fr/manage/forward_auth_rules.yml +29 -0
- data/config/locales/fr/manage/group_sources.yml +44 -0
- data/config/locales/fr/manage/health.yml +85 -0
- data/config/locales/fr/manage/links.yml +20 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20261020100000_create_access_requests.rb +28 -0
- data/db/migrate/20261020110000_add_app_passwords_to_clients.rb +5 -0
- data/db/migrate/20261020120000_add_forward_auth_rules_to_clients.rb +6 -0
- data/db/migrate/20261020130000_add_sources_to_groups.rb +15 -0
- data/db/migrate/20261020140000_create_health_checks.rb +25 -0
- data/db/migrate/20261020150000_add_link_lifetimes_to_tenants.rb +8 -0
- data/db/migrate/20261021100000_add_pinned_origin_to_tenants.rb +5 -0
- data/db/migrate/20261031100000_default_key_rotation_to_ninety_days.rb +5 -0
- data/lib/generators/masks/server/install/install_generator.rb +22 -2
- data/lib/masks/server/configuration.rb +16 -19
- data/lib/masks/server/engine.rb +2 -1
- data/lib/masks/server/forwarding.rb +26 -3
- data/lib/masks/server/schema_reference.rb +12 -0
- data/lib/masks/server/tenancy/middleware.rb +21 -13
- data/lib/masks/server/tenant_isolation.rb +12 -0
- data/public/masks-assets/.vite/manifest.json +13 -13
- data/public/masks-assets/assets/{account-D46qDQCo.js → account-h7I6VUPR.js} +2 -2
- data/public/masks-assets/assets/{account-D46qDQCo.js.map → account-h7I6VUPR.js.map} +1 -1
- data/public/masks-assets/assets/{app-D7W0gAyT.css → app-BXOScMjL.css} +1 -1
- data/public/masks-assets/assets/{handshake-DIFZPSAK.js → handshake-YQpRNMIs.js} +2 -2
- data/public/masks-assets/assets/{handshake-DIFZPSAK.js.map → handshake-YQpRNMIs.js.map} +1 -1
- data/public/masks-assets/assets/initial-Cn-UuAsy.js +3 -0
- data/public/masks-assets/assets/{initial-C1GpCiEh.js.map → initial-Cn-UuAsy.js.map} +1 -1
- data/public/masks-assets/assets/{login-C_6igRZ3.js → login-C21_jb6L.js} +3 -3
- data/public/masks-assets/assets/{login-C_6igRZ3.js.map → login-C21_jb6L.js.map} +1 -1
- data/public/masks-assets/assets/{manage-D4MzaDvM.js → manage-DpVxe5sv.js} +80 -51
- data/public/masks-assets/assets/manage-DpVxe5sv.js.map +1 -0
- data/public/masks-assets/assets/passkey-5UYPVD3c.js +2 -0
- data/public/masks-assets/assets/{passkey-DIj_5LCY.js.map → passkey-5UYPVD3c.js.map} +1 -1
- data/version.txt +1 -1
- metadata +89 -14
- data/public/masks-assets/assets/initial-C1GpCiEh.js +0 -3
- data/public/masks-assets/assets/manage-D4MzaDvM.js.map +0 -1
- data/public/masks-assets/assets/passkey-DIj_5LCY.js +0 -2
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
require "net/smtp"
|
|
2
|
+
|
|
3
|
+
module Masks
|
|
4
|
+
module Server
|
|
5
|
+
module Health
|
|
6
|
+
Result = Struct.new(:name, :status, :summary, :details) do
|
|
7
|
+
def failing?
|
|
8
|
+
status == HealthCheck::FAILING
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
SERVER = %w[jobs worker queue ldap metrics].freeze
|
|
13
|
+
TENANT = %w[mail origin domains keys proxy].freeze
|
|
14
|
+
NAMES = (SERVER + TENANT).freeze
|
|
15
|
+
|
|
16
|
+
FAILING_KEY = "masks:health:failing".freeze
|
|
17
|
+
ORIGIN_PREFIX = "masks:health:origin:".freeze
|
|
18
|
+
PROXY_KEY = "masks:health:proxy".freeze
|
|
19
|
+
PROXY_ADDRESSES = 5
|
|
20
|
+
PROXY_LOG = {
|
|
21
|
+
"untrusted" => "proxy: forwarded headers arrived from %{address}, which MASKS_TRUSTED_PROXIES does not list, so they were ignored",
|
|
22
|
+
"plain" => "proxy: %{address} forwards requests as plain http; have it send X-Forwarded-Proto: https"
|
|
23
|
+
}.freeze
|
|
24
|
+
ONE_ADDRESS_FLOOR = 20
|
|
25
|
+
LOOPBACK = /\A(?:(?:.+\.)?localhost|127(?:\.\d{1,3}){3}|\[::1\])\z/i
|
|
26
|
+
KEEP = 1.day
|
|
27
|
+
NOTE_EVERY = 10.minutes
|
|
28
|
+
SMTP_TIMEOUT = 5
|
|
29
|
+
TOKEN_LENGTH = 24
|
|
30
|
+
|
|
31
|
+
SMTP_ERRORS = [
|
|
32
|
+
Net::SMTPError, IOError, SystemCallError, SocketError, Timeout::Error, OpenSSL::SSL::SSLError
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
def check!(tenant = Current.tenant)
|
|
38
|
+
rows = store!(server_results + tenant_results)
|
|
39
|
+
remember!(tenant, rows)
|
|
40
|
+
rows
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def server_results
|
|
44
|
+
[ jobs_result, worker_result, queue_result, ldap_result, metrics_result ]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def tenant_results
|
|
48
|
+
[ mail_result, origin_result, domains_result, keys_result, proxy_result ]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def store!(results)
|
|
52
|
+
now = Time.current
|
|
53
|
+
|
|
54
|
+
results.map do |result|
|
|
55
|
+
row = HealthCheck.find_or_initialize_by(name: result.name)
|
|
56
|
+
was = row.status
|
|
57
|
+
|
|
58
|
+
row.assign_attributes(status: result.status, summary: result.summary, details: result.details, checked_at: now)
|
|
59
|
+
row.changed_at = now if was != result.status
|
|
60
|
+
row.save!
|
|
61
|
+
|
|
62
|
+
announce(row, was)
|
|
63
|
+
|
|
64
|
+
row
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def announce(row, was)
|
|
69
|
+
if row.failing? && was != HealthCheck::FAILING
|
|
70
|
+
Event.record!(Event::HEALTH_FAILING, by: nil, check: row.name, summary: row.summary)
|
|
71
|
+
elsif row.ok? && was == HealthCheck::FAILING
|
|
72
|
+
Event.record!(Event::HEALTH_RECOVERED, by: nil, check: row.name, summary: row.summary)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def remember!(tenant, rows)
|
|
77
|
+
held = (::Rails.cache.read(FAILING_KEY) || {}).transform_values { |subdomains| subdomains - [ tenant.subdomain ] }
|
|
78
|
+
rows.select(&:failing?).each { |row| (held[row.name] ||= []) << tenant.subdomain }
|
|
79
|
+
|
|
80
|
+
remember_all!(held)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def remember_all!(failing)
|
|
84
|
+
::Rails.cache.write(FAILING_KEY, failing.reject { |_, subdomains| subdomains.empty? }, expires_in: KEEP)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def failing_named
|
|
88
|
+
(::Rails.cache.read(FAILING_KEY) || {}).sort.map do |name, subdomains|
|
|
89
|
+
SERVER.include?(name) ? name : "#{name} (#{subdomains.sort.join(', ')})"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def jobs_result
|
|
94
|
+
return ok("jobs", "elsewhere") unless Queue.run?
|
|
95
|
+
|
|
96
|
+
failed = Queue.failing_jobs
|
|
97
|
+
|
|
98
|
+
return ok("jobs", "none") if failed.empty?
|
|
99
|
+
|
|
100
|
+
failing("jobs", "failed", count: failed.size, jobs: failed.map(&:demodulize))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def worker_result
|
|
104
|
+
return ok("worker", "elsewhere") unless Queue.run?
|
|
105
|
+
return ok("worker", "unchecked") unless Queue.silence.positive?
|
|
106
|
+
return failing("worker", "silent", minutes: Queue.silence.to_i / 60) if Queue.worker_silent?
|
|
107
|
+
|
|
108
|
+
ok("worker", "reporting")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def queue_result
|
|
112
|
+
return ok("queue", "elsewhere") unless Queue.run?
|
|
113
|
+
return ok("queue", "unchecked") unless Queue.wait.positive?
|
|
114
|
+
|
|
115
|
+
stuck = Queue.stuck_since
|
|
116
|
+
|
|
117
|
+
return failing("queue", "stuck", since: stuck.utc.iso8601, minutes: Queue.wait.to_i / 60) if stuck
|
|
118
|
+
|
|
119
|
+
ok("queue", "moving")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def ldap_result
|
|
123
|
+
settings = ::Rails.configuration.masks
|
|
124
|
+
|
|
125
|
+
return ok("ldap", "tls") if settings.ldap_cert.present?
|
|
126
|
+
return ok("ldap", "local") unless settings.ldap_plaintext
|
|
127
|
+
|
|
128
|
+
warning("ldap", "plain", port: settings.ldap_port)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def metrics_result
|
|
132
|
+
token = ENV["MASKS_METRICS_TOKEN"]
|
|
133
|
+
|
|
134
|
+
return ok("metrics", "off") if token.blank?
|
|
135
|
+
return warning("metrics", "short", length: token.length, needed: TOKEN_LENGTH) if token.length < TOKEN_LENGTH
|
|
136
|
+
|
|
137
|
+
ok("metrics", "on")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def mail_result
|
|
141
|
+
settings = smtp_settings
|
|
142
|
+
|
|
143
|
+
return ok("mail", "none") if settings.nil?
|
|
144
|
+
|
|
145
|
+
return failing("mail", "silent", error: Senders::Smtp::UNREACHABLE) if settings.delete(:vetted) && !public_smtp?(settings)
|
|
146
|
+
|
|
147
|
+
error = smtp_problem(settings)
|
|
148
|
+
|
|
149
|
+
error ? failing("mail", "silent", error: error) : ok("mail", "answers")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def smtp_settings
|
|
153
|
+
sender = Sender.primary(Sender::MAIL)
|
|
154
|
+
|
|
155
|
+
if sender.is_a?(Senders::Smtp)
|
|
156
|
+
{ address: sender[:address], port: sender[:port].to_i, tls: sender[:tls] == true, domain: sender[:domain], vetted: true }
|
|
157
|
+
elsif (held = ::Rails.configuration.masks.smtp_settings)
|
|
158
|
+
{ address: held[:address], port: held[:port].to_i, tls: held[:tls] == true, domain: held[:domain] }
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def public_smtp?(settings)
|
|
163
|
+
::Rails.env.local? || Outbound.public_address(settings[:address], settings[:port]).present?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def smtp_problem(settings)
|
|
167
|
+
smtp = Net::SMTP.new(settings[:address], settings[:port])
|
|
168
|
+
smtp.open_timeout = SMTP_TIMEOUT
|
|
169
|
+
smtp.read_timeout = SMTP_TIMEOUT
|
|
170
|
+
settings[:tls] ? smtp.enable_tls : smtp.enable_starttls_auto
|
|
171
|
+
|
|
172
|
+
smtp.start(settings[:domain].presence || "localhost") { nil }
|
|
173
|
+
|
|
174
|
+
nil
|
|
175
|
+
rescue *SMTP_ERRORS => error
|
|
176
|
+
error.class.name
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def origin_result
|
|
180
|
+
return ok("origin", "unset") if Current.tenant&.public_origin.nil?
|
|
181
|
+
|
|
182
|
+
noted = ::Rails.cache.read(origin_key(Current.tenant))
|
|
183
|
+
|
|
184
|
+
return ok("origin", "matches") if noted.nil?
|
|
185
|
+
|
|
186
|
+
warning("origin", "mismatch", seen: noted["seen"], origin: noted["origin"])
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def note_origin!(tenant, seen, origin)
|
|
190
|
+
return if seen.nil? || origin.nil? || seen.casecmp?(origin)
|
|
191
|
+
|
|
192
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
193
|
+
last = noted[tenant.id]
|
|
194
|
+
|
|
195
|
+
return if last && now - last < NOTE_EVERY
|
|
196
|
+
|
|
197
|
+
noted[tenant.id] = now
|
|
198
|
+
::Rails.cache.write(origin_key(tenant), { "seen" => seen, "origin" => origin }, expires_in: KEEP)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def forget_origin!(tenant)
|
|
202
|
+
noted.delete(tenant.id)
|
|
203
|
+
::Rails.cache.delete(origin_key(tenant))
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def noted
|
|
207
|
+
@noted ||= Concurrent::Map.new
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def origin_key(tenant)
|
|
211
|
+
"#{ORIGIN_PREFIX}#{tenant.id}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def note_proxy!(kind, address)
|
|
215
|
+
return if address.blank? || !PROXY_LOG.key?(kind)
|
|
216
|
+
|
|
217
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
218
|
+
key = "proxy:#{kind}:#{address}"
|
|
219
|
+
last = noted[key]
|
|
220
|
+
|
|
221
|
+
return if last && now - last < NOTE_EVERY
|
|
222
|
+
|
|
223
|
+
noted[key] = now
|
|
224
|
+
::Rails.logger.warn(format(PROXY_LOG.fetch(kind), address: address))
|
|
225
|
+
|
|
226
|
+
held = ::Rails.cache.read(PROXY_KEY) || {}
|
|
227
|
+
seen = (held[kind] ||= {})
|
|
228
|
+
seen[address] = Time.current.utc.iso8601 if seen.key?(address) || seen.size < PROXY_ADDRESSES
|
|
229
|
+
::Rails.cache.write(PROXY_KEY, held, expires_in: KEEP)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def forget_proxy!
|
|
233
|
+
noted.keys.grep(/\Aproxy:/).each { |key| noted.delete(key) }
|
|
234
|
+
::Rails.cache.delete(PROXY_KEY)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def proxy_result
|
|
238
|
+
held = ::Rails.cache.read(PROXY_KEY) || {}
|
|
239
|
+
untrusted = held["untrusted"].to_h.keys.sort
|
|
240
|
+
plain = held["plain"].to_h.keys.sort
|
|
241
|
+
|
|
242
|
+
return warning("proxy", "untrusted", addresses: untrusted) if untrusted.any?
|
|
243
|
+
return warning("proxy", "plain", addresses: plain) if plain.any?
|
|
244
|
+
|
|
245
|
+
address, count = sole_private_address
|
|
246
|
+
|
|
247
|
+
return warning("proxy", "one", address: address, count: count) if address
|
|
248
|
+
|
|
249
|
+
ok("proxy", "quiet")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def sole_private_address
|
|
253
|
+
origin = Current.tenant&.public_origin
|
|
254
|
+
|
|
255
|
+
return if origin.nil? || URI.parse(origin).host.to_s.match?(LOOPBACK)
|
|
256
|
+
|
|
257
|
+
recent = Event.where(created_at: 1.hour.ago..).where.not(ip_address: [ nil, "" ])
|
|
258
|
+
addresses = recent.distinct.limit(2).pluck(:ip_address)
|
|
259
|
+
|
|
260
|
+
return unless addresses.size == 1
|
|
261
|
+
|
|
262
|
+
count = recent.count
|
|
263
|
+
|
|
264
|
+
return if count < ONE_ADDRESS_FLOOR
|
|
265
|
+
|
|
266
|
+
address = IPAddr.new(addresses.first)
|
|
267
|
+
|
|
268
|
+
[ addresses.first, count ] if address.private? || address.loopback? || address.link_local?
|
|
269
|
+
rescue IPAddr::InvalidAddressError, URI::InvalidURIError
|
|
270
|
+
nil
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def domains_result
|
|
274
|
+
return ok("domains", "none") unless DomainClaim.exists?
|
|
275
|
+
|
|
276
|
+
missing = DomainClaim.where.not(missing_since: nil).order(:domain).pluck(:domain)
|
|
277
|
+
|
|
278
|
+
return ok("domains", "proven") if missing.empty?
|
|
279
|
+
|
|
280
|
+
warning("domains", "missing", domains: missing)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def keys_result
|
|
284
|
+
active = SigningKey.active.first
|
|
285
|
+
|
|
286
|
+
return failing("keys", "missing") if active.nil?
|
|
287
|
+
|
|
288
|
+
days = ((Time.current - active.activated_at) / 1.day).floor
|
|
289
|
+
period = Current.tenant.key_rotation_days
|
|
290
|
+
|
|
291
|
+
return warning("keys", "manual", days: days) if period.nil?
|
|
292
|
+
return warning("keys", "stale", days: days, period: period) if days > period
|
|
293
|
+
|
|
294
|
+
ok("keys", "fresh", days: days, period: period)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def ok(name, summary, **details)
|
|
298
|
+
Result.new(name, HealthCheck::OK, summary, details.stringify_keys)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def warning(name, summary, **details)
|
|
302
|
+
Result.new(name, HealthCheck::WARNING, summary, details.stringify_keys)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def failing(name, summary, **details)
|
|
306
|
+
Result.new(name, HealthCheck::FAILING, summary, details.stringify_keys)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
class HealthCheck < ApplicationRecord
|
|
4
|
+
include TenantScoped
|
|
5
|
+
|
|
6
|
+
OK = "ok".freeze
|
|
7
|
+
WARNING = "warning".freeze
|
|
8
|
+
FAILING = "failing".freeze
|
|
9
|
+
STATUSES = [ OK, WARNING, FAILING ].freeze
|
|
10
|
+
|
|
11
|
+
validates :name, presence: true, uniqueness: { scope: :tenant_id }
|
|
12
|
+
validates :status, inclusion: { in: STATUSES }
|
|
13
|
+
validates :summary, presence: true
|
|
14
|
+
|
|
15
|
+
scope :failing, -> { where(status: FAILING) }
|
|
16
|
+
scope :by_name, -> { order(:name) }
|
|
17
|
+
|
|
18
|
+
def ok?
|
|
19
|
+
status == OK
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def failing?
|
|
23
|
+
status == FAILING
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -7,12 +7,12 @@ module Masks
|
|
|
7
7
|
class Refused < StandardError; end
|
|
8
8
|
|
|
9
9
|
class << self
|
|
10
|
-
def issue!(label:, by:)
|
|
10
|
+
def issue!(label:, by:, client: nil)
|
|
11
11
|
name = label.to_s.strip.downcase
|
|
12
12
|
raise Refused, "a label is lowercase letters, digits, dots, dashes, and underscores" unless name.match?(LABEL)
|
|
13
13
|
raise Refused, "a live LDAP credential is already labeled #{name}" if named(name)
|
|
14
14
|
|
|
15
|
-
mint!(payload: { "label" => name, "issued_by" => by&.uuid }.compact, expires_at: LIFETIME.from_now)
|
|
15
|
+
mint!(client: client, payload: { "label" => name, "issued_by" => by&.uuid }.compact, expires_at: LIFETIME.from_now)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def named(label)
|
|
@@ -30,6 +30,9 @@ module Masks
|
|
|
30
30
|
|
|
31
31
|
SETTLED = "settled".freeze
|
|
32
32
|
FIRST_FACTOR_BY = "first_factor_by".freeze
|
|
33
|
+
RELAXED = "relaxed".freeze
|
|
34
|
+
STEP_UP = "step_up".freeze
|
|
35
|
+
ASKED_FOR = 10.minutes
|
|
33
36
|
|
|
34
37
|
def self.permitted_updates
|
|
35
38
|
STATES.flat_map { |state| state.declared_updates }.uniq
|
|
@@ -256,7 +259,30 @@ module Masks
|
|
|
256
259
|
|
|
257
260
|
wanted = request.respond_to?(:multi_factor?) && request.multi_factor?
|
|
258
261
|
|
|
259
|
-
wanted || (request.present? && policy.apps_require_second_factor) || state("risk-check").stepping_up?
|
|
262
|
+
wanted || (request.present? && policy.apps_require_second_factor) || state("risk-check").stepping_up? ||
|
|
263
|
+
asked?(STEP_UP)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def relaxed?
|
|
267
|
+
asked?(RELAXED) && !stepping_up? && !policy.second_factor_required
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def skipped_second_factor?
|
|
271
|
+
relaxed? && actor.present? && actor.second_factor? && !touched?(:second_factor) && !amr.include?(Issuer::MULTI_FACTOR)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def relax!(now = Time.current)
|
|
275
|
+
store[RELAXED] = { "until" => (now + ASKED_FOR).to_i }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def step_up!(now = Time.current)
|
|
279
|
+
store[STEP_UP] = { "until" => (now + ASKED_FOR).to_i }
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def asked?(key)
|
|
283
|
+
held = store[key]
|
|
284
|
+
|
|
285
|
+
held.is_a?(Hash) && held["until"].to_i > Time.current.to_i
|
|
260
286
|
end
|
|
261
287
|
|
|
262
288
|
def multi_factored?
|
|
@@ -268,7 +294,7 @@ module Masks
|
|
|
268
294
|
def second_factored?
|
|
269
295
|
return false if reauthenticating? || stale? || actor.nil? || stepping_up?
|
|
270
296
|
|
|
271
|
-
touched?(:second_factor) || signed_in? || remembered?(:second_factor)
|
|
297
|
+
touched?(:second_factor) || (signed_in? && !session.second_factor_skipped?) || remembered?(:second_factor)
|
|
272
298
|
end
|
|
273
299
|
|
|
274
300
|
def second_factor_pending?
|
|
@@ -29,6 +29,7 @@ module Masks
|
|
|
29
29
|
acme = Organization.new(key: "acme", name: "Acme", roles: [ "billing" ])
|
|
30
30
|
joining = Membership.new(organization: acme, actor: person, role: "billing", invited_by: manager,
|
|
31
31
|
invited_as: person.email, pending: true)
|
|
32
|
+
asked = AccessRequest.new(actor: person, client: client || Client.new(name: "Grafana"), label: "admins")
|
|
32
33
|
promoted = Event.new(action: Event::MEMBERSHIP_ROLE_CHANGED, actor: person, by: manager, organization: acme,
|
|
33
34
|
created_at: Time.current, details: { "was" => "member", "now" => "owner" })
|
|
34
35
|
|
|
@@ -51,6 +52,10 @@ module Masks
|
|
|
51
52
|
ActorMailer.organization_invitation(joining, journey: managing) ],
|
|
52
53
|
[ "approved", "Account approved", managing,
|
|
53
54
|
ActorMailer.approved(person, journey: managing) ],
|
|
55
|
+
[ "access_granted", "Access granted", managing,
|
|
56
|
+
ActorMailer.access_granted(asked, journey: managing) ],
|
|
57
|
+
[ "access_declined", "Access declined", managing,
|
|
58
|
+
ActorMailer.access_declined(asked, journey: managing) ],
|
|
54
59
|
[ "notification", "Security notice", system,
|
|
55
60
|
ActorMailer.notification(person, changed, journey: system) ],
|
|
56
61
|
[ "notification_organization", "Organization role changed", system,
|
|
@@ -13,6 +13,8 @@ module Masks
|
|
|
13
13
|
"approval_requested" => %w[tenant nickname],
|
|
14
14
|
"recovery_requested" => %w[tenant nickname],
|
|
15
15
|
"approved" => %w[tenant name nickname],
|
|
16
|
+
"access_granted" => %w[tenant name nickname app],
|
|
17
|
+
"access_declined" => %w[tenant name nickname app],
|
|
16
18
|
SIGNATURE => %w[tenant]
|
|
17
19
|
}.freeze
|
|
18
20
|
SUBJECT_LIMIT = 150
|
|
@@ -18,8 +18,8 @@ module Masks
|
|
|
18
18
|
|
|
19
19
|
before_create { self.invited_at ||= Time.current if pending? }
|
|
20
20
|
|
|
21
|
-
def self.lifetime
|
|
22
|
-
|
|
21
|
+
def self.lifetime(tenant = Current.tenant)
|
|
22
|
+
Tenant.link_lifetime(:organization_invitation_lifetime, tenant)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
normalizes :external_id, with: Actor::EXTERNAL_ID
|
|
@@ -29,13 +29,14 @@ module Masks
|
|
|
29
29
|
validate :an_owner_remains, on: :update
|
|
30
30
|
|
|
31
31
|
before_destroy :keep_an_owner
|
|
32
|
+
after_commit :settle_nested_groups
|
|
32
33
|
|
|
33
34
|
def owner?
|
|
34
35
|
role == Organization::OWNER && !pending?
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def expires_at
|
|
38
|
-
invited_at + self.class.lifetime if pending?
|
|
39
|
+
invited_at + self.class.lifetime(tenant) if pending?
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
def expired?
|
|
@@ -72,6 +73,12 @@ module Masks
|
|
|
72
73
|
|
|
73
74
|
private
|
|
74
75
|
|
|
76
|
+
def settle_nested_groups
|
|
77
|
+
return if organization.nil? || actor.nil?
|
|
78
|
+
|
|
79
|
+
Group.nested_under_organization(organization.key).find_each { |group| group.settle!(actor) }
|
|
80
|
+
end
|
|
81
|
+
|
|
75
82
|
def role_is_offered
|
|
76
83
|
errors.add(:role, "is not one #{organization&.name} offers") unless organization&.role?(role)
|
|
77
84
|
end
|
|
@@ -18,7 +18,9 @@ module Masks
|
|
|
18
18
|
Event::TEXT_CODES_ENABLED,
|
|
19
19
|
Event::TEXT_CODES_DISABLED,
|
|
20
20
|
Event::RECOVERY_REQUESTED,
|
|
21
|
-
Event::SECOND_FACTORS_RESET
|
|
21
|
+
Event::SECOND_FACTORS_RESET,
|
|
22
|
+
Event::APP_PASSWORD_CREATED,
|
|
23
|
+
Event::APP_PASSWORD_REVOKED
|
|
22
24
|
],
|
|
23
25
|
"sessions" => [
|
|
24
26
|
Event::SESSION_STARTED,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
module Queue
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def run?
|
|
7
|
+
::Rails.application.config.active_job.queue_adapter == :solid_queue
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def silence
|
|
11
|
+
::Rails.configuration.masks.worker_silence
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def wait
|
|
15
|
+
::Rails.configuration.masks.queue_wait
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def failing_jobs
|
|
19
|
+
::SolidQueue::FailedExecution.where(created_at: 1.day.ago..).joins(:job)
|
|
20
|
+
.distinct.pluck("solid_queue_jobs.class_name").sort
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def worker_heartbeat_at
|
|
24
|
+
::SolidQueue::Process.where(kind: "Worker").maximum(:last_heartbeat_at)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def oldest_waiting_at
|
|
28
|
+
::SolidQueue::ReadyExecution.minimum(:created_at)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def worker_silent?
|
|
32
|
+
return false unless run? && silence.positive?
|
|
33
|
+
|
|
34
|
+
heartbeat = worker_heartbeat_at
|
|
35
|
+
|
|
36
|
+
heartbeat.nil? || heartbeat < silence.ago
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def stuck_since
|
|
40
|
+
return nil unless run? && wait.positive?
|
|
41
|
+
|
|
42
|
+
oldest = oldest_waiting_at
|
|
43
|
+
|
|
44
|
+
oldest if oldest && oldest < wait.ago
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def plain(duration)
|
|
48
|
+
ActiveSupport::Duration.build(duration.to_i).inspect
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def worker_problem
|
|
52
|
+
return "no worker has reported in the last #{plain(silence)}" if worker_silent?
|
|
53
|
+
|
|
54
|
+
stuck = stuck_since
|
|
55
|
+
|
|
56
|
+
"a job has waited since #{stuck.utc.iso8601}, longer than #{plain(wait)}" if stuck
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
module Release
|
|
4
|
+
URL = "https://api.github.com/repos/masksrb/masks/releases?per_page=20".freeze
|
|
5
|
+
KEY = "masks:release".freeze
|
|
6
|
+
KEEP = 3.days
|
|
7
|
+
TAG = /\Aserver-v(\d+\.\d+\.\d+)\z/
|
|
8
|
+
OPERATOR = /^\s*Operator:/i
|
|
9
|
+
TIMEOUT = 5
|
|
10
|
+
CEILING = 1.megabyte
|
|
11
|
+
UNREADABLE = [ Outbound::Refused, JSON::ParserError, TypeError, NoMethodError ].freeze
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def check!
|
|
16
|
+
found = fetch
|
|
17
|
+
|
|
18
|
+
return nil if found.nil?
|
|
19
|
+
|
|
20
|
+
was = latest
|
|
21
|
+
remember!(found)
|
|
22
|
+
|
|
23
|
+
found if newer?(found["version"]) && was&.dig("version") != found["version"]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def fetch
|
|
27
|
+
headers = { "Accept" => "application/vnd.github+json", "User-Agent" => "masks-server/#{VERSION}" }
|
|
28
|
+
body = Outbound.get!(URI(URL), open: TIMEOUT, read: TIMEOUT, within: TIMEOUT, ceiling: CEILING, headers: headers).body
|
|
29
|
+
|
|
30
|
+
JSON.parse(body).each do |release|
|
|
31
|
+
version = release["tag_name"].to_s[TAG, 1]
|
|
32
|
+
|
|
33
|
+
next if version.nil? || release["draft"] || release["prerelease"]
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
"version" => version,
|
|
37
|
+
"url" => release["html_url"].to_s,
|
|
38
|
+
"operator_step" => release["body"].to_s.match?(OPERATOR),
|
|
39
|
+
"checked_at" => Time.current.utc.iso8601
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
nil
|
|
44
|
+
rescue *UNREADABLE => error
|
|
45
|
+
::Rails.logger.warn("the release check could not read the releases: #{error.class}")
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def remember!(found)
|
|
50
|
+
::Rails.cache.write(KEY, found, expires_in: KEEP)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def forget!
|
|
54
|
+
::Rails.cache.delete(KEY)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def latest
|
|
58
|
+
::Rails.cache.read(KEY)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def available
|
|
62
|
+
found = latest
|
|
63
|
+
|
|
64
|
+
found if found && newer?(found["version"])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def newer?(version)
|
|
68
|
+
Gem::Version.new(version) > Gem::Version.new(VERSION)
|
|
69
|
+
rescue ArgumentError
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -11,6 +11,7 @@ module Masks
|
|
|
11
11
|
held = ::Rails.cache.read(key)
|
|
12
12
|
|
|
13
13
|
return nil unless held.is_a?(Hash) && held["client_id"] == client.client_id
|
|
14
|
+
return nil unless Replay.first?("saml-farewell", request_id, within: client.id, expires_in: WAIT)
|
|
14
15
|
|
|
15
16
|
::Rails.cache.delete(key)
|
|
16
17
|
new(held["steps"], held["finish"])
|
|
@@ -118,6 +118,14 @@ module Masks
|
|
|
118
118
|
true
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
def multi_factored?
|
|
122
|
+
Array(amr).include?(Issuer::MULTI_FACTOR) || !actor.second_factor?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def complete?
|
|
126
|
+
!second_factor_skipped?
|
|
127
|
+
end
|
|
128
|
+
|
|
121
129
|
def fresh?
|
|
122
130
|
authenticated_at.present? && authenticated_at > FRESHNESS.ago
|
|
123
131
|
end
|