1use std::sync::{Arc, Mutex};
2
3use http::header::SET_COOKIE;
4use http::{HeaderMap, HeaderValue};
5use sofie_auth::{OpenError, SessionClaims, SigningKeys};
6use sofie_core::{Result, Timestamp, Uuid, unpoisoned};
7#[cfg(any(
8 feature = "permissions",
9 feature = "tokens",
10 feature = "shield",
11 feature = "session-list"
12))]
13use sofie_db::BoxFuture;
14use sofie_db::{Id, Resolve};
15
16use crate::answer::{self, Response};
17use crate::caller::Caller;
18use crate::cookie::CookieNames;
19use crate::{Area, PrincipalRecord, RequestContext};
20
21pub trait AreaMembers: Send + Sync + 'static {
22 #[cfg(feature = "permissions")]
23 fn grants<'a>(
24 &'a self,
25 read: &'a sofie_db::Read,
26 area: &'static str,
27 principal: Uuid,
28 ) -> BoxFuture<'a, Result<Vec<String>>> {
29 let _ = (read, area, principal);
30 Box::pin(std::future::ready(Ok(Vec::new())))
31 }
32
33 #[cfg(feature = "tokens")]
34 fn bearer<'a>(
35 &'a self,
36 db: &'a sofie_db::Db,
37 area: &'static str,
38 digest: &'a str,
39 ) -> BoxFuture<'a, Result<Option<crate::Bearer>>> {
40 let _ = (db, area, digest);
41 Box::pin(std::future::ready(Ok(None)))
42 }
43
44 #[cfg(feature = "shield")]
45 fn sign_in_locked<'a>(
46 &'a self,
47 db: &'a sofie_db::Db,
48 account: Uuid,
49 until: Timestamp,
50 ) -> BoxFuture<'a, Result<()>> {
51 let _ = (db, account, until);
52 Box::pin(std::future::ready(Ok(())))
53 }
54
55 #[cfg(feature = "session-list")]
59 fn session_record<'a>(
60 &'a self,
61 db: &'a sofie_db::Db,
62 session: Uuid,
63 seen: Option<Timestamp>,
64 ) -> BoxFuture<'a, Result<SessionRecord>> {
65 let _ = (db, session, seen);
66 Box::pin(std::future::ready(Ok(SessionRecord::Unrecorded)))
67 }
68
69 #[cfg(feature = "session-list")]
72 fn session_topic(&self) -> Option<sofie_db::Topic> {
73 None
74 }
75}
76
77#[cfg(feature = "session-list")]
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub enum SessionRecord {
81 Unrecorded,
83 Live {
84 account: Uuid,
85 expires_at: Option<sofie_core::Instant>,
87 impersonator: Option<Uuid>,
89 },
90 Ended,
91}
92
93#[cfg(feature = "session-list")]
94impl SessionRecord {
95 #[must_use]
96 pub fn is_live(self, now: sofie_core::Instant) -> bool {
97 match self {
98 Self::Unrecorded => true,
99 Self::Live { expires_at, .. } => expires_at.is_none_or(|at| at > now),
100 Self::Ended => false,
101 }
102 }
103}
104
105pub struct Session {
107 changes: Arc<Mutex<Changes>>,
108 current: Option<Uuid>,
109 #[cfg(feature = "flows")]
110 flow: Option<String>,
111 #[cfg(feature = "client-headers")]
112 client: Client,
113}
114
115#[cfg(feature = "client-headers")]
117#[derive(Clone, Default)]
118struct Client {
119 user_agent: Option<HeaderValue>,
120 origin: Option<HeaderValue>,
121}
122
123#[cfg(feature = "client-headers")]
124impl Client {
125 fn of(request: &http::request::Parts) -> Self {
126 let origin = request.headers.get(http::header::ORIGIN).filter(|origin| {
127 crate::csrf::host_of(request)
128 .is_some_and(|host| crate::csrf::is_origin_of(origin, host))
129 });
130 Self {
131 user_agent: request.headers.get(http::header::USER_AGENT).cloned(),
132 origin: origin.cloned(),
133 }
134 }
135}
136
137#[derive(Default)]
138struct Changes {
139 requested: Option<Change>,
140 #[cfg(feature = "flows")]
141 begun_flow: Option<String>,
142 committed: bool,
143}
144
145#[derive(Clone, Copy)]
146enum Change {
147 SignIn {
148 session: Uuid,
149 area: &'static str,
150 principal: Uuid,
151 issued_at: Option<Timestamp>,
153 },
154 SignOut,
155}
156
157pub trait Admitter {}
161
162#[derive(Clone, Copy)]
174pub struct SignInRight {
175 sealed: (),
176}
177
178const _: () = assert!(size_of::<SignInRight>() == 0);
179
180impl SignInRight {
181 #[must_use]
182 pub const fn of<K: Admitter>() -> Self {
183 Self { sealed: () }
184 }
185}
186
187impl Session {
188 pub fn sign_in<A: Area>(
199 &self,
200 right: SignInRight,
201 principal: Id<PrincipalRecord<A>>,
202 ) -> Result<Uuid> {
203 let SignInRight { sealed: () } = right;
204 let session = Uuid::new_v4()
205 .map_err(|error| sofie_core::Error::internal("choosing a session id", error))?;
206 #[cfg(feature = "product-events")]
207 sofie_telemetry::attach_principal(&tracing::Span::current(), principal.uuid());
208 self.request(Change::SignIn {
209 session,
210 area: A::NAME,
211 principal: principal.uuid(),
212 issued_at: None,
213 });
214 Ok(session)
215 }
216
217 pub fn resume<A: Area>(
219 &self,
220 right: SignInRight,
221 session: Uuid,
222 principal: Id<PrincipalRecord<A>>,
223 issued_at: Timestamp,
224 ) {
225 let SignInRight { sealed: () } = right;
226 #[cfg(feature = "product-events")]
227 sofie_telemetry::attach_principal(&tracing::Span::current(), principal.uuid());
228 self.request(Change::SignIn {
229 session,
230 area: A::NAME,
231 principal: principal.uuid(),
232 issued_at: Some(issued_at),
233 });
234 }
235
236 #[must_use]
237 pub fn current(&self) -> Option<Uuid> {
238 self.current
239 }
240
241 #[cfg(feature = "client-headers")]
243 #[must_use]
244 pub fn user_agent(&self) -> Option<std::borrow::Cow<'_, str>> {
245 let value = self.client.user_agent.as_ref()?;
246 Some(String::from_utf8_lossy(value.as_bytes()))
247 }
248
249 #[cfg(feature = "client-headers")]
251 #[must_use]
252 pub fn origin(&self) -> Option<&str> {
253 self.client.origin.as_ref()?.to_str().ok()
254 }
255
256 pub fn sign_out(&self) {
257 self.request(Change::SignOut);
258 }
259
260 #[cfg(feature = "flows")]
263 pub fn begin_flow(&self) -> Result<String> {
264 let value = Uuid::new_v4()
265 .map_err(|error| sofie_core::Error::internal("choosing a flow's value", error))?
266 .to_string();
267 unpoisoned::lock(&self.changes).begun_flow = Some(value.clone());
268 Ok(value)
269 }
270
271 #[cfg(feature = "flows")]
272 #[must_use]
273 pub fn flow(&self) -> Option<&str> {
274 self.flow.as_deref()
275 }
276
277 fn request(&self, change: Change) {
278 unpoisoned::lock(&self.changes).requested = Some(change);
279 }
280}
281
282impl<A: Area> Resolve<RequestContext<A>> for Session {
283 fn resolve(context: &mut RequestContext<A>) -> impl Future<Output = Result<Self>> + Send {
284 let session = context.session();
285 std::future::ready(Ok(Self {
286 changes: Arc::clone(&session.changes),
287 current: session.recorded,
288 #[cfg(feature = "flows")]
289 flow: session.flow.clone(),
290 #[cfg(feature = "client-headers")]
291 client: session.client.clone(),
292 }))
293 }
294}
295
296pub struct CurrentSession {
298 id: Option<Uuid>,
299 account: Option<Uuid>,
300}
301
302impl CurrentSession {
303 #[must_use]
304 pub fn id(&self) -> Option<Uuid> {
305 self.id
306 }
307
308 #[must_use]
311 pub fn account(&self) -> Option<Uuid> {
312 self.account
313 }
314}
315
316impl<A: Area, M> Resolve<RequestContext<A, M>> for CurrentSession {
317 fn resolve(context: &mut RequestContext<A, M>) -> impl Future<Output = Result<Self>> + Send {
318 let session = context.session();
319 std::future::ready(Ok(Self {
320 id: session.recorded,
321 #[cfg(feature = "session-list")]
322 account: session.account,
323 #[cfg(not(feature = "session-list"))]
324 account: None,
325 }))
326 }
327}
328
329pub(crate) struct Sessions {
330 pub(crate) keys: Arc<SigningKeys>,
331 #[cfg(any(feature = "tokens", feature = "session-list"))]
332 pub(crate) members: Option<Arc<dyn AreaMembers>>,
333 pub(crate) cookies: CookieNames,
334 #[cfg(feature = "session-list")]
335 pub(crate) records: crate::session_cache::SessionCache,
336}
337
338impl Sessions {
339 pub(crate) fn open(&self, headers: &HeaderMap, now: Timestamp) -> RequestSession {
340 let sealed = self.cookies.session_value(headers);
341 let claims = sealed.and_then(|sealed| opened(&self.keys, sealed, now));
342 let is_dead = sealed.is_some() && claims.is_none();
343 let recorded = claims.as_ref().map(SessionClaims::session);
344 #[cfg(all(feature = "toolbar", debug_assertions))]
345 let toolbar = crate::toolbar::Overrides::of(headers, self.cookies.toolbar());
346 #[cfg(all(feature = "toolbar", debug_assertions))]
347 let (claims, recorded) = match toolbar.claims(now) {
348 Some(acting) => (Some(acting), None),
349 None => (claims, recorded),
350 };
351 RequestSession {
352 claims,
353 recorded,
354 #[cfg(feature = "flows")]
355 flow: self.cookies.flow_value(headers).map(str::to_owned),
356 is_dead,
357 #[cfg(all(feature = "toolbar", debug_assertions))]
358 toolbar,
359 ..RequestSession::default()
360 }
361 }
362}
363
364fn opened(keys: &SigningKeys, sealed: &str, now: Timestamp) -> Option<SessionClaims> {
365 SessionClaims::open(keys, sealed, now)
366 .map_err(log_unopened)
367 .ok()
368}
369
370fn log_unopened(error: OpenError) {
372 let refused = match error {
373 OpenError::Malformed => "malformed",
374 OpenError::Unauthenticated => "unauthenticated",
375 OpenError::Expired => "expired",
376 OpenError::Corrupt => "corrupt",
377 };
378 match error {
379 OpenError::Malformed | OpenError::Expired => {
380 tracing::debug!(session.refused = refused, "the session cookie did not open");
381 }
382 OpenError::Unauthenticated => {
383 tracing::info!(session.refused = refused, "the session cookie did not open");
384 }
385 OpenError::Corrupt => {
386 tracing::error!(error.kind = "session", session.refused = refused, error = %error, "the session cookie did not open");
387 }
388 }
389}
390
391#[derive(Clone, Default)]
392pub(crate) struct RequestSession {
393 claims: Option<SessionClaims>,
394 recorded: Option<Uuid>,
396 #[cfg(feature = "session-list")]
398 account: Option<Uuid>,
399 #[cfg(feature = "flows")]
400 flow: Option<String>,
401 #[cfg(feature = "client-headers")]
402 client: Client,
403 is_dead: bool,
405 changes: Arc<Mutex<Changes>>,
406 #[cfg(feature = "tokens")]
408 scopes: Option<Arc<[String]>>,
409 #[cfg(all(feature = "toolbar", debug_assertions))]
411 pub(crate) toolbar: crate::toolbar::Overrides,
412}
413
414impl RequestSession {
415 #[cfg(feature = "tokens")]
416 pub(crate) fn bearer(area: &str, bearer: crate::Bearer, now: Timestamp) -> Self {
417 let claims = SessionClaims::new(bearer.token, area, bearer.principal, now);
418 Self {
419 claims: Some(claims),
420 scopes: Some(bearer.scopes.into()),
421 ..Self::default()
422 }
423 }
424
425 #[cfg(feature = "engine")]
426 pub(crate) fn for_loader(&self) -> Self {
427 Self {
428 changes: Arc::default(),
429 ..self.clone()
430 }
431 }
432
433 #[cfg(feature = "client-headers")]
434 pub(crate) fn with_client(self, request: &http::request::Parts) -> Self {
435 Self {
436 client: Client::of(request),
437 ..self
438 }
439 }
440
441 #[cfg(feature = "session-list")]
442 pub(crate) async fn confirmed(
443 mut self,
444 sessions: &Sessions,
445 db: &sofie_db::Db,
446 now: Timestamp,
447 ) -> Result<Self> {
448 let (Some(session), Some(members)) = (self.recorded, sessions.members.as_deref()) else {
449 return Ok(self);
450 };
451 let seen = self
452 .claims
453 .as_ref()
454 .is_some_and(|claims| claims.needs_refresh(now))
455 .then_some(now);
456 match sessions.records.live(members, db, session, seen).await? {
457 Some(SessionRecord::Live {
458 account,
459 impersonator,
460 ..
461 }) => {
462 self.account = Some(account);
463 if let Some(impersonator) = impersonator {
464 tracing::info!(
465 impersonation.account = %account,
466 impersonation.impersonator = %impersonator,
467 "a request made while impersonating"
468 );
469 }
470 }
471 Some(_) => {}
472 None => {
473 self.claims = None;
474 self.recorded = None;
475 self.is_dead = true;
476 }
477 }
478 Ok(self)
479 }
480
481 #[cfg(feature = "tokens")]
482 pub(crate) fn scopes(&self) -> Option<Arc<[String]>> {
483 self.scopes.clone()
484 }
485
486 #[cfg_attr(
487 not(feature = "tokens"),
488 expect(clippy::unused_self, reason = "only a token opens a bearer session")
489 )]
490 pub(crate) fn is_bearer(&self) -> bool {
491 #[cfg(feature = "tokens")]
492 return self.scopes.is_some();
493 #[cfg(not(feature = "tokens"))]
494 false
495 }
496
497 pub(crate) fn principal(&self) -> Option<Uuid> {
498 self.claims.as_ref().map(SessionClaims::principal)
499 }
500
501 #[cfg(feature = "remote-contracts")]
502 pub(crate) fn acting(&self) -> Option<sofie_auth::Acting> {
503 self.claims.as_ref().map(|claims| sofie_auth::Acting {
504 area: claims.area().to_owned(),
505 principal: claims.principal(),
506 })
507 }
508
509 pub(crate) fn principal_in(&self, area: &str) -> Option<Uuid> {
510 self.claims
511 .as_ref()
512 .filter(|claims| claims.area() == area)
513 .map(SessionClaims::principal)
514 }
515
516 pub(crate) fn committed(&self) {
517 unpoisoned::lock(&self.changes).committed = true;
518 }
519
520 pub(crate) fn signed_in_area(&self) -> Option<&'static str> {
521 match self.committed_change() {
522 Some(Change::SignIn { area, .. }) => Some(area),
523 _ => None,
524 }
525 }
526
527 #[cfg(feature = "flows")]
528 fn begun_flow(&self) -> Option<String> {
529 let changes = unpoisoned::lock(&self.changes);
530 changes.begun_flow.clone().filter(|_| changes.committed)
531 }
532
533 fn committed_change(&self) -> Option<Change> {
534 let changes = unpoisoned::lock(&self.changes);
535 changes.requested.filter(|_| changes.committed)
536 }
537
538 pub(crate) fn finish(
539 &self,
540 mut response: Response,
541 sessions: &Sessions,
542 now: Timestamp,
543 caller: Caller<'_>,
544 ) -> Response {
545 if self.is_bearer() {
546 return response;
547 }
548 #[cfg(feature = "flows")]
549 if let Some(flow) = self.begun_flow() {
550 match sessions.cookies.begun_flow(&flow) {
551 Ok(cookie) => response.headers_mut().append(SET_COOKIE, cookie),
552 Err(error) => return answer::failure(error, caller),
553 };
554 }
555 match self.cookie(sessions, now) {
556 Ok(Some(cookie)) => {
557 response.headers_mut().append(SET_COOKIE, cookie);
558 response
559 }
560 Ok(None) => response,
561 Err(error) => answer::failure(error, caller),
562 }
563 }
564
565 fn cookie(&self, sessions: &Sessions, now: Timestamp) -> Result<Option<HeaderValue>> {
566 let (keys, cookies) = (&sessions.keys, &sessions.cookies);
567 match self.committed_change() {
568 Some(Change::SignIn {
569 session,
570 area,
571 principal,
572 issued_at,
573 }) => {
574 let claims = SessionClaims::new(session, area, principal, now)
575 .issued_at(issued_at.unwrap_or(now));
576 cookies.sealed_session(keys, &claims, now).map(Some)
577 }
578 Some(Change::SignOut) => cookies.session_removal().map(Some),
579 None => self.refreshed_cookie(sessions, now),
580 }
581 }
582
583 fn refreshed_cookie(&self, sessions: &Sessions, now: Timestamp) -> Result<Option<HeaderValue>> {
584 let cookies = &sessions.cookies;
585 match &self.claims {
586 Some(claims) if claims.needs_refresh(now) => cookies
587 .sealed_session(&sessions.keys, &claims.clone().seen_at(now), now)
588 .map(Some),
589 None if self.is_dead => cookies.session_removal().map(Some),
590 Some(_) | None => Ok(None),
591 }
592 }
593}