use std::time::Duration;
use jiff::Timestamp;
use tokio::sync::broadcast::error::RecvError;
use tokio::sync::{Mutex, watch};
use tokio::task::JoinHandle;
use tokio::time::{Instant, MissedTickBehavior};
use super::{Auth, AuthEvent};
use crate::store::{Session, User};
use crate::types::{Access, UserId, UserInfo};
const RECHECK: Duration = Duration::from_secs(5 * 60);
impl Auth {
pub fn guard(&self, user: &User, session: &Session) -> SessionGuard {
let (ended_tx, ended) = watch::channel(false);
let (changed_tx, changed) = watch::channel(user.info());
let watcher = Watcher {
auth: self.clone(),
events: self.events(),
session: session.id.clone(),
user: user.id,
impersonator: session.impersonator,
expires_at: session.expires_at,
ended: ended_tx,
changed: changed_tx,
};
SessionGuard {
ended: Mutex::new(ended),
changed: Mutex::new(changed),
task: tokio::spawn(watcher.run()),
}
}
}
pub struct SessionGuard {
ended: Mutex<watch::Receiver<bool>>,
changed: Mutex<watch::Receiver<UserInfo>>,
task: JoinHandle<()>,
}
impl SessionGuard {
pub async fn ended(&self) {
let mut ended = self.ended.lock().await;
let _ = ended.wait_for(|ended| *ended).await;
}
pub async fn changed(&self) -> UserInfo {
let mut changed = self.changed.lock().await;
if changed.changed().await.is_err() {
return std::future::pending().await;
}
changed.borrow_and_update().clone()
}
}
impl Drop for SessionGuard {
fn drop(&mut self) {
self.task.abort();
}
}
struct Watcher {
auth: Auth,
events: tokio::sync::broadcast::Receiver<AuthEvent>,
session: String,
user: UserId,
impersonator: Option<UserId>,
expires_at: Timestamp,
ended: watch::Sender<bool>,
changed: watch::Sender<UserInfo>,
}
impl Watcher {
async fn run(mut self) {
let expiry = tokio::time::sleep(until(self.expires_at));
tokio::pin!(expiry);
let mut recheck = tokio::time::interval_at(Instant::now() + RECHECK, RECHECK);
recheck.set_missed_tick_behavior(MissedTickBehavior::Delay);
let mut live = self.recheck().await;
while live {
live = tokio::select! {
_ = &mut expiry => false,
_ = recheck.tick() => self.recheck().await,
event = self.events.recv() => match event {
Ok(event) => self.apply(event),
Err(RecvError::Lagged(_)) => self.recheck().await,
Err(RecvError::Closed) => false,
},
};
}
let _ = self.ended.send(true);
}
fn apply(&self, event: AuthEvent) -> bool {
match event {
AuthEvent::SessionEnded(id) => id != self.session,
AuthEvent::UserSessionsEnded { user, keep } => {
user != self.user || keep.as_deref() == Some(self.session.as_str())
}
AuthEvent::UserDeleted(user) => user != self.user && Some(user) != self.impersonator,
AuthEvent::UserChanged(info) if Some(info.id) == self.impersonator => {
info.access == Access::Active && info.is_admin()
}
AuthEvent::UserChanged(info) if info.id == self.user => {
if info.access == Access::Revoked {
return false;
}
self.change(info);
true
}
AuthEvent::UserChanged(_) | AuthEvent::UserCreated(_) => true,
}
}
async fn recheck(&self) -> bool {
match self.auth.session(&self.session).await {
Ok(Some((user, _))) => {
self.change(user.info());
true
}
Ok(None) => false,
Err(error) => {
tracing::warn!(error = ?error, "auth: re-checking a socket's session failed");
true
}
}
}
fn change(&self, info: UserInfo) {
self.changed.send_if_modified(|current| {
if *current == info {
return false;
}
*current = info;
true
});
}
}
fn until(at: Timestamp) -> Duration {
let left = at.duration_since(Timestamp::now());
Duration::try_from(left).unwrap_or(Duration::ZERO)
}