masks-server 0.7.0 → 0.8.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/enrollments_controller.rb +66 -0
- data/app/controllers/masks/server/introspections_controller.rb +2 -2
- data/app/controllers/masks/server/registrations_controller.rb +31 -1
- data/app/graphql/masks/server/manage/types/client_type.rb +8 -0
- data/app/graphql/masks/server/manage/types/query_type.rb +6 -1
- data/app/helpers/masks/server/application_helper.rb +0 -1
- data/app/helpers/masks/server/scopes_helper.rb +28 -0
- data/app/jobs/masks/server/cleanup_job.rb +1 -0
- data/app/models/masks/server/client.rb +81 -1
- data/app/models/masks/server/client_keys.rb +6 -1
- data/app/models/masks/server/enrollment_token.rb +54 -0
- data/app/models/masks/server/event.rb +3 -0
- data/app/models/masks/server/issuer.rb +1 -0
- data/app/models/masks/server/namespace.rb +6 -3
- data/app/models/masks/server/replay.rb +22 -4
- data/app/models/masks/server/token.rb +1 -0
- data/app/views/layouts/masks/server/manage.html.erb +2 -2
- data/app/views/masks/server/application/_person.html.erb +1 -1
- data/app/views/masks/server/handshakes/show.html.erb +129 -59
- data/config/locales/de/account.yml +1 -1
- data/config/locales/de/events.yml +5 -2
- data/config/locales/de/flashes.yml +2 -2
- data/config/locales/de/handshakes.yml +33 -14
- data/config/locales/de/mailers.yml +1 -1
- data/config/locales/de/manage/activity.yml +2 -2
- data/config/locales/de/manage/actor.yml +41 -23
- data/config/locales/de/manage/actors.yml +16 -15
- data/config/locales/de/manage/api.yml +1 -1
- data/config/locales/de/manage/app.yml +6 -6
- data/config/locales/de/manage/client.yml +45 -45
- data/config/locales/de/manage/clients.yml +28 -32
- data/config/locales/de/manage/connections.yml +2 -2
- data/config/locales/de/manage/consents.yml +2 -2
- data/config/locales/de/manage/device.yml +7 -7
- data/config/locales/de/manage/devices.yml +18 -18
- data/config/locales/de/manage/directory.yml +1 -1
- data/config/locales/de/manage/domains.yml +6 -6
- data/config/locales/de/manage/emails.yml +4 -4
- data/config/locales/de/manage/events.yml +3 -0
- data/config/locales/de/manage/groups.yml +6 -6
- data/config/locales/de/manage/ldap.yml +8 -8
- data/config/locales/de/manage/namespaces.yml +1 -1
- data/config/locales/de/manage/organization.yml +15 -8
- data/config/locales/de/manage/organizations.yml +3 -3
- data/config/locales/de/manage/policies.yml +6 -6
- data/config/locales/de/manage/policy_rules.yml +1 -1
- data/config/locales/de/manage/presence.yml +6 -6
- data/config/locales/de/manage/providers.yml +70 -48
- data/config/locales/de/manage/provisioning.yml +4 -4
- data/config/locales/de/manage/senders.yml +1 -1
- data/config/locales/de/manage/settings.yml +37 -26
- data/config/locales/de/manage/settings_shell.yml +2 -2
- data/config/locales/de/refusals.yml +5 -5
- data/config/locales/en/account.yml +3 -3
- data/config/locales/en/events.yml +4 -1
- data/config/locales/en/flashes.yml +3 -3
- data/config/locales/en/handshakes.yml +33 -14
- data/config/locales/en/logins.yml +2 -2
- data/config/locales/en/manage/actor.yml +32 -14
- data/config/locales/en/manage/actors.yml +16 -15
- data/config/locales/en/manage/app.yml +5 -5
- data/config/locales/en/manage/client.yml +9 -9
- data/config/locales/en/manage/clients.yml +8 -12
- data/config/locales/en/manage/connections.yml +1 -1
- data/config/locales/en/manage/consents.yml +1 -1
- data/config/locales/en/manage/device.yml +1 -1
- data/config/locales/en/manage/devices.yml +1 -1
- data/config/locales/en/manage/directory.yml +1 -1
- data/config/locales/en/manage/domains.yml +6 -6
- data/config/locales/en/manage/emails.yml +4 -4
- data/config/locales/en/manage/events.yml +3 -0
- data/config/locales/en/manage/groups.yml +6 -6
- data/config/locales/en/manage/ldap.yml +8 -8
- data/config/locales/en/manage/messages.yml +1 -1
- data/config/locales/en/manage/namespaces.yml +1 -1
- data/config/locales/en/manage/organization.yml +16 -9
- data/config/locales/en/manage/organizations.yml +3 -3
- data/config/locales/en/manage/policies.yml +6 -6
- data/config/locales/en/manage/policy_rules.yml +2 -2
- data/config/locales/en/manage/presence.yml +1 -1
- data/config/locales/en/manage/providers.yml +70 -48
- data/config/locales/en/manage/provisioning.yml +4 -4
- data/config/locales/en/manage/senders.yml +2 -2
- data/config/locales/en/manage/settings.yml +34 -23
- data/config/locales/es/account.yml +1 -1
- data/config/locales/es/application.yml +1 -1
- data/config/locales/es/events.yml +4 -1
- data/config/locales/es/flashes.yml +4 -4
- data/config/locales/es/handshakes.yml +33 -14
- data/config/locales/es/logins.yml +12 -12
- data/config/locales/es/mailers.yml +7 -7
- data/config/locales/es/manage/activity.yml +2 -2
- data/config/locales/es/manage/actor.yml +33 -15
- data/config/locales/es/manage/actors.yml +17 -16
- data/config/locales/es/manage/api.yml +1 -1
- data/config/locales/es/manage/app.yml +6 -6
- data/config/locales/es/manage/client.yml +30 -30
- data/config/locales/es/manage/clients.yml +22 -26
- data/config/locales/es/manage/connections.yml +1 -1
- data/config/locales/es/manage/consents.yml +1 -1
- data/config/locales/es/manage/device.yml +1 -1
- data/config/locales/es/manage/directory.yml +1 -1
- data/config/locales/es/manage/domains.yml +5 -5
- data/config/locales/es/manage/emails.yml +7 -7
- data/config/locales/es/manage/events.yml +3 -0
- data/config/locales/es/manage/groups.yml +6 -6
- data/config/locales/es/manage/ldap.yml +8 -8
- data/config/locales/es/manage/namespaces.yml +1 -1
- data/config/locales/es/manage/organization.yml +16 -9
- data/config/locales/es/manage/organizations.yml +3 -3
- data/config/locales/es/manage/policies.yml +8 -8
- data/config/locales/es/manage/policy_rules.yml +2 -2
- data/config/locales/es/manage/providers.yml +70 -48
- data/config/locales/es/manage/provisioning.yml +4 -4
- data/config/locales/es/manage/senders.yml +1 -1
- data/config/locales/es/manage/settings.yml +38 -27
- data/config/locales/es/manage/settings_shell.yml +2 -2
- data/config/locales/es/refusals.yml +3 -3
- data/config/locales/fr/events.yml +3 -0
- data/config/locales/fr/flashes.yml +1 -1
- data/config/locales/fr/handshakes.yml +33 -14
- data/config/locales/fr/manage/activity.yml +4 -4
- data/config/locales/fr/manage/actor.yml +43 -25
- data/config/locales/fr/manage/actors.yml +22 -21
- data/config/locales/fr/manage/api.yml +1 -1
- data/config/locales/fr/manage/app.yml +7 -7
- data/config/locales/fr/manage/client.yml +50 -50
- data/config/locales/fr/manage/clients.yml +28 -32
- data/config/locales/fr/manage/connections.yml +2 -2
- data/config/locales/fr/manage/consents.yml +1 -1
- data/config/locales/fr/manage/device.yml +1 -1
- data/config/locales/fr/manage/directory.yml +1 -1
- data/config/locales/fr/manage/domains.yml +6 -6
- data/config/locales/fr/manage/emails.yml +5 -5
- data/config/locales/fr/manage/events.yml +3 -0
- data/config/locales/fr/manage/groups.yml +6 -6
- data/config/locales/fr/manage/ldap.yml +8 -8
- data/config/locales/fr/manage/namespaces.yml +1 -1
- data/config/locales/fr/manage/organization.yml +15 -8
- data/config/locales/fr/manage/organizations.yml +3 -3
- data/config/locales/fr/manage/policies.yml +6 -6
- data/config/locales/fr/manage/policy_rules.yml +1 -1
- data/config/locales/fr/manage/providers.yml +70 -48
- data/config/locales/fr/manage/provisioning.yml +4 -4
- data/config/locales/fr/manage/senders.yml +1 -1
- data/config/locales/fr/manage/settings.yml +38 -27
- data/config/locales/fr/manage/settings_shell.yml +2 -2
- data/config/routes.rb +2 -0
- data/config/vite.json +1 -0
- data/db/migrate/20261011190000_add_enrolled_by_to_clients.rb +5 -0
- data/db/migrate/20261012100000_create_replays.rb +23 -0
- data/public/masks-assets/.vite/manifest-assets.json +1 -0
- data/public/masks-assets/.vite/manifest.json +62 -0
- data/public/masks-assets/assets/app-DoQZrM_J.css +2 -0
- data/public/masks-assets/assets/console-DoyNbIw3.css +1 -0
- data/public/masks-assets/assets/handshake-DIFZPSAK.js +2 -0
- data/public/masks-assets/assets/handshake-DIFZPSAK.js.map +1 -0
- data/public/masks-assets/assets/manage-isLCrWiU.js +567 -0
- data/public/masks-assets/assets/manage-isLCrWiU.js.map +1 -0
- data/version.txt +1 -1
- metadata +14 -7
- data/public/masks-assets/assets/app-olx-5PkA.css +0 -2
- data/public/masks-assets/assets/console-CiuQ3qj0.css +0 -1
- data/public/masks-assets/assets/handshake-BYhhQ_X3.js +0 -2
- data/public/masks-assets/assets/handshake-BYhhQ_X3.js.map +0 -1
- data/public/masks-assets/assets/manage-Z3GmPDNQ.js +0 -560
- data/public/masks-assets/assets/manage-Z3GmPDNQ.js.map +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0815510a0ee659d9ba2cf35bb8840d7a3a7b7abb7306e0e0cf3f64fd7890d7dd'
|
|
4
|
+
data.tar.gz: 188b4436824c6f113da49a7880ad15d6f4d0c915f5c5df9b94ba7affcb51fca8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: faeda2a8132f3e2e92ce8a1f41d235cae52f0640ad33ed3220d526619c5ecd113fece66af5923cb8e610564399ba3db0f22bdd16e9e0306d0155ba63f9e13f91
|
|
7
|
+
data.tar.gz: bef27fdefa6bc7d1f88a386ac23261cad8b487137ea8130c0bfa41467f7bbec11b59058386ceb0fabe0f97cca1a914a1a3a9aaef6c74a48478b266e3a9c62858
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
class EnrollmentsController < ApplicationController
|
|
4
|
+
include TokenPresented
|
|
5
|
+
|
|
6
|
+
rate_limit to: 30, within: 1.minute,
|
|
7
|
+
by: -> { [ current_tenant.id, request.remote_ip ].join(":") },
|
|
8
|
+
with: -> {
|
|
9
|
+
render json: {
|
|
10
|
+
"error" => "too_many_requests",
|
|
11
|
+
"error_description" => "too many enrollment tokens asked for from this address"
|
|
12
|
+
}, status: :too_many_requests
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
before_action :no_store!
|
|
16
|
+
|
|
17
|
+
def create
|
|
18
|
+
client = authenticate_app!
|
|
19
|
+
token = EnrollmentToken.issue!(client, scope: params[:scope], name: params[:client_name],
|
|
20
|
+
expires_in: params[:expires_in])
|
|
21
|
+
|
|
22
|
+
Event.record!(Event::CLIENT_ENROLLMENT_ISSUED, client: client, scopes: token.scopes,
|
|
23
|
+
name: token.held("name"), expires_at: token.expires_at.iso8601)
|
|
24
|
+
|
|
25
|
+
render json: {
|
|
26
|
+
"initial_access_token" => token.secret,
|
|
27
|
+
"expires_in" => token.expires_in,
|
|
28
|
+
"scope" => token.scopes
|
|
29
|
+
}, status: :created
|
|
30
|
+
rescue Client::EnrollmentRefused => e
|
|
31
|
+
render json: { "error" => "invalid_request", "error_description" => e.message }, status: :bad_request
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def destroy
|
|
35
|
+
app = authenticate_app!
|
|
36
|
+
machine = app.enrolled.find_by(client_id: params[:machine].to_s)
|
|
37
|
+
|
|
38
|
+
return not_enrolled if machine.nil?
|
|
39
|
+
|
|
40
|
+
revoked = machine.unenroll!
|
|
41
|
+
|
|
42
|
+
Event.record!(Event::CLIENT_UNENROLLED, client: machine, name: machine.name, enrolled_by: app.client_id,
|
|
43
|
+
revoked: revoked)
|
|
44
|
+
|
|
45
|
+
head :no_content
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def authenticate_app!
|
|
51
|
+
client = authenticate_client!
|
|
52
|
+
|
|
53
|
+
client_refused!("a public client proves nothing about itself, so it enrolls and unenrolls nothing") if client.public?
|
|
54
|
+
|
|
55
|
+
client
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def not_enrolled
|
|
59
|
+
render json: {
|
|
60
|
+
"error" => "invalid_request",
|
|
61
|
+
"error_description" => "that client was not enrolled by this app"
|
|
62
|
+
}, status: :not_found
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -13,7 +13,7 @@ module Masks
|
|
|
13
13
|
client = authenticate_client!
|
|
14
14
|
token = presented_token
|
|
15
15
|
|
|
16
|
-
return render json: INACTIVE unless token&.live? && entitled?(client, token)
|
|
16
|
+
return render json: INACTIVE unless token&.live? && !token.client&.archived? && entitled?(client, token)
|
|
17
17
|
|
|
18
18
|
render json: described(token)
|
|
19
19
|
end
|
|
@@ -22,7 +22,7 @@ module Masks
|
|
|
22
22
|
|
|
23
23
|
def entitled?(client, token)
|
|
24
24
|
return true if token.client_id == client.id
|
|
25
|
-
return false
|
|
25
|
+
return false if client.enrolled? || !client.approved?
|
|
26
26
|
|
|
27
27
|
(Array(token.audience) & Array(client.resources)).any?
|
|
28
28
|
end
|
|
@@ -14,6 +14,7 @@ module Masks
|
|
|
14
14
|
|
|
15
15
|
before_action :no_store!
|
|
16
16
|
before_action :require_registration_token, except: :create
|
|
17
|
+
before_action :refuse_enrolled, only: :update
|
|
17
18
|
|
|
18
19
|
def create
|
|
19
20
|
return redeem if bearer.present?
|
|
@@ -26,7 +27,7 @@ module Masks
|
|
|
26
27
|
render json: issued(client), status: :created
|
|
27
28
|
rescue ActiveRecord::RecordInvalid => e
|
|
28
29
|
invalid_metadata(e.record.errors.full_messages.join("; "))
|
|
29
|
-
rescue Client::ScopesUnavailable => e
|
|
30
|
+
rescue Client::ScopesUnavailable, Client::EnrollmentRefused => e
|
|
30
31
|
invalid_metadata(e.message)
|
|
31
32
|
end
|
|
32
33
|
|
|
@@ -47,6 +48,8 @@ module Masks
|
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
def destroy
|
|
51
|
+
return unenroll if @client.enrolled?
|
|
52
|
+
|
|
50
53
|
@client.update!(archived_at: Time.current)
|
|
51
54
|
|
|
52
55
|
Event.record!(Event::CLIENT_ARCHIVED, client: @client, name: @client.name)
|
|
@@ -56,7 +59,27 @@ module Masks
|
|
|
56
59
|
|
|
57
60
|
private
|
|
58
61
|
|
|
62
|
+
def unenroll
|
|
63
|
+
revoked = @client.unenroll!
|
|
64
|
+
|
|
65
|
+
Event.record!(Event::CLIENT_UNENROLLED, client: @client, name: @client.name,
|
|
66
|
+
enrolled_by: @client.enrolled_by&.client_id, revoked: revoked)
|
|
67
|
+
|
|
68
|
+
head :no_content
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def refuse_enrolled
|
|
72
|
+
return unless @client.enrolled?
|
|
73
|
+
|
|
74
|
+
render json: {
|
|
75
|
+
"error" => "access_denied",
|
|
76
|
+
"error_description" => "an enrolled machine is changed only by the app that enrolled it"
|
|
77
|
+
}, status: :forbidden
|
|
78
|
+
end
|
|
79
|
+
|
|
59
80
|
def redeem
|
|
81
|
+
return enrolled(@enrolled) if (@enrolled = EnrollmentToken.redeem!(bearer, body))
|
|
82
|
+
|
|
60
83
|
token = InitialAccessToken.claim(bearer)
|
|
61
84
|
|
|
62
85
|
return unauthorized("that initial access token is not valid or has expired") if token&.client.nil?
|
|
@@ -64,6 +87,13 @@ module Masks
|
|
|
64
87
|
render json: issued(token.issue!), status: :created
|
|
65
88
|
end
|
|
66
89
|
|
|
90
|
+
def enrolled(client)
|
|
91
|
+
Event.record!(Event::CLIENT_ENROLLED, client: client, name: client.name,
|
|
92
|
+
enrolled_by: client.enrolled_by.client_id)
|
|
93
|
+
|
|
94
|
+
render json: issued(client), status: :created
|
|
95
|
+
end
|
|
96
|
+
|
|
67
97
|
def bearer
|
|
68
98
|
request.authorization.to_s[/\ABearer (\S+)\z/, 1]
|
|
69
99
|
end
|
|
@@ -57,6 +57,10 @@ module Masks
|
|
|
57
57
|
field :dynamic, Boolean, null: false
|
|
58
58
|
field :approved_at, GraphQL::Types::ISO8601DateTime
|
|
59
59
|
field :approved_by, ActorType
|
|
60
|
+
field :enrolled_by, ClientType,
|
|
61
|
+
description: "The app that enrolled this client as one of its machines. Archiving that app archives this client."
|
|
62
|
+
field :enrolled, [ ClientType ], null: false,
|
|
63
|
+
description: "The machines this app enrolled that are not archived."
|
|
60
64
|
field :archived_at, GraphQL::Types::ISO8601DateTime
|
|
61
65
|
field :secret_expires_at, GraphQL::Types::ISO8601DateTime
|
|
62
66
|
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
|
|
@@ -75,6 +79,10 @@ module Masks
|
|
|
75
79
|
argument :limit, Integer, required: false
|
|
76
80
|
end
|
|
77
81
|
|
|
82
|
+
def enrolled
|
|
83
|
+
object.enrolled.active.order(:name)
|
|
84
|
+
end
|
|
85
|
+
|
|
78
86
|
def tokens(limit: nil)
|
|
79
87
|
Masks::Server::Token.where(kind: TokenType::GRANTS, client_id: object.id)
|
|
80
88
|
.live
|
|
@@ -169,6 +169,8 @@ module Masks
|
|
|
169
169
|
argument :action, String, required: false
|
|
170
170
|
argument :grave, Boolean, required: false
|
|
171
171
|
argument :organization, ID, required: false
|
|
172
|
+
argument :since, GraphQL::Types::ISO8601DateTime, required: false,
|
|
173
|
+
description: "Only events recorded at or after this time."
|
|
172
174
|
argument :after_id, ID, required: false
|
|
173
175
|
argument :limit, Integer, required: false
|
|
174
176
|
end
|
|
@@ -180,6 +182,8 @@ module Masks
|
|
|
180
182
|
argument :action, String, required: false
|
|
181
183
|
argument :grave, Boolean, required: false
|
|
182
184
|
argument :organization, ID, required: false
|
|
185
|
+
argument :since, GraphQL::Types::ISO8601DateTime, required: false,
|
|
186
|
+
description: "Only events recorded at or after this time."
|
|
183
187
|
end
|
|
184
188
|
|
|
185
189
|
field :event_actions, [ EventActionType ], null: false
|
|
@@ -484,7 +488,7 @@ module Masks
|
|
|
484
488
|
scope.where("name ILIKE :term OR client_id = :exact", term: term, exact: search.strip)
|
|
485
489
|
end
|
|
486
490
|
|
|
487
|
-
def event_scope(actor: nil, client: nil, device: nil, action: nil, grave: false, organization: nil)
|
|
491
|
+
def event_scope(actor: nil, client: nil, device: nil, action: nil, grave: false, organization: nil, since: nil)
|
|
488
492
|
subject = actor.present? ? Actor.find_by(uuid: actor) : nil
|
|
489
493
|
group = organization.present? ? Masks::Server::Organization.find_by(key: organization) : nil
|
|
490
494
|
|
|
@@ -504,6 +508,7 @@ module Masks
|
|
|
504
508
|
scope = scope.where(organization: group) if group
|
|
505
509
|
scope = scope.where(action: action) if action.present?
|
|
506
510
|
scope = scope.where(action: Masks::Server::Event::GRAVE) if grave
|
|
511
|
+
scope = scope.where(created_at: since..) if since
|
|
507
512
|
|
|
508
513
|
scope
|
|
509
514
|
end
|
|
@@ -3,13 +3,41 @@ module Masks
|
|
|
3
3
|
module ScopesHelper
|
|
4
4
|
CONSEQUENTIAL = /(:write|:command|:exec|:delete|:admin)\z|\Aoffline_access\z|\Amasks:manage(:[a-z]+)?\z|\Amasks:delegate:./
|
|
5
5
|
|
|
6
|
+
SEEN = [
|
|
7
|
+
Scopes::OPENID, Scopes::PROFILE, Scopes::EMAIL,
|
|
8
|
+
Scopes::IDENTITIES, Scopes::ORGANIZATION, Scopes::GROUPS
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
6
11
|
def consequential_scope?(scope)
|
|
7
12
|
scope.to_s.match?(CONSEQUENTIAL)
|
|
8
13
|
end
|
|
9
14
|
|
|
15
|
+
def heeded_scope?(scope)
|
|
16
|
+
scope == Scopes::DELEGATE || consequential_scope?(scope)
|
|
17
|
+
end
|
|
18
|
+
|
|
10
19
|
def ranked_scopes(described)
|
|
11
20
|
described.to_a.partition { |scope, _| consequential_scope?(scope) }
|
|
12
21
|
end
|
|
22
|
+
|
|
23
|
+
def plain_scopes(described, areas: [])
|
|
24
|
+
held = { sees: [], heeds: [], also: [] }
|
|
25
|
+
|
|
26
|
+
described.to_a.each do |scope, description|
|
|
27
|
+
next if areas.include?(scope)
|
|
28
|
+
|
|
29
|
+
group =
|
|
30
|
+
if heeded_scope?(scope) then :heeds
|
|
31
|
+
elsif SEEN.include?(scope) then :sees
|
|
32
|
+
else :also
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
held[group] << [ scope, description ]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
held[:sees].sort_by! { |scope, _| SEEN.index(scope) }
|
|
39
|
+
held
|
|
40
|
+
end
|
|
13
41
|
end
|
|
14
42
|
end
|
|
15
43
|
end
|
|
@@ -32,6 +32,7 @@ module Masks
|
|
|
32
32
|
SigningKey.where.not(retired_at: nil).where(retired_at: ...cutoff).delete_all
|
|
33
33
|
Event.where(created_at: ...tenant.event_retention.ago).in_batches(of: BATCH).delete_all
|
|
34
34
|
Members.purge_lapsed!
|
|
35
|
+
Replay.where(expires_at: ...Time.current).in_batches(of: BATCH).delete_all
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def sweep_tokens(cutoff)
|
|
@@ -26,6 +26,7 @@ module Masks
|
|
|
26
26
|
CHALLENGE_METHODS = %w[S256].freeze
|
|
27
27
|
LOOPBACK = %w[localhost 127.0.0.1 ::1].freeze
|
|
28
28
|
DEFAULT_SCOPES = Scopes::STANDARD
|
|
29
|
+
MACHINE_ACCESS_LIFETIME = 10.minutes
|
|
29
30
|
LINKS = %i[client_uri tos_uri policy_uri].freeze
|
|
30
31
|
METADATA_URIS = (LINKS + [ :logo_uri ]).freeze
|
|
31
32
|
URI_LIMIT = 2048
|
|
@@ -47,12 +48,15 @@ module Masks
|
|
|
47
48
|
].freeze
|
|
48
49
|
|
|
49
50
|
class ScopesUnavailable < StandardError; end
|
|
51
|
+
class EnrollmentRefused < StandardError; end
|
|
50
52
|
|
|
51
53
|
has_many :tokens, dependent: :destroy
|
|
52
54
|
has_many :consents, dependent: :destroy
|
|
53
55
|
has_many :delegations, dependent: :destroy
|
|
54
56
|
has_many :saml_participations, dependent: :delete_all
|
|
55
57
|
has_many :namespaces, -> { order(:name) }, dependent: :nullify
|
|
58
|
+
has_many :enrolled, class_name: "Client", foreign_key: :enrolled_by_id, inverse_of: :enrolled_by,
|
|
59
|
+
dependent: :nullify
|
|
56
60
|
has_one :logo, class_name: "ClientLogo", dependent: :delete
|
|
57
61
|
|
|
58
62
|
validate :allowed_groups_are_named
|
|
@@ -86,8 +90,10 @@ module Masks
|
|
|
86
90
|
normalizes(*METADATA_URIS, with: ->(value) { value.to_s.strip.presence })
|
|
87
91
|
|
|
88
92
|
after_commit :refresh_logo!, on: %i[create update], if: :saved_change_to_logo_uri?
|
|
93
|
+
after_update :archived!, if: -> { saved_change_to_archived_at? && archived? }
|
|
89
94
|
|
|
90
95
|
belongs_to :approved_by, class_name: "Actor", optional: true
|
|
96
|
+
belongs_to :enrolled_by, class_name: "Client", optional: true, inverse_of: :enrolled
|
|
91
97
|
belongs_to :sign_in_policy, optional: true
|
|
92
98
|
|
|
93
99
|
scope :approved, -> { where.not(approved_at: nil) }
|
|
@@ -132,7 +138,7 @@ module Masks
|
|
|
132
138
|
def approved_for(resource)
|
|
133
139
|
return nil if resource.blank?
|
|
134
140
|
|
|
135
|
-
active.approved.where("resources @> ?", [ resource.to_s ].to_json).first
|
|
141
|
+
active.approved.where(enrolled_by_id: nil).where("resources @> ?", [ resource.to_s ].to_json).first
|
|
136
142
|
end
|
|
137
143
|
|
|
138
144
|
def register!(metadata)
|
|
@@ -169,6 +175,37 @@ module Masks
|
|
|
169
175
|
)
|
|
170
176
|
end
|
|
171
177
|
|
|
178
|
+
def enroll!(token, metadata)
|
|
179
|
+
app = token.client
|
|
180
|
+
app&.lock!
|
|
181
|
+
refusal = app&.enrollment_refusal(token.scope_list)
|
|
182
|
+
|
|
183
|
+
raise EnrollmentRefused, refusal || "the app that issued this enrollment token is gone" if app.nil? || refusal
|
|
184
|
+
|
|
185
|
+
held = metadata.to_h.stringify_keys
|
|
186
|
+
|
|
187
|
+
raise EnrollmentRefused, "jwks_uri is not accepted here; register the public keys as jwks" if held["jwks_uri"].present?
|
|
188
|
+
raise EnrollmentRefused, "jwks is required: the machine signs in with its own key" if held["jwks"].blank?
|
|
189
|
+
|
|
190
|
+
new(
|
|
191
|
+
client_id: SecureRandom.uuid,
|
|
192
|
+
name: held["client_name"].to_s.strip.presence || token.held("name") || "A machine",
|
|
193
|
+
jwks: held["jwks"],
|
|
194
|
+
token_endpoint_auth_method: PRIVATE_KEY_JWT,
|
|
195
|
+
grant_types: [ CLIENT_CREDENTIALS ],
|
|
196
|
+
response_types: [],
|
|
197
|
+
dpop_bound_access_tokens: true,
|
|
198
|
+
resources: app.resources,
|
|
199
|
+
allowed_scopes: Scopes.join(token.scope_list),
|
|
200
|
+
application_type: "native",
|
|
201
|
+
subject_type: Subjects::PUBLIC,
|
|
202
|
+
enrolled_by: app,
|
|
203
|
+
access_token_lifetime: MACHINE_ACCESS_LIFETIME.to_i,
|
|
204
|
+
approved_at: Time.current,
|
|
205
|
+
dynamic: false
|
|
206
|
+
).issue_credentials!
|
|
207
|
+
end
|
|
208
|
+
|
|
172
209
|
def bounded(requested)
|
|
173
210
|
reserved = Scopes.reserved(requested) + Scopes.list(requested).select { |s| Scopes.prefix?(s) }
|
|
174
211
|
|
|
@@ -216,6 +253,38 @@ module Masks
|
|
|
216
253
|
end
|
|
217
254
|
end
|
|
218
255
|
|
|
256
|
+
def enrollment_refusal(scopes)
|
|
257
|
+
return "an archived client enrolls nothing" if archived?
|
|
258
|
+
return "a public client enrolls nothing" if public?
|
|
259
|
+
return "only an approved client enrolls machines" unless approved?
|
|
260
|
+
return "a client enrolls machines only after it claims a namespace" if namespaces.empty?
|
|
261
|
+
return "a scope is required" if Scopes.list(scopes).empty?
|
|
262
|
+
|
|
263
|
+
held = namespaces.map(&:name)
|
|
264
|
+
outside = Scopes.list(scopes).select do |scope|
|
|
265
|
+
Scopes.prefix?(scope) || scope.start_with?(Scopes::NAMESPACE) || !Scopes.covered?(held, scope)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
"#{Scopes.join(outside)} is outside #{Scopes.join(held)}" if outside.any?
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def enrolled?
|
|
272
|
+
enrolled_by_id.present?
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def unenroll!
|
|
276
|
+
transaction do
|
|
277
|
+
revoked = end_tokens!
|
|
278
|
+
update!(archived_at: Time.current) unless archived?
|
|
279
|
+
revoked
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def end_tokens!
|
|
284
|
+
now = Time.current
|
|
285
|
+
tokens.where(consumed_at: nil).update_all(consumed_at: now, updated_at: now)
|
|
286
|
+
end
|
|
287
|
+
|
|
219
288
|
def public?
|
|
220
289
|
token_endpoint_auth_method == "none"
|
|
221
290
|
end
|
|
@@ -412,6 +481,17 @@ module Masks
|
|
|
412
481
|
|
|
413
482
|
private
|
|
414
483
|
|
|
484
|
+
def archived!
|
|
485
|
+
end_tokens!
|
|
486
|
+
|
|
487
|
+
machines = enrolled.active.pluck(:id)
|
|
488
|
+
return if machines.empty?
|
|
489
|
+
|
|
490
|
+
now = Time.current
|
|
491
|
+
Client.where(id: machines).update_all(archived_at: archived_at, updated_at: now)
|
|
492
|
+
Token.where(client_id: machines, consumed_at: nil).update_all(consumed_at: now, updated_at: now)
|
|
493
|
+
end
|
|
494
|
+
|
|
415
495
|
def allowed_groups_are_named
|
|
416
496
|
unknown = allowed_groups.grep_v(ALLOWED_GROUP)
|
|
417
497
|
|
|
@@ -9,6 +9,8 @@ module Masks
|
|
|
9
9
|
SECRET = %w[d p q dp dq qi k].freeze
|
|
10
10
|
ALGORITHMS = %w[RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES384 ES512].freeze
|
|
11
11
|
KINDS = %w[RSA EC].freeze
|
|
12
|
+
CURVES = %w[P-256 P-384 P-521].freeze
|
|
13
|
+
SHORTEST_RSA = 2048
|
|
12
14
|
LEEWAY = 30
|
|
13
15
|
LONGEST = 1.hour
|
|
14
16
|
|
|
@@ -23,8 +25,11 @@ module Masks
|
|
|
23
25
|
raise Refused, "every key must be a JSON object" unless key.is_a?(Hash)
|
|
24
26
|
raise Refused, "#{key['kty'].presence || 'that'} is not a kind of key a client signs with" unless KINDS.include?(key["kty"])
|
|
25
27
|
raise Refused, "a client's keys must be public" if key.keys.any? { |field| SECRET.include?(field.to_s) }
|
|
28
|
+
raise Refused, "#{key['crv'].presence || 'that'} is not a curve a client signs with" if key["kty"] == "EC" && !CURVES.include?(key["crv"])
|
|
26
29
|
|
|
27
|
-
JWT::JWK.new(key).verify_key
|
|
30
|
+
verified = JWT::JWK.new(key).verify_key
|
|
31
|
+
|
|
32
|
+
raise Refused, "an RSA key must be at least #{SHORTEST_RSA} bits" if verified.is_a?(OpenSSL::PKey::RSA) && verified.n.num_bits < SHORTEST_RSA
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
held
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Server
|
|
3
|
+
class EnrollmentToken < Token
|
|
4
|
+
SHORTEST = 1.minute
|
|
5
|
+
LONGEST = 1.hour
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def lifetime
|
|
9
|
+
15.minutes
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def issue!(client, scope:, name: nil, expires_in: nil)
|
|
13
|
+
scopes = Scopes.list(scope)
|
|
14
|
+
refusal = client.enrollment_refusal(scopes)
|
|
15
|
+
|
|
16
|
+
raise Client::EnrollmentRefused, refusal if refusal
|
|
17
|
+
|
|
18
|
+
mint!(
|
|
19
|
+
client: client,
|
|
20
|
+
scopes: Scopes.join(scopes),
|
|
21
|
+
audience: client.resources,
|
|
22
|
+
payload: { "name" => name.to_s.strip.presence }.compact,
|
|
23
|
+
expires_at: lasting(expires_in).from_now
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def redeem!(secret, metadata)
|
|
28
|
+
transaction(requires_new: true) do
|
|
29
|
+
token = claim(secret)
|
|
30
|
+
|
|
31
|
+
next nil if token.nil?
|
|
32
|
+
|
|
33
|
+
Client.enroll!(token, metadata)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def lasting(expires_in)
|
|
40
|
+
return lifetime if expires_in.blank?
|
|
41
|
+
|
|
42
|
+
seconds = Integer(expires_in.to_s, 10)
|
|
43
|
+
|
|
44
|
+
raise ArgumentError unless seconds.between?(SHORTEST.to_i, LONGEST.to_i)
|
|
45
|
+
|
|
46
|
+
seconds.seconds
|
|
47
|
+
rescue ArgumentError
|
|
48
|
+
raise Client::EnrollmentRefused,
|
|
49
|
+
"expires_in must be between #{SHORTEST.to_i} and #{LONGEST.to_i} seconds"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -106,6 +106,9 @@ module Masks
|
|
|
106
106
|
CLIENT_RESTORED = "client.restored".freeze
|
|
107
107
|
CLIENT_SECRET_ROTATED = "client.secret_rotated".freeze
|
|
108
108
|
CLIENT_LOGO_REFUSED = "client.logo_refused".freeze
|
|
109
|
+
CLIENT_ENROLLMENT_ISSUED = "client.enrollment_issued".freeze
|
|
110
|
+
CLIENT_ENROLLED = "client.enrolled".freeze
|
|
111
|
+
CLIENT_UNENROLLED = "client.unenrolled".freeze
|
|
109
112
|
|
|
110
113
|
TOKEN_REVOKED = "token.revoked".freeze
|
|
111
114
|
SAML_ASSERTED = "saml.asserted".freeze
|
|
@@ -163,6 +163,7 @@ module Masks
|
|
|
163
163
|
"pushed_authorization_request_endpoint" => "#{url}/par",
|
|
164
164
|
"device_authorization_endpoint" => "#{url}/device_authorization",
|
|
165
165
|
"handshake_endpoint" => "#{url}/handshake",
|
|
166
|
+
"enrollment_endpoint" => "#{url}/enrollments",
|
|
166
167
|
"masks_protocol_version" => Masks::PROTOCOL_VERSION,
|
|
167
168
|
"revocation_endpoint" => "#{url}/revoke",
|
|
168
169
|
"introspection_endpoint" => "#{url}/introspect",
|
|
@@ -17,12 +17,15 @@ module Masks
|
|
|
17
17
|
Scopes.list(scopes).select { |scope| Scopes.prefix?(scope) && !scope.start_with?(Scopes::NAMESPACE) }
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def
|
|
21
|
-
|
|
20
|
+
def overlapping(scopes)
|
|
21
|
+
names = prefixes(scopes)
|
|
22
|
+
return none if names.empty?
|
|
23
|
+
|
|
24
|
+
where(names.map { "(starts_with(name, ?) OR starts_with(?, name))" }.join(" OR "), *names.flat_map { |name| [ name, name ] })
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def taken(handshake)
|
|
25
|
-
|
|
28
|
+
overlapping(handshake.scopes).where.not(resource: handshake.resource.to_s)
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
def refusal(handshake)
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
module Masks
|
|
2
2
|
module Server
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
key = [ kind, Current.tenant&.id, within, Digest::SHA256.hexdigest(id.to_s) ].compact.join(":")
|
|
3
|
+
class Replay < ApplicationRecord
|
|
4
|
+
include TenantScoped
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
class << self
|
|
7
|
+
def first?(kind, id, expires_in:, within: nil)
|
|
8
|
+
now = Time.current
|
|
9
|
+
claimed = connection.exec_query(sanitize_sql_array([ <<~SQL, Current.tenant.id, kind.to_s, digest(within, id), now + expires_in, now, now ]), "Replay")
|
|
10
|
+
INSERT INTO replays (tenant_id, kind, digest, expires_at, created_at)
|
|
11
|
+
VALUES (?, ?, ?, ?, ?)
|
|
12
|
+
ON CONFLICT (tenant_id, kind, digest) DO UPDATE
|
|
13
|
+
SET expires_at = EXCLUDED.expires_at, created_at = EXCLUDED.created_at
|
|
14
|
+
WHERE replays.expires_at <= ?
|
|
15
|
+
RETURNING id
|
|
16
|
+
SQL
|
|
17
|
+
|
|
18
|
+
claimed.rows.any?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def digest(within, id)
|
|
24
|
+
Digest::SHA256.hexdigest([ within&.to_s, id.to_s ].to_json)
|
|
25
|
+
end
|
|
8
26
|
end
|
|
9
27
|
end
|
|
10
28
|
end
|
|
@@ -16,6 +16,7 @@ module Masks
|
|
|
16
16
|
"password_reset" => "PasswordReset",
|
|
17
17
|
"confirmation" => "ConfirmationCode",
|
|
18
18
|
"initial_access" => "InitialAccessToken",
|
|
19
|
+
"enrollment" => "EnrollmentToken",
|
|
19
20
|
"request" => "PendingRequest",
|
|
20
21
|
"login" => "PendingLogin",
|
|
21
22
|
"pushed" => "PushedRequest",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="<%= Masks::Server::Locales.tag %>"
|
|
2
|
+
<html lang="<%= Masks::Server::Locales.tag %>">
|
|
3
3
|
<head>
|
|
4
4
|
<title><%= content_for(:title) || "Manage #{Masks::Server::Current.tenant&.name}" %></title>
|
|
5
5
|
<meta charset="utf-8">
|
|
6
6
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
7
7
|
<meta name="masks-root" content="<%= request.script_name %>">
|
|
8
|
-
<meta name="color-scheme" content="dark">
|
|
8
|
+
<meta name="color-scheme" content="light dark">
|
|
9
9
|
<link rel="icon" href="/masks-public/favicon.svg" type="image/svg+xml">
|
|
10
10
|
<link rel="apple-touch-icon" href="/masks-public/icon.png">
|
|
11
11
|
<%= csrf_meta_tags %>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<span class="person-sub">
|
|
12
12
|
<%= details.join(" · ") %>
|
|
13
13
|
<% if actor.email_unconfirmed? %>
|
|
14
|
-
<span class="person-note"><%= t("application.person.unconfirmed") %></span>
|
|
14
|
+
<span class="person-note"><%= local_assigns[:unconfirmed] || t("application.person.unconfirmed") %></span>
|
|
15
15
|
<% end %>
|
|
16
16
|
</span>
|
|
17
17
|
<% end %>
|