1use std::time::Duration;
6
7use sofie::core::Instant;
8use sofie::db::{Outcome, Plan, Resolve, Stamp, Uuid};
9use sofie::http::{
10 Area, CurrentSession, NeedsPermission, Permission, Permissions, RequestContext, Routes, Shipped,
11};
12use sofie::prelude::*;
13
14use crate::account::Account;
15use crate::membership::{self, held};
16use crate::signing_in::{SignedIn, sign_in};
17use crate::{AccountArea, AccountId, MemberArea, Unscoped, account_of};
18
19const READ: Permission = Permission::new("sofie_identity.impersonation.*.read");
20const CREATE: Permission = Permission::new("sofie_identity.impersonation.*.create");
21const LASTS: Duration = Duration::from_mins(30);
23const SHOWN: u64 = 50;
24const NOT_A_MEMBER: &str = "has no account that is a member there";
25const PERMITTED_ELSEWHERE: &str =
26 "is a member of the area impersonating it, which nobody impersonates";
27const NOT_IMPERSONATING: &str = "This session is no impersonation.";
28
29#[derive(Aggregate)]
32pub struct Impersonation {
33 meta: Meta,
34 #[owner]
35 account: AccountId,
36 impersonator: AccountId,
37 impersonator_email: Email,
39 area: String,
40 reason: String,
41 session: Uuid,
42 started_from: Option<Uuid>,
44 ends_at: Instant,
45 ended_at: Option<Instant>,
46 granted: String,
49}
50
51const GRANTED_SEPARATOR: char = ' ';
52
53impl Impersonation {
54 pub(crate) fn end(&mut self, now: Instant) -> Result<()> {
55 self.ended_at = Some(now);
56 self.meta.raise(ImpersonationEnded {
57 impersonation: self.id().uuid(),
58 })
59 }
60
61 fn still_covers(&self, granted: &[String]) -> bool {
62 granted.iter().all(|permission| {
63 self.granted
64 .split(GRANTED_SEPARATOR)
65 .any(|held| held == permission)
66 })
67 }
68}
69
70#[derive(Event)]
72pub struct ImpersonationStarted {
73 pub impersonation: Uuid,
74 pub impersonator: Uuid,
75 pub member: Uuid,
76 pub area: String,
77 pub reason: String,
78}
79
80#[derive(Event)]
81pub struct ImpersonationEnded {
82 pub impersonation: Uuid,
83}
84
85pub fn routes<S, M>(routes: &mut Routes<S>)
87where
88 S: AccountArea,
89 M: MemberArea,
90{
91 let describe: sofie::http::Describe =
92 |screens, key| screens.describe_form::<Starting>(key, "impersonate");
93 routes
94 .shipped(
95 "/impersonation",
96 "impersonation",
97 (include_str!("screen.html"), Some(describe)),
98 record,
99 )
100 .name("list_impersonations")
101 .permission(READ);
102 routes
103 .post("/impersonation", start::<S, M>)
104 .name("impersonate")
105 .permission(CREATE)
106 .then_redirect(M::HOME);
107}
108
109pub fn stop_routes<M, S>(routes: &mut Routes<M>)
111where
112 M: MemberArea,
113 S: Area,
114{
115 routes
116 .post("/impersonation/stop", stop::<M, S>)
117 .name("stop_impersonating")
118 .then_redirect(&format!("{}/impersonation", S::PREFIX));
119}
120
121#[derive(ReadModel)]
122pub struct Record {
123 impersonations: Vec<Listed>,
124}
125
126#[derive(ReadModel)]
127struct Listed {
128 impersonator: Email,
129 member: Email,
130 area: String,
131 reason: String,
132 created_at: Option<Instant>,
133 ended_at: Option<Instant>,
134}
135
136#[derive(ReadModel)]
137#[read(from = Account)]
138struct Member {
139 id: AccountId,
140 email: Email,
141}
142
143async fn record(read: Read, clock: Clock) -> Result<Record> {
144 let now = clock.now();
145 let held = Impersonation::query()
146 .latest()
147 .limit(SHOWN)
148 .all(&read)
149 .await?;
150 let members = Member::query()
151 .where_(Account::ID.in_(held.iter().map(|impersonation| impersonation.account)))
152 .all(&read)
153 .await?;
154 let email_of = |account| {
155 members
156 .iter()
157 .find(|member| member.id == account)
158 .map(|member| member.email.clone())
159 .ok_or(Error::NotFound)
160 };
161 let impersonations = held
162 .into_iter()
163 .map(|impersonation| {
164 Ok(Listed {
165 member: email_of(impersonation.account)?,
166 created_at: impersonation.created_at(),
167 impersonator: impersonation.impersonator_email,
168 area: impersonation.area,
169 reason: impersonation.reason,
170 ended_at: impersonation
171 .ended_at
172 .or(Some(impersonation.ends_at).filter(|ends_at| *ends_at <= now)),
173 })
174 })
175 .collect::<Result<_>>()?;
176 Ok(Record { impersonations })
177}
178
179#[derive(Input)]
180pub struct Starting {
181 email: Email,
182 #[min(3)]
184 #[max(500)]
185 reason: String,
186}
187
188pub struct Started {
189 impersonation: Impersonation,
190 signed_in: SignedIn,
191}
192
193async fn start<S, M>(
194 principal: Principal<S>,
195 permissions: Permissions,
196 starting: Starting,
197 read: Read,
198 session: Session,
199 clock: Clock,
200) -> Result<Started>
201where
202 S: AccountArea,
203 M: MemberArea,
204{
205 let reason = starting.reason.trim();
206 if reason.chars().count() < 3 {
207 return Err(Error::invalid(
208 "reason",
209 "must say what the impersonation is for",
210 ));
211 }
212 let impersonator = Account::stored(&read, account_of(&principal)).await?;
213 let member = Account::of_email(&read, starting.email)
214 .await?
215 .filter(|member| !member.is_closed())
216 .ok_or_else(|| Error::invalid("email", NOT_A_MEMBER))?;
217 if held(&read, member.id(), M::NAME).await?.is_none() {
218 return Err(Error::invalid("email", NOT_A_MEMBER));
219 }
220 if held(&read, member.id(), S::NAME).await?.is_some() {
221 return Err(Error::invalid("email", PERMITTED_ELSEWHERE));
222 }
223 let granted = refuse_unless_within(&read, &permissions, member.id(), M::NAME).await?;
224 let now = clock.now();
225 let ends_at = now.checked_add(LASTS)?;
226 let started_from = session.current();
227 let signed_in =
228 sign_in::<M>(&session, member.id(), &clock)?.impersonated(impersonator.email(), ends_at);
229 let current = signed_in.session();
230 let mut impersonation = Impersonation {
231 meta: Meta::new()?,
232 account: member.id(),
233 impersonator: impersonator.id(),
234 impersonator_email: impersonator.email().clone(),
235 area: M::NAME.to_owned(),
236 reason: reason.to_owned(),
237 session: current,
238 started_from,
239 ends_at,
240 ended_at: None,
241 granted: granted.join(&GRANTED_SEPARATOR.to_string()),
242 };
243 impersonation.meta.raise(ImpersonationStarted {
244 impersonation: impersonation.id().uuid(),
245 impersonator: impersonator.id().uuid(),
246 member: member.id().uuid(),
247 area: M::NAME.to_owned(),
248 reason: impersonation.reason.clone(),
249 })?;
250 Ok(Started {
251 impersonation,
252 signed_in,
253 })
254}
255
256async fn refuse_unless_within(
258 read: &Read,
259 impersonator: &Permissions,
260 member: AccountId,
261 area: &str,
262) -> Result<Vec<String>> {
263 let granted = membership::roles::granted(read, area, member).await?;
264 match granted
265 .iter()
266 .find(|permission| !impersonator.can_named(permission))
267 {
268 Some(beyond) => Err(Error::invalid(
269 "email",
270 format!(
271 "holds `{beyond}`, which your own permissions do not cover, so you cannot impersonate it"
272 ),
273 )),
274 None => Ok(granted),
275 }
276}
277
278impl Outcome for Started {
279 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
280 self.impersonation.plan(plan)?;
281 self.signed_in.plan(plan)
282 }
283
284 fn committed(&mut self, stamp: Stamp) {
285 self.impersonation.committed(stamp);
286 self.signed_in.committed(stamp);
287 }
288}
289
290async fn stop<M, S>(
291 _member: Principal<M>,
292 read: Read,
293 unscoped: Unscoped,
294 session: Session,
295 clock: Clock,
296) -> Result<()>
297where
298 M: MemberArea,
299 S: Area,
300{
301 let mut impersonation = of_session(&read, session.current())
302 .await?
303 .ok_or_else(|| Error::Rule(NOT_IMPERSONATING.to_owned()))?;
304 impersonation.end(clock.now())?;
305 let sessions = crate::sessions::ending_current(&read, &session, &clock).await?;
306 match returning::<S>(&unscoped.0.read(), &impersonation).await? {
307 Some(resumed) => crate::signing_in::resume::<S>(&session, &resumed),
308 None => session.sign_out(),
309 }
310 unscoped.0.commit(&mut (impersonation, sessions)).await
311}
312
313async fn returning<S: Area>(
317 read: &Read,
318 impersonation: &Impersonation,
319) -> Result<Option<crate::sessions::Resumable>> {
320 let Some(started_from) = impersonation.started_from else {
321 return Ok(None);
322 };
323 let impersonator = impersonation.impersonator;
324 let Some(resumed) = crate::sessions::recorded(read, started_from)
325 .await?
326 .and_then(|record| record.resumable(impersonator, S::NAME))
327 else {
328 return Ok(None);
329 };
330 Ok(held(read, impersonator, S::NAME).await?.map(|_| resumed))
331}
332
333#[cfg(feature = "account-data")]
334pub(crate) async fn exported(read: &Read, account: AccountId) -> Result<sofie::serde_json::Value> {
335 let impersonations = Impersonation::query()
336 .where_(Impersonation::ACCOUNT.eq(account))
337 .all(read)
338 .await?;
339 Ok(sofie::serde_json::json!(
340 impersonations
341 .iter()
342 .map(|impersonation| sofie::serde_json::json!({
343 "by": impersonation.impersonator_email.as_str(),
344 "area": impersonation.area,
345 "reason": impersonation.reason,
346 "ends_at": impersonation.ends_at,
347 "ended_at": impersonation.ended_at,
348 }))
349 .collect::<Vec<_>>()
350 ))
351}
352
353pub(crate) async fn ending_in_sessions(
355 read: &Read,
356 sessions: &[Uuid],
357 now: Instant,
358) -> Result<Vec<Impersonation>> {
359 if sessions.is_empty() {
360 return Ok(Vec::new());
361 }
362 let mut open = Impersonation::query()
363 .where_(Impersonation::SESSION.in_(sessions.to_vec()))
364 .where_(Impersonation::ENDED_AT.is_null())
365 .all(read)
366 .await?;
367 for impersonation in &mut open {
368 impersonation.end(now)?;
369 }
370 Ok(open)
371}
372
373pub(crate) async fn sessions_started_by(read: &Read, impersonator: AccountId) -> Result<Vec<Uuid>> {
374 let open = Impersonation::query()
375 .where_(Impersonation::IMPERSONATOR.eq(impersonator))
376 .where_(Impersonation::ENDED_AT.is_null())
377 .all(read)
378 .await?;
379 Ok(open
380 .into_iter()
381 .map(|impersonation| impersonation.session)
382 .collect())
383}
384
385async fn of_session(read: &Read, session: Option<Uuid>) -> Result<Option<Impersonation>> {
386 let Some(session) = session else {
387 return Ok(None);
388 };
389 let found = Impersonation::query()
390 .where_(Impersonation::SESSION.eq(session))
391 .first(read)
392 .await?;
393 Ok(found.filter(|impersonation| impersonation.ended_at.is_none()))
394}
395
396pub(crate) enum Checked {
397 Absent,
398 By(Uuid),
399 Beyond(Box<Impersonation>),
400}
401
402pub(crate) async fn checked(read: &Read, session: Uuid) -> Result<Checked> {
404 let Some(impersonation) = of_session(read, Some(session)).await? else {
405 return Ok(Checked::Absent);
406 };
407 let granted =
408 membership::roles::granted(read, &impersonation.area, impersonation.account).await?;
409 if impersonation.still_covers(&granted) {
410 return Ok(Checked::By(impersonation.impersonator.uuid()));
411 }
412 Ok(Checked::Beyond(Box::new(impersonation)))
413}
414
415pub struct Impersonating(Option<String>);
418
419impl Impersonating {
420 #[must_use]
421 pub fn by(&self) -> Option<&str> {
422 self.0.as_deref()
423 }
424}
425
426impl<A: Area, M> Resolve<RequestContext<A, M>> for Impersonating {
427 async fn resolve(context: &mut RequestContext<A, M>) -> Result<Self> {
428 let current = CurrentSession::resolve(context).await?;
429 let read = Read::resolve(context).await?;
430 let impersonation = of_session(&read, current.id()).await?;
431 Ok(Self(impersonation.map(|impersonation| {
432 impersonation.impersonator_email.as_str().to_owned()
433 })))
434 }
435}