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