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]
119 pub fn impersonation(mut self, impersonation: Impersonation<U>) -> Self {
120 let allows = impersonation.allows;
121 self.config.impersonation = Some(ImpersonationConfig {
122 allows: Arc::new(move |cx, actor, target| {
123 let (actor, target): (&dyn Any, &dyn Any) = (actor, target);
124 match (actor.downcast_ref::<U>(), target.downcast_ref::<U>()) {
125 (Some(actor), Some(target)) => allows(cx, actor, target),
126 _ => false,
127 }
128 }),
129 lifetime: impersonation.lifetime,
130 across_tenants: impersonation.across_tenants,
131 });
132 self
133 }
134}
135
136type Allows = dyn Fn(&Cx, &dyn PanelUser, &dyn PanelUser) -> bool + Send + Sync;
137
138#[derive(Clone)]
140pub(crate) struct ImpersonationConfig {
141 allows: Arc<Allows>,
142 lifetime: Duration,
143 across_tenants: bool,
144}
145
146impl std::fmt::Debug for ImpersonationConfig {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 f.debug_struct("Impersonation")
149 .field("lifetime", &self.lifetime)
150 .field("across_tenants", &self.across_tenants)
151 .finish_non_exhaustive()
152 }
153}
154
155impl ImpersonationConfig {
156 fn permits(
160 &self,
161 cx: &Cx,
162 panel: &Realm,
163 actor: &dyn PanelUser,
164 target: &dyn PanelUser,
165 ) -> bool {
166 let within_tenants = !panel.serves_tenants
167 || self.across_tenants
168 || target.tenants().iter().all(|membership| {
169 actor
170 .tenants()
171 .iter()
172 .any(|own| own.tenant == membership.tenant)
173 });
174 actor.user_id() != target.user_id()
175 && actor.can_access_panel(&panel.prefix)
176 && target.can_access_panel(&panel.prefix)
177 && within_tenants
178 && (self.allows)(cx, actor, target)
179 }
180}
181
182impl AuthConfig {
183 pub(crate) fn impersonation(&self) -> Option<&ImpersonationConfig> {
185 self.authenticator().and(self.impersonation.as_ref())
186 }
187}
188
189pub fn impersonator<U: PanelUser>(cx: &Cx) -> Option<&U> {
192 let actor: &dyn Any = &**signed(cx)?.impersonator.as_ref()?;
193 actor.downcast_ref()
194}
195
196pub(super) async fn resolve(
199 cx: &Cx,
200 row: AuthSession,
201 actor: Arc<dyn PanelUser>,
202 panel: &Arc<Realm>,
203 authenticator: &dyn DynAuthenticator,
204) -> topcoat::Result<SignedIn> {
205 if let Some(target) = live_target(cx, &row, &*actor, panel, authenticator).await? {
206 return Ok(SignedIn {
207 user: target,
208 realm: Arc::clone(panel),
209 tenant: row.tenant,
210 impersonator: Some(actor),
211 });
212 }
213 let mut db = crate::db::db(cx)?;
215 let mut tx = db.transaction().await.map_err(infrastructure_failure)?;
216 session::set_impersonation(&mut tx, &row.token_hash, None).await?;
217 if let Some(target) = row.impersonating.as_deref() {
218 let tenant = row.tenant;
219 record(
220 cx,
221 &mut tx,
222 panel,
223 tenant,
224 "stop_impersonating",
225 &*actor,
226 target,
227 None,
228 )
229 .await?;
230 }
231 tx.commit().await.map_err(infrastructure_failure)?;
232 Ok(SignedIn {
233 user: actor,
234 realm: Arc::clone(panel),
235 tenant: None,
236 impersonator: None,
237 })
238}
239
240async fn live_target(
241 cx: &Cx,
242 row: &AuthSession,
243 actor: &dyn PanelUser,
244 panel: &Realm,
245 authenticator: &dyn DynAuthenticator,
246) -> topcoat::Result<Option<Arc<dyn PanelUser>>> {
247 let (Some(id), Some(until), Some(config)) = (
248 row.impersonating.as_deref(),
249 row.impersonating_until,
250 panel.auth.impersonation(),
251 ) else {
252 return Ok(None);
253 };
254 if until <= Timestamp::now() || super::gate::owes_step(cx, panel, actor).await? {
256 return Ok(None);
257 }
258 let target = authenticator
259 .find_by_id(cx, id)
260 .await
261 .map_err(infrastructure_failure)?;
262 Ok(target.filter(|target| config.permits(cx, panel, actor, &**target)))
263}
264
265fn actor(cx: &Cx) -> Option<(&SignedIn, &ImpersonationConfig)> {
268 let signed = signed(cx).filter(|signed| signed.impersonator.is_none())?;
269 Some((signed, signed.realm.auth.impersonation()?))
270}
271
272pub fn impersonate_button<'a>(cx: &'a Cx, target: &dyn PanelUser) -> BoxView<'a> {
289 if !may_impersonate(cx, target) {
290 return view! {}.boxed();
291 }
292 let token = crate::csrf::ensure_token(cx);
293 let action = format!("{}/impersonate", panel_prefix(cx));
294 let id = target.user_id();
295 view! {
296 <form method="post" action=(action) data-impersonate=(id.clone())>
297 (crate::csrf::field(cx, &token))
298 <input type="hidden" name=(IMPERSONATE_FIELD) value=(id)>
299 tablo_ui::button(
300 variant: tablo_ui::ButtonVariant::Outline,
301 size: tablo_ui::ButtonSize::Sm,
302 attrs: attributes! { type="submit" },
303 icon(data: tablo_ui::icons::EYE)
304 (crate::t!(cx, "tablo-impersonation-start"))
305 )
306 </form>
307 }
308 .boxed()
309}
310
311pub(crate) fn may_impersonate(cx: &Cx, target: &dyn PanelUser) -> bool {
314 actor(cx)
315 .is_some_and(|(signed, config)| config.permits(cx, &signed.realm, &*signed.user, target))
316}
317
318pub(crate) async fn start(
321 cx: &Cx,
322 ex: &mut dyn toasty::Executor,
323 target: &dyn PanelUser,
324) -> topcoat::Result<()> {
325 let Some((signed, config)) = actor(cx) else {
326 return Err(forbidden().into());
327 };
328 if !config.permits(cx, &signed.realm, &*signed.user, target) {
329 return Err(forbidden().into());
330 }
331 let until =
332 Timestamp::try_from(SystemTime::now() + config.lifetime).map_err(topcoat::Error::from)?;
333 session::impersonate(cx, &mut *ex, Some((target.user_id(), until))).await?;
334 let (realm, tenant) = (&signed.realm, crate::tenant_id(cx));
335 let (id, name) = (target.user_id(), Some(target.display_name()));
336 record(
337 cx,
338 ex,
339 realm,
340 tenant,
341 "impersonate",
342 &*signed.user,
343 &id,
344 name,
345 )
346 .await?;
347 crate::bus::bus(cx).wake(&signed.realm.prefix, &signed.user.user_id());
348 Ok(())
349}
350
351pub(super) fn start_post(cx: &Cx, body: Body) -> RouteFuture<'_> {
354 Box::pin(async move {
355 let Some(signed) = signed(cx) else {
356 return Err(unauthenticated_error(cx));
357 };
358 let parts = super::input::read_form(cx, body).await?;
359 crate::csrf::verify(cx, parts.last(crate::csrf::FIELD_NAME))?;
360 let authenticator = signed.realm.auth.authenticator().ok_or_else(forbidden)?;
361 let id = parts.last(IMPERSONATE_FIELD).ok_or_else(forbidden)?;
362 let target = authenticator
364 .find_by_id(cx, id)
365 .await
366 .map_err(infrastructure_failure)?
367 .ok_or_else(forbidden)?;
368 let mut db = crate::db::db(cx)?;
369 let mut tx = db.transaction().await.map_err(infrastructure_failure)?;
370 start(cx, &mut tx, &*target).await?;
371 tx.commit().await.map_err(infrastructure_failure)?;
372 see_other(panel_root(cx)).into_response(cx)
373 })
374}
375
376pub(super) fn stop_post(cx: &Cx, body: Body) -> RouteFuture<'_> {
378 Box::pin(async move {
379 let Some(signed) = signed(cx) else {
380 return Err(unauthenticated_error(cx));
381 };
382 let parts = super::input::read_form(cx, body).await?;
383 crate::csrf::verify(cx, parts.last(crate::csrf::FIELD_NAME))?;
384 if let Some(actor) = &signed.impersonator {
385 let mut db = crate::db::db(cx)?;
386 let mut tx = db.transaction().await.map_err(infrastructure_failure)?;
387 session::impersonate(cx, &mut tx, None).await?;
388 let (realm, tenant) = (&signed.realm, crate::tenant_id(cx));
389 let (id, name) = (signed.user.user_id(), Some(signed.user.display_name()));
390 let event = "stop_impersonating";
391 record(cx, &mut tx, realm, tenant, event, &**actor, &id, name).await?;
392 tx.commit().await.map_err(infrastructure_failure)?;
393 crate::bus::bus(cx).wake(&signed.realm.prefix, &actor.user_id());
394 }
395 see_other(panel_root(cx)).into_response(cx)
396 })
397}
398
399pub(crate) fn stop_url(cx: &Cx) -> String {
401 format!("{}/impersonate/stop", panel_prefix(cx))
402}
403
404#[expect(
407 clippy::too_many_arguments,
408 reason = "one call per event, each naming every part"
409)]
410async fn record(
411 cx: &Cx,
412 ex: &mut dyn toasty::Executor,
413 panel: &Realm,
414 tenant: Option<crate::TenantId>,
415 event: &str,
416 actor: &dyn PanelUser,
417 target_id: &str,
418 target_name: Option<&str>,
419) -> topcoat::Result<()> {
420 if !panel.audit_log {
421 return Ok(());
422 }
423 AuditEvent::new(event)
424 .key(target_id.to_owned(), None)
425 .change(
426 "user",
427 crate::t!(cx, "tablo-impersonation-user"),
428 None,
429 Some(target_name.unwrap_or(target_id).to_owned()),
430 )
431 .write(
432 ex,
433 panel.prefix.clone(),
434 tenant,
435 Some((actor.user_id(), actor.display_name().to_owned())),
436 )
437 .await
438 .map_err(|error| {
439 crate::error::driver_failure(error, "The audit log is unavailable right now.")
440 })?;
441 Ok(())
442}
443
444pub(super) fn refused(cx: &Cx, panel: &Realm) -> bool {
447 let path = uri(cx).path();
448 let prefix = &panel.prefix;
449 [
450 panel.pages.profile.clone(),
451 panel.pages.two_factor.clone(),
452 panel.pages.tenant_profile.clone(),
453 format!("{prefix}/register-tenant"),
454 format!("{prefix}/verify-email"),
455 ]
456 .iter()
457 .any(|root| {
458 path.strip_prefix(root.as_str())
459 .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
460 })
461}