masks-server 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/concerns/masks/server/client_authentication.rb +1 -1
- data/app/controllers/concerns/masks/server/resource_token.rb +12 -3
- data/app/controllers/masks/server/application_controller.rb +8 -6
- data/app/controllers/masks/server/authorize_controller.rb +1 -1
- 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/logins_controller.rb +2 -1
- data/app/controllers/masks/server/registrations_controller.rb +40 -2
- data/app/controllers/masks/server/scim/metadata_controller.rb +1 -1
- data/app/controllers/masks/server/tokens_controller.rb +1 -1
- data/app/graphql/masks/server/manage/mutations/add_app.rb +1 -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/account_helper.rb +1 -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/actor.rb +3 -3
- data/app/models/masks/server/client.rb +127 -7
- 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/handshake.rb +12 -6
- data/app/models/masks/server/issuer.rb +1 -0
- data/app/models/masks/server/login.rb +3 -6
- data/app/models/masks/server/namespace.rb +6 -3
- data/app/models/masks/server/pending_handshake.rb +2 -1
- data/app/models/masks/server/replay.rb +22 -4
- data/app/models/masks/server/session.rb +7 -5
- data/app/models/masks/server/token.rb +1 -0
- data/app/policies/masks/server/exchange_policy.rb +1 -1
- data/app/policies/masks/server/grant_checks.rb +1 -1
- data/app/views/layouts/masks/server/manage.html.erb +2 -2
- data/app/views/masks/server/account/_info.html.erb +1 -1
- data/app/views/masks/server/account/_organizations.html.erb +4 -4
- data/app/views/masks/server/application/_actor.html.erb +27 -0
- data/app/views/masks/server/handshakes/refused.html.erb +1 -1
- data/app/views/masks/server/handshakes/show.html.erb +129 -59
- data/app/views/masks/server/logins/_choose_organization.html.erb +1 -1
- data/app/views/masks/server/logins/_consent.html.erb +1 -1
- data/app/views/masks/server/sessions/confirm.html.erb +1 -1
- data/config/locales/de/account.yml +3 -3
- data/config/locales/de/application.yml +1 -1
- data/config/locales/de/events.yml +10 -7
- 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 +7 -7
- 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 +5 -5
- data/config/locales/en/application.yml +1 -1
- data/config/locales/en/events.yml +9 -6
- 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 +7 -7
- 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 +3 -3
- data/config/locales/es/application.yml +2 -2
- data/config/locales/es/events.yml +9 -6
- 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 +7 -7
- 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/account.yml +2 -2
- data/config/locales/fr/application.yml +1 -1
- data/config/locales/fr/events.yml +8 -5
- 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 +7 -7
- 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/db/migrate/20261013010000_add_handshake_return_to_to_clients.rb +5 -0
- data/lib/generators/masks/server/install/install_generator.rb +2 -2
- data/lib/masks/server/browser_access.rb +35 -0
- data/lib/masks/server/configuration.rb +3 -3
- data/lib/masks/server/engine.rb +1 -0
- data/lib/masks/server/schema_reference.rb +17 -4
- data/lib/masks/server.rb +1 -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-B27oPUll.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/{initial-D99AzFOd.js → initial-C1GpCiEh.js} +2 -2
- data/public/masks-assets/assets/{initial-D99AzFOd.js.map → initial-C1GpCiEh.js.map} +1 -1
- data/public/masks-assets/assets/login-C_6igRZ3.js +4 -0
- data/public/masks-assets/assets/login-C_6igRZ3.js.map +1 -0
- data/public/masks-assets/assets/manage-Dbfiy3Mj.js +567 -0
- data/public/masks-assets/assets/manage-Dbfiy3Mj.js.map +1 -0
- data/version.txt +1 -1
- metadata +21 -12
- data/app/views/masks/server/application/_person.html.erb +0 -27
- 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/login-CNs1UpxJ.js +0 -4
- data/public/masks-assets/assets/login-CNs1UpxJ.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
|
@@ -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) }
|
|
@@ -117,7 +123,7 @@ module Masks
|
|
|
117
123
|
{
|
|
118
124
|
name: handshake.name,
|
|
119
125
|
redirect_uris: handshake.redirect_uris,
|
|
120
|
-
post_logout_redirect_uris: [ handshake.
|
|
126
|
+
post_logout_redirect_uris: [ handshake.post_logout_redirect_uri ].compact,
|
|
121
127
|
resources: [ handshake.resource ],
|
|
122
128
|
allowed_scopes: Scopes.join(handshake.scopes),
|
|
123
129
|
grant_types: handshake.grant_types,
|
|
@@ -125,6 +131,7 @@ module Masks
|
|
|
125
131
|
token_endpoint_auth_method: handshake.auth_method,
|
|
126
132
|
backchannel_logout_uri: handshake.backchannel_logout_uri,
|
|
127
133
|
client_uri: handshake.origin,
|
|
134
|
+
handshake_return_to: handshake.return_to,
|
|
128
135
|
dynamic: false
|
|
129
136
|
}
|
|
130
137
|
end
|
|
@@ -132,7 +139,7 @@ module Masks
|
|
|
132
139
|
def approved_for(resource)
|
|
133
140
|
return nil if resource.blank?
|
|
134
141
|
|
|
135
|
-
active.approved.where("resources @> ?", [ resource.to_s ].to_json).first
|
|
142
|
+
active.approved.where(enrolled_by_id: nil).where("resources @> ?", [ resource.to_s ].to_json).first
|
|
136
143
|
end
|
|
137
144
|
|
|
138
145
|
def register!(metadata)
|
|
@@ -163,12 +170,48 @@ module Masks
|
|
|
163
170
|
name: held[:name] || name,
|
|
164
171
|
grant_types: grant_types,
|
|
165
172
|
response_types: held[:response_types].presence || response_types_for(grant_types),
|
|
166
|
-
allowed_scopes: Scopes.join(
|
|
167
|
-
Current.tenant&.dynamic_client_ceiling || DEFAULT_SCOPES)),
|
|
173
|
+
allowed_scopes: Scopes.join(declared_scopes(metadata)),
|
|
168
174
|
dynamic: true
|
|
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
|
+
|
|
209
|
+
def declared_scopes(metadata)
|
|
210
|
+
declared = metadata.to_h.stringify_keys["scope"]
|
|
211
|
+
|
|
212
|
+
Scopes.list(declared).empty? ? [] : bounded(declared)
|
|
213
|
+
end
|
|
214
|
+
|
|
172
215
|
def bounded(requested)
|
|
173
216
|
reserved = Scopes.reserved(requested) + Scopes.list(requested).select { |s| Scopes.prefix?(s) }
|
|
174
217
|
|
|
@@ -216,6 +259,38 @@ module Masks
|
|
|
216
259
|
end
|
|
217
260
|
end
|
|
218
261
|
|
|
262
|
+
def enrollment_refusal(scopes)
|
|
263
|
+
return "an archived client enrolls nothing" if archived?
|
|
264
|
+
return "a public client enrolls nothing" if public?
|
|
265
|
+
return "only an approved client enrolls machines" unless approved?
|
|
266
|
+
return "a client enrolls machines only after it claims a namespace" if namespaces.empty?
|
|
267
|
+
return "a scope is required" if Scopes.list(scopes).empty?
|
|
268
|
+
|
|
269
|
+
held = namespaces.map(&:name)
|
|
270
|
+
outside = Scopes.list(scopes).select do |scope|
|
|
271
|
+
Scopes.prefix?(scope) || scope.start_with?(Scopes::NAMESPACE) || !Scopes.covered?(held, scope)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
"#{Scopes.join(outside)} is outside #{Scopes.join(held)}" if outside.any?
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def enrolled?
|
|
278
|
+
enrolled_by_id.present?
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def unenroll!
|
|
282
|
+
transaction do
|
|
283
|
+
revoked = end_tokens!
|
|
284
|
+
update!(archived_at: Time.current) unless archived?
|
|
285
|
+
revoked
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def end_tokens!
|
|
290
|
+
now = Time.current
|
|
291
|
+
tokens.where(consumed_at: nil).update_all(consumed_at: now, updated_at: now)
|
|
292
|
+
end
|
|
293
|
+
|
|
219
294
|
def public?
|
|
220
295
|
token_endpoint_auth_method == "none"
|
|
221
296
|
end
|
|
@@ -311,7 +386,7 @@ module Masks
|
|
|
311
386
|
end
|
|
312
387
|
|
|
313
388
|
def redirect_uri?(candidate)
|
|
314
|
-
redirect_uris.include?(candidate.to_s)
|
|
389
|
+
redirect_uris.include?(candidate.to_s) || redirect_uris.any? { |registered| same_loopback?(registered, candidate.to_s) }
|
|
315
390
|
end
|
|
316
391
|
|
|
317
392
|
def grants?(grant_type)
|
|
@@ -355,7 +430,11 @@ module Masks
|
|
|
355
430
|
end
|
|
356
431
|
|
|
357
432
|
def scope_list
|
|
358
|
-
Scopes.union(required_scopes,
|
|
433
|
+
Scopes.union(required_scopes, offered_scopes)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def follows_registration?
|
|
437
|
+
dynamic? && Scopes.list(allowed_scopes).empty?
|
|
359
438
|
end
|
|
360
439
|
|
|
361
440
|
def access_lifetime
|
|
@@ -373,7 +452,19 @@ module Masks
|
|
|
373
452
|
def permitted_scopes(requested)
|
|
374
453
|
return scope_list if Scopes.list(requested).empty?
|
|
375
454
|
|
|
376
|
-
Scopes.union(required_scopes,
|
|
455
|
+
return Scopes.union(required_scopes, unreserved(requested)) if open_to_anything?
|
|
456
|
+
|
|
457
|
+
Scopes.union(required_scopes, Scopes.granted(requested, offered_scopes))
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def refused_scopes(requested)
|
|
461
|
+
Scopes.list(requested) - permitted_scopes(requested)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def offered_scopes
|
|
465
|
+
return Scopes.list(allowed_scopes) unless follows_registration?
|
|
466
|
+
|
|
467
|
+
Current.tenant&.dynamic_client_ceiling || DEFAULT_SCOPES
|
|
377
468
|
end
|
|
378
469
|
|
|
379
470
|
def metadata
|
|
@@ -412,6 +503,17 @@ module Masks
|
|
|
412
503
|
|
|
413
504
|
private
|
|
414
505
|
|
|
506
|
+
def archived!
|
|
507
|
+
end_tokens!
|
|
508
|
+
|
|
509
|
+
machines = enrolled.active.pluck(:id)
|
|
510
|
+
return if machines.empty?
|
|
511
|
+
|
|
512
|
+
now = Time.current
|
|
513
|
+
Client.where(id: machines).update_all(archived_at: archived_at, updated_at: now)
|
|
514
|
+
Token.where(client_id: machines, consumed_at: nil).update_all(consumed_at: now, updated_at: now)
|
|
515
|
+
end
|
|
516
|
+
|
|
415
517
|
def allowed_groups_are_named
|
|
416
518
|
unknown = allowed_groups.grep_v(ALLOWED_GROUP)
|
|
417
519
|
|
|
@@ -464,6 +566,24 @@ module Masks
|
|
|
464
566
|
self.class.loopback?(uri)
|
|
465
567
|
end
|
|
466
568
|
|
|
569
|
+
def open_to_anything?
|
|
570
|
+
follows_registration? && Current.tenant&.dynamic_registration == Tenant::REGISTRATION_ANYTHING
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def unreserved(requested)
|
|
574
|
+
Scopes.list(requested).reject { |scope| Scopes.prefix?(scope) || Scopes.reserved(scope).any? }
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def same_loopback?(registered, candidate)
|
|
578
|
+
held = URI.parse(registered)
|
|
579
|
+
asked = URI.parse(candidate)
|
|
580
|
+
|
|
581
|
+
[ held, asked ].all? { |uri| uri.instance_of?(URI::HTTP) && LOOPBACK.include?(uri.hostname) && uri.userinfo.nil? && uri.fragment.nil? } &&
|
|
582
|
+
held.hostname == asked.hostname && held.path == asked.path && held.query == asked.query
|
|
583
|
+
rescue URI::InvalidURIError
|
|
584
|
+
false
|
|
585
|
+
end
|
|
586
|
+
|
|
467
587
|
def grant_types_are_known
|
|
468
588
|
unknown = grant_types - GRANT_TYPES
|
|
469
589
|
errors.add(:grant_types, "not supported: #{unknown.join(', ')}") if unknown.any?
|
|
@@ -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
|
|
@@ -4,7 +4,7 @@ module Masks
|
|
|
4
4
|
GRANT_TYPES = %w[authorization_code refresh_token].freeze
|
|
5
5
|
|
|
6
6
|
attr_reader :name, :redirect_uris, :resource, :scopes, :return_to, :state, :auth_method,
|
|
7
|
-
:backchannel_logout_uri
|
|
7
|
+
:backchannel_logout_uri, :post_logout_redirect_uri
|
|
8
8
|
|
|
9
9
|
class << self
|
|
10
10
|
def from_request(request)
|
|
@@ -19,7 +19,8 @@ module Masks
|
|
|
19
19
|
return_to: params["return_to"],
|
|
20
20
|
state: params["state"],
|
|
21
21
|
auth_method: params["token_endpoint_auth_method"],
|
|
22
|
-
backchannel_logout_uri: params["backchannel_logout_uri"]
|
|
22
|
+
backchannel_logout_uri: params["backchannel_logout_uri"],
|
|
23
|
+
post_logout_redirect_uri: params["post_logout_redirect_uri"]
|
|
23
24
|
)
|
|
24
25
|
end
|
|
25
26
|
|
|
@@ -36,7 +37,8 @@ module Masks
|
|
|
36
37
|
return_to: row.redirect_uri,
|
|
37
38
|
state: held["state"],
|
|
38
39
|
auth_method: held["auth_method"],
|
|
39
|
-
backchannel_logout_uri: held["backchannel_logout_uri"]
|
|
40
|
+
backchannel_logout_uri: held["backchannel_logout_uri"],
|
|
41
|
+
post_logout_redirect_uri: held["post_logout_redirect_uri"]
|
|
40
42
|
)
|
|
41
43
|
end
|
|
42
44
|
end
|
|
@@ -54,7 +56,8 @@ module Masks
|
|
|
54
56
|
"state" => state,
|
|
55
57
|
"redirect_uris" => redirect_uris.sort,
|
|
56
58
|
"auth_method" => auth_method,
|
|
57
|
-
"backchannel_logout_uri" => backchannel_logout_uri
|
|
59
|
+
"backchannel_logout_uri" => backchannel_logout_uri,
|
|
60
|
+
"post_logout_redirect_uri" => post_logout_redirect_uri
|
|
58
61
|
}.compact
|
|
59
62
|
end
|
|
60
63
|
|
|
@@ -73,12 +76,14 @@ module Masks
|
|
|
73
76
|
|
|
74
77
|
pairs << [ "state", state ] if state
|
|
75
78
|
pairs << [ "backchannel_logout_uri", backchannel_logout_uri ] if backchannel_logout_uri
|
|
79
|
+
pairs << [ "post_logout_redirect_uri", post_logout_redirect_uri ] if post_logout_redirect_uri
|
|
76
80
|
redirect_uris.each { |uri| pairs << [ "redirect_uris", uri ] }
|
|
77
81
|
pairs
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
def initialize(name: nil, redirect_uris: nil, resource: nil, scopes: nil,
|
|
81
|
-
return_to: nil, state: nil, auth_method: nil, backchannel_logout_uri: nil
|
|
85
|
+
return_to: nil, state: nil, auth_method: nil, backchannel_logout_uri: nil,
|
|
86
|
+
post_logout_redirect_uri: nil)
|
|
82
87
|
@name = name.to_s.strip.presence || "An application"
|
|
83
88
|
@redirect_uris = Array(redirect_uris).map(&:to_s).reject(&:empty?).uniq
|
|
84
89
|
@resource = resource.to_s
|
|
@@ -87,6 +92,7 @@ module Masks
|
|
|
87
92
|
@state = state.presence
|
|
88
93
|
@auth_method = auth_method.to_s.presence || Client::DEFAULT_AUTH_METHOD
|
|
89
94
|
@backchannel_logout_uri = backchannel_logout_uri.to_s.strip.presence
|
|
95
|
+
@post_logout_redirect_uri = post_logout_redirect_uri.to_s.strip.presence
|
|
90
96
|
end
|
|
91
97
|
|
|
92
98
|
def origin
|
|
@@ -134,7 +140,7 @@ module Masks
|
|
|
134
140
|
return "token_endpoint_auth_method must be one of #{Client::AUTH_METHODS.join(', ')}"
|
|
135
141
|
end
|
|
136
142
|
|
|
137
|
-
elsewhere = ([ return_to ] + redirect_uris + Array(backchannel_logout_uri))
|
|
143
|
+
elsewhere = ([ return_to ] + redirect_uris + Array(backchannel_logout_uri) + Array(post_logout_redirect_uri))
|
|
138
144
|
.reject { |uri| origin_of(uri) == origin }
|
|
139
145
|
return "everything must share the origin #{origin}: #{elsewhere.join(', ')}" if elsewhere.any?
|
|
140
146
|
|
|
@@ -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",
|
|
@@ -416,8 +416,7 @@ module Masks
|
|
|
416
416
|
"rid" => rid,
|
|
417
417
|
"docs" => ::Rails.configuration.masks.docs_url,
|
|
418
418
|
"actor" => actor && { "nickname" => actor.nickname, "name" => actor.name,
|
|
419
|
-
"identifier" => actor.identifier },
|
|
420
|
-
"person" => person_json,
|
|
419
|
+
"identifier" => actor.identifier, "card" => card_json },
|
|
421
420
|
"client" => client_json,
|
|
422
421
|
"tenant" => tenant && {
|
|
423
422
|
"name" => tenant.name, "logo" => tenant.logo_url
|
|
@@ -429,10 +428,8 @@ module Masks
|
|
|
429
428
|
answering.reduce(base) { |json, state| state.enabled? ? json.merge(state.as_json) : json }
|
|
430
429
|
end
|
|
431
430
|
|
|
432
|
-
def
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
actor.person_json
|
|
431
|
+
def card_json
|
|
432
|
+
actor.card_json if authenticated_at
|
|
436
433
|
end
|
|
437
434
|
|
|
438
435
|
def forget_vanished_actor!
|
|
@@ -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)
|
|
@@ -19,7 +19,8 @@ module Masks
|
|
|
19
19
|
"redirect_uris" => handshake.redirect_uris,
|
|
20
20
|
"state" => handshake.state,
|
|
21
21
|
"auth_method" => handshake.auth_method,
|
|
22
|
-
"backchannel_logout_uri" => handshake.backchannel_logout_uri
|
|
22
|
+
"backchannel_logout_uri" => handshake.backchannel_logout_uri,
|
|
23
|
+
"post_logout_redirect_uri" => handshake.post_logout_redirect_uri
|
|
23
24
|
}.compact
|
|
24
25
|
)
|
|
25
26
|
end
|
|
@@ -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
|
|
@@ -23,7 +23,7 @@ module Masks
|
|
|
23
23
|
|
|
24
24
|
class << self
|
|
25
25
|
def start!(actor:, device: nil, user_agent: nil, ip_address: nil, amr: [], origin: nil, policy: nil,
|
|
26
|
-
first_factor: {})
|
|
26
|
+
first_factor: {}, authenticated_at: nil)
|
|
27
27
|
secret = SecureRandom.urlsafe_base64(48)
|
|
28
28
|
lifetime = policy&.session_lifetime&.seconds || LIFETIME
|
|
29
29
|
now = Time.current
|
|
@@ -36,7 +36,7 @@ module Masks
|
|
|
36
36
|
user_agent: user_agent,
|
|
37
37
|
ip_address: ip_address,
|
|
38
38
|
origin: origin.presence || Current.origin,
|
|
39
|
-
authenticated_at: now,
|
|
39
|
+
authenticated_at: authenticated_at || now,
|
|
40
40
|
last_seen_at: now,
|
|
41
41
|
amr: Array(amr),
|
|
42
42
|
first_factor: first_factor["factor"],
|
|
@@ -71,10 +71,12 @@ module Masks
|
|
|
71
71
|
{ "factor" => first_factor, "provider_id" => provider_id }.compact
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
def first_factored!(held)
|
|
75
|
-
|
|
74
|
+
def first_factored!(held, at: nil)
|
|
75
|
+
changes = {}
|
|
76
|
+
changes[:authenticated_at] = at if at.present? && (authenticated_at.nil? || at > authenticated_at)
|
|
77
|
+
changes.merge!(first_factor: held["factor"], provider_id: held["provider_id"]) if held["factor"].present? && held != first_factored_by
|
|
76
78
|
|
|
77
|
-
update!(
|
|
79
|
+
update!(changes) if changes.any?
|
|
78
80
|
end
|
|
79
81
|
|
|
80
82
|
def bound?
|
|
@@ -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",
|
|
@@ -85,7 +85,7 @@ module Masks
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def scopes_stay_within_the_client
|
|
88
|
-
beyond =
|
|
88
|
+
beyond = client.refused_scopes(requested_scopes)
|
|
89
89
|
|
|
90
90
|
deny!("invalid_scope", "this client may not hold #{beyond.join(', ')}") if beyond.any?
|
|
91
91
|
end
|
|
@@ -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 %>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<div class="entries">
|
|
5
5
|
<details class="entry" id="profile">
|
|
6
6
|
<summary>
|
|
7
|
-
<span class="entry-icon"><%= account_icon(:
|
|
7
|
+
<span class="entry-icon"><%= account_icon(:actor) %></span>
|
|
8
8
|
<span class="entry-label"><%= account_text("profile.heading") %></span>
|
|
9
9
|
<span class="entry-state"><%= actor.name.presence || actor.nickname %></span>
|
|
10
10
|
<span class="entry-mark" aria-hidden="true"></span>
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
|
|
73
73
|
<% if own.owner? %>
|
|
74
74
|
<p class="item-meta">
|
|
75
|
-
<%= account_text("organizations.
|
|
75
|
+
<%= account_text("organizations.member_count", count: joined) %>
|
|
76
76
|
· <%= account_text("organizations.owners", count: owners) %>
|
|
77
77
|
<% if waiting.positive? %>
|
|
78
78
|
· <%= account_text("organizations.waiting_count", count: waiting) %>
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
|
|
82
82
|
<div class="items">
|
|
83
83
|
<% members.each do |member| %>
|
|
84
|
-
<%
|
|
85
|
-
<% yourself =
|
|
84
|
+
<% actor = member.actor %>
|
|
85
|
+
<% yourself = actor.id == own.actor_id %>
|
|
86
86
|
<% label = Masks::Server::Members.label(member) || account_text("organizations.invited_by_manager") %>
|
|
87
87
|
<div class="item item-member">
|
|
88
88
|
<span class="item-name">
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
class: "control control-sm", id: "role-#{organization.key}-#{member.id}",
|
|
112
112
|
disabled: yourself && alone,
|
|
113
113
|
data: { autosubmit: true },
|
|
114
|
-
aria: { label: account_text("organizations.role_of",
|
|
114
|
+
aria: { label: account_text("organizations.role_of", name: label) } %>
|
|
115
115
|
<button type="submit" class="action action-quiet action-sm" data-autosubmitted <%= "disabled" if yourself && alone %>>
|
|
116
116
|
<%= account_text("organizations.set_role") %>
|
|
117
117
|
</button>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<% sign_out = local_assigns.fetch(:sign_out, false) %>
|
|
2
|
+
<% details = actor.display_details %>
|
|
3
|
+
|
|
4
|
+
<div class="actor">
|
|
5
|
+
<%= avatar_tag actor, size: 44, class: "actor-avatar" %>
|
|
6
|
+
|
|
7
|
+
<div class="actor-who">
|
|
8
|
+
<span class="actor-name"><%= actor.display_name %></span>
|
|
9
|
+
|
|
10
|
+
<% if details.any? || actor.email_unconfirmed? %>
|
|
11
|
+
<span class="actor-sub">
|
|
12
|
+
<%= details.join(" · ") %>
|
|
13
|
+
<% if actor.email_unconfirmed? %>
|
|
14
|
+
<span class="actor-note"><%= local_assigns[:unconfirmed] || t("application.actor.unconfirmed") %></span>
|
|
15
|
+
<% end %>
|
|
16
|
+
</span>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<% if actor.manages? %>
|
|
20
|
+
<span class="actor-role"><%= t("application.actor.manager") %></span>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<% if sign_out %>
|
|
25
|
+
<%= button_to t("application.actor.sign_out"), logout_path, class: "textlink", form_class: "signout" %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<h1 class="prompt-title"><%= t(".title") %></h1>
|
|
7
7
|
|
|
8
8
|
<% if current_actor %>
|
|
9
|
-
<%= render "masks/server/application/
|
|
9
|
+
<%= render "masks/server/application/actor", actor: current_actor, sign_out: true %>
|
|
10
10
|
<% end %>
|
|
11
11
|
|
|
12
12
|
<p class="said"><%= @description %></p>
|