1use std::{
5 any::Any,
6 sync::Arc,
7 time::{Duration, SystemTime},
8};
9
10use jiff::Timestamp;
11use topcoat::{
12 context::Cx,
13 icon::icon,
14 router::{
15 Body, RouteFuture,
16 error::{forbidden, see_other},
17 request::uri,
18 response::IntoResponse,
19 },
20 view::{BoxView, ViewExt, attributes, view},
21};
22
23use super::{
24 Auth, AuthConfig, DynAuthenticator, PanelUser, SignedIn, infrastructure_failure, panel_root,
25 realm::{Realm, panel_prefix},
26 session::{self, AuthSession},
27 signed, unauthenticated_error,
28};
29use crate::AuditEvent;
30
31pub const IMPERSONATE_FIELD: &str = "user";
34
35type Rule<U> = dyn Fn(&Cx, &U, &U) -> bool + Send + Sync;
37
38pub struct Impersonation<U> {
52 allows: Arc<Rule<U>>,
53 lifetime: Duration,
54 across_tenants: bool,
55}
56
57const LONGEST: Duration = Duration::from_hours(24);
59
60impl<U: PanelUser> Impersonation<U> {
61 pub fn new(allows: impl Fn(&Cx, &U, &U) -> bool + Send + Sync + 'static) -> Self {
72 Self {
73 allows: Arc::new(allows),
74 lifetime: Duration::from_hours(1),
75 across_tenants: false,
76 }
77 }
78
79 #[must_use]
82 pub fn lifetime(mut self, lifetime: Duration) -> Self {
83 self.lifetime = lifetime.min(LONGEST);
84 self
85 }
86
87 #[must_use]
92 pub fn across_tenants(mut self) -> Self {
93 self.across_tenants = true;
94 self
95 }
96}
97
98impl<U: PanelUser> Auth<U> {
99 #[must_use]
122 pub fn impersonation(mut self, impersonation: Impersonation<U>) -> Self {
123 let allows = impersonation.allows;
124 self.config.impersonation = Some(ImpersonationConfig {
125 allows: Arc::new(move |cx, actor, target| {
126 let (actor, target): (&dyn Any, &dyn Any) = (actor, target);
127 match (actor.downcast_ref::<U>(), target.downcast_ref::<U>()) {
128 (Some(actor), Some(target)) => allows(cx, actor, target),
129 _ => false,
130 }
131 }),
132 lifetime: impersonation.lifetime,
133 across_tenants: impersonation.across_tenants,
134 });
135 self
136 }
137}
138
139type Allows = dyn Fn(&Cx, &dyn PanelUser, &dyn PanelUser) -> bool + Send + Sync;
140
141#[derive(Clone)]
143pub(crate) struct ImpersonationConfig {
144 allows: Arc<Allows>,
145 lifetime: Duration,
146 across_tenants: bool,
147}
148
149impl std::fmt::Debug for ImpersonationConfig {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 f.debug_struct("Impersonation")
152 .field("lifetime", &self.lifetime)
153 .field("across_tenants", &self.across_tenants)
154 .finish_non_exhaustive()
155 }
156}
157
158impl ImpersonationConfig {
159 fn permits(
163 &self,
164 cx: &Cx,
165 panel: &Realm,
166 actor: &dyn PanelUser,
167 target: &dyn PanelUser,
168 ) -> bool {
169 let within_tenants = !panel.serves_tenants
170 || self.across_tenants
171 || target.tenants().iter().all(|membership| {
172 actor
173 .tenants()
174 .iter()
175 .any(|own| own.tenant == membership.tenant)
176 });
177 actor.user_id() != target.user_id()
178 && actor.can_access_panel(&panel.prefix)
179 && target.can_access_panel(&panel.prefix)
180 && within_tenants
181 && (self.allows)(cx, actor, target)
182 }
183}
184
185impl AuthConfig {
186 pub(crate) fn impersonation(&self) -> Option<&ImpersonationConfig> {
188 self.authenticator().and(self.impersonation.as_ref())
189 }
190}
191
192pub fn impersonator<U: PanelUser>(cx: &Cx) -> Option<&U> {
195 let actor: &dyn Any = &**signed(cx)?.impersonator.as_ref()?;
196 actor.downcast_ref()
197}
198
199pub(super) async fn resolve(
202 cx: &Cx,
203 row: AuthSession,
204 actor: Arc<dyn PanelUser>,
205 panel: &Arc<Realm>,
206 authenticator: &dyn DynAuthenticator,
207) -> topcoat::Result<SignedIn> {
208 if let Some(target) = live_target(cx, &row, &*actor, panel, authenticator).await? {
209 return Ok(SignedIn {
210 user: target,
211 realm: Arc::clone(panel),
212 tenant: row.tenant,
213 impersonator: Some(actor),
214 });
215 }
216 let mut db = crate::db::db(cx)?;
218 let mut tx = db.transaction().await.map_err(infrastructure_failure)?;
219 session::set_impersonation(&mut tx, &row.token_hash, None).await?;
220 if let Some(target) = row.impersonating.as_deref() {
221 let tenant = row.tenant;
222 record(
223 cx,
224 &mut tx,
225 panel,
226 tenant,
227 "stop_impersonating",
228 &*actor,
229 target,
230 None,
231 None,
232 )
233 .await?;
234 }
235 tx.commit().await.map_err(infrastructure_failure)?;
236 Ok(SignedIn {
237 user: actor,
238 realm: Arc::clone(panel),
239 tenant: None,
240 impersonator: None,
241 })
242}
243
244async fn live_target(
245 cx: &Cx,
246 row: &AuthSession,
247 actor: &dyn PanelUser,
248 panel: &Realm,
249 authenticator: &dyn DynAuthenticator,
250) -> topcoat::Result<Option<Arc<dyn PanelUser>>> {
251 let (Some(id), Some(until), Some(config)) = (
252 row.impersonating.as_deref(),
253 row.impersonating_until,
254 panel.auth.impersonation(),
255 ) else {
256 return Ok(None);
257 };
258 if until <= Timestamp::now() || super::gate::owes_step(cx, panel, actor).await? {
260 return Ok(None);
261 }
262 let target = authenticator
263 .find_by_id(cx, id)
264 .await
265 .map_err(infrastructure_failure)?;
266 Ok(target.filter(|target| config.permits(cx, panel, actor, &**target)))
267}
268
269fn actor(cx: &Cx) -> Option<(&SignedIn, &ImpersonationConfig)> {
272 let signed = signed(cx).filter(|signed| signed.impersonator.is_none())?;
273 Some((signed, signed.realm.auth.impersonation()?))
274}
275
276pub fn impersonate_button<'a>(cx: &'a Cx, target: &dyn PanelUser) -> BoxView<'a> {
293 if !may_impersonate(cx, target) {
294 return view! {}.boxed();
295 }
296 let token = crate::csrf::ensure_token(cx);
297 let action = format!("{}/impersonate", panel_prefix(cx));
298 let id = target.user_id();
299 view! {
300 <form method="post" action=(action) data-impersonate=(id.clone())>
301 (crate::csrf::field(cx, &token))
302 <input type="hidden" name=(IMPERSONATE_FIELD) value=(id)>
303 tablo_ui::button(
304 variant: tablo_ui::ButtonVariant::Outline,
305 size: tablo_ui::ButtonSize::Sm,
306 attrs: attributes! { type="submit" },
307 icon(data: tablo_ui::icons::EYE)
308 (crate::t!(cx, "tablo-impersonation-start"))
309 )
310 </form>
311 }
312 .boxed()
313}
314
315pub(crate) fn may_impersonate(cx: &Cx, target: &dyn PanelUser) -> bool {
318 actor(cx)
319 .is_some_and(|(signed, config)| config.permits(cx, &signed.realm, &*signed.user, target))
320}
321
322pub(crate) async fn start(
325 cx: &Cx,
326 ex: &mut dyn toasty::Executor,
327 target: &dyn PanelUser,
328) -> topcoat::Result<()> {
329 let Some((signed, config)) = actor(cx) else {
330 return Err(forbidden().into());
331 };
332 if !config.permits(cx, &signed.realm, &*signed.user, target) {
333 return Err(forbidden().into());
334 }
335 let until =
336 Timestamp::try_from(SystemTime::now() + config.lifetime).map_err(topcoat::Error::from)?;
337 session::impersonate(cx, &mut *ex, Some((target.user_id(), until))).await?;
338 let (realm, tenant) = (&signed.realm, crate::tenant_id(cx));
339 let (id, name) = (target.user_id(), Some(target.display_name()));
340 record(
341 cx,
342 ex,
343 realm,
344 tenant,
345 "impersonate",
346 &*signed.user,
347 &id,
348 name,
349 Some(until),
350 )
351 .await?;
352 crate::bus::bus(cx).wake(&signed.realm.prefix, &signed.user.user_id());
353 Ok(())
354}
355
356pub(super) fn start_post(cx: &Cx, body: Body) -> RouteFuture<'_> {
359 Box::pin(async move {
360 let Some(signed) = signed(cx) else {
361 return Err(unauthenticated_error(cx));
362 };
363 let parts = super::input::read_form(cx, body).await?;
364 crate::csrf::verify(cx, parts.last(crate::csrf::FIELD_NAME))?;
365 let authenticator = signed.realm.auth.authenticator().ok_or_else(forbidden)?;
366 let id = parts.last(IMPERSONATE_FIELD).ok_or_else(forbidden)?;
367 let target = authenticator
369 .find_by_id(cx, id)
370 .await
371 .map_err(infrastructure_failure)?
372 .ok_or_else(forbidden)?;
373 let mut db = crate::db::db(cx)?;
374 let mut tx = db.transaction().await.map_err(infrastructure_failure)?;
375 start(cx, &mut tx, &*target).await?;
376 tx.commit().await.map_err(infrastructure_failure)?;
377 see_other(panel_root(cx)).into_response(cx)
378 })
379}
380
381pub(super) fn stop_post(cx: &Cx, body: Body) -> RouteFuture<'_> {
383 Box::pin(async move {
384 let Some(signed) = signed(cx) else {
385 return Err(unauthenticated_error(cx));
386 };
387 let parts = super::input::read_form(cx, body).await?;
388 crate::csrf::verify(cx, parts.last(crate::csrf::FIELD_NAME))?;
389 if let Some(actor) = &signed.impersonator {
390 let mut db = crate::db::db(cx)?;
391 let mut tx = db.transaction().await.map_err(infrastructure_failure)?;
392 session::impersonate(cx, &mut tx, None).await?;
393 let (realm, tenant) = (&signed.realm, crate::tenant_id(cx));
394 let (id, name) = (signed.user.user_id(), Some(signed.user.display_name()));
395 let event = "stop_impersonating";
396 record(cx, &mut tx, realm, tenant, event, &**actor, &id, name, None).await?;
397 tx.commit().await.map_err(infrastructure_failure)?;
398 crate::bus::bus(cx).wake(&signed.realm.prefix, &actor.user_id());
399 }
400 see_other(panel_root(cx)).into_response(cx)
401 })
402}
403
404pub(super) async fn stop_on_sign_out(cx: &Cx, signed: &SignedIn) -> topcoat::Result<()> {
407 let Some(actor) = &signed.impersonator else {
408 return Ok(());
409 };
410 let mut db = crate::db::db(cx)?;
411 let (realm, tenant) = (&signed.realm, crate::tenant_id(cx));
412 let (id, name) = (signed.user.user_id(), Some(signed.user.display_name()));
413 let event = "stop_impersonating";
414 record(cx, &mut db, realm, tenant, event, &**actor, &id, name, None).await
415}
416
417pub(crate) fn stop_url(cx: &Cx) -> String {
419 format!("{}/impersonate/stop", panel_prefix(cx))
420}
421
422#[expect(
426 clippy::too_many_arguments,
427 reason = "one call per event, each naming every part"
428)]
429async fn record(
430 cx: &Cx,
431 ex: &mut dyn toasty::Executor,
432 panel: &Realm,
433 tenant: Option<crate::TenantId>,
434 event: &str,
435 actor: &dyn PanelUser,
436 target_id: &str,
437 target_name: Option<&str>,
438 until: Option<Timestamp>,
439) -> topcoat::Result<()> {
440 if !panel.audit_log {
441 return Ok(());
442 }
443 let mut entry = AuditEvent::new(event)
444 .key(target_id.to_owned(), None)
445 .change(
446 "user",
447 crate::t!(cx, "tablo-impersonation-user"),
448 None,
449 Some(target_name.unwrap_or(target_id).to_owned()),
450 );
451 if let Some(until) = until {
454 entry = entry.change(
455 "until",
456 crate::t!(cx, "tablo-impersonation-until"),
457 None,
458 Some(until.to_string()),
459 );
460 }
461 entry
462 .write(
463 ex,
464 panel.prefix.clone(),
465 tenant,
466 Some((actor.user_id(), actor.display_name().to_owned())),
467 )
468 .await
469 .map_err(|error| {
470 crate::error::driver_failure(error, "The audit log is unavailable right now.")
471 })?;
472 Ok(())
473}
474
475pub(super) fn refused(cx: &Cx, panel: &Realm) -> bool {
478 let path = uri(cx).path();
479 let prefix = &panel.prefix;
480 [
481 panel.pages.profile.clone(),
482 panel.pages.two_factor.clone(),
483 panel.pages.tenant_profile.clone(),
484 format!("{prefix}/register-tenant"),
485 format!("{prefix}/verify-email"),
486 ]
487 .iter()
488 .any(|root| {
489 path.strip_prefix(root.as_str())
490 .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
491 })
492}