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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74cc1568b822c061657b6625a809deb750f5503e66fa8d0f390c7a596004953e
|
|
4
|
+
data.tar.gz: 8bdb0e298ee2c901c71f654215fd0b958d36cd6e69befa231313dd2f98a5d249
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e0aa16a41f23221d2274bf66aac45ba8a1c20c901526cfd79905d14a229b3614921dba96c90a4ad296f69a210c8aee6d6aa11453d063d8a07cd9b1b724f13feb
|
|
7
|
+
data.tar.gz: 4c36c8378c7d459c4c4280227d36ec6f91234d3c2db47f857b8165312af340958789373497de7aa244f8c13ceb09a33415e2de40af99eed39331d1c3161b0838
|
|
@@ -9,6 +9,9 @@ module Masks
|
|
|
9
9
|
return redirect_to login_path if @actor.nil?
|
|
10
10
|
|
|
11
11
|
@apps = Apps.held_by(@actor)
|
|
12
|
+
@app_passwords = AppPassword.live.where(actor: @actor).includes(:client).newest_first.to_a
|
|
13
|
+
@app_password_clients = AppPassword.offered(@actor)
|
|
14
|
+
@app_password = flash[:app_password]
|
|
12
15
|
@launchable = Apps.launchable(@actor)
|
|
13
16
|
@invitations = @actor.memberships.outstanding.joins(:organization).merge(Organization.active)
|
|
14
17
|
.includes(:organization, :invited_by).order("organizations.name").to_a
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
class AppPasswordsController < ApplicationController
|
|
4
|
+
ANCHOR = "app-passwords".freeze
|
|
5
|
+
|
|
6
|
+
before_action :require_actor
|
|
7
|
+
|
|
8
|
+
def create
|
|
9
|
+
return unless reauthenticated!(ANCHOR, t("app_passwords.again"))
|
|
10
|
+
|
|
11
|
+
client = Client.active.find_by(client_id: params[:client_id].to_s)
|
|
12
|
+
held = AppPassword.issue!(actor: current_actor, client: client, name: params[:name])
|
|
13
|
+
|
|
14
|
+
Event.record!(Event::APP_PASSWORD_CREATED, actor: current_actor, client: client, name: held.name)
|
|
15
|
+
|
|
16
|
+
flash[:app_password] = { "name" => held.name, "app" => client.name, "secret" => held.secret }
|
|
17
|
+
redirect_to root_path(anchor: ANCHOR)
|
|
18
|
+
rescue AppPassword::Refused => e
|
|
19
|
+
redirect_to root_path(anchor: ANCHOR), alert: t("app_passwords.refused.#{e.message}", most: AppPassword::MOST)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def destroy
|
|
23
|
+
held = AppPassword.live.find_by(id: params[:id].to_s[/\A\d+\z/], actor: current_actor)
|
|
24
|
+
|
|
25
|
+
return redirect_to root_path(anchor: ANCHOR), alert: t("app_passwords.unknown") if held.nil?
|
|
26
|
+
|
|
27
|
+
held.revoke!
|
|
28
|
+
|
|
29
|
+
Event.record!(Event::APP_PASSWORD_REVOKED, actor: current_actor, client: held.client, name: held.name)
|
|
30
|
+
|
|
31
|
+
redirect_to root_path(anchor: ANCHOR), notice: t("app_passwords.revoked", name: held.name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def require_actor
|
|
37
|
+
redirect_to login_path unless current_actor
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -9,6 +9,10 @@ module Masks
|
|
|
9
9
|
refuse(bad_request(:invalid_request_object, refusal.message))
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
rate_limit to: 1, within: 1.hour, only: :ask,
|
|
13
|
+
by: -> { [ current_tenant.id, current_actor&.id, refused_request(params[:rid])&.client_id ].join(":") },
|
|
14
|
+
with: -> { redirect_to authorize_refused_path(rid: params[:rid]), alert: t("authorize.error.slow_down") }
|
|
15
|
+
|
|
12
16
|
def show
|
|
13
17
|
authorization = Authorization.from_request(request)
|
|
14
18
|
|
|
@@ -45,7 +49,7 @@ module Masks
|
|
|
45
49
|
|
|
46
50
|
login = advance(pending)
|
|
47
51
|
|
|
48
|
-
return redirect_to(refuse_on_masks(pending, login.refusal)) if login.refused? && !answerable?(pending)
|
|
52
|
+
return redirect_to(refuse_on_masks(pending, login.refusal)) if login.refused? && (!answerable?(pending) || askable?(pending, login))
|
|
49
53
|
return answer(deny(pending, login.refusal.error, login.refusal.description)) if login.refused?
|
|
50
54
|
return interaction_required(pending) if login.prompted? && pending.silent?
|
|
51
55
|
return redirect_to(login.redirect_to, allow_other_host: true) if login.redirect_to.present?
|
|
@@ -55,14 +59,31 @@ module Masks
|
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
def refused
|
|
58
|
-
|
|
62
|
+
pending = refused_request(params[:rid])
|
|
63
|
+
refusal = pending&.refusal
|
|
59
64
|
|
|
60
65
|
return show_error("invalid_request", "that sign-in request has expired", :bad_request) if refusal.nil?
|
|
61
66
|
|
|
62
67
|
@refused = true
|
|
68
|
+
@asking = asking_for(pending)
|
|
69
|
+
@waiting = @asking && AccessRequest.open.find_by(actor: current_actor, client: @asking)
|
|
70
|
+
@return_to = answerable?(pending) && pending.authorization.redirect_with(
|
|
71
|
+
issuer: issuer, error: refusal["error"], error_description: refusal["description"]
|
|
72
|
+
)
|
|
63
73
|
show_error(refusal["error"], refusal["description"], :forbidden)
|
|
64
74
|
end
|
|
65
75
|
|
|
76
|
+
def ask
|
|
77
|
+
pending = refused_request(params[:rid])
|
|
78
|
+
asking = pending && asking_for(pending)
|
|
79
|
+
|
|
80
|
+
return show_error("invalid_request", "that sign-in request has expired", :bad_request) if asking.nil?
|
|
81
|
+
|
|
82
|
+
AccessRequest.ask!(actor: current_actor, client: asking)
|
|
83
|
+
|
|
84
|
+
redirect_to authorize_refused_path(rid: params[:rid]), notice: t("authorize.error.asked")
|
|
85
|
+
end
|
|
86
|
+
|
|
66
87
|
private
|
|
67
88
|
|
|
68
89
|
def validate(authorization)
|
|
@@ -149,6 +170,16 @@ module Masks
|
|
|
149
170
|
end
|
|
150
171
|
end
|
|
151
172
|
|
|
173
|
+
def askable?(pending, login)
|
|
174
|
+
!pending.silent? && login.actor.present? && asking_for(pending, login.actor).present?
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def asking_for(pending, actor = current_actor)
|
|
178
|
+
client = pending.client
|
|
179
|
+
|
|
180
|
+
client if actor && client && client.allowed_groups.any? && !client.admits?(actor)
|
|
181
|
+
end
|
|
182
|
+
|
|
152
183
|
def admitted?(authorization)
|
|
153
184
|
tracked(REQUESTS).key?(authorization.fingerprint)
|
|
154
185
|
end
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
module Masks
|
|
2
2
|
module Server
|
|
3
3
|
class DiscoveryController < ApplicationController
|
|
4
|
+
OPENID_ISSUER = "http://openid.net/specs/connect/1.0/issuer".freeze
|
|
5
|
+
LONGEST_RESOURCE = 512
|
|
6
|
+
|
|
4
7
|
def openid
|
|
5
8
|
render json: issuer.discovery
|
|
6
9
|
end
|
|
@@ -13,6 +16,39 @@ module Masks
|
|
|
13
16
|
def resource
|
|
14
17
|
render json: issuer.protected_resource
|
|
15
18
|
end
|
|
19
|
+
|
|
20
|
+
def webfinger
|
|
21
|
+
resource = params[:resource].to_s
|
|
22
|
+
host = resource_host(resource)
|
|
23
|
+
|
|
24
|
+
return head(:bad_request) if host.nil?
|
|
25
|
+
return head(:not_found) unless host.casecmp?(URI.parse(issuer.url).host)
|
|
26
|
+
|
|
27
|
+
asked = Array(params[:rel]).map(&:to_s)
|
|
28
|
+
links = [ { "rel" => OPENID_ISSUER, "href" => issuer.url } ]
|
|
29
|
+
links = links.select { |link| asked.include?(link["rel"]) } if asked.any?
|
|
30
|
+
|
|
31
|
+
response.headers["Access-Control-Allow-Origin"] = "*"
|
|
32
|
+
render json: { "subject" => resource, "links" => links }, content_type: "application/jrd+json"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def resource_host(resource)
|
|
38
|
+
return nil if resource.blank? || resource.length > LONGEST_RESOURCE
|
|
39
|
+
|
|
40
|
+
if resource.start_with?("acct:")
|
|
41
|
+
account, separator, host = resource.delete_prefix("acct:").rpartition("@")
|
|
42
|
+
|
|
43
|
+
return nil if separator.empty? || account.blank? || host.blank? || host.match?(%r{[/\\\s?#]})
|
|
44
|
+
|
|
45
|
+
host.sub(/:\d+\z/, "")
|
|
46
|
+
elsif resource.match?(%r{\Ahttps?://})
|
|
47
|
+
URI.parse(resource).host
|
|
48
|
+
end
|
|
49
|
+
rescue URI::InvalidURIError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
16
52
|
end
|
|
17
53
|
end
|
|
18
54
|
end
|
|
@@ -12,6 +12,15 @@ module Masks
|
|
|
12
12
|
}, status: :too_many_requests
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
rate_limit to: 60, within: 1.hour, only: :create, name: "app",
|
|
16
|
+
by: -> { [ current_tenant.id, claimed_client_id ].join(":") },
|
|
17
|
+
with: -> {
|
|
18
|
+
render json: {
|
|
19
|
+
"error" => "too_many_requests",
|
|
20
|
+
"error_description" => "too many enrollment tokens asked for by this app"
|
|
21
|
+
}, status: :too_many_requests
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
before_action :no_store!
|
|
16
25
|
|
|
17
26
|
def create
|
|
@@ -55,6 +64,14 @@ module Masks
|
|
|
55
64
|
client
|
|
56
65
|
end
|
|
57
66
|
|
|
67
|
+
def claimed_client_id
|
|
68
|
+
basic = ActionController::HttpAuthentication::Basic
|
|
69
|
+
|
|
70
|
+
named, = basic.user_name_and_password(request) if basic.has_basic_credentials?(request)
|
|
71
|
+
|
|
72
|
+
(named.presence || params[:client_id]).to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
58
75
|
def not_enrolled
|
|
59
76
|
render json: {
|
|
60
77
|
"error" => "invalid_request",
|
|
@@ -15,13 +15,21 @@ module Masks
|
|
|
15
15
|
|
|
16
16
|
client = Client.forwarding_to(target)
|
|
17
17
|
return head(:forbidden) if client.nil?
|
|
18
|
+
return basic(client, target) if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request)
|
|
18
19
|
|
|
19
20
|
held = ForwardSession.held(cookies[ForwardSession::COOKIE], client)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
decided = client.forward_rules.decide(path: target.path, method: forwarded_method, address: request.remote_ip,
|
|
22
|
+
actor: held&.actor)
|
|
23
|
+
|
|
24
|
+
case decided.outcome
|
|
25
|
+
when ForwardAuthRules::OPEN then admit(held&.actor)
|
|
26
|
+
when ForwardAuthRules::DENY then head(:forbidden)
|
|
27
|
+
when ForwardAuthRules::ONE_FACTOR then held ? admit(held.actor) : ask_to_sign_in(target)
|
|
28
|
+
when ForwardAuthRules::TWO_FACTOR then held&.session&.multi_factored? ? admit(held.actor) : ask_to_sign_in(target)
|
|
29
|
+
else
|
|
30
|
+
complete = held if held&.session&.complete?
|
|
31
|
+
decided.outcome == ForwardAuthRules::TOKEN ? admit_presented(client, complete) : answer(complete, target)
|
|
32
|
+
end
|
|
25
33
|
end
|
|
26
34
|
|
|
27
35
|
def start
|
|
@@ -29,8 +37,20 @@ module Masks
|
|
|
29
37
|
client = Client.forwarding_to(target)
|
|
30
38
|
|
|
31
39
|
return render(plain: t("forward_auth.unknown"), status: :not_found) if client.nil?
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
|
|
41
|
+
decided = client.forward_rules.decide(path: target.path, method: "GET", address: request.remote_ip, actor: current_actor)
|
|
42
|
+
|
|
43
|
+
return render(plain: t("forward_auth.denied", app: client.name), status: :forbidden) if decided.outcome == ForwardAuthRules::DENY
|
|
44
|
+
return send_to_login(decided) if current_actor.nil?
|
|
45
|
+
|
|
46
|
+
unless client.admits?(current_actor)
|
|
47
|
+
return redirect_to(target.to_s, allow_other_host: true) if decided.outcome == ForwardAuthRules::OPEN
|
|
48
|
+
|
|
49
|
+
return render(plain: t("forward_auth.not_admitted", app: client.name), status: :forbidden)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
return step_up if decided.outcome == ForwardAuthRules::TWO_FACTOR && !current_session.multi_factored?
|
|
53
|
+
return step_up if [ ForwardAuthRules::ONE_FACTOR, ForwardAuthRules::OPEN ].exclude?(decided.outcome) && !current_session.complete?
|
|
34
54
|
|
|
35
55
|
code = ForwardCode.mint!(actor: current_actor, client: client, session: current_session,
|
|
36
56
|
payload: { "return_to" => target.to_s })
|
|
@@ -67,15 +87,87 @@ module Masks
|
|
|
67
87
|
redirect_to code.return_to, allow_other_host: true
|
|
68
88
|
end
|
|
69
89
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
def basic(client, target)
|
|
91
|
+
identifier, secret = ActionController::HttpAuthentication::Basic.user_name_and_password(request)
|
|
92
|
+
outcome = AppPasswords.authenticate(client: client, identifier: identifier, secret: secret, via: "forward_auth")
|
|
93
|
+
|
|
94
|
+
return head(:too_many_requests) if outcome.throttled?
|
|
95
|
+
return head(:unauthorized) if outcome.actor.nil?
|
|
96
|
+
|
|
97
|
+
decided = client.forward_rules.decide(path: target.path, method: forwarded_method, address: request.remote_ip,
|
|
98
|
+
actor: outcome.actor)
|
|
99
|
+
return head(:forbidden) if decided.outcome == ForwardAuthRules::DENY
|
|
100
|
+
|
|
101
|
+
admit(outcome.actor)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def answer(held, target)
|
|
105
|
+
held ? admit(held.actor) : ask_to_sign_in(target)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def ask_to_sign_in(target)
|
|
109
|
+
return head(:unauthorized) if params[:status] == "401"
|
|
110
|
+
|
|
111
|
+
redirect_to forward_auth_start_url(rd: target.to_s), allow_other_host: true
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def send_to_login(decided)
|
|
115
|
+
Login.new(store: login_store).relax! if decided.outcome == ForwardAuthRules::ONE_FACTOR
|
|
116
|
+
|
|
117
|
+
redirect_to login_path(return_to: request.fullpath)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def step_up
|
|
121
|
+
Login.new(store: login_store).step_up!
|
|
122
|
+
|
|
123
|
+
redirect_to login_path(return_to: request.fullpath)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def admit_presented(client, held)
|
|
127
|
+
return admit(held.actor) if held
|
|
128
|
+
|
|
129
|
+
token = presented_token(client)
|
|
130
|
+
return admit(token.actor, machine: token.client) if token
|
|
131
|
+
|
|
132
|
+
response.headers["WWW-Authenticate"] = %(Bearer realm="#{client.forward_origin}")
|
|
133
|
+
head :unauthorized
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def presented_token(client)
|
|
137
|
+
scheme, secret = request.authorization.to_s.split(" ", 2)
|
|
138
|
+
return nil unless scheme.to_s.casecmp?("Bearer") && secret.present?
|
|
139
|
+
|
|
140
|
+
claims = AccessToken.decode(secret.strip, issuer: issuer)
|
|
141
|
+
token = AccessToken.live.includes(:actor, :client).find_by(digest: claims["jti"])
|
|
142
|
+
|
|
143
|
+
return nil if token.nil? || token.bound? || token.client.nil?
|
|
144
|
+
return nil unless token.client_id == client.id || Array(token.audience).include?(client.forward_origin)
|
|
145
|
+
return nil if token.actor && (token.actor.suspended? || !client.admits?(token.actor))
|
|
146
|
+
|
|
147
|
+
token
|
|
148
|
+
rescue JWT::DecodeError
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def admit(actor, machine: nil)
|
|
153
|
+
if actor
|
|
154
|
+
response.headers["Remote-User"] = actor.nickname.presence || actor.identifier
|
|
155
|
+
response.headers["Remote-Email"] = actor.email if actor.email_verified_at.present?
|
|
156
|
+
response.headers["Remote-Name"] = actor.name if actor.name.present?
|
|
157
|
+
response.headers["Remote-Groups"] = actor.group_labels.join(",")
|
|
158
|
+
elsif machine
|
|
159
|
+
response.headers["Remote-User"] = machine.client_id
|
|
160
|
+
response.headers["Remote-Name"] = machine.name
|
|
161
|
+
response.headers["Remote-Groups"] = ""
|
|
162
|
+
end
|
|
75
163
|
|
|
76
164
|
head :ok
|
|
77
165
|
end
|
|
78
166
|
|
|
167
|
+
def forwarded_method
|
|
168
|
+
request.headers["X-Forwarded-Method"].presence || request.headers["X-Original-Method"].presence || "GET"
|
|
169
|
+
end
|
|
170
|
+
|
|
79
171
|
def return_to
|
|
80
172
|
raw = request.query_string[/(?:\A|&)rd=(.*)\z/m, 1].to_s
|
|
81
173
|
|
|
@@ -35,9 +35,9 @@ module Masks
|
|
|
35
35
|
|
|
36
36
|
def show
|
|
37
37
|
hold_return_to
|
|
38
|
-
return redirect_to after_login_path if current_actor && pending.nil?
|
|
39
|
-
|
|
40
38
|
@login = run
|
|
39
|
+
return redirect_to after_login_path if current_actor && pending.nil? && !@login.stepping_up?
|
|
40
|
+
|
|
41
41
|
@login.carry!(flash[:warnings]) if flash[:warnings].present?
|
|
42
42
|
|
|
43
43
|
redirect_to after_login_path if @login.settled?
|
|
@@ -153,8 +153,9 @@ module Masks
|
|
|
153
153
|
if login.touched?(:first_factor)
|
|
154
154
|
sign_in(login.actor, amr: login.amr, policy: login.policy, first_factor: login.first_factored_by,
|
|
155
155
|
authenticated_at: login.authenticated_at)
|
|
156
|
+
current_session.update!(second_factor_skipped: true) if login.skipped_second_factor?
|
|
156
157
|
elsif login.signed_in? && login.touched?(:second_factor)
|
|
157
|
-
current_session.update!(amr: Array(current_session.amr) | login.amr)
|
|
158
|
+
current_session.update!(amr: Array(current_session.amr) | login.amr, second_factor_skipped: false)
|
|
158
159
|
end
|
|
159
160
|
|
|
160
161
|
forget_login
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
class OrganizationAccessController < OrganizationMembersController
|
|
4
|
+
def create
|
|
5
|
+
own!
|
|
6
|
+
|
|
7
|
+
asked = asked!
|
|
8
|
+
role = params[:role].to_s
|
|
9
|
+
|
|
10
|
+
asked.grant!(label: "#{@organization.key}:#{role}", by: current_actor, journey: Journey.account(current_actor))
|
|
11
|
+
|
|
12
|
+
back(notice: t("organization_access.granted", identifier: asked.actor.identifier, app: asked.client.name,
|
|
13
|
+
role: role, organization: @organization.name))
|
|
14
|
+
rescue AccessRequest::Refused => e
|
|
15
|
+
back(alert: e.message)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def destroy
|
|
19
|
+
own!
|
|
20
|
+
|
|
21
|
+
asked = asked!
|
|
22
|
+
asked.decline!(by: current_actor, journey: Journey.account(current_actor))
|
|
23
|
+
|
|
24
|
+
back(notice: t("organization_access.declined", identifier: asked.actor.identifier, app: asked.client.name))
|
|
25
|
+
rescue AccessRequest::Refused => e
|
|
26
|
+
back(alert: e.message)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def asked!
|
|
32
|
+
id = params[:id].to_s[/\A\d+\z/]
|
|
33
|
+
|
|
34
|
+
(id && AccessRequest.for_organization(@organization).includes(:actor, :client).find_by(id: id)) ||
|
|
35
|
+
raise(AccessRequest::Refused, t("organization_access.unknown", organization: @organization.name))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -99,7 +99,9 @@ module Masks
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
def member!
|
|
102
|
-
|
|
102
|
+
membership_id = params[:id].to_s[/\A\d+\z/]
|
|
103
|
+
|
|
104
|
+
(membership_id && @organization.memberships.includes(:actor).find_by(id: membership_id)) ||
|
|
103
105
|
raise(Members::Refused, t("organization_members.not_a_member"))
|
|
104
106
|
end
|
|
105
107
|
|
|
@@ -102,6 +102,13 @@ module Masks
|
|
|
102
102
|
Masks::Server::Group.find_by(key: key) || refuse!("no group keyed #{key}")
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
def access_request!(id)
|
|
106
|
+
held = id.to_s[/\A\d+\z/] && Masks::Server::AccessRequest.open.find_by(id: id)
|
|
107
|
+
|
|
108
|
+
managed!(held.actor) && held if held
|
|
109
|
+
held || refuse!("no access request with that id is waiting")
|
|
110
|
+
end
|
|
111
|
+
|
|
105
112
|
def invited!(actor)
|
|
106
113
|
actor_created!(actor, invited: true)
|
|
107
114
|
|
|
@@ -3,6 +3,8 @@ module Masks
|
|
|
3
3
|
module Manage
|
|
4
4
|
module Mutations
|
|
5
5
|
class CreateGroup < BaseMutation
|
|
6
|
+
include GroupSource
|
|
7
|
+
|
|
6
8
|
requires :security
|
|
7
9
|
|
|
8
10
|
argument :key, String, description: "Lowercase letters, digits, dots, dashes, and underscores. Apps see it in the groups claim."
|
|
@@ -10,13 +12,13 @@ module Masks
|
|
|
10
12
|
|
|
11
13
|
field :group, Types::GroupType, null: false
|
|
12
14
|
|
|
13
|
-
def resolve(key:, name
|
|
14
|
-
group = Masks::Server::Group.new(key: key, name: name)
|
|
15
|
+
def resolve(key:, name:, **source)
|
|
16
|
+
group = fill_source(Masks::Server::Group.new(key: key, name: name), **source)
|
|
15
17
|
save!(group)
|
|
16
18
|
|
|
17
|
-
audit!(Masks::Server::Event::GROUP_CREATED, group: group.key)
|
|
19
|
+
audit!(Masks::Server::Event::GROUP_CREATED, group: group.key, source: group.source)
|
|
18
20
|
|
|
19
|
-
{ group: group }
|
|
21
|
+
{ group: group.reload }
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
module Manage
|
|
4
|
+
module Mutations
|
|
5
|
+
class DeclineAccess < BaseMutation
|
|
6
|
+
requires :security
|
|
7
|
+
|
|
8
|
+
description "Declines a waiting access request. The actor is told by email and can ask again in an hour."
|
|
9
|
+
|
|
10
|
+
argument :id, ID
|
|
11
|
+
|
|
12
|
+
field :access_request, Types::AccessRequestType, null: false
|
|
13
|
+
|
|
14
|
+
def resolve(id:)
|
|
15
|
+
asked = access_request!(id)
|
|
16
|
+
|
|
17
|
+
asked.decline!(by: viewer, journey: Masks::Server::Journey.manage(viewer))
|
|
18
|
+
|
|
19
|
+
{ access_request: asked }
|
|
20
|
+
rescue Masks::Server::AccessRequest::Refused => e
|
|
21
|
+
refuse!(e.message)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
module Manage
|
|
4
|
+
module Mutations
|
|
5
|
+
class GrantAccess < BaseMutation
|
|
6
|
+
requires :security
|
|
7
|
+
|
|
8
|
+
description "Grants a waiting access request by putting the actor in one of the groups the client allows. " \
|
|
9
|
+
"Name the group with label, or leave it out for the first one the client allows."
|
|
10
|
+
|
|
11
|
+
argument :id, ID
|
|
12
|
+
argument :label, String, required: false
|
|
13
|
+
|
|
14
|
+
field :access_request, Types::AccessRequestType, null: false
|
|
15
|
+
|
|
16
|
+
def resolve(id:, label: nil)
|
|
17
|
+
asked = access_request!(id)
|
|
18
|
+
|
|
19
|
+
asked.grant!(label: label, by: viewer, journey: Masks::Server::Journey.manage(viewer))
|
|
20
|
+
|
|
21
|
+
{ access_request: asked }
|
|
22
|
+
rescue Masks::Server::AccessRequest::Refused => e
|
|
23
|
+
refuse!(e.message)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
module Manage
|
|
4
|
+
module Mutations
|
|
5
|
+
module GroupSource
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
argument :source, String, required: false,
|
|
10
|
+
description: "manual, rule, provider, or group. A sourced group fills itself and keeps anyone added by hand."
|
|
11
|
+
argument :rule, [ Types::GroupConditionInput ], required: false,
|
|
12
|
+
description: "For rule: up to five conditions, all of which must hold."
|
|
13
|
+
argument :provider_key, String, required: false, description: "For provider: the provider whose sign-ins list groups."
|
|
14
|
+
argument :upstream_group, String, required: false, description: "For provider: the group name the provider lists."
|
|
15
|
+
argument :parent_labels, [ String ], required: false,
|
|
16
|
+
description: "For group: group keys and organization roles, as key:role, the group holds everyone from."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def fill_source(group, source: nil, rule: nil, provider_key: nil, upstream_group: nil, parent_labels: nil)
|
|
22
|
+
group.source = source unless source.nil?
|
|
23
|
+
group.rule = rule.map(&:to_h) unless rule.nil?
|
|
24
|
+
group.provider = provider_key.blank? ? nil : provider!(provider_key) unless provider_key.nil?
|
|
25
|
+
group.upstream_group = upstream_group unless upstream_group.nil?
|
|
26
|
+
group.parent_labels = parent_labels unless parent_labels.nil?
|
|
27
|
+
|
|
28
|
+
group
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -8,12 +8,15 @@ module Masks
|
|
|
8
8
|
description "Issues a bind DN and secret an app uses to search this tenant's LDAP directory. The secret is returned once."
|
|
9
9
|
|
|
10
10
|
argument :label, String, description: "Lowercase letters, digits, dots, dashes, and underscores. It becomes the cn of the bind DN."
|
|
11
|
+
argument :client_id, ID, required: false,
|
|
12
|
+
description: "The client this app is registered as. People who make an app password for that client can bind with it on a connection that bound with this credential first."
|
|
11
13
|
|
|
12
14
|
field :ldap_credential, Types::LdapCredentialType, null: false
|
|
13
15
|
field :secret, String, null: false
|
|
14
16
|
|
|
15
|
-
def resolve(label:)
|
|
16
|
-
|
|
17
|
+
def resolve(label:, client_id: nil)
|
|
18
|
+
client = client_id.present? ? client!(client_id) : nil
|
|
19
|
+
credential = Masks::Server::LdapCredential.issue!(label: label, by: viewer, client: client)
|
|
17
20
|
|
|
18
21
|
audit!(Masks::Server::Event::LDAP_CREDENTIAL_ISSUED, label: credential.label)
|
|
19
22
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
module Manage
|
|
4
|
+
module Mutations
|
|
5
|
+
class RefreshGroup < BaseMutation
|
|
6
|
+
requires :security
|
|
7
|
+
|
|
8
|
+
description "Fills a sourced group now instead of waiting for the hourly run."
|
|
9
|
+
|
|
10
|
+
argument :key, String
|
|
11
|
+
|
|
12
|
+
field :group, Types::GroupType, null: false
|
|
13
|
+
|
|
14
|
+
def resolve(key:)
|
|
15
|
+
{ group: group!(key).refresh!(by: viewer) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
module Manage
|
|
4
|
+
module Mutations
|
|
5
|
+
class RunHealthChecks < BaseMutation
|
|
6
|
+
requires :support
|
|
7
|
+
|
|
8
|
+
description "Runs every health check now, instead of waiting for the next scheduled run."
|
|
9
|
+
|
|
10
|
+
field :health, [ Types::HealthCheckType ], null: false
|
|
11
|
+
|
|
12
|
+
def resolve
|
|
13
|
+
{ health: Masks::Server::Health.check!.sort_by(&:name) }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|