use std::fmt;
use std::time::Duration;
use ferrohealth_secret::secret::Secret;
use ferrohealth_ui::view::session::Left;
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Eq)]
pub enum Credential {
Bearer(Secret),
Basic {
username: String,
password: Secret,
},
}
impl fmt::Debug for Credential {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bearer(_) => f.write_str("Bearer(..)"),
Self::Basic { username, .. } => f
.debug_struct("Basic")
.field("username", username)
.finish_non_exhaustive(),
}
}
}
pub struct SignedIn {
pub(crate) operator: Option<String>,
pub(crate) subject: Option<String>,
pub(crate) scopes: Vec<String>,
pub(crate) credential: Credential,
pub(crate) expires_in: Option<Duration>,
pub(crate) refresh_token: Option<Secret>,
pub(crate) id_token: Option<Secret>,
}
impl fmt::Debug for SignedIn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SignedIn")
.field("operator", &self.operator)
.field("subject", &self.subject)
.field("scopes", &self.scopes)
.field("credential", &self.credential)
.field("expires_in", &self.expires_in)
.finish_non_exhaustive()
}
}
impl SignedIn {
#[must_use]
pub fn from_oidc(signed_in: ferrohealth_smart::oidc::SignedIn) -> Self {
Self {
operator: signed_in.claims.display_name().map(str::to_owned),
subject: Some(signed_in.claims.subject.clone()),
scopes: signed_in.scopes,
credential: Credential::Bearer(signed_in.access_token),
expires_in: signed_in.expires_in,
refresh_token: signed_in.refresh_token,
id_token: Some(signed_in.id_token),
}
}
#[must_use]
pub fn basic(username: impl Into<String>, password: Secret) -> Self {
let username = username.into();
Self {
operator: Some(username.clone()),
subject: None,
scopes: Vec::new(),
credential: Credential::Basic { username, password },
expires_in: None,
refresh_token: None,
id_token: None,
}
}
}
#[derive(Clone)]
pub struct Session {
pub(crate) operator: Option<String>,
pub(crate) subject: Option<String>,
pub(crate) scopes: Vec<String>,
pub(crate) credential: Credential,
pub(crate) signed_in_at: i64,
pub(crate) left: Left,
}
impl fmt::Debug for Session {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Session")
.field("operator", &self.operator)
.field("scopes", &self.scopes)
.field("credential", &self.credential)
.field("signed_in_at", &self.signed_in_at)
.field("left", &self.left)
.finish_non_exhaustive()
}
}
impl Session {
#[must_use]
pub fn operator(&self) -> Option<&str> {
self.operator.as_deref()
}
#[must_use]
pub fn subject(&self) -> Option<&str> {
self.subject.as_deref()
}
#[must_use]
pub fn scopes(&self) -> &[String] {
&self.scopes
}
#[must_use]
pub const fn credential(&self) -> &Credential {
&self.credential
}
#[must_use]
pub const fn signed_in_at(&self) -> i64 {
self.signed_in_at
}
#[must_use]
pub const fn left(&self) -> Left {
self.left
}
}
#[derive(Debug, Clone)]
pub enum Held {
SignedIn(Session),
TokenExpired,
Ended,
Absent,
}
pub struct Ended {
pub id_token: Option<Secret>,
pub revocable: Option<(Secret, ferrohealth_smart::oidc::TokenTypeHint)>,
}
impl fmt::Debug for Ended {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Ended")
.field("hint", &self.revocable.as_ref().map(|(_, hint)| hint))
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub(crate) struct Record {
pub(crate) operator: Option<String>,
pub(crate) subject: Option<String>,
pub(crate) scopes: Vec<String>,
pub(crate) credential: Credential,
pub(crate) expires: Option<i64>,
pub(crate) lifetime: Option<u64>,
pub(crate) refresh_token: Option<Secret>,
pub(crate) id_token: Option<Secret>,
pub(crate) signed_in: i64,
pub(crate) touched: i64,
}
impl Record {
pub(crate) fn begin(signed_in: SignedIn, now: i64) -> Self {
Self {
operator: signed_in.operator,
subject: signed_in.subject,
scopes: signed_in.scopes,
credential: signed_in.credential,
expires: signed_in
.expires_in
.map(|lifetime| now.saturating_add(crate::lifetime::seconds(lifetime))),
lifetime: signed_in.expires_in.map(|lifetime| lifetime.as_secs()),
refresh_token: signed_in.refresh_token,
id_token: signed_in.id_token,
signed_in: now,
touched: now,
}
}
pub(crate) fn times(&self) -> crate::lifetime::Times {
crate::lifetime::Times {
signed_in: self.signed_in,
touched: self.touched,
last_token: if self.refresh_token.is_some() {
None
} else {
self.expires
},
}
}
pub(crate) fn token_valid(&self, now: i64) -> bool {
self.expires.is_none_or(|expires| now < expires)
}
pub(crate) fn session(&self, left: Left) -> Session {
Session {
operator: self.operator.clone(),
subject: self.subject.clone(),
scopes: self.scopes.clone(),
credential: self.credential.clone(),
signed_in_at: self.signed_in,
left,
}
}
pub(crate) fn ended(self) -> Ended {
use ferrohealth_smart::oidc::TokenTypeHint;
let revocable = match (self.refresh_token, self.credential) {
(Some(refresh), _) => Some((refresh, TokenTypeHint::RefreshToken)),
(None, Credential::Bearer(access)) => Some((access, TokenTypeHint::AccessToken)),
(None, Credential::Basic { .. }) => None,
};
Ended {
id_token: self.id_token,
revocable,
}
}
}
#[derive(Serialize, Deserialize)]
pub(crate) struct Sealed {
operator: Option<String>,
subject: Option<String>,
scopes: Vec<String>,
bearer: Option<String>,
basic: Option<(String, String)>,
expires: Option<i64>,
lifetime: Option<u64>,
id_token: Option<String>,
signed_in: i64,
touched: i64,
}
impl From<&Record> for Sealed {
fn from(record: &Record) -> Self {
let (bearer, basic) = match &record.credential {
Credential::Bearer(token) => (Some(token.expose().to_owned()), None),
Credential::Basic { username, password } => {
(None, Some((username.clone(), password.expose().to_owned())))
}
};
Self {
operator: record.operator.clone(),
subject: record.subject.clone(),
scopes: record.scopes.clone(),
bearer,
basic,
expires: record.expires,
lifetime: record.lifetime,
id_token: record
.id_token
.as_ref()
.map(|token| token.expose().to_owned()),
signed_in: record.signed_in,
touched: record.touched,
}
}
}
impl Sealed {
pub(crate) fn record(self) -> Option<Record> {
let credential = match (self.bearer, self.basic) {
(Some(token), None) => Credential::Bearer(Secret::new(token)),
(None, Some((username, password))) => Credential::Basic {
username,
password: Secret::new(password),
},
_ => return None,
};
Some(Record {
operator: self.operator,
subject: self.subject,
scopes: self.scopes,
credential,
expires: self.expires,
lifetime: self.lifetime,
refresh_token: None,
id_token: self.id_token.map(Secret::new),
signed_in: self.signed_in,
touched: self.touched,
})
}
}