mod authenticate;
mod github;
mod solana;
mod token;
use std::{collections::HashMap, ops::Deref, sync::Arc};
use reifydb_catalog::{catalog::Catalog, create_token};
use reifydb_core::interface::catalog::token::Token;
use reifydb_runtime::context::{clock::Clock, rng::Rng as SystemRng};
use reifydb_transaction::transaction::{Transaction, admin::AdminTransaction, query::QueryTransaction};
use reifydb_value::{
error::Error,
value::{Value, datetime::DateTime, duration::Duration, identity::IdentityId, value_type::ValueType},
};
use crate::{
challenge::ChallengeStore,
github::{GithubApi, GithubConfig, default_api},
registry::AuthenticationRegistry,
};
pub trait AuthEngine: Send + Sync {
fn begin_admin(&self) -> Result<AdminTransaction, Error>;
fn begin_query(&self) -> Result<QueryTransaction, Error>;
fn catalog(&self) -> Catalog;
}
#[derive(Debug, Clone)]
pub enum AuthResponse {
Authenticated {
identity: IdentityId,
token: String,
},
Challenge {
challenge_id: String,
payload: HashMap<String, String>,
},
Failed {
reason: String,
},
}
pub struct AuthConfigurator {
session_ttl: Option<Duration>,
challenge_ttl: Duration,
github: Option<GithubConfig>,
}
impl Default for AuthConfigurator {
fn default() -> Self {
Self::new()
}
}
impl AuthConfigurator {
pub fn new() -> Self {
Self {
session_ttl: Some(Duration::from_seconds(24 * 60 * 60).unwrap()),
challenge_ttl: Duration::from_seconds(60).unwrap(),
github: None,
}
}
pub fn session_ttl(mut self, ttl: Duration) -> Self {
self.session_ttl = Some(ttl);
self
}
pub fn no_session_ttl(mut self) -> Self {
self.session_ttl = None;
self
}
pub fn challenge_ttl(mut self, ttl: Duration) -> Self {
self.challenge_ttl = ttl;
self
}
pub fn github(mut self, config: GithubConfig) -> Self {
self.github = Some(config);
self
}
pub fn configure(self) -> AuthServiceConfig {
AuthServiceConfig {
session_ttl: self.session_ttl,
challenge_ttl: self.challenge_ttl,
github: self.github,
}
}
}
#[derive(Debug, Clone)]
pub struct AuthServiceConfig {
pub session_ttl: Option<Duration>,
pub challenge_ttl: Duration,
pub github: Option<GithubConfig>,
}
impl Default for AuthServiceConfig {
fn default() -> Self {
AuthConfigurator::new().configure()
}
}
pub struct Inner {
pub(crate) engine: Arc<dyn AuthEngine>,
pub(crate) auth_registry: Arc<AuthenticationRegistry>,
pub(crate) challenges: ChallengeStore,
pub(crate) rng: SystemRng,
pub(crate) clock: Clock,
pub(crate) session_ttl: Option<Duration>,
pub(crate) github: Option<GithubAuth>,
}
pub(crate) struct GithubAuth {
pub(crate) config: GithubConfig,
pub(crate) api: Arc<dyn GithubApi>,
}
#[derive(Clone)]
pub struct AuthService(Arc<Inner>);
impl Deref for AuthService {
type Target = Inner;
fn deref(&self) -> &Inner {
&self.0
}
}
impl AuthService {
pub fn new(
engine: Arc<dyn AuthEngine>,
auth_registry: Arc<AuthenticationRegistry>,
rng: SystemRng,
clock: Clock,
config: AuthServiceConfig,
) -> Self {
Self::with_github_api(engine, auth_registry, rng, clock, config, default_api())
}
pub fn with_github_api(
engine: Arc<dyn AuthEngine>,
auth_registry: Arc<AuthenticationRegistry>,
rng: SystemRng,
clock: Clock,
config: AuthServiceConfig,
api: Arc<dyn GithubApi>,
) -> Self {
Self(Arc::new(Inner {
engine,
auth_registry,
challenges: ChallengeStore::new(config.challenge_ttl),
rng,
clock,
session_ttl: config.session_ttl,
github: config.github.map(|config| GithubAuth {
config,
api,
}),
}))
}
pub(super) fn now(&self) -> Result<DateTime, Error> {
Ok(self.clock.now())
}
pub(super) fn expires_at(&self) -> Result<Option<DateTime>, Error> {
match self.session_ttl {
Some(ttl) => {
let ttl_nanos = ttl.as_nanos()? as u64;
let nanos = self.clock.now().to_nanos().saturating_add(ttl_nanos);
Ok(Some(DateTime::from_nanos(nanos)))
}
None => Ok(None),
}
}
pub(super) fn persist_token(&self, token: &str, identity: IdentityId) -> Result<Token, Error> {
let mut admin = self.engine.begin_admin()?;
let def = create_token(&mut admin, token, identity, self.expires_at()?, self.now()?)?;
admin.commit()?;
Ok(def)
}
pub fn create_token(
&self,
token: &str,
identity: IdentityId,
expires_at: Option<DateTime>,
) -> Result<Token, Error> {
let mut admin = self.engine.begin_admin()?;
let def = create_token(&mut admin, token, identity, expires_at, self.now()?)?;
admin.commit()?;
Ok(def)
}
pub(super) fn set_lookup_attribute(
&self,
admin: &mut AdminTransaction,
identity: IdentityId,
name: &str,
value: &str,
) -> Result<(), Error> {
let catalog = self.engine.catalog();
let attribute =
match catalog.find_identity_attribute_by_name(&mut Transaction::Admin(&mut *admin), name)? {
Some(attribute) => attribute,
None => catalog.create_identity_attribute(admin, name, ValueType::Utf8)?,
};
catalog.set_identity_attribute_value(admin, identity, &attribute, Value::Utf8(value.to_string()))?;
Ok(())
}
}
pub(super) fn generate_session_token(rng: &SystemRng) -> String {
let bytes = rng.infra_bytes_32();
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}