Skip to main content

tablo_core/panel/
impersonation.rs

1//! What the panel shows of [`Auth::impersonation`](crate::Auth::impersonation): the banner on every
2//! page while a user acts as another, and the [`Impersonate`] action that starts it.
3
4use std::convert::Infallible;
5
6use topcoat::{
7    context::Cx,
8    icon::{IconData, icon},
9    view::{BoxView, ViewExt, attributes, view},
10};
11
12use crate::{Action, Outcome, PanelUser, Places, Resource, WriteError};
13
14/// Acts as the record's user: an [`Action`] for a resource whose model is the panel's user type,
15/// placed on its rows and record pages.
16///
17/// Its button shows only on a user the signed-in user may act as under the panel's
18/// [`Impersonation`](crate::Impersonation) rule, and never while they act as someone already.
19/// Running it lands on the panel root as that user. The resource's policy must also allow
20/// [`Ability::Run`](crate::Ability::Run) on the record.
21///
22/// ```rust
23/// # use tablo_core::{Allow, Impersonate, NoForm, Resource, ResourceDef, auth::AdminUser};
24/// struct Users;
25///
26/// impl Resource for Users {
27///     type Model = AdminUser;
28///     type Form = NoForm<AdminUser>;
29///
30///     fn declare() -> ResourceDef<Self> {
31///         ResourceDef::new().policy(Allow).action::<Impersonate>()
32///     }
33/// }
34/// ```
35#[derive(Debug, Clone, Copy)]
36pub struct Impersonate;
37
38impl<R: Resource> Action<R> for Impersonate
39where
40    R::Model: PanelUser,
41{
42    type Input = ();
43
44    const NAME: &'static str = "impersonate";
45    const PLACES: Places = Places::RECORD;
46    const ICON: Option<IconData> = Some(tablo_ui::icons::EYE);
47
48    fn label() -> crate::Text {
49        crate::text!("tablo-impersonation-start")
50    }
51
52    fn can_run(cx: &Cx, record: &R::Model) -> bool {
53        crate::auth::may_impersonate(cx, record)
54    }
55
56    async fn run(
57        cx: &Cx,
58        records: &[R::Model],
59        (): (),
60        ex: &mut dyn toasty::Executor,
61    ) -> Result<Outcome, WriteError<Infallible>> {
62        let [target] = records else {
63            return Err(WriteError::declined(crate::text!(
64                "tablo-impersonation-one"
65            )));
66        };
67        crate::auth::impersonate(cx, ex, target)
68            .await
69            .map_err(WriteError::Failed)?;
70        Ok(Outcome::done().redirect(crate::auth::panel_prefix(cx)))
71    }
72
73    fn success(_count: usize) -> crate::Text {
74        crate::text!("tablo-impersonation-started")
75    }
76}
77
78/// The strip above every page while the signed-in user is acted as: whom, and Stop. Nothing
79/// otherwise.
80pub(super) fn banner(cx: &Cx) -> BoxView<'_> {
81    match crate::auth::signed(cx) {
82        Some(signed) if signed.impersonator.is_some() => strip(cx, signed),
83        _ => view! {}.boxed(),
84    }
85}
86
87fn strip<'a>(cx: &'a Cx, signed: &'a crate::auth::SignedIn) -> BoxView<'a> {
88    let csrf = crate::csrf::ensure_token(cx);
89    let stop = crate::auth::stop_impersonating_url(cx);
90    let name = signed.user.display_name().to_owned();
91    view! {
92        <div
93            class="flex flex-wrap items-center justify-between gap-2 border-b border-warning/40 \
94                       bg-warning/15 px-4 py-2 text-sm text-foreground"
95            role="status"
96            data-impersonating=(signed.user.user_id())
97        >
98            <span>(crate::t!(cx, "tablo-impersonation-banner", name = name))</span>
99            <form method="post" action=(stop)>
100                (crate::csrf::field(cx, &csrf))
101                tablo_ui::button(
102                    variant: tablo_ui::ButtonVariant::Outline,
103                    size: tablo_ui::ButtonSize::Sm,
104                    attrs: attributes! { type="submit" },
105                    icon(data: tablo_ui::icons::LOG_OUT)
106                    (crate::t!(cx, "tablo-impersonation-stop"))
107                )
108            </form>
109        </div>
110    }
111    .boxed()
112}