magnetar/src/web/auth.rs

264 lines
8.0 KiB
Rust

use crate::service::local_user_cache::UserCacheError;
use crate::service::MagnetarService;
use crate::web::{ApiError, IntoErrorCode};
use axum::async_trait;
use axum::extract::rejection::ExtensionRejection;
use axum::extract::{FromRequestParts, Request, State};
use axum::http::request::Parts;
use axum::http::{HeaderMap, StatusCode};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use headers::authorization::Bearer;
use headers::{Authorization, HeaderMapExt};
use magnetar_model::{ck, CalckeyDbError};
use std::convert::Infallible;
use std::sync::Arc;
use strum::IntoStaticStr;
use thiserror::Error;
use tracing::error;
#[derive(Clone, Debug)]
pub enum AuthMode {
User {
user: Arc<ck::user::Model>,
},
AccessToken {
user: Arc<ck::user::Model>,
access_token: Arc<ck::access_token::Model>,
},
Anonymous,
}
impl AuthMode {
fn get_user(&self) -> Option<&Arc<ck::user::Model>> {
match self {
AuthMode::User { user } | AuthMode::AccessToken { user, .. } => Some(user),
AuthMode::Anonymous => None,
}
}
}
pub struct AuthUserRejection(ApiError);
impl From<ExtensionRejection> for AuthUserRejection {
fn from(rejection: ExtensionRejection) -> Self {
AuthUserRejection(ApiError {
status: StatusCode::UNAUTHORIZED,
code: "Unauthorized".error_code(),
message: if cfg!(debug_assertions) {
format!("Missing auth extension: {}", rejection)
} else {
"Unauthorized".to_string()
},
})
}
}
impl IntoResponse for AuthUserRejection {
fn into_response(self) -> Response {
self.0.into_response()
}
}
#[derive(Clone, FromRequestParts)]
#[from_request(via(axum::Extension), rejection(AuthUserRejection))]
pub struct AuthenticatedUser(pub Arc<ck::user::Model>);
#[derive(Clone)]
pub struct MaybeUser(pub Option<Arc<ck::user::Model>>);
#[async_trait]
impl<S> FromRequestParts<S> for MaybeUser {
type Rejection = Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(MaybeUser(
parts
.extensions
.get::<AuthenticatedUser>()
.map(|part| part.0.clone()),
))
}
}
#[derive(Clone)]
pub struct AuthState {
service: Arc<MagnetarService>,
}
#[derive(Debug, Error, IntoStaticStr)]
enum AuthError {
#[error("Unsupported authorization scheme")]
UnsupportedScheme,
#[error("Cache error: {0}")]
CacheError(#[from] UserCacheError),
#[error("Database error: {0}")]
DbError(#[from] CalckeyDbError),
#[error("Invalid token")]
InvalidToken,
#[error("Invalid token \"{token}\" referencing user \"{user}\"")]
InvalidTokenUser { token: String, user: String },
#[error("Invalid access token \"{access_token}\" referencing app \"{app}\"")]
InvalidAccessTokenApp { access_token: String, app: String },
}
impl From<AuthError> for ApiError {
fn from(err: AuthError) -> Self {
match err {
AuthError::UnsupportedScheme => ApiError {
status: StatusCode::UNAUTHORIZED,
code: err.error_code(),
message: "Unsupported authorization scheme".to_string(),
},
AuthError::CacheError(err) => err.into(),
AuthError::DbError(err) => err.into(),
AuthError::InvalidTokenUser {
ref token,
ref user,
} => {
error!("Invalid token \"{}\" referencing user \"{}\"", token, user);
ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
code: err.error_code(),
message: if cfg!(debug_assertions) {
format!("Invalid token \"{}\" referencing user \"{}\"", token, user)
} else {
"Invalid token-user link".to_string()
},
}
}
AuthError::InvalidAccessTokenApp {
ref access_token,
ref app,
} => {
error!(
"Invalid access token \"{}\" referencing app \"{}\"",
access_token, app
);
ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
code: err.error_code(),
message: if cfg!(debug_assertions) {
format!(
"Invalid access token \"{}\" referencing app \"{}\"",
access_token, app
)
} else {
"Invalid access token-app link".to_string()
},
}
}
AuthError::InvalidToken => ApiError {
status: StatusCode::UNAUTHORIZED,
code: err.error_code(),
message: "Invalid token".to_string(),
},
}
}
}
pub fn is_user_token(token: &str) -> bool {
token.chars().count() == 16
}
impl AuthState {
pub fn new(magnetar: Arc<MagnetarService>) -> Self {
Self { service: magnetar }
}
async fn authorize_token(
&self,
Authorization(token): &Authorization<Bearer>,
) -> Result<AuthMode, AuthError> {
let token = token.token();
if is_user_token(token) {
let user_cache = &self.service.local_user_cache;
let user = user_cache.get_by_token(token).await?;
if let Some(user) = user {
return Ok(AuthMode::User { user });
}
Err(AuthError::InvalidToken)
} else {
let access_token = self.service.db.get_access_token(token).await?;
if access_token.is_none() {
return Err(AuthError::InvalidToken);
}
let access_token = access_token.unwrap();
let user = self
.service
.local_user_cache
.get_by_id(&access_token.user_id)
.await?;
if user.is_none() {
return Err(AuthError::InvalidTokenUser {
token: access_token.id,
user: access_token.user_id,
});
}
let user = user.unwrap();
if let Some(app_id) = &access_token.app_id {
return match self.service.db.get_app_by_id(app_id).await? {
Some(app) => Ok(AuthMode::AccessToken {
user,
access_token: Arc::new(ck::access_token::Model {
permission: app.permission,
..access_token
}),
}),
None => Err(AuthError::InvalidAccessTokenApp {
access_token: access_token.id,
app: access_token.user_id,
}),
};
}
let access_token = Arc::new(access_token);
Ok(AuthMode::AccessToken { user, access_token })
}
}
}
pub async fn auth(
State(state): State<AuthState>,
header_map: HeaderMap,
mut req: Request,
next: Next,
) -> Result<Response, ApiError> {
let auth_bearer = match header_map.typed_try_get::<Authorization<Bearer>>() {
Ok(Some(auth)) => auth,
Ok(None) => {
req.extensions_mut().insert(AuthMode::Anonymous);
return Ok(next.run(req).await);
}
Err(_) => {
return Err(AuthError::UnsupportedScheme.into());
}
};
match state.authorize_token(&auth_bearer).await {
Ok(auth) => {
if let Some(user) = auth.get_user() {
let user = AuthenticatedUser(user.clone());
req.extensions_mut().insert(user);
}
req.extensions_mut().insert(auth);
Ok(next.run(req).await)
}
Err(e) => Err(e.into()),
}
}