264 lines
8.0 KiB
Rust
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()),
|
|
}
|
|
}
|