Files
papercrate/backend/src/auth/mod.rs
T
nils 9bf1fc1983
ci / docker (frontend, frontend/Dockerfile, frontend) (push) Failing after 49s
ci / docker (backend, backend/Dockerfile, backend) (push) Failing after 17m1s
fix
2025-11-07 11:29:48 +01:00

147 lines
4.0 KiB
Rust

pub mod api_tokens;
pub mod capability_guard;
pub mod capability_sets;
pub mod jwt;
pub mod passkeys;
pub mod password;
use std::sync::{Arc, Mutex};
use axum::{async_trait, extract::FromRequestParts, http::request::Parts};
use axum_extra::headers::{authorization::Bearer, Authorization};
use axum_extra::TypedHeader;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::{
auth::capability_sets::{load_capabilities_for_set, load_capability_set},
error::AppError,
models::ApiCapability,
state::{AppState, PgPooledConnection},
};
use uuid::Uuid;
use crate::auth::jwt::PrincipalKind;
#[derive(Clone)]
pub struct TenantConnectionHolder {
inner: Arc<Mutex<Option<PgPooledConnection>>>,
}
impl TenantConnectionHolder {
pub fn new(conn: PgPooledConnection) -> Self {
Self {
inner: Arc::new(Mutex::new(Some(conn))),
}
}
pub fn into_conn(self) -> Option<PgPooledConnection> {
self.inner.lock().ok()?.take()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct AuthenticatedUser {
pub user_id: uuid::Uuid,
pub username: String,
pub tenant_id: uuid::Uuid,
pub principal_kind: PrincipalKind,
pub principal_id: Uuid,
pub capability_set_id: Uuid,
pub cap_version: i32,
pub capabilities: Vec<ApiCapability>,
}
#[async_trait]
impl FromRequestParts<AppState> for AuthenticatedUser {
type Rejection = AppError;
async fn from_request_parts(
parts: &mut Parts,
state: &AppState,
) -> Result<Self, Self::Rejection> {
if let Some(user) = parts.extensions.get::<AuthenticatedUser>() {
return Ok(user.clone());
}
let TypedHeader(Authorization(bearer)) =
TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, state)
.await
.map_err(|_| AppError::unauthorized())?;
let claims = state
.jwt
.verify_token(bearer.token())
.map_err(|_| AppError::unauthorized())?;
let mut tenant_conn = state.db_for_tenant(claims.tenant_id)?;
let capability_set = load_capability_set(&mut tenant_conn, claims.capability_set_id)
.map_err(|_| AppError::unauthorized())?;
if capability_set.cap_version != claims.cap_version {
return Err(AppError::unauthorized());
}
let capabilities = load_capabilities_for_set(&mut tenant_conn, capability_set.id)
.map_err(|_| AppError::unauthorized())?;
let user = AuthenticatedUser {
user_id: claims.sub,
username: claims.username,
tenant_id: claims.tenant_id,
principal_kind: claims.principal_kind,
principal_id: claims.principal_id,
capability_set_id: claims.capability_set_id,
cap_version: claims.cap_version,
capabilities,
};
parts.extensions.insert(user.clone());
parts
.extensions
.insert(TenantConnectionHolder::new(tenant_conn));
Ok(user)
}
}
pub struct TenantScopedConn {
pub conn: PgPooledConnection,
pub tenant_id: Uuid,
pub user_id: Uuid,
pub user: AuthenticatedUser,
}
impl TenantScopedConn {
pub fn conn(&mut self) -> &mut PgPooledConnection {
&mut self.conn
}
}
#[async_trait]
impl FromRequestParts<AppState> for TenantScopedConn {
type Rejection = AppError;
async fn from_request_parts(
parts: &mut Parts,
state: &AppState,
) -> Result<Self, Self::Rejection> {
let user = AuthenticatedUser::from_request_parts(parts, state).await?;
let tenant_id = user.tenant_id;
let conn = if let Some(holder) = parts.extensions.remove::<TenantConnectionHolder>() {
holder
.into_conn()
.ok_or_else(|| AppError::internal("tenant connection unavailable"))?
} else {
state.db_for_tenant(tenant_id)?
};
Ok(Self {
conn,
tenant_id,
user_id: user.user_id,
user,
})
}
}