mirror of
https://github.com/CloudNebulaProject/webfingerd.git
synced 2026-04-10 13:10:41 +00:00
feat: add SeaORM entities, cache, auth helpers, and AppState
This commit is contained in:
parent
c993f4d703
commit
1d4873ba75
10 changed files with 451 additions and 0 deletions
44
src/auth.rs
Normal file
44
src/auth.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||
Argon2,
|
||||
};
|
||||
use base64::Engine;
|
||||
use rand::Rng;
|
||||
|
||||
/// Generate a prefixed token: `{id}.{random_secret}`.
|
||||
/// The id allows O(1) lookup; the secret is verified via argon2.
|
||||
/// The `id` parameter is the entity UUID this token belongs to.
|
||||
pub fn generate_token(id: &str) -> String {
|
||||
let bytes: [u8; 32] = rand::thread_rng().gen();
|
||||
let secret = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
|
||||
format!("{id}.{secret}")
|
||||
}
|
||||
|
||||
/// Generate a non-prefixed secret (for registration secrets that don't need lookup).
|
||||
pub fn generate_secret() -> String {
|
||||
let bytes: [u8; 32] = rand::thread_rng().gen();
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
|
||||
}
|
||||
|
||||
/// Hash a token (or its secret part) with argon2.
|
||||
pub fn hash_token(token: &str) -> Result<String, argon2::password_hash::Error> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
Ok(argon2.hash_password(token.as_bytes(), &salt)?.to_string())
|
||||
}
|
||||
|
||||
/// Verify a token against a stored argon2 hash.
|
||||
pub fn verify_token(token: &str, hash: &str) -> bool {
|
||||
let Ok(parsed_hash) = PasswordHash::new(hash) else {
|
||||
return false;
|
||||
};
|
||||
Argon2::default()
|
||||
.verify_password(token.as_bytes(), &parsed_hash)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Split a prefixed token into (id, secret).
|
||||
/// Returns None if the token is not in `id.secret` format.
|
||||
pub fn split_token(token: &str) -> Option<(&str, &str)> {
|
||||
token.split_once('.')
|
||||
}
|
||||
167
src/cache.rs
Normal file
167
src/cache.rs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
use dashmap::DashMap;
|
||||
use sea_orm::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::entity::{links, resources};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedLink {
|
||||
pub rel: String,
|
||||
pub href: Option<String>,
|
||||
pub link_type: Option<String>,
|
||||
pub titles: Option<String>,
|
||||
pub properties: Option<String>,
|
||||
pub template: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedResource {
|
||||
pub subject: String,
|
||||
pub aliases: Option<Vec<String>>,
|
||||
pub properties: Option<serde_json::Value>,
|
||||
pub links: Vec<CachedLink>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cache {
|
||||
inner: Arc<DashMap<String, CachedResource>>,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, resource_uri: &str) -> Option<CachedResource> {
|
||||
self.inner.get(resource_uri).map(|r| r.value().clone())
|
||||
}
|
||||
|
||||
pub fn set(&self, resource_uri: String, resource: CachedResource) {
|
||||
self.inner.insert(resource_uri, resource);
|
||||
}
|
||||
|
||||
pub fn remove(&self, resource_uri: &str) {
|
||||
self.inner.remove(resource_uri);
|
||||
}
|
||||
|
||||
/// Remove all cache entries for the given resource URIs.
|
||||
/// Callers should query the DB for all resource URIs belonging to a domain
|
||||
/// before deleting, then pass them here. This handles non-acct: URI schemes.
|
||||
pub fn remove_many(&self, resource_uris: &[String]) {
|
||||
for uri in resource_uris {
|
||||
self.inner.remove(uri.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
/// Load all non-expired resources and links from DB into cache.
|
||||
pub async fn hydrate(&self, db: &DatabaseConnection) -> Result<(), DbErr> {
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
|
||||
let all_resources = resources::Entity::find().all(db).await?;
|
||||
|
||||
for resource in all_resources {
|
||||
let resource_links = links::Entity::find()
|
||||
.filter(links::Column::ResourceId.eq(&resource.id))
|
||||
.filter(
|
||||
Condition::any()
|
||||
.add(links::Column::ExpiresAt.is_null())
|
||||
.add(links::Column::ExpiresAt.gt(now)),
|
||||
)
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
if resource_links.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cached = CachedResource {
|
||||
subject: resource.resource_uri.clone(),
|
||||
aliases: resource
|
||||
.aliases
|
||||
.as_deref()
|
||||
.and_then(|a| serde_json::from_str(a).ok()),
|
||||
properties: resource
|
||||
.properties
|
||||
.as_deref()
|
||||
.and_then(|p| serde_json::from_str(p).ok()),
|
||||
links: resource_links
|
||||
.into_iter()
|
||||
.map(|l| CachedLink {
|
||||
rel: l.rel,
|
||||
href: l.href,
|
||||
link_type: l.link_type,
|
||||
titles: l.titles,
|
||||
properties: l.properties,
|
||||
template: l.template,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
self.set(resource.resource_uri, cached);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rebuild cache entry for a single resource from DB.
|
||||
pub async fn refresh_resource(
|
||||
&self,
|
||||
db: &DatabaseConnection,
|
||||
resource_uri: &str,
|
||||
) -> Result<(), DbErr> {
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
|
||||
let resource = resources::Entity::find()
|
||||
.filter(resources::Column::ResourceUri.eq(resource_uri))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
let Some(resource) = resource else {
|
||||
self.remove(resource_uri);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let resource_links = links::Entity::find()
|
||||
.filter(links::Column::ResourceId.eq(&resource.id))
|
||||
.filter(
|
||||
Condition::any()
|
||||
.add(links::Column::ExpiresAt.is_null())
|
||||
.add(links::Column::ExpiresAt.gt(now)),
|
||||
)
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
if resource_links.is_empty() {
|
||||
self.remove(resource_uri);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let cached = CachedResource {
|
||||
subject: resource.resource_uri.clone(),
|
||||
aliases: resource
|
||||
.aliases
|
||||
.as_deref()
|
||||
.and_then(|a| serde_json::from_str(a).ok()),
|
||||
properties: resource
|
||||
.properties
|
||||
.as_deref()
|
||||
.and_then(|p| serde_json::from_str(p).ok()),
|
||||
links: resource_links
|
||||
.into_iter()
|
||||
.map(|l| CachedLink {
|
||||
rel: l.rel,
|
||||
href: l.href,
|
||||
link_type: l.link_type,
|
||||
titles: l.titles,
|
||||
properties: l.properties,
|
||||
template: l.template,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
self.set(resource.resource_uri, cached);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
25
src/challenge.rs
Normal file
25
src/challenge.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
/// Trait for verifying domain ownership challenges (DNS-01 and HTTP-01).
|
||||
#[async_trait]
|
||||
pub trait ChallengeVerifier: Send + Sync + std::fmt::Debug {
|
||||
async fn verify_dns01(&self, domain: &str, expected_token: &str) -> Result<bool, String>;
|
||||
async fn verify_http01(&self, domain: &str, expected_token: &str) -> Result<bool, String>;
|
||||
}
|
||||
|
||||
/// Production implementation using real DNS and HTTP lookups.
|
||||
#[derive(Debug)]
|
||||
pub struct RealChallengeVerifier;
|
||||
|
||||
#[async_trait]
|
||||
impl ChallengeVerifier for RealChallengeVerifier {
|
||||
async fn verify_dns01(&self, _domain: &str, _expected_token: &str) -> Result<bool, String> {
|
||||
// TODO: implement with hickory-resolver in a later task
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn verify_http01(&self, _domain: &str, _expected_token: &str) -> Result<bool, String> {
|
||||
// TODO: implement with reqwest in a later task
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
47
src/entity/domains.rs
Normal file
47
src/entity/domains.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "domains")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
#[sea_orm(unique)]
|
||||
pub domain: String,
|
||||
pub owner_token_hash: String,
|
||||
pub registration_secret: String,
|
||||
pub challenge_type: String,
|
||||
pub challenge_token: Option<String>,
|
||||
pub verified: bool,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub verified_at: Option<chrono::NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::resources::Entity")]
|
||||
Resources,
|
||||
#[sea_orm(has_many = "super::service_tokens::Entity")]
|
||||
ServiceTokens,
|
||||
#[sea_orm(has_many = "super::links::Entity")]
|
||||
Links,
|
||||
}
|
||||
|
||||
impl Related<super::resources::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Resources.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::service_tokens::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServiceTokens.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::links::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Links.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
63
src/entity/links.rs
Normal file
63
src/entity/links.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "links")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub resource_id: String,
|
||||
pub service_token_id: String,
|
||||
pub domain_id: String,
|
||||
pub rel: String,
|
||||
pub href: Option<String>,
|
||||
#[sea_orm(column_name = "type")]
|
||||
pub link_type: Option<String>,
|
||||
pub titles: Option<String>,
|
||||
pub properties: Option<String>,
|
||||
pub template: Option<String>,
|
||||
pub ttl_seconds: Option<i32>,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub expires_at: Option<chrono::NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::resources::Entity",
|
||||
from = "Column::ResourceId",
|
||||
to = "super::resources::Column::Id"
|
||||
)]
|
||||
Resource,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::service_tokens::Entity",
|
||||
from = "Column::ServiceTokenId",
|
||||
to = "super::service_tokens::Column::Id"
|
||||
)]
|
||||
ServiceToken,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::domains::Entity",
|
||||
from = "Column::DomainId",
|
||||
to = "super::domains::Column::Id"
|
||||
)]
|
||||
Domain,
|
||||
}
|
||||
|
||||
impl Related<super::resources::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Resource.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::service_tokens::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ServiceToken.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::domains::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Domain.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
4
src/entity/mod.rs
Normal file
4
src/entity/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod domains;
|
||||
pub mod links;
|
||||
pub mod resources;
|
||||
pub mod service_tokens;
|
||||
41
src/entity/resources.rs
Normal file
41
src/entity/resources.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "resources")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub domain_id: String,
|
||||
#[sea_orm(unique)]
|
||||
pub resource_uri: String,
|
||||
pub aliases: Option<String>,
|
||||
pub properties: Option<String>,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub updated_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::domains::Entity",
|
||||
from = "Column::DomainId",
|
||||
to = "super::domains::Column::Id"
|
||||
)]
|
||||
Domain,
|
||||
#[sea_orm(has_many = "super::links::Entity")]
|
||||
Links,
|
||||
}
|
||||
|
||||
impl Related<super::domains::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Domain.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::links::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Links.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
41
src/entity/service_tokens.rs
Normal file
41
src/entity/service_tokens.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "service_tokens")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub domain_id: String,
|
||||
pub name: String,
|
||||
pub token_hash: String,
|
||||
pub allowed_rels: String,
|
||||
pub resource_pattern: String,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub revoked_at: Option<chrono::NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::domains::Entity",
|
||||
from = "Column::DomainId",
|
||||
to = "super::domains::Column::Id"
|
||||
)]
|
||||
Domain,
|
||||
#[sea_orm(has_many = "super::links::Entity")]
|
||||
Links,
|
||||
}
|
||||
|
||||
impl Related<super::domains::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Domain.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::links::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Links.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
|
@ -1,2 +1,7 @@
|
|||
pub mod auth;
|
||||
pub mod cache;
|
||||
pub mod challenge;
|
||||
pub mod config;
|
||||
pub mod entity;
|
||||
pub mod error;
|
||||
pub mod state;
|
||||
|
|
|
|||
14
src/state.rs
Normal file
14
src/state.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
use sea_orm::DatabaseConnection;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::cache::Cache;
|
||||
use crate::challenge::ChallengeVerifier;
|
||||
use crate::config::Settings;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub db: DatabaseConnection,
|
||||
pub cache: Cache,
|
||||
pub settings: Arc<Settings>,
|
||||
pub challenge_verifier: Arc<dyn ChallengeVerifier>,
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue