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.
- checksums.yaml +4 -4
- data/app/controllers/masks/server/account_controller.rb +3 -0
- data/app/controllers/masks/server/app_passwords_controller.rb +41 -0
- data/app/controllers/masks/server/authorize_controller.rb +33 -2
- data/app/controllers/masks/server/discovery_controller.rb +36 -0
- data/app/controllers/masks/server/enrollments_controller.rb +17 -0
- data/app/controllers/masks/server/forward_auth_controller.rb +104 -12
- data/app/controllers/masks/server/logins_controller.rb +4 -3
- data/app/controllers/masks/server/organization_access_controller.rb +39 -0
- data/app/controllers/masks/server/organization_members_controller.rb +3 -1
- 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/unenroll_client.rb +28 -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 +18 -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 +5 -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/application_job.rb +28 -0
- data/app/jobs/masks/server/check_domain_claims_job.rb +1 -3
- data/app/jobs/masks/server/cleanup_job.rb +5 -6
- data/app/jobs/masks/server/health_check_job.rb +21 -0
- data/app/jobs/masks/server/idle_accounts_job.rb +1 -3
- data/app/jobs/masks/server/prune_queue_job.rb +19 -0
- data/app/jobs/masks/server/refresh_groups_job.rb +18 -0
- data/app/jobs/masks/server/refresh_saml_metadata_job.rb +1 -3
- data/app/jobs/masks/server/release_check_job.rb +21 -0
- data/app/jobs/masks/server/rotate_signing_keys_job.rb +1 -3
- data/app/jobs/masks/server/text_job.rb +12 -1
- data/app/ldap/masks/server/ldap/connection.rb +3 -2
- data/app/ldap/masks/server/ldap/gate.rb +31 -4
- 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 +82 -0
- data/app/models/masks/server/app_preset.rb +102 -0
- data/app/models/masks/server/boot_task.rb +41 -0
- data/app/models/masks/server/client.rb +15 -0
- data/app/models/masks/server/connection.rb +7 -0
- data/app/models/masks/server/declared.rb +10 -2
- data/app/models/masks/server/email_verification.rb +2 -2
- data/app/models/masks/server/enrollment_token.rb +1 -1
- data/app/models/masks/server/event.rb +15 -1
- 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 +243 -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/job_failures.rb +25 -0
- 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/session.rb +8 -0
- data/app/models/masks/server/tenant.rb +39 -2
- 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 +9 -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/client.yml +1 -1
- 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 +78 -0
- data/config/locales/de/manage/links.yml +20 -0
- data/config/locales/de/manage/machines.yml +13 -0
- data/config/locales/de/manage/namespaces.yml +1 -1
- 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 +9 -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 +78 -0
- data/config/locales/en/manage/links.yml +20 -0
- data/config/locales/en/manage/machines.yml +13 -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 +9 -0
- data/config/locales/es/flashes.yml +1 -0
- data/config/locales/es/group_sources.yml +5 -0
- data/config/locales/es/handshakes.yml +1 -1
- 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 +78 -0
- data/config/locales/es/manage/links.yml +20 -0
- data/config/locales/es/manage/machines.yml +13 -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 +9 -0
- data/config/locales/fr/flashes.yml +1 -0
- data/config/locales/fr/group_sources.yml +5 -0
- data/config/locales/fr/handshakes.yml +1 -1
- 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/client.yml +1 -1
- data/config/locales/fr/manage/clients.yml +1 -1
- 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 +78 -0
- data/config/locales/fr/manage/links.yml +20 -0
- data/config/locales/fr/manage/machines.yml +13 -0
- data/config/locales/fr/manage/providers.yml +1 -1
- data/config/routes.rb +6 -0
- data/db/migrate/20261019140000_add_indexes_for_cleanup_and_metrics.rb +10 -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/lib/generators/masks/server/install/install_generator.rb +3 -2
- data/lib/masks/server/configuration.rb +6 -11
- data/lib/masks/server/schema_reference.rb +12 -0
- data/lib/masks/server/tenancy/middleware.rb +17 -6
- data/lib/tasks/masks.rake +31 -7
- 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-DR9qQiBU.js → manage-DpVxe5sv.js} +86 -50
- 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 +96 -14
- data/public/masks-assets/assets/initial-C1GpCiEh.js +0 -3
- data/public/masks-assets/assets/manage-DR9qQiBU.js.map +0 -1
- data/public/masks-assets/assets/passkey-DIj_5LCY.js +0 -2
|
@@ -3,7 +3,11 @@ module Masks
|
|
|
3
3
|
class TextJob < ApplicationJob
|
|
4
4
|
queue_as :mailers
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
ATTEMPTS = 3
|
|
7
|
+
|
|
8
|
+
retry_on Sender::Failed, wait: :polynomially_longer, attempts: ATTEMPTS do |job, error|
|
|
9
|
+
job.gave_up!(error)
|
|
10
|
+
end
|
|
7
11
|
|
|
8
12
|
def perform(to, body)
|
|
9
13
|
sender = Current.tenant&.sms_sender
|
|
@@ -12,6 +16,13 @@ module Masks
|
|
|
12
16
|
|
|
13
17
|
sender.deliver(to: to, body: body)
|
|
14
18
|
end
|
|
19
|
+
|
|
20
|
+
def gave_up!(error)
|
|
21
|
+
Tenant.switch(held_tenant) do
|
|
22
|
+
Event.record!(Event::TEXT_UNDELIVERED, actor: nil, by: nil, device: nil, ip_address: nil, user_agent: nil,
|
|
23
|
+
said: error.message)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
15
26
|
end
|
|
16
27
|
end
|
|
17
28
|
end
|
|
@@ -58,7 +58,7 @@ module Masks
|
|
|
58
58
|
|
|
59
59
|
def bind(message)
|
|
60
60
|
request = message.operation
|
|
61
|
-
@bound = nil
|
|
61
|
+
previous, @bound = @bound, nil
|
|
62
62
|
|
|
63
63
|
if request.version != 3
|
|
64
64
|
return write(Protocol.bind_response(message.id, Protocol::PROTOCOL_ERROR, "only LDAPv3 is spoken here"))
|
|
@@ -68,7 +68,8 @@ module Masks
|
|
|
68
68
|
return write(Protocol.bind_response(message.id, Protocol::AUTH_METHOD_NOT_SUPPORTED, "only simple binds are supported"))
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
outcome = Gate.new(ip_address: @ip_address
|
|
71
|
+
outcome = Gate.new(ip_address: @ip_address, client_id: previous&.service? ? previous.client_id : nil)
|
|
72
|
+
.bind(request.name, request.password)
|
|
72
73
|
@bound = outcome.bound
|
|
73
74
|
|
|
74
75
|
write(Protocol.bind_response(message.id, outcome.code, outcome.message))
|
|
@@ -5,7 +5,11 @@ module Masks
|
|
|
5
5
|
WINDOW = 3.minutes
|
|
6
6
|
VIA = "ldap".freeze
|
|
7
7
|
|
|
8
|
-
Bound = Data.define(:kind, :tenant, :dn, :subject) do
|
|
8
|
+
Bound = Data.define(:kind, :tenant, :dn, :subject, :client_id) do
|
|
9
|
+
def initialize(client_id: nil, **rest)
|
|
10
|
+
super(client_id: client_id, **rest)
|
|
11
|
+
end
|
|
12
|
+
|
|
9
13
|
def service?
|
|
10
14
|
kind == :service
|
|
11
15
|
end
|
|
@@ -17,8 +21,9 @@ module Masks
|
|
|
17
21
|
|
|
18
22
|
Outcome = Data.define(:code, :message, :bound)
|
|
19
23
|
|
|
20
|
-
def initialize(ip_address:)
|
|
24
|
+
def initialize(ip_address:, client_id: nil)
|
|
21
25
|
@ip_address = ip_address
|
|
26
|
+
@client_id = client_id
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
def bind(name, password)
|
|
@@ -80,7 +85,8 @@ module Masks
|
|
|
80
85
|
|
|
81
86
|
Outcome.new(
|
|
82
87
|
code: Protocol::SUCCESS, message: "",
|
|
83
|
-
bound: Bound.new(kind: :service, tenant: tenant, dn: directory.service_dn(credential.label), subject: credential.id
|
|
88
|
+
bound: Bound.new(kind: :service, tenant: tenant, dn: directory.service_dn(credential.label), subject: credential.id,
|
|
89
|
+
client_id: credential.client_id)
|
|
84
90
|
)
|
|
85
91
|
end
|
|
86
92
|
|
|
@@ -93,6 +99,7 @@ module Masks
|
|
|
93
99
|
end
|
|
94
100
|
|
|
95
101
|
actor = Actor.authenticate(uid, password)
|
|
102
|
+
return app_password(tenant, directory, uid, password, account) if actor.nil? && app_password?(password)
|
|
96
103
|
|
|
97
104
|
if actor.nil?
|
|
98
105
|
failed!(ip_key, account)
|
|
@@ -103,8 +110,28 @@ module Masks
|
|
|
103
110
|
return refuse(uid, reason, actor) if reason
|
|
104
111
|
|
|
105
112
|
actor.active!
|
|
113
|
+
bound(tenant, directory, actor)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def app_password?(password)
|
|
117
|
+
@client_id && AppPassword.shaped?(password)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def app_password(tenant, directory, uid, password, account)
|
|
121
|
+
client = Client.active.find_by(id: @client_id)
|
|
122
|
+
outcome = AppPasswords.authenticate(client: client, identifier: uid, secret: password, via: VIA,
|
|
123
|
+
ip_address: @ip_address, tenant: tenant)
|
|
124
|
+
|
|
125
|
+
return throttled if outcome.throttled?
|
|
126
|
+
return bound(tenant, directory, outcome.actor, factor: AppPasswords::FACTOR) if outcome.actor
|
|
127
|
+
|
|
128
|
+
failed!(ip_key, account) if outcome.wrong?
|
|
129
|
+
invalid
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def bound(tenant, directory, actor, factor: "password")
|
|
106
133
|
dn = directory.person_dn(actor)
|
|
107
|
-
Event.record!(Event::LDAP_BOUND, actor: actor, dn: dn.to_s)
|
|
134
|
+
Event.record!(Event::LDAP_BOUND, actor: actor, dn: dn.to_s, factor: factor)
|
|
108
135
|
|
|
109
136
|
Outcome.new(code: Protocol::SUCCESS, message: "", bound: Bound.new(kind: :person, tenant: tenant, dn: dn, subject: actor.id))
|
|
110
137
|
end
|
|
@@ -160,6 +160,37 @@ module Masks
|
|
|
160
160
|
)
|
|
161
161
|
end
|
|
162
162
|
|
|
163
|
+
def access_granted(asked, journey:)
|
|
164
|
+
return message unless deliverable?
|
|
165
|
+
|
|
166
|
+
journey!(journey)
|
|
167
|
+
@actor = asked.actor
|
|
168
|
+
@app = asked.client
|
|
169
|
+
@url = (@app.approved? && @app.client_uri.presence) || home
|
|
170
|
+
|
|
171
|
+
mail(
|
|
172
|
+
from: self.class.from,
|
|
173
|
+
to: @actor.email,
|
|
174
|
+
subject: customize!(:access_granted, actor: @actor, app: @app.name,
|
|
175
|
+
default: t("actor_mailer.access_granted.subject", app: @app.name))
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def access_declined(asked, journey:)
|
|
180
|
+
return message unless deliverable?
|
|
181
|
+
|
|
182
|
+
journey!(journey)
|
|
183
|
+
@actor = asked.actor
|
|
184
|
+
@app = asked.client
|
|
185
|
+
|
|
186
|
+
mail(
|
|
187
|
+
from: self.class.from,
|
|
188
|
+
to: @actor.email,
|
|
189
|
+
subject: customize!(:access_declined, actor: @actor, app: @app.name,
|
|
190
|
+
default: t("actor_mailer.access_declined.subject", app: @app.name))
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
163
194
|
private
|
|
164
195
|
|
|
165
196
|
def tenant_lede(manager)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
class AccessRequest < ApplicationRecord
|
|
4
|
+
include TenantScoped
|
|
5
|
+
|
|
6
|
+
class Refused < StandardError; end
|
|
7
|
+
|
|
8
|
+
LIFETIME = 30.days
|
|
9
|
+
|
|
10
|
+
belongs_to :actor
|
|
11
|
+
belongs_to :client
|
|
12
|
+
belongs_to :decided_by, class_name: "Actor", optional: true
|
|
13
|
+
|
|
14
|
+
scope :open, -> { where(granted_at: nil, declined_at: nil) }
|
|
15
|
+
scope :lapsed, -> { where(created_at: ...LIFETIME.ago) }
|
|
16
|
+
scope :newest_first, -> { order(created_at: :desc) }
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def ask!(actor:, client:)
|
|
20
|
+
return nil if client.admits?(actor)
|
|
21
|
+
|
|
22
|
+
open.find_by(actor: actor, client: client) || begin
|
|
23
|
+
created = create!(actor: actor, client: client)
|
|
24
|
+
Event.record!(Event::ACCESS_REQUESTED, actor: actor, client: client)
|
|
25
|
+
created
|
|
26
|
+
end
|
|
27
|
+
rescue ActiveRecord::RecordNotUnique
|
|
28
|
+
open.find_by!(actor: actor, client: client)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def for_organization(organization)
|
|
32
|
+
open.joins(:client).where("EXISTS (SELECT 1 FROM unnest(clients.allowed_groups) AS held WHERE held LIKE ?)",
|
|
33
|
+
"#{sanitize_sql_like(organization.key)}:%")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def open?
|
|
38
|
+
granted_at.nil? && declined_at.nil?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def grant!(label: nil, by:, journey:)
|
|
42
|
+
raise Refused, "#{actor.identifier} is already admitted to #{client.name}" if client.admits?(actor)
|
|
43
|
+
raise Refused, "this request is already decided" unless open?
|
|
44
|
+
|
|
45
|
+
chosen = label.presence || client.allowed_groups.first
|
|
46
|
+
raise Refused, "#{chosen} is not one of the groups #{client.name} allows" unless client.allowed_groups.include?(chosen)
|
|
47
|
+
|
|
48
|
+
transaction do
|
|
49
|
+
admit!(chosen, by: by)
|
|
50
|
+
update!(granted_at: Time.current, decided_by: by, label: chosen)
|
|
51
|
+
Event.record!(Event::ACCESS_GRANTED, actor: actor, by: by, client: client, label: chosen)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
ActorMailer.access_granted(self, journey: journey).deliver_later if actor.email.present?
|
|
55
|
+
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def decline!(by:, journey:)
|
|
60
|
+
raise Refused, "this request is already decided" unless open?
|
|
61
|
+
|
|
62
|
+
transaction do
|
|
63
|
+
update!(declined_at: Time.current, decided_by: by)
|
|
64
|
+
Event.record!(Event::ACCESS_DECLINED, actor: actor, by: by, client: client)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
ActorMailer.access_declined(self, journey: journey).deliver_later if actor.email.present?
|
|
68
|
+
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def organization_roles
|
|
73
|
+
client.allowed_groups.filter_map do |held|
|
|
74
|
+
key, role = held.split(":", 2)
|
|
75
|
+
[ key, role ] if role
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def admit!(label, by:)
|
|
82
|
+
key, role = label.split(":", 2)
|
|
83
|
+
|
|
84
|
+
if role.nil?
|
|
85
|
+
group = Group.find_by(key: key) || raise(Refused, "there is no group keyed #{key}")
|
|
86
|
+
group.add!(actor, by: by)
|
|
87
|
+
else
|
|
88
|
+
organization = Organization.active.find_by(key: key) || raise(Refused, "there is no organization keyed #{key}")
|
|
89
|
+
held = organization.memberships.find_by(actor: actor)
|
|
90
|
+
held = held.destroy! && nil if held&.pending?
|
|
91
|
+
|
|
92
|
+
if held.nil?
|
|
93
|
+
Members.enroll!(organization, actor, role: role, by: by)
|
|
94
|
+
elsif held.role != role
|
|
95
|
+
Members.assign!(held, role: role, by: by)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
rescue Members::Refused => e
|
|
99
|
+
raise Refused, e.message
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -30,6 +30,7 @@ module Masks
|
|
|
30
30
|
has_many :approvals, class_name: "Client", foreign_key: :approved_by_id, dependent: :nullify
|
|
31
31
|
has_one :avatar, dependent: :destroy
|
|
32
32
|
has_many :group_members, dependent: :delete_all
|
|
33
|
+
has_many :access_requests, dependent: :delete_all
|
|
33
34
|
has_many :groups, through: :group_members
|
|
34
35
|
|
|
35
36
|
before_destroy :sign_out_everywhere!, prepend: true
|
|
@@ -61,6 +62,7 @@ module Masks
|
|
|
61
62
|
after_save :release_previous_nickname, if: :saved_change_to_nickname?
|
|
62
63
|
before_save :forget_code_factors_for_new_addresses
|
|
63
64
|
after_update :forget_mailed_links, if: :saved_change_to_email?
|
|
65
|
+
after_commit :settle_sourced_groups, on: %i[create update], if: -> { saved_changes.keys.intersect?(Group::WATCHED) }
|
|
64
66
|
|
|
65
67
|
normalizes :nickname, with: ->(value) { value.to_s.strip.presence }
|
|
66
68
|
normalizes :email, with: ->(value) { value.to_s.strip.downcase.presence }
|
|
@@ -469,6 +471,10 @@ module Masks
|
|
|
469
471
|
|
|
470
472
|
private
|
|
471
473
|
|
|
474
|
+
def settle_sourced_groups
|
|
475
|
+
Group.ruled.find_each { |group| group.settle!(self) }
|
|
476
|
+
end
|
|
477
|
+
|
|
472
478
|
def forget_mailed_links
|
|
473
479
|
[ PasswordReset, Invitation, EmailVerification ].each do |kind|
|
|
474
480
|
kind.where(actor_id: id).live.find_each(&:consume!)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
class AppPassword < Token
|
|
4
|
+
ALPHABET = "abcdefghjkmnpqrstuvwxyz23456789".freeze
|
|
5
|
+
LENGTH = 24
|
|
6
|
+
GROUP = 4
|
|
7
|
+
LIFETIME = 100.years
|
|
8
|
+
NAME_LIMIT = 64
|
|
9
|
+
MOST = 20
|
|
10
|
+
USED_GRAIN = 1.minute
|
|
11
|
+
|
|
12
|
+
class Refused < StandardError; end
|
|
13
|
+
|
|
14
|
+
scope :newest_first, -> { order(created_at: :desc) }
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def offered(actor)
|
|
18
|
+
Client.active.where(app_passwords: true).order(:name).select { |client| client.admits?(actor) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def issue!(actor:, client:, name:)
|
|
22
|
+
label = name.to_s.strip
|
|
23
|
+
|
|
24
|
+
raise Refused, "name" if label.empty? || label.length > NAME_LIMIT
|
|
25
|
+
raise Refused, "app" if client.nil? || offered(actor).none? { |offered| offered.id == client.id }
|
|
26
|
+
raise Refused, "too_many" if live.where(actor: actor).count >= MOST
|
|
27
|
+
|
|
28
|
+
secret = Array.new(LENGTH) { ALPHABET[SecureRandom.random_number(ALPHABET.length)] }.join
|
|
29
|
+
held = create!(actor: actor, client: client, digest: BCrypt::Password.create(secret, cost: cost),
|
|
30
|
+
payload: { "name" => label }, expires_at: LIFETIME.from_now)
|
|
31
|
+
held.instance_variable_set(:@secret, grouped(secret))
|
|
32
|
+
held
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def normalize(secret)
|
|
36
|
+
secret.to_s.downcase.delete("- ")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def shaped?(secret)
|
|
40
|
+
offered = normalize(secret)
|
|
41
|
+
|
|
42
|
+
offered.length == LENGTH && offered.each_char.all? { |char| ALPHABET.include?(char) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def check(actor:, client:, secret:)
|
|
46
|
+
offered = normalize(secret)
|
|
47
|
+
return burn(offered) unless actor && client && shaped?(offered)
|
|
48
|
+
|
|
49
|
+
found = live.where(actor: actor, client: client).to_a.find { |held| BCrypt::Password.new(held.digest) == offered }
|
|
50
|
+
|
|
51
|
+
found || burn(offered)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def grouped(secret)
|
|
55
|
+
secret.scan(/.{1,#{GROUP}}/o).join("-")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def cost
|
|
61
|
+
ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def burn(secret)
|
|
65
|
+
BCrypt::Password.new(Actor.decoy_digest) == secret
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def name
|
|
71
|
+
held("name")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def used_at
|
|
75
|
+
held("used_at")&.then { |value| Time.zone.parse(value) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def used!
|
|
79
|
+
return if used_at && used_at > USED_GRAIN.ago
|
|
80
|
+
|
|
81
|
+
now = Time.current
|
|
82
|
+
today = now.to_date.iso8601
|
|
83
|
+
first_today = held("used_on") != today
|
|
84
|
+
|
|
85
|
+
update_columns(payload: (payload || {}).merge("used_at" => now.iso8601, "used_on" => today))
|
|
86
|
+
|
|
87
|
+
Event.record!(Event::APP_PASSWORD_USED, actor: actor, client: client, name: name) if first_today
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
module AppPasswords
|
|
4
|
+
FACTOR = "app_password".freeze
|
|
5
|
+
THROTTLED = "throttled".freeze
|
|
6
|
+
WINDOW = 3.minutes
|
|
7
|
+
SCOPE = "masks/server/logins".freeze
|
|
8
|
+
|
|
9
|
+
Outcome = Data.define(:actor, :refusal) do
|
|
10
|
+
def throttled?
|
|
11
|
+
refusal == THROTTLED
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def wrong?
|
|
15
|
+
refusal == FACTOR
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def authenticate(client:, identifier:, secret:, via:, ip_address: Current.ip_address, tenant: Current.tenant)
|
|
21
|
+
return refused(nil, identifier, "factor-not-offered", via, client) unless client&.app_passwords? && !client.archived?
|
|
22
|
+
|
|
23
|
+
actor = Actor.locate(identifier)
|
|
24
|
+
keys = [ account_key(tenant, actor, identifier), address_key(tenant, ip_address) ]
|
|
25
|
+
|
|
26
|
+
if throttled?(keys)
|
|
27
|
+
Event.record!(Event::LOGIN_THROTTLED, actor: actor, by: nil, client: client, identifier: identifier, via: via)
|
|
28
|
+
return Outcome.new(actor: nil, refusal: THROTTLED)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
held = AppPassword.check(actor: actor, client: client, secret: secret)
|
|
32
|
+
|
|
33
|
+
if held.nil?
|
|
34
|
+
failed!(keys)
|
|
35
|
+
return refused(actor, identifier, FACTOR, via, client)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
reason = obstacle(client, actor)
|
|
39
|
+
return refused(actor, identifier, reason, via, client) if reason
|
|
40
|
+
|
|
41
|
+
actor.active!
|
|
42
|
+
held.used!
|
|
43
|
+
|
|
44
|
+
Outcome.new(actor: actor, refusal: nil)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def obstacle(client, actor)
|
|
50
|
+
return "suspended" if actor.suspended?
|
|
51
|
+
return "pending_approval" if actor.pending_approval_at.present?
|
|
52
|
+
|
|
53
|
+
"not_admitted" unless client.admits?(actor)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def refused(actor, identifier, factor, via, client)
|
|
57
|
+
Event.record!(Event::LOGIN_REFUSED, actor: actor, by: nil, client: client, factor: factor, identifier: identifier, via: via)
|
|
58
|
+
|
|
59
|
+
Outcome.new(actor: nil, refusal: factor)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def account_key(tenant, actor, identifier)
|
|
63
|
+
[ "rate-limit", SCOPE, "identifier", tenant.id, actor&.id || identifier.to_s.strip.downcase ].join(":")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def address_key(tenant, ip_address)
|
|
67
|
+
[ "rate-limit", SCOPE, tenant.id, ip_address ].join(":")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def throttled?(keys)
|
|
71
|
+
limits = [ ::Rails.configuration.masks.account_attempt_limit, ::Rails.configuration.masks.attempt_limit ]
|
|
72
|
+
|
|
73
|
+
keys.zip(limits).any? { |key, limit| ::Rails.cache.read(key).to_i >= limit }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def failed!(keys)
|
|
77
|
+
keys.each { |key| ::Rails.cache.increment(key, 1, expires_in: WINDOW) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -182,6 +182,108 @@ module Masks
|
|
|
182
182
|
[ "OAUTH_PROVIDER_NAME", "{tenant}" ], [ "ENABLE_OAUTH_SIGNUP", "true" ],
|
|
183
183
|
[ "ENABLE_OAUTH_ROLE_MANAGEMENT", "true" ], [ "OAUTH_ROLES_CLAIM", "groups" ], [ "OAUTH_ADMIN_ROLES", "admins" ]
|
|
184
184
|
]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
key: "tailscale", name: "Tailscale",
|
|
188
|
+
redirects: [ "https://login.tailscale.com/a/oauth_response" ],
|
|
189
|
+
scopes: %w[openid profile email],
|
|
190
|
+
guide: "https://tailscale.com/kb/1240/sso-custom-oidc",
|
|
191
|
+
settings: [
|
|
192
|
+
[ "WebFinger domain", "{masks_host}" ], [ "Issuer URL", "{issuer}" ],
|
|
193
|
+
[ "Client ID", "{client_id}" ], [ "Client secret", "{client_secret}" ]
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
key: "headscale", name: "Headscale",
|
|
198
|
+
redirects: [ "{base}/oidc/callback" ],
|
|
199
|
+
scopes: %w[openid profile email groups],
|
|
200
|
+
guide: "https://headscale.net/stable/ref/oidc/",
|
|
201
|
+
settings: [
|
|
202
|
+
[ "oidc.issuer", "{issuer}" ], [ "oidc.client_id", "{client_id}" ], [ "oidc.client_secret", "{client_secret}" ],
|
|
203
|
+
[ "oidc.scope", "[openid, profile, email, groups]" ], [ "oidc.allowed_groups", "[admins]" ]
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
key: "home-assistant", name: "Home Assistant (OIDC integration)",
|
|
208
|
+
redirects: [ "{base}/auth/oidc/callback" ],
|
|
209
|
+
scopes: %w[openid profile email groups],
|
|
210
|
+
guide: "https://github.com/christiaangoossens/hass-oidc-auth",
|
|
211
|
+
settings: [
|
|
212
|
+
[ "auth_oidc.discovery_url", "{discovery}" ], [ "auth_oidc.client_id", "{client_id}" ],
|
|
213
|
+
[ "auth_oidc.client_secret", "{client_secret}" ], [ "auth_oidc.display_name", "{tenant}" ],
|
|
214
|
+
[ "auth_oidc.roles.admin", "admins" ]
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
key: "synapse", name: "Matrix (Synapse)", asks: { "name" => "masks" },
|
|
219
|
+
redirects: [ "{base}/_synapse/client/oidc/callback" ],
|
|
220
|
+
scopes: %w[openid profile email],
|
|
221
|
+
guide: "https://element-hq.github.io/synapse/latest/openid.html",
|
|
222
|
+
settings: [
|
|
223
|
+
[ "oidc_providers", "- idp_id: {name}\n idp_name: {tenant}\n issuer: {issuer}\n client_id: {client_id}\n" \
|
|
224
|
+
" client_secret: {client_secret}\n scopes: [openid, profile, email]\n" \
|
|
225
|
+
" user_mapping_provider:\n config:\n localpart_template: \"{{ user.preferred_username }}\"\n" \
|
|
226
|
+
" display_name_template: \"{{ user.name }}\"\n email_template: \"{{ user.email }}\"" ]
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
key: "bookstack", name: "BookStack",
|
|
231
|
+
redirects: [ "{base}/oidc/callback" ],
|
|
232
|
+
scopes: %w[openid profile email groups],
|
|
233
|
+
guide: "https://www.bookstackapp.com/docs/admin/oidc-auth/",
|
|
234
|
+
settings: [
|
|
235
|
+
[ "AUTH_METHOD", "oidc" ], [ "OIDC_NAME", "{tenant}" ], [ "OIDC_ISSUER", "{issuer}" ], [ "OIDC_ISSUER_DISCOVER", "true" ],
|
|
236
|
+
[ "OIDC_CLIENT_ID", "{client_id}" ], [ "OIDC_CLIENT_SECRET", "{client_secret}" ],
|
|
237
|
+
[ "OIDC_DISPLAY_NAME_CLAIMS", "name" ], [ "OIDC_ADDITIONAL_SCOPES", "groups" ],
|
|
238
|
+
[ "OIDC_USER_TO_GROUPS", "true" ], [ "OIDC_GROUPS_CLAIM", "groups" ], [ "OIDC_REMOVE_FROM_GROUPS", "true" ]
|
|
239
|
+
]
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
key: "wikijs", name: "Wiki.js", asks: { "name" => "masks" },
|
|
243
|
+
redirects: [ "{base}/login/{name}/callback" ],
|
|
244
|
+
scopes: %w[openid profile email groups],
|
|
245
|
+
guide: "https://docs.requarks.io/auth/oidc",
|
|
246
|
+
settings: [
|
|
247
|
+
[ "Client ID", "{client_id}" ], [ "Client Secret", "{client_secret}" ],
|
|
248
|
+
[ "Authorization Endpoint URL", "{authorize}" ], [ "Token Endpoint URL", "{token}" ],
|
|
249
|
+
[ "User Info Endpoint URL", "{userinfo}" ], [ "Issuer", "{issuer}" ], [ "Logout URL", "{logout}" ],
|
|
250
|
+
[ "Email Claim", "email" ], [ "Display Name Claim", "name" ], [ "Groups Claim", "groups" ],
|
|
251
|
+
[ "Callback URL", "{base}/login/{name}/callback" ]
|
|
252
|
+
]
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
key: "minio", name: "MinIO",
|
|
256
|
+
redirects: [ "{base}/oauth_callback" ],
|
|
257
|
+
scopes: %w[openid profile email groups],
|
|
258
|
+
guide: "https://min.io/docs/minio/linux/operations/external-iam/configure-openid-external-identity-management.html",
|
|
259
|
+
settings: [
|
|
260
|
+
[ "MINIO_IDENTITY_OPENID_CONFIG_URL", "{discovery}" ], [ "MINIO_IDENTITY_OPENID_CLIENT_ID", "{client_id}" ],
|
|
261
|
+
[ "MINIO_IDENTITY_OPENID_CLIENT_SECRET", "{client_secret}" ], [ "MINIO_IDENTITY_OPENID_SCOPES", "openid,profile,email,groups" ],
|
|
262
|
+
[ "MINIO_IDENTITY_OPENID_CLAIM_NAME", "groups" ], [ "MINIO_IDENTITY_OPENID_REDIRECT_URI", "{base}/oauth_callback" ],
|
|
263
|
+
[ "MINIO_IDENTITY_OPENID_DISPLAY_NAME", "{tenant}" ]
|
|
264
|
+
]
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
key: "argocd", name: "Argo CD",
|
|
268
|
+
redirects: [ "{base}/auth/callback", "{base}/api/dex/callback" ],
|
|
269
|
+
scopes: %w[openid profile email groups],
|
|
270
|
+
guide: "https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/#existing-oidc-provider",
|
|
271
|
+
settings: [
|
|
272
|
+
[ "argocd-cm oidc.config", "name: {tenant}\nissuer: {issuer}\nclientID: {client_id}\nclientSecret: {client_secret}\n" \
|
|
273
|
+
"requestedScopes: [openid, profile, email, groups]" ],
|
|
274
|
+
[ "argocd-rbac-cm policy.csv", "g, admins, role:admin" ], [ "argocd-rbac-cm scopes", "[groups]" ]
|
|
275
|
+
]
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
key: "synology", name: "Synology DSM",
|
|
279
|
+
redirects: [ "{base}", "{base}/" ],
|
|
280
|
+
scopes: %w[openid profile email],
|
|
281
|
+
guide: "https://kb.synology.com/en-global/DSM/help/DSM/AdminCenter/connection_sso_client",
|
|
282
|
+
settings: [
|
|
283
|
+
[ "Profile", "OIDC" ], [ "Name", "{tenant}" ], [ "Well Known URL", "{discovery}" ],
|
|
284
|
+
[ "Client ID", "{client_id}" ], [ "Client secret", "{client_secret}" ],
|
|
285
|
+
[ "Scope", "openid profile email" ], [ "Username claim", "preferred_username" ], [ "Redirect URI", "{base}" ]
|
|
286
|
+
]
|
|
185
287
|
}
|
|
186
288
|
].map(&:freeze).freeze
|
|
187
289
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "socket"
|
|
2
|
+
require "zlib"
|
|
3
|
+
|
|
4
|
+
module Masks
|
|
5
|
+
module Server
|
|
6
|
+
class BootTask
|
|
7
|
+
WINDOW = 10.minutes
|
|
8
|
+
|
|
9
|
+
Outcome = Struct.new(:name, :host, :value, :done) do
|
|
10
|
+
def done? = done
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def once(name, fingerprint: nil)
|
|
15
|
+
key = "masks:boot:#{name}:#{Digest::SHA256.hexdigest(fingerprint.to_s)[0, 16]}"
|
|
16
|
+
host = Socket.gethostname
|
|
17
|
+
outcome = nil
|
|
18
|
+
|
|
19
|
+
ActiveRecord::Base.transaction do
|
|
20
|
+
ActiveRecord::Base.connection.execute("SELECT pg_advisory_xact_lock(#{lock_id(name)})")
|
|
21
|
+
done_by = ::Rails.cache.read(key)
|
|
22
|
+
|
|
23
|
+
if done_by
|
|
24
|
+
outcome = Outcome.new(name, done_by, nil, false)
|
|
25
|
+
else
|
|
26
|
+
value = yield
|
|
27
|
+
::Rails.cache.write(key, host, expires_in: WINDOW)
|
|
28
|
+
outcome = Outcome.new(name, host, value, true)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
outcome
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def lock_id(name)
|
|
36
|
+
Zlib.crc32("masks:boot:#{name}")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -54,6 +54,7 @@ module Masks
|
|
|
54
54
|
has_many :consents, dependent: :destroy
|
|
55
55
|
has_many :delegations, dependent: :destroy
|
|
56
56
|
has_many :saml_participations, dependent: :delete_all
|
|
57
|
+
has_many :access_requests, dependent: :delete_all
|
|
57
58
|
has_many :namespaces, -> { order(:name) }, dependent: :nullify
|
|
58
59
|
has_many :enrolled, class_name: "Client", foreign_key: :enrolled_by_id, inverse_of: :enrolled_by,
|
|
59
60
|
dependent: :nullify
|
|
@@ -73,6 +74,7 @@ module Masks
|
|
|
73
74
|
validate :keys_are_usable
|
|
74
75
|
validate :saml_is_described, if: :saml?
|
|
75
76
|
validate :forward_is_described, if: :forward?
|
|
77
|
+
validate :forward_auth_rules_are_usable
|
|
76
78
|
validate :backchannel_logout_uri_is_usable
|
|
77
79
|
validate :sector_is_derivable
|
|
78
80
|
validate :consent_is_skipped_only_when_approved
|
|
@@ -89,6 +91,7 @@ module Masks
|
|
|
89
91
|
|
|
90
92
|
normalizes :allowed_groups, with: ->(value) { Array(value).map { |group| group.to_s.strip.downcase }.compact_blank.uniq.sort }
|
|
91
93
|
normalizes :saml_entity_id, :saml_slo_url, with: ->(value) { value.to_s.strip.presence }
|
|
94
|
+
normalizes :forward_auth_rules, with: ->(value) { ForwardAuthRules.normalize(value) }
|
|
92
95
|
normalizes(*METADATA_URIS, with: ->(value) { value.to_s.strip.presence })
|
|
93
96
|
|
|
94
97
|
after_commit :refresh_logo!, on: %i[create update], if: :saved_change_to_logo_uri?
|
|
@@ -464,6 +467,10 @@ module Masks
|
|
|
464
467
|
allowed_groups.empty? || actor.group_labels.intersect?(allowed_groups)
|
|
465
468
|
end
|
|
466
469
|
|
|
470
|
+
def forward_rules
|
|
471
|
+
ForwardAuthRules.new(forward_auth_rules)
|
|
472
|
+
end
|
|
473
|
+
|
|
467
474
|
def permitted_scopes(requested)
|
|
468
475
|
return scope_list if Scopes.list(requested).empty?
|
|
469
476
|
|
|
@@ -604,6 +611,14 @@ module Masks
|
|
|
604
611
|
errors.add(:grant_types, "not supported: #{unknown.join(', ')}") if unknown.any?
|
|
605
612
|
end
|
|
606
613
|
|
|
614
|
+
def forward_auth_rules_are_usable
|
|
615
|
+
held = forward_auth_rules
|
|
616
|
+
|
|
617
|
+
return errors.add(:base, "forward auth rules apply only to an app behind a proxy") if held.present? && !forward?
|
|
618
|
+
|
|
619
|
+
ForwardAuthRules.problems(held).each { |problem| errors.add(:base, problem) }
|
|
620
|
+
end
|
|
621
|
+
|
|
607
622
|
def forward_is_described
|
|
608
623
|
errors.add(:grant_types, "are not used by an app behind a proxy") if grant_types.any?
|
|
609
624
|
errors.add(:token_endpoint_auth_method, "is none for an app behind a proxy") unless public?
|