use std::marker::PhantomData;
use std::net::SocketAddr;
use axum::extract::{ConnectInfo, FromRef, FromRequestParts};
use axum::http::header;
use axum::http::request::Parts;
use sqlx::PgConnection;
use sqlx::pool::PoolConnection;
use yorishiro_core::YorishiroError;
use yorishiro_core::services::auth;
use yorishiro_core::services::auth::ApiKeyScope;
use crate::error::ApiError;
use crate::state::AppState;
fn log_auth_rejection(parts: &Parts, err: &YorishiroError) {
let client = parts
.extensions
.get::<ConnectInfo<SocketAddr>>()
.map(|ConnectInfo(addr)| addr.ip().to_string())
.unwrap_or_else(|| "unknown".to_string());
tracing::warn!(client = %client, path = %parts.uri.path(), error = %err, "request rejected during authentication");
}
fn header_pairs(parts: &Parts) -> Vec<(String, String)> {
parts
.headers
.iter()
.filter_map(|(name, value)| {
value
.to_str()
.ok()
.map(|value| (name.as_str().to_owned(), value.to_owned()))
})
.collect()
}
fn extract_bearer_key(parts: &Parts) -> Result<&str, ApiError> {
auth::bearer_credential(
parts
.headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok()),
)
.ok_or_else(|| {
let err = YorishiroError::Unauthenticated;
log_auth_rejection(parts, &err);
ApiError(err)
})
}
pub struct AuthContext(pub auth::AuthContext);
impl<S> FromRequestParts<S> for AuthContext
where
AppState: FromRef<S>,
S: Send + Sync,
{
type Rejection = ApiError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let presented_key = extract_bearer_key(parts)?;
let headers = header_pairs(parts);
let app_state = AppState::from_ref(state);
let db = app_state.tenant_db.clone();
let ctx = app_state
.authenticator
.authenticate(db.pool(), presented_key, &headers)
.await
.inspect_err(|err| log_auth_rejection(parts, err))?;
match db
.acquire_for_workspace(ctx.tenant_id, ctx.workspace_id)
.await
{
Ok(mut conn) => {
if let Err(err) = auth::touch_last_used(&mut conn, ctx.api_key_id).await {
tracing::warn!(error = %err, "failed to update api key last_used_at");
}
}
Err(err) => {
tracing::warn!(error = %err, "failed to acquire connection to touch last_used_at");
}
}
Ok(AuthContext(ctx))
}
}
pub trait RequiredScope {
const SCOPE: ApiKeyScope;
}
pub struct ReadScope;
impl RequiredScope for ReadScope {
const SCOPE: ApiKeyScope = ApiKeyScope::Read;
}
pub struct WriteScope;
impl RequiredScope for WriteScope {
const SCOPE: ApiKeyScope = ApiKeyScope::Write;
}
pub struct SchemaScope;
impl RequiredScope for SchemaScope {
const SCOPE: ApiKeyScope = ApiKeyScope::Schema;
}
pub struct MigrationScope;
impl RequiredScope for MigrationScope {
const SCOPE: ApiKeyScope = ApiKeyScope::Migration;
}
pub struct Authorized<R> {
pub ctx: auth::AuthContext,
conn: PoolConnection<sqlx::Postgres>,
_scope: PhantomData<R>,
}
impl<R> Authorized<R> {
pub fn conn(&mut self) -> &mut PgConnection {
&mut self.conn
}
}
impl<S, R> FromRequestParts<S> for Authorized<R>
where
AppState: FromRef<S>,
S: Send + Sync,
R: RequiredScope,
{
type Rejection = ApiError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let presented_key = extract_bearer_key(parts)?;
let headers = header_pairs(parts);
let app_state = AppState::from_ref(state);
let (ctx, conn) = auth::authorize(
&app_state.tenant_db,
app_state.authenticator.as_ref(),
presented_key,
R::SCOPE,
&headers,
)
.await
.inspect_err(|err| log_auth_rejection(parts, err))?;
Ok(Authorized {
ctx,
conn,
_scope: PhantomData,
})
}
}
pub struct Verified<R> {
pub ctx: auth::AuthContext,
_scope: PhantomData<R>,
}
impl<S, R> FromRequestParts<S> for Verified<R>
where
AppState: FromRef<S>,
S: Send + Sync,
R: RequiredScope,
{
type Rejection = ApiError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let presented_key = extract_bearer_key(parts)?;
let headers = header_pairs(parts);
let app_state = AppState::from_ref(state);
let ctx = auth::authorize_scope(
&app_state.tenant_db,
app_state.authenticator.as_ref(),
presented_key,
R::SCOPE,
&headers,
)
.await
.inspect_err(|err| log_auth_rejection(parts, err))?;
Ok(Verified {
ctx,
_scope: PhantomData,
})
}
}
#[cfg(test)]
#[path = "../../../../tests/http/middleware/auth/mod.rs"]
mod tests;