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,25 @@
|
|
|
1
|
+
class CreateHealthChecks < ActiveRecord::Migration[8.1]
|
|
2
|
+
include Masks::Server::TenantIsolation
|
|
3
|
+
|
|
4
|
+
def up
|
|
5
|
+
create_table :health_checks do |t|
|
|
6
|
+
t.references :tenant, null: false, foreign_key: { on_delete: :cascade }, index: false
|
|
7
|
+
t.string :name, null: false
|
|
8
|
+
t.string :status, null: false
|
|
9
|
+
t.string :summary, null: false
|
|
10
|
+
t.jsonb :details, null: false, default: {}
|
|
11
|
+
t.datetime :checked_at, null: false
|
|
12
|
+
t.datetime :changed_at, null: false
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :health_checks, %i[tenant_id name], unique: true
|
|
17
|
+
|
|
18
|
+
enable_row_level_security(:health_checks)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def down
|
|
22
|
+
disable_row_level_security(:health_checks)
|
|
23
|
+
drop_table :health_checks
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class AddLinkLifetimesToTenants < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
add_column :tenants, :invitation_lifetime, :integer, null: false, default: 7 * 24 * 60 * 60
|
|
4
|
+
add_column :tenants, :organization_invitation_lifetime, :integer, null: false, default: 14 * 24 * 60 * 60
|
|
5
|
+
add_column :tenants, :password_reset_lifetime, :integer, null: false, default: 30 * 60
|
|
6
|
+
add_column :tenants, :email_verification_lifetime, :integer, null: false, default: 2 * 24 * 60 * 60
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -39,9 +39,11 @@ module Masks
|
|
|
39
39
|
say ""
|
|
40
40
|
|
|
41
41
|
engine? ? say_engine_database : say_server_database
|
|
42
|
+
say_cache_store if engine?
|
|
42
43
|
|
|
43
|
-
say "In production,
|
|
44
|
-
say "emailed link
|
|
44
|
+
say "In production, masks keeps the address its setup screen was first opened at, and builds the"
|
|
45
|
+
say "issuer and every emailed link from it. To name the address yourself, set"
|
|
46
|
+
say "MASKS_PUBLIC_ORIGIN_TEMPLATE to #{origin_example}."
|
|
45
47
|
say ""
|
|
46
48
|
say "Then run bin/rails db:prepare and bin/rails masks:tenants, and open #{login_path} to set up"
|
|
47
49
|
say "the first account with the setup token masks prints."
|
|
@@ -104,6 +106,24 @@ module Masks
|
|
|
104
106
|
say ""
|
|
105
107
|
end
|
|
106
108
|
|
|
109
|
+
def say_cache_store
|
|
110
|
+
say "masks holds one-time sign-in state, rate limits, and health findings in Rails.cache, so"
|
|
111
|
+
say "every process that serves it must share one cache store, such as :solid_cache_store,"
|
|
112
|
+
say ":redis_cache_store or :mem_cache_store. In production, masks refuses to boot on"
|
|
113
|
+
say ":memory_store or :null_store."
|
|
114
|
+
|
|
115
|
+
if Configuration.unshared_cache_store?(cache_store)
|
|
116
|
+
say ""
|
|
117
|
+
say "This app's cache_store is #{Array(cache_store).first.inspect}. Change it before you deploy.", :yellow
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
say ""
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def cache_store
|
|
124
|
+
::Rails.application&.config&.cache_store if defined?(::Rails.application)
|
|
125
|
+
end
|
|
126
|
+
|
|
107
127
|
def say_server_database
|
|
108
128
|
say "masks keeps its tables in the primary database. Connect as a role holding neither"
|
|
109
129
|
say "SUPERUSER nor BYPASSRLS, and set config.active_record.schema_format = :sql, since"
|
|
@@ -4,6 +4,7 @@ module Masks
|
|
|
4
4
|
module Server
|
|
5
5
|
module Configuration
|
|
6
6
|
MODES = %i[server engine].freeze
|
|
7
|
+
UNSHARED_CACHE_STORES = %i[memory_store null_store].freeze
|
|
7
8
|
|
|
8
9
|
module_function
|
|
9
10
|
|
|
@@ -12,19 +13,13 @@ module Masks
|
|
|
12
13
|
config.mode = ENV.fetch("MASKS_MODE", "server").to_sym
|
|
13
14
|
config.database = nil
|
|
14
15
|
config.secret = nil
|
|
15
|
-
config.attempt_limit = ENV.fetch("MASKS_ATTEMPT_LIMIT", 10).to_i
|
|
16
|
-
config.account_attempt_limit = ENV.fetch("MASKS_ACCOUNT_ATTEMPT_LIMIT", 5).to_i
|
|
17
|
-
config.registration_limit = ENV.fetch("MASKS_REGISTRATION_LIMIT", 10).to_i
|
|
18
|
-
config.recovery_limit = ENV.fetch("MASKS_RECOVERY_LIMIT", 5).to_i
|
|
19
|
-
config.device_code_limit = ENV.fetch("MASKS_DEVICE_CODE_LIMIT", 30).to_i
|
|
20
|
-
config.claim_limit = ENV.fetch("MASKS_CLAIM_LIMIT", 3).to_i
|
|
21
|
-
config.claim_ceiling = ENV.fetch("MASKS_CLAIM_CEILING", 30).to_i
|
|
22
16
|
config.setup_token = ENV["MASKS_SETUP_TOKEN"].presence
|
|
23
17
|
config.recover = ENV["MASKS_RECOVER"].presence
|
|
24
18
|
config.config_path = ENV["MASKS_CONFIG"].presence
|
|
25
19
|
config.themes_path = ENV["MASKS_THEMES_PATH"].presence
|
|
26
20
|
config.docs_url = ENV.fetch("MASKS_DOCS_URL", "https://masks.pages.dev")
|
|
27
21
|
config.public_origin_template = ENV["MASKS_PUBLIC_ORIGIN_TEMPLATE"].presence
|
|
22
|
+
config.pin_origin = !::Rails.env.local?
|
|
28
23
|
config.tenant = ENV["MASKS_TENANT"].presence
|
|
29
24
|
config.tenants = ENV["MASKS_TENANTS"].to_s.split(/[\s,]+/).reject(&:empty?)
|
|
30
25
|
config.dynamic_client_scopes = ENV["MASKS_DYNAMIC_CLIENT_SCOPES"].presence
|
|
@@ -35,11 +30,6 @@ module Masks
|
|
|
35
30
|
config.mail_from = ENV["MASKS_MAIL_FROM"].presence
|
|
36
31
|
config.smtp_address = ENV["MASKS_SMTP_ADDRESS"].presence
|
|
37
32
|
config.smtp_settings = smtp_settings(config.smtp_address)
|
|
38
|
-
config.invitation_lifetime = ENV.fetch("MASKS_INVITATION_LIFETIME", 7 * 24 * 60 * 60).to_i.seconds
|
|
39
|
-
config.organization_invitation_lifetime =
|
|
40
|
-
ENV.fetch("MASKS_ORGANIZATION_INVITATION_LIFETIME", 14 * 24 * 60 * 60).to_i.seconds
|
|
41
|
-
config.password_reset_lifetime = ENV.fetch("MASKS_PASSWORD_RESET_LIFETIME", 30 * 60).to_i.seconds
|
|
42
|
-
config.email_verification_lifetime = ENV.fetch("MASKS_EMAIL_VERIFICATION_LIFETIME", 2 * 24 * 60 * 60).to_i.seconds
|
|
43
33
|
config.outbound_allowed = outbound_allowed(ENV["MASKS_OUTBOUND_ALLOWED"])
|
|
44
34
|
config.ldap_port = ENV.fetch("MASKS_LDAP_PORT", 3389).to_i
|
|
45
35
|
config.on_demand_tls = ENV.fetch("MASKS_ON_DEMAND_TLS", "false")
|
|
@@ -50,6 +40,7 @@ module Masks
|
|
|
50
40
|
config.queue_wait = ENV.fetch("MASKS_QUEUE_WAIT", 600).to_i.seconds
|
|
51
41
|
config.finished_job_retention = ENV.fetch("MASKS_FINISHED_JOB_RETENTION", 7 * 24 * 60 * 60).to_i.seconds
|
|
52
42
|
config.failed_job_retention = ENV.fetch("MASKS_FAILED_JOB_RETENTION", 30 * 24 * 60 * 60).to_i.seconds
|
|
43
|
+
config.release_check = %w[1 true].include?(ENV.fetch("MASKS_RELEASE_CHECK", "false"))
|
|
53
44
|
config
|
|
54
45
|
end
|
|
55
46
|
|
|
@@ -91,7 +82,11 @@ module Masks
|
|
|
91
82
|
}.compact
|
|
92
83
|
end
|
|
93
84
|
|
|
94
|
-
def
|
|
85
|
+
def unshared_cache_store?(cache_store)
|
|
86
|
+
UNSHARED_CACHE_STORES.include?(Array(cache_store).first&.to_sym)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate!(config, local:, building:, cache_store: nil)
|
|
95
90
|
unless MODES.include?(config.mode)
|
|
96
91
|
raise "masks runs in server or engine mode, and config.masks.mode is #{config.mode.inspect}."
|
|
97
92
|
end
|
|
@@ -124,12 +119,6 @@ module Masks
|
|
|
124
119
|
"be dropped without an error anyone sees."
|
|
125
120
|
end
|
|
126
121
|
|
|
127
|
-
if config.public_origin_template.nil?
|
|
128
|
-
raise "MASKS_PUBLIC_ORIGIN_TEMPLATE is required outside development. Without it the " \
|
|
129
|
-
"issuer, the registration endpoint and every emailed link follow the Host header, " \
|
|
130
|
-
"so anyone who can reach this server can have a password reset delivered to theirs."
|
|
131
|
-
end
|
|
132
|
-
|
|
133
122
|
if config.setup_token && config.setup_token.length < 24
|
|
134
123
|
raise "MASKS_SETUP_TOKEN is #{config.setup_token.length} characters, and at least 24 are " \
|
|
135
124
|
"required outside development. Whoever holds it creates the first account of every " \
|
|
@@ -140,6 +129,14 @@ module Masks
|
|
|
140
129
|
raise "masks in engine mode needs a database of its own. Set config.masks.database to " \
|
|
141
130
|
"the name of a database.yml entry whose role cannot bypass row-level security."
|
|
142
131
|
end
|
|
132
|
+
|
|
133
|
+
if unshared_cache_store?(cache_store)
|
|
134
|
+
raise "config.cache_store is #{Array(cache_store).first.inspect}, which every process keeps " \
|
|
135
|
+
"to itself. masks holds one-time sign-in state, rate limits, and health findings in " \
|
|
136
|
+
"Rails.cache, so a provider's callback redeemed in one process is lost in the next. " \
|
|
137
|
+
"Use a store every process shares, such as :solid_cache_store, :redis_cache_store " \
|
|
138
|
+
"or :mem_cache_store."
|
|
139
|
+
end
|
|
143
140
|
end
|
|
144
141
|
end
|
|
145
142
|
end
|
data/lib/masks/server/engine.rb
CHANGED
|
@@ -20,7 +20,8 @@ module Masks
|
|
|
20
20
|
Configuration.validate!(
|
|
21
21
|
app.config.masks,
|
|
22
22
|
local: ::Rails.env.local?,
|
|
23
|
-
building: ENV["SECRET_KEY_BASE_DUMMY"].present
|
|
23
|
+
building: ENV["SECRET_KEY_BASE_DUMMY"].present?,
|
|
24
|
+
cache_store: app.config.cache_store
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
middleware.insert_before Tenancy::Middleware, Isolation if app.config.masks.mode == :engine
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
module Masks
|
|
2
2
|
module Server
|
|
3
3
|
class Forwarding
|
|
4
|
-
HEADERS = %w[
|
|
4
|
+
HEADERS = %w[
|
|
5
|
+
HTTP_X_FORWARDED_FOR HTTP_X_FORWARDED_HOST HTTP_CLIENT_IP HTTP_FORWARDED
|
|
6
|
+
HTTP_X_FORWARDED_PROTO HTTP_X_FORWARDED_SCHEME HTTP_X_FORWARDED_SSL HTTP_X_FORWARDED_PORT
|
|
7
|
+
].freeze
|
|
8
|
+
PROTO = "HTTP_X_FORWARDED_PROTO".freeze
|
|
5
9
|
|
|
6
10
|
def initialize(app, proxies)
|
|
7
11
|
@app = app
|
|
@@ -9,9 +13,20 @@ module Masks
|
|
|
9
13
|
end
|
|
10
14
|
|
|
11
15
|
def call(env)
|
|
12
|
-
|
|
16
|
+
address = env["REMOTE_ADDR"]
|
|
17
|
+
trusted = trusted?(address)
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
unless trusted
|
|
20
|
+
Health.note_proxy!("untrusted", address) if HEADERS.any? { |header| env.key?(header) }
|
|
21
|
+
HEADERS.each { |header| env.delete(header) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
plain = trusted && plain?(env)
|
|
25
|
+
status, headers, body = @app.call(env)
|
|
26
|
+
|
|
27
|
+
Health.note_proxy!("plain", address) if plain && !sent_to_https?(status, headers)
|
|
28
|
+
|
|
29
|
+
[ status, headers, body ]
|
|
15
30
|
end
|
|
16
31
|
|
|
17
32
|
private
|
|
@@ -24,6 +39,14 @@ module Masks
|
|
|
24
39
|
rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
|
|
25
40
|
false
|
|
26
41
|
end
|
|
42
|
+
|
|
43
|
+
def plain?(env)
|
|
44
|
+
env[PROTO].to_s.split(",").first.to_s.strip.casecmp?("http")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def sent_to_https?(status, headers)
|
|
48
|
+
(300..399).cover?(status) && headers["location"].to_s.downcase.start_with?("https://")
|
|
49
|
+
end
|
|
27
50
|
end
|
|
28
51
|
end
|
|
29
52
|
end
|
|
@@ -65,6 +65,8 @@ module Masks
|
|
|
65
65
|
values(type)
|
|
66
66
|
elsif type.kind.scalar?
|
|
67
67
|
"A scalar.\n"
|
|
68
|
+
elsif type.kind.input_object?
|
|
69
|
+
arguments(type)
|
|
68
70
|
else
|
|
69
71
|
fields(type)
|
|
70
72
|
end
|
|
@@ -88,6 +90,16 @@ module Masks
|
|
|
88
90
|
table([ "Value", "Description" ], rows, described)
|
|
89
91
|
end
|
|
90
92
|
|
|
93
|
+
def arguments(type)
|
|
94
|
+
described = type.arguments.values.map { |argument| argument.description.to_s.strip }
|
|
95
|
+
|
|
96
|
+
rows = type.arguments.values.zip(described).map do |argument, text|
|
|
97
|
+
cells([ "`#{argument.graphql_name}`", link(argument.type), text ], described)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
table([ "Field", "Type", "Description" ], rows, described)
|
|
101
|
+
end
|
|
102
|
+
|
|
91
103
|
def fields(type)
|
|
92
104
|
described = type.fields.values.map { |field| field.description.to_s.strip }
|
|
93
105
|
|
|
@@ -3,8 +3,9 @@ module Masks
|
|
|
3
3
|
module Tenancy
|
|
4
4
|
class Middleware
|
|
5
5
|
UNSERVED = "no tenant is served at this hostname".freeze
|
|
6
|
+
MOVED = "masks answers at %s. To serve it at another address, set MASKS_PUBLIC_ORIGIN_TEMPLATE to that address.".freeze
|
|
7
|
+
UNKEPT = "This tenant was set up without a kept address. Set MASKS_PUBLIC_ORIGIN_TEMPLATE to the address it answers at.".freeze
|
|
6
8
|
CROWDED = "too many tenants were claimed recently; try again later".freeze
|
|
7
|
-
CLAIM_WINDOW = 1.hour
|
|
8
9
|
TENANTLESS = %w[/up /metrics].freeze
|
|
9
10
|
TLS_ASK = "/tls/allowed".freeze
|
|
10
11
|
|
|
@@ -40,11 +41,16 @@ module Masks
|
|
|
40
41
|
|
|
41
42
|
return unserved if tenant.nil?
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
held = tenant.held_origin
|
|
44
45
|
|
|
45
|
-
return unserved if
|
|
46
|
+
return unserved(UNKEPT) if held.nil? && Tenant.pins_origin? && tenant.set_up?
|
|
47
|
+
|
|
48
|
+
origin = origin_for(request, tenant, held)
|
|
49
|
+
|
|
50
|
+
return unserved(held ? format(MOVED, held) : UNSERVED) if origin.nil?
|
|
46
51
|
|
|
47
52
|
Current.origin = origin
|
|
53
|
+
Health.note_origin!(tenant, Forwarded.origin(request.base_url), origin)
|
|
48
54
|
Current.ip_address = request.remote_ip
|
|
49
55
|
Current.user_agent = request.user_agent
|
|
50
56
|
|
|
@@ -67,9 +73,13 @@ module Masks
|
|
|
67
73
|
nil
|
|
68
74
|
end
|
|
69
75
|
|
|
70
|
-
def origin_for(request, tenant)
|
|
71
|
-
|
|
76
|
+
def origin_for(request, tenant, held)
|
|
77
|
+
untemplated = ::Rails.configuration.masks.public_origin_template.nil?
|
|
78
|
+
seen = "#{request.base_url}#{request.script_name}"
|
|
79
|
+
|
|
80
|
+
return seen if untemplated && held.nil?
|
|
72
81
|
return tenant.custom_origin if tenant.custom_host&.casecmp?(request.host.to_s)
|
|
82
|
+
return (held if held.casecmp?(seen)) if untemplated
|
|
73
83
|
|
|
74
84
|
canonical = tenant.templated_origin
|
|
75
85
|
|
|
@@ -83,27 +93,25 @@ module Masks
|
|
|
83
93
|
end
|
|
84
94
|
|
|
85
95
|
def crowded?(request)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
from_anywhere = ::Rails.cache.increment("tenant-claims:#{window}", 1, expires_in: CLAIM_WINDOW)
|
|
96
|
+
here = Throttle.crowded?(:claim, "claims:#{request.remote_ip}")
|
|
97
|
+
anywhere = Throttle.crowded?(:claim_ceiling, "claims")
|
|
89
98
|
|
|
90
|
-
|
|
91
|
-
from_anywhere.to_i > ::Rails.configuration.masks.claim_ceiling
|
|
99
|
+
(here || anywhere).tap { |crowded| Throttle.hit!(here ? :claim : :claim_ceiling, address: request.remote_ip) if crowded }
|
|
92
100
|
end
|
|
93
101
|
|
|
94
102
|
def crowded
|
|
95
103
|
[
|
|
96
104
|
429,
|
|
97
|
-
{ "content-type" => "text/plain; charset=utf-8", "cache-control" => "no-store", "retry-after" =>
|
|
105
|
+
{ "content-type" => "text/plain; charset=utf-8", "cache-control" => "no-store", "retry-after" => Throttle.budget(:claim).within.to_i.to_s },
|
|
98
106
|
[ CROWDED ]
|
|
99
107
|
]
|
|
100
108
|
end
|
|
101
109
|
|
|
102
|
-
def unserved
|
|
110
|
+
def unserved(body = UNSERVED)
|
|
103
111
|
[
|
|
104
112
|
404,
|
|
105
113
|
{ "content-type" => "text/plain; charset=utf-8", "cache-control" => "no-store" },
|
|
106
|
-
[
|
|
114
|
+
[ body ]
|
|
107
115
|
]
|
|
108
116
|
end
|
|
109
117
|
end
|
|
@@ -4,11 +4,23 @@ module Masks
|
|
|
4
4
|
SETTING = "masks.tenant_id".freeze
|
|
5
5
|
|
|
6
6
|
def self.adopt(connection)
|
|
7
|
+
return unless masks?(connection)
|
|
8
|
+
|
|
7
9
|
connection.exec_query("SELECT set_config($1, $2, false)", "tenant", [ SETTING, Current.tenant&.id.to_s ])
|
|
8
10
|
rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished, ActiveRecord::ConnectionFailed
|
|
9
11
|
nil
|
|
10
12
|
end
|
|
11
13
|
|
|
14
|
+
def self.masks?(connection)
|
|
15
|
+
database = Server.config.database
|
|
16
|
+
|
|
17
|
+
return true if database.nil?
|
|
18
|
+
|
|
19
|
+
pool = connection.pool
|
|
20
|
+
|
|
21
|
+
pool.respond_to?(:db_config) && pool.db_config.name == database.to_s
|
|
22
|
+
end
|
|
23
|
+
|
|
12
24
|
def enable_row_level_security(table)
|
|
13
25
|
execute <<~SQL
|
|
14
26
|
ALTER TABLE #{table} ENABLE ROW LEVEL SECURITY;
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_initial-
|
|
3
|
-
"file": "assets/initial-
|
|
2
|
+
"_initial-Cn-UuAsy.js": {
|
|
3
|
+
"file": "assets/initial-Cn-UuAsy.js",
|
|
4
4
|
"name": "initial"
|
|
5
5
|
},
|
|
6
|
-
"_passkey-
|
|
7
|
-
"file": "assets/passkey-
|
|
6
|
+
"_passkey-5UYPVD3c.js": {
|
|
7
|
+
"file": "assets/passkey-5UYPVD3c.js",
|
|
8
8
|
"name": "passkey"
|
|
9
9
|
},
|
|
10
10
|
"entrypoints/account.js": {
|
|
11
|
-
"file": "assets/account-
|
|
11
|
+
"file": "assets/account-h7I6VUPR.js",
|
|
12
12
|
"name": "entrypoints/account.js",
|
|
13
13
|
"src": "entrypoints/account.js",
|
|
14
14
|
"isEntry": true,
|
|
15
15
|
"imports": [
|
|
16
|
-
"_passkey-
|
|
16
|
+
"_passkey-5UYPVD3c.js"
|
|
17
17
|
]
|
|
18
18
|
},
|
|
19
19
|
"entrypoints/app.css": {
|
|
20
|
-
"file": "assets/app-
|
|
20
|
+
"file": "assets/app-BXOScMjL.css",
|
|
21
21
|
"name": "entrypoints/app.css",
|
|
22
22
|
"names": [
|
|
23
23
|
"entrypoints/app.css"
|
|
@@ -35,28 +35,28 @@
|
|
|
35
35
|
"isEntry": true
|
|
36
36
|
},
|
|
37
37
|
"entrypoints/handshake.js": {
|
|
38
|
-
"file": "assets/handshake-
|
|
38
|
+
"file": "assets/handshake-YQpRNMIs.js",
|
|
39
39
|
"name": "entrypoints/handshake.js",
|
|
40
40
|
"src": "entrypoints/handshake.js",
|
|
41
41
|
"isEntry": true
|
|
42
42
|
},
|
|
43
43
|
"entrypoints/login.js": {
|
|
44
|
-
"file": "assets/login-
|
|
44
|
+
"file": "assets/login-C21_jb6L.js",
|
|
45
45
|
"name": "entrypoints/login.js",
|
|
46
46
|
"src": "entrypoints/login.js",
|
|
47
47
|
"isEntry": true,
|
|
48
48
|
"imports": [
|
|
49
|
-
"_passkey-
|
|
50
|
-
"_initial-
|
|
49
|
+
"_passkey-5UYPVD3c.js",
|
|
50
|
+
"_initial-Cn-UuAsy.js"
|
|
51
51
|
]
|
|
52
52
|
},
|
|
53
53
|
"entrypoints/manage.js": {
|
|
54
|
-
"file": "assets/manage-
|
|
54
|
+
"file": "assets/manage-DpVxe5sv.js",
|
|
55
55
|
"name": "entrypoints/manage.js",
|
|
56
56
|
"src": "entrypoints/manage.js",
|
|
57
57
|
"isEntry": true,
|
|
58
58
|
"imports": [
|
|
59
|
-
"_initial-
|
|
59
|
+
"_initial-Cn-UuAsy.js"
|
|
60
60
|
]
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as e,i as t,o as n}from"./passkey-
|
|
2
|
-
//# sourceMappingURL=account-
|
|
1
|
+
import{a as e,i as t,o as n}from"./passkey-5UYPVD3c.js";var r=()=>document.querySelector(`meta[name="csrf-token"]`)?.content??``,i=document.getElementById(`add-passkey`),a=document.getElementById(`passkey-credential`),o=document.getElementById(`passkey-name`),s=document.getElementById(`add-passkey-button`),c=document.getElementById(`passkey-unusable`);async function l(){let e=await fetch(i.dataset.challenge,{method:`POST`,credentials:`same-origin`,headers:{Accept:`application/json`,"X-CSRF-Token":r()}});if(e.status===401){let{redirectTo:t}=await e.json();throw window.location.assign(t),Error(`signing in again first`)}if(!e.ok)throw Error(`could not start enrolment: ${e.status}`);return e.json()}async function u(t){t.preventDefault(),s.disabled=!0,c.hidden=!0;try{a.value=await e(await l()),i.submit()}catch(e){n(e)||(c.textContent=`That passkey could not be added on this device.`,c.hidden=!1),s.disabled=!1}}i&&t()?(i.hidden=!1,s.addEventListener(`click`,u)):o&&o.closest(`[data-passkeys]`)?.setAttribute(`data-unsupported`,`true`);var d=document.getElementById(`picture-file`);d?.addEventListener(`change`,()=>{d.files?.length&&d.form?.requestSubmit()});for(let e of document.querySelectorAll(`[data-autosubmit]`)){let t=e.form?.querySelector(`[data-autosubmitted]`);t&&(t.hidden=!0),e.addEventListener(`change`,()=>e.form?.requestSubmit())}
|
|
2
|
+
//# sourceMappingURL=account-h7I6VUPR.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-
|
|
1
|
+
{"version":3,"file":"account-h7I6VUPR.js","names":["enrol","available","refused","csrf","form","field","name","button","unusable","options","response","redirectTo","add","event","error","picture"],"sources":["../../../../server/app/frontend/entrypoints/account.js"],"sourcesContent":["import { available, enrol, refused } from \"../lib/passkey.js\";\n\nconst csrf = () =>\n document.querySelector('meta[name=\"csrf-token\"]')?.content ?? \"\";\n\nconst form = document.getElementById(\"add-passkey\");\nconst field = document.getElementById(\"passkey-credential\");\nconst name = document.getElementById(\"passkey-name\");\nconst button = document.getElementById(\"add-passkey-button\");\nconst unusable = document.getElementById(\"passkey-unusable\");\n\nasync function options() {\n const response = await fetch(form.dataset.challenge, {\n method: \"POST\",\n credentials: \"same-origin\",\n headers: { Accept: \"application/json\", \"X-CSRF-Token\": csrf() },\n });\n\n if (response.status === 401) {\n const { redirectTo } = await response.json();\n window.location.assign(redirectTo);\n throw new Error(\"signing in again first\");\n }\n\n if (!response.ok)\n throw new Error(`could not start enrolment: ${response.status}`);\n\n return response.json();\n}\n\nasync function add(event) {\n event.preventDefault();\n\n button.disabled = true;\n unusable.hidden = true;\n\n try {\n field.value = await enrol(await options());\n form.submit();\n } catch (error) {\n if (!refused(error)) {\n unusable.textContent = \"That passkey could not be added on this device.\";\n unusable.hidden = false;\n }\n\n button.disabled = false;\n }\n}\n\nif (form && available()) {\n form.hidden = false;\n button.addEventListener(\"click\", add);\n} else if (name) {\n name.closest(\"[data-passkeys]\")?.setAttribute(\"data-unsupported\", \"true\");\n}\n\nconst picture = document.getElementById(\"picture-file\");\n\npicture?.addEventListener(\"change\", () => {\n if (picture.files?.length) picture.form?.requestSubmit();\n});\n\nfor (const field of document.querySelectorAll(\"[data-autosubmit]\")) {\n const button = field.form?.querySelector(\"[data-autosubmitted]\");\n\n if (button) button.hidden = true;\n field.addEventListener(\"change\", () => field.form?.requestSubmit());\n}\n"],"mappings":"A,wDAEA,IAAMG,MACJ,SAAS,cAAc,yBAAyB,CAAC,EAAE,SAAW,GAE1DC,EAAO,SAAS,eAAe,aAAa,EAC5CC,EAAQ,SAAS,eAAe,oBAAoB,EACpDC,EAAO,SAAS,eAAe,cAAc,EAC7CC,EAAS,SAAS,eAAe,oBAAoB,EACrDC,EAAW,SAAS,eAAe,kBAAkB,EAE3D,eAAeC,GAAU,CACvB,IAAMC,EAAW,MAAM,MAAMN,EAAK,QAAQ,UAAW,CACnD,OAAQ,OACR,YAAa,cACb,QAAS,CAAE,OAAQ,mBAAoB,eAAgBD,EAAK,CAAE,CAChE,CAAC,EAED,GAAIO,EAAS,SAAW,IAAK,CAC3B,GAAM,CAAE,cAAe,MAAMA,EAAS,KAAK,EAE3C,MADA,OAAO,SAAS,OAAOC,CAAU,EACvB,MAAM,wBAAwB,CAC1C,CAEA,GAAI,CAACD,EAAS,GACZ,MAAU,MAAM,8BAA8BA,EAAS,QAAQ,EAEjE,OAAOA,EAAS,KAAK,CACvB,CAEA,eAAeE,EAAI,EAAO,CACxBC,EAAM,eAAe,EAErB,EAAO,SAAW,GAClB,EAAS,OAAS,GAElB,GAAI,CACF,EAAM,MAAQ,MAAMb,EAAM,MAAMS,EAAQ,CAAC,EACzCL,EAAK,OAAO,CACd,OAASU,EAAO,CACTZ,EAAQY,CAAK,IAChB,EAAS,YAAc,kDACvB,EAAS,OAAS,IAGpB,EAAO,SAAW,EACpB,CACF,CAEIV,GAAQH,EAAU,GACpB,EAAK,OAAS,GACdM,EAAO,iBAAiB,QAASK,CAAG,GAC3BN,GACTA,EAAK,QAAQ,iBAAiB,CAAC,EAAE,aAAa,mBAAoB,MAAM,EAG1E,IAAMS,EAAU,SAAS,eAAe,cAAc,EAEtDA,GAAS,iBAAiB,aAAgB,CACpCA,EAAQ,OAAO,QAAQA,EAAQ,MAAM,cAAc,CACzD,CAAC,EAED,IAAK,IAAMV,KAAS,SAAS,iBAAiB,mBAAmB,EAAG,CAClE,IAAME,EAASF,EAAM,MAAM,cAAc,sBAAsB,EAE3DE,IAAQ,EAAO,OAAS,IAC5BF,EAAM,iBAAiB,aAAgBA,EAAM,MAAM,cAAc,CAAC,CACpE"}
|