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 },
152 SignOut,
153}
154
155pub trait Admitter {}
159
160#[derive(Clone, Copy)]
172pub struct SignInRight {
173 sealed: (),
174}
175
176const _: () = assert!(size_of::<SignInRight>() == 0);
177
178impl SignInRight {
179 #[must_use]
180 pub const fn of<K: Admitter>() -> Self {
181 Self { sealed: () }
182 }
183}
184
185impl Session {
186 pub fn sign_in<A: Area>(
197 &self,
198 right: SignInRight,
199 principal: Id<PrincipalRecord<A>>,
200 ) -> Result<Uuid> {
201 let SignInRight { sealed: () } = right;
202 let session = Uuid::new_v4()
203 .map_err(|error| sofie_core::Error::internal("choosing a session id", error))?;
204 #[cfg(feature = "product-events")]
205 sofie_telemetry::attach_principal(&tracing::Span::current(), principal.uuid());
206 self.request(Change::SignIn {
207 session,
208 area: A::NAME,
209 principal: principal.uuid(),
210 });
211 Ok(session)
212 }
213
214 #[must_use]
215 pub fn current(&self) -> Option<Uuid> {
216 self.current
217 }
218
219 #[cfg(feature = "client-headers")]
221 #[must_use]
222 pub fn user_agent(&self) -> Option<std::borrow::Cow<'_, str>> {
223 let value = self.client.user_agent.as_ref()?;
224 Some(String::from_utf8_lossy(value.as_bytes()))
225 }
226
227 #[cfg(feature = "client-headers")]
229 #[must_use]
230 pub fn origin(&self) -> Option<&str> {
231 self.client.origin.as_ref()?.to_str().ok()
232 }
233
234 pub fn sign_out(&self) {
235 self.request(Change::SignOut);
236 }
237
238 #[cfg(feature = "flows")]
241 pub fn begin_flow(&self) -> Result<String> {
242 let value = Uuid::new_v4()
243 .map_err(|error| sofie_core::Error::internal("choosing a flow's value", error))?
244 .to_string();
245 unpoisoned::lock(&self.changes).begun_flow = Some(value.clone());
246 Ok(value)
247 }
248
249 #[cfg(feature = "flows")]
250 #[must_use]
251 pub fn flow(&self) -> Option<&str> {
252 self.flow.as_deref()
253 }
254
255 fn request(&self, change: Change) {
256 unpoisoned::lock(&self.changes).requested = Some(change);
257 }
258}
259
260impl<A: Area> Resolve<RequestContext<A>> for Session {
261 fn resolve(context: &mut RequestContext<A>) -> impl Future<Output = Result<Self>> + Send {
262 let session = context.session();
263 std::future::ready(Ok(Self {
264 changes: Arc::clone(&session.changes),
265 current: session.recorded,
266 #[cfg(feature = "flows")]
267 flow: session.flow.clone(),
268 #[cfg(feature = "client-headers")]
269 client: session.client.clone(),
270 }))
271 }
272}
273
274pub struct CurrentSession {
276 id: Option<Uuid>,
277 account: Option<Uuid>,
278}
279
280impl CurrentSession {
281 #[must_use]
282 pub fn id(&self) -> Option<Uuid> {
283 self.id
284 }
285
286 #[must_use]
289 pub fn account(&self) -> Option<Uuid> {
290 self.account
291 }
292}
293
294impl<A: Area, M> Resolve<RequestContext<A, M>> for CurrentSession {
295 fn resolve(context: &mut RequestContext<A, M>) -> impl Future<Output = Result<Self>> + Send {
296 let session = context.session();
297 std::future::ready(Ok(Self {
298 id: session.recorded,
299 #[cfg(feature = "session-list")]
300 account: session.account,
301 #[cfg(not(feature = "session-list"))]
302 account: None,
303 }))
304 }
305}
306
307pub(crate) struct Sessions {
308 pub(crate) keys: Arc<SigningKeys>,
309 #[cfg(any(feature = "tokens", feature = "session-list"))]
310 pub(crate) members: Option<Arc<dyn AreaMembers>>,
311 pub(crate) cookies: CookieNames,
312 #[cfg(feature = "session-list")]
313 pub(crate) records: crate::session_cache::SessionCache,
314}
315
316impl Sessions {
317 pub(crate) fn open(&self, headers: &HeaderMap, now: Timestamp) -> RequestSession {
318 let sealed = self.cookies.session_value(headers);
319 let claims = sealed.and_then(|sealed| opened(&self.keys, sealed, now));
320 let is_dead = sealed.is_some() && claims.is_none();
321 let recorded = claims.as_ref().map(SessionClaims::session);
322 #[cfg(all(feature = "toolbar", debug_assertions))]
323 let toolbar = crate::toolbar::Overrides::of(headers, self.cookies.toolbar());
324 #[cfg(all(feature = "toolbar", debug_assertions))]
325 let (claims, recorded) = match toolbar.claims(now) {
326 Some(acting) => (Some(acting), None),
327 None => (claims, recorded),
328 };
329 RequestSession {
330 claims,
331 recorded,
332 #[cfg(feature = "flows")]
333 flow: self.cookies.flow_value(headers).map(str::to_owned),
334 is_dead,
335 #[cfg(all(feature = "toolbar", debug_assertions))]
336 toolbar,
337 ..RequestSession::default()
338 }
339 }
340}
341
342fn opened(keys: &SigningKeys, sealed: &str, now: Timestamp) -> Option<SessionClaims> {
343 SessionClaims::open(keys, sealed, now)
344 .map_err(log_unopened)
345 .ok()
346}
347
348fn log_unopened(error: OpenError) {
350 let refused = match error {
351 OpenError::Malformed => "malformed",
352 OpenError::Unauthenticated => "unauthenticated",
353 OpenError::Expired => "expired",
354 OpenError::Corrupt => "corrupt",
355 };
356 match error {
357 OpenError::Malformed | OpenError::Expired => {
358 tracing::debug!(session.refused = refused, "the session cookie did not open");
359 }
360 OpenError::Unauthenticated => {
361 tracing::info!(session.refused = refused, "the session cookie did not open");
362 }
363 OpenError::Corrupt => {
364 tracing::error!(error.kind = "session", session.refused = refused, error = %error, "the session cookie did not open");
365 }
366 }
367}
368
369#[derive(Clone, Default)]
370pub(crate) struct RequestSession {
371 claims: Option<SessionClaims>,
372 recorded: Option<Uuid>,
374 #[cfg(feature = "session-list")]
376 account: Option<Uuid>,
377 #[cfg(feature = "flows")]
378 flow: Option<String>,
379 #[cfg(feature = "client-headers")]
380 client: Client,
381 is_dead: bool,
383 changes: Arc<Mutex<Changes>>,
384 #[cfg(feature = "tokens")]
386 scopes: Option<Arc<[String]>>,
387 #[cfg(all(feature = "toolbar", debug_assertions))]
389 pub(crate) toolbar: crate::toolbar::Overrides,
390}
391
392impl RequestSession {
393 #[cfg(feature = "tokens")]
394 pub(crate) fn bearer(area: &str, bearer: crate::Bearer, now: Timestamp) -> Self {
395 let claims = SessionClaims::new(bearer.token, area, bearer.principal, now);
396 Self {
397 claims: Some(claims),
398 scopes: Some(bearer.scopes.into()),
399 ..Self::default()
400 }
401 }
402
403 #[cfg(feature = "engine")]
404 pub(crate) fn for_loader(&self) -> Self {
405 Self {
406 changes: Arc::default(),
407 ..self.clone()
408 }
409 }
410
411 #[cfg(feature = "client-headers")]
412 pub(crate) fn with_client(self, request: &http::request::Parts) -> Self {
413 Self {
414 client: Client::of(request),
415 ..self
416 }
417 }
418
419 #[cfg(feature = "session-list")]
420 pub(crate) async fn confirmed(
421 mut self,
422 sessions: &Sessions,
423 db: &sofie_db::Db,
424 now: Timestamp,
425 ) -> Result<Self> {
426 let (Some(session), Some(members)) = (self.recorded, sessions.members.as_deref()) else {
427 return Ok(self);
428 };
429 let seen = self
430 .claims
431 .as_ref()
432 .is_some_and(|claims| claims.needs_refresh(now))
433 .then_some(now);
434 match sessions.records.live(members, db, session, seen).await? {
435 Some(SessionRecord::Live {
436 account,
437 impersonator,
438 ..
439 }) => {
440 self.account = Some(account);
441 if let Some(impersonator) = impersonator {
442 tracing::info!(
443 impersonation.account = %account,
444 impersonation.impersonator = %impersonator,
445 "a request made while impersonating"
446 );
447 }
448 }
449 Some(_) => {}
450 None => {
451 self.claims = None;
452 self.recorded = None;
453 self.is_dead = true;
454 }
455 }
456 Ok(self)
457 }
458
459 #[cfg(feature = "tokens")]
460 pub(crate) fn scopes(&self) -> Option<Arc<[String]>> {
461 self.scopes.clone()
462 }
463
464 #[cfg_attr(
465 not(feature = "tokens"),
466 expect(clippy::unused_self, reason = "only a token opens a bearer session")
467 )]
468 pub(crate) fn is_bearer(&self) -> bool {
469 #[cfg(feature = "tokens")]
470 return self.scopes.is_some();
471 #[cfg(not(feature = "tokens"))]
472 false
473 }
474
475 pub(crate) fn principal(&self) -> Option<Uuid> {
476 self.claims.as_ref().map(SessionClaims::principal)
477 }
478
479 #[cfg(feature = "remote-contracts")]
480 pub(crate) fn acting(&self) -> Option<sofie_auth::Acting> {
481 self.claims.as_ref().map(|claims| sofie_auth::Acting {
482 area: claims.area().to_owned(),
483 principal: claims.principal(),
484 })
485 }
486
487 pub(crate) fn principal_in(&self, area: &str) -> Option<Uuid> {
488 self.claims
489 .as_ref()
490 .filter(|claims| claims.area() == area)
491 .map(SessionClaims::principal)
492 }
493
494 pub(crate) fn committed(&self) {
495 unpoisoned::lock(&self.changes).committed = true;
496 }
497
498 pub(crate) fn signed_in_area(&self) -> Option<&'static str> {
499 match self.committed_change() {
500 Some(Change::SignIn { area, .. }) => Some(area),
501 _ => None,
502 }
503 }
504
505 #[cfg(feature = "flows")]
506 fn begun_flow(&self) -> Option<String> {
507 let changes = unpoisoned::lock(&self.changes);
508 changes.begun_flow.clone().filter(|_| changes.committed)
509 }
510
511 fn committed_change(&self) -> Option<Change> {
512 let changes = unpoisoned::lock(&self.changes);
513 changes.requested.filter(|_| changes.committed)
514 }
515
516 pub(crate) fn finish(
517 &self,
518 mut response: Response,
519 sessions: &Sessions,
520 now: Timestamp,
521 caller: Caller<'_>,
522 ) -> Response {
523 if self.is_bearer() {
524 return response;
525 }
526 #[cfg(feature = "flows")]
527 if let Some(flow) = self.begun_flow() {
528 match sessions.cookies.begun_flow(&flow) {
529 Ok(cookie) => response.headers_mut().append(SET_COOKIE, cookie),
530 Err(error) => return answer::failure(error, caller),
531 };
532 }
533 match self.cookie(sessions, now) {
534 Ok(Some(cookie)) => {
535 response.headers_mut().append(SET_COOKIE, cookie);
536 response
537 }
538 Ok(None) => response,
539 Err(error) => answer::failure(error, caller),
540 }
541 }
542
543 fn cookie(&self, sessions: &Sessions, now: Timestamp) -> Result<Option<HeaderValue>> {
544 let (keys, cookies) = (&sessions.keys, &sessions.cookies);
545 match self.committed_change() {
546 Some(Change::SignIn {
547 session,
548 area,
549 principal,
550 }) => cookies
551 .sealed_session(
552 keys,
553 &SessionClaims::new(session, area, principal, now),
554 now,
555 )
556 .map(Some),
557 Some(Change::SignOut) => cookies.session_removal().map(Some),
558 None => self.refreshed_cookie(sessions, now),
559 }
560 }
561
562 fn refreshed_cookie(&self, sessions: &Sessions, now: Timestamp) -> Result<Option<HeaderValue>> {
563 let cookies = &sessions.cookies;
564 match &self.claims {
565 Some(claims) if claims.needs_refresh(now) => cookies
566 .sealed_session(&sessions.keys, &claims.clone().seen_at(now), now)
567 .map(Some),
568 None if self.is_dead => cookies.session_removal().map(Some),
569 Some(_) | None => Ok(None),
570 }
571 }
572}