feat: add permission control
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use axum::http::{request::Parts, HeaderValue};
|
||||
use axum::http::{HeaderValue, request::Parts};
|
||||
use base64::{self, Engine};
|
||||
use loco_rs::app::AppContext;
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
|
||||
use super::{
|
||||
@@ -59,7 +60,11 @@ pub struct BasicAuthService {
|
||||
|
||||
#[async_trait]
|
||||
impl AuthService for BasicAuthService {
|
||||
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> {
|
||||
async fn extract_user_info(
|
||||
&self,
|
||||
ctx: &AppContext,
|
||||
request: &mut Parts,
|
||||
) -> Result<AuthUserInfo, AuthError> {
|
||||
if let Ok(AuthBasic {
|
||||
user: found_user,
|
||||
password: found_password,
|
||||
@@ -68,8 +73,11 @@ impl AuthService for BasicAuthService {
|
||||
if self.config.user == found_user
|
||||
&& self.config.password == found_password.unwrap_or_default()
|
||||
{
|
||||
let subscriber_auth = crate::models::auth::Model::find_by_pid(ctx, SEED_SUBSCRIBER)
|
||||
.await
|
||||
.map_err(AuthError::FindAuthRecordError)?;
|
||||
return Ok(AuthUserInfo {
|
||||
user_pid: SEED_SUBSCRIBER.to_string(),
|
||||
subscriber_auth,
|
||||
auth_type: AuthType::Basic,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use std::fmt;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use loco_rs::model::ModelError;
|
||||
use openidconnect::{
|
||||
core::CoreErrorResponseType, ConfigurationError, RequestTokenError, SignatureVerificationError,
|
||||
SigningError, StandardErrorResponse,
|
||||
ConfigurationError, RequestTokenError, SignatureVerificationError, SigningError,
|
||||
StandardErrorResponse, core::CoreErrorResponseType,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
@@ -19,6 +22,8 @@ pub enum AuthError {
|
||||
supported: Vec<AuthType>,
|
||||
current: AuthType,
|
||||
},
|
||||
#[error("Failed to find auth record")]
|
||||
FindAuthRecordError(ModelError),
|
||||
#[error("Invalid credentials")]
|
||||
BasicInvalidCredentials,
|
||||
#[error(transparent)]
|
||||
@@ -69,6 +74,15 @@ pub enum AuthError {
|
||||
OidcAudMissingError(String),
|
||||
#[error("Subject missing")]
|
||||
OidcSubMissingError,
|
||||
#[error(fmt = display_graphql_permission_error)]
|
||||
GraphQLPermissionError(async_graphql::Error),
|
||||
}
|
||||
|
||||
fn display_graphql_permission_error(
|
||||
error: &async_graphql::Error,
|
||||
formatter: &mut fmt::Formatter<'_>,
|
||||
) -> fmt::Result {
|
||||
write!(formatter, "GraphQL permission denied: {}", error.message)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -19,7 +19,7 @@ pub async fn api_auth_middleware(
|
||||
|
||||
let (mut parts, body) = request.into_parts();
|
||||
|
||||
let mut response = match auth_service.extract_user_info(&mut parts).await {
|
||||
let mut response = match auth_service.extract_user_info(&ctx, &mut parts).await {
|
||||
Ok(auth_user_info) => {
|
||||
let mut request = Request::from_parts(parts, body);
|
||||
request.extensions_mut().insert(auth_user_info);
|
||||
|
||||
@@ -4,14 +4,15 @@ use std::{
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::http::{request::Parts, HeaderValue};
|
||||
use axum::http::{HeaderValue, request::Parts};
|
||||
use itertools::Itertools;
|
||||
use jwt_authorizer::{authorizer::Authorizer, NumericDate, OneOrArray};
|
||||
use jwt_authorizer::{NumericDate, OneOrArray, authorizer::Authorizer};
|
||||
use loco_rs::{app::AppContext, model::ModelError};
|
||||
use moka::future::Cache;
|
||||
use openidconnect::{
|
||||
core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata},
|
||||
AccessTokenHash, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce,
|
||||
OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, TokenResponse,
|
||||
core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -258,7 +259,11 @@ impl OidcAuthService {
|
||||
|
||||
#[async_trait]
|
||||
impl AuthService for OidcAuthService {
|
||||
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> {
|
||||
async fn extract_user_info(
|
||||
&self,
|
||||
ctx: &AppContext,
|
||||
request: &mut Parts,
|
||||
) -> Result<AuthUserInfo, AuthError> {
|
||||
let config = &self.config;
|
||||
let token = self.api_authorizer.extract_token(&request.headers).ok_or(
|
||||
AuthError::OidcJwtAuthError(jwt_authorizer::AuthError::MissingToken()),
|
||||
@@ -266,9 +271,11 @@ impl AuthService for OidcAuthService {
|
||||
|
||||
let token_data = self.api_authorizer.check_auth(&token).await?;
|
||||
let claims = token_data.claims;
|
||||
if claims.sub.as_deref().is_none_or(|s| s.trim().is_empty()) {
|
||||
let sub = if let Some(sub) = claims.sub.as_deref() {
|
||||
sub
|
||||
} else {
|
||||
return Err(AuthError::OidcSubMissingError);
|
||||
}
|
||||
};
|
||||
if !claims.contains_audience(&config.audience) {
|
||||
return Err(AuthError::OidcAudMissingError(config.audience.clone()));
|
||||
}
|
||||
@@ -298,12 +305,16 @@ impl AuthService for OidcAuthService {
|
||||
}
|
||||
}
|
||||
}
|
||||
let subscriber_auth = match crate::models::auth::Model::find_by_pid(ctx, sub).await {
|
||||
Err(ModelError::EntityNotFound) => {
|
||||
crate::models::auth::Model::create_from_oidc(ctx, sub.to_string()).await
|
||||
}
|
||||
r => r,
|
||||
}
|
||||
.map_err(AuthError::FindAuthRecordError)?;
|
||||
|
||||
Ok(AuthUserInfo {
|
||||
user_pid: claims
|
||||
.sub
|
||||
.as_deref()
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|| unreachable!("sub should be present and validated")),
|
||||
subscriber_auth,
|
||||
auth_type: AuthType::Oidc,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,24 +13,24 @@ use once_cell::sync::OnceCell;
|
||||
use reqwest::header::HeaderValue;
|
||||
|
||||
use super::{
|
||||
AppAuthConfig,
|
||||
basic::BasicAuthService,
|
||||
errors::AuthError,
|
||||
oidc::{OidcAuthClaims, OidcAuthService},
|
||||
AppAuthConfig,
|
||||
};
|
||||
use crate::{
|
||||
app::AppContextExt as _,
|
||||
config::AppConfigExt,
|
||||
fetch::{
|
||||
client::{HttpClientCacheBackendConfig, HttpClientCachePresetConfig},
|
||||
HttpClient, HttpClientConfig,
|
||||
client::{HttpClientCacheBackendConfig, HttpClientCachePresetConfig},
|
||||
},
|
||||
models::auth::AuthType,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AuthUserInfo {
|
||||
pub user_pid: String,
|
||||
pub subscriber_auth: crate::models::auth::Model,
|
||||
pub auth_type: AuthType,
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ impl FromRequestParts<AppContext> for AuthUserInfo {
|
||||
let auth_service = state.get_auth_service();
|
||||
|
||||
auth_service
|
||||
.extract_user_info(parts)
|
||||
.extract_user_info(state, parts)
|
||||
.await
|
||||
.map_err(|err| err.into_response())
|
||||
}
|
||||
@@ -52,7 +52,11 @@ impl FromRequestParts<AppContext> for AuthUserInfo {
|
||||
|
||||
#[async_trait]
|
||||
pub trait AuthService {
|
||||
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError>;
|
||||
async fn extract_user_info(
|
||||
&self,
|
||||
ctx: &AppContext,
|
||||
request: &mut Parts,
|
||||
) -> Result<AuthUserInfo, AuthError>;
|
||||
fn www_authenticate_header_value(&self) -> Option<HeaderValue>;
|
||||
fn auth_type(&self) -> AuthType;
|
||||
}
|
||||
@@ -79,21 +83,23 @@ impl AppAuthService {
|
||||
.iss(&[&config.issuer])
|
||||
.aud(&[&config.audience]);
|
||||
|
||||
let jwt_auth = JwtAuthorizer::<OidcAuthClaims>::from_oidc(&config.issuer)
|
||||
let oidc_provider_client = HttpClient::from_config(HttpClientConfig {
|
||||
exponential_backoff_max_retries: Some(3),
|
||||
cache_backend: Some(HttpClientCacheBackendConfig::Moka { cache_size: 1 }),
|
||||
cache_preset: Some(HttpClientCachePresetConfig::RFC7234),
|
||||
..Default::default()
|
||||
})
|
||||
.map_err(AuthError::OidcProviderHttpClientError)?;
|
||||
|
||||
let api_authorizer = JwtAuthorizer::<OidcAuthClaims>::from_oidc(&config.issuer)
|
||||
.validation(validation)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
AppAuthService::Oidc(OidcAuthService {
|
||||
config,
|
||||
api_authorizer: jwt_auth,
|
||||
oidc_provider_client: HttpClient::from_config(HttpClientConfig {
|
||||
exponential_backoff_max_retries: Some(3),
|
||||
cache_backend: Some(HttpClientCacheBackendConfig::Moka { cache_size: 1 }),
|
||||
cache_preset: Some(HttpClientCachePresetConfig::RFC7234),
|
||||
..Default::default()
|
||||
})
|
||||
.map_err(AuthError::OidcProviderHttpClientError)?,
|
||||
api_authorizer,
|
||||
oidc_provider_client,
|
||||
oidc_request_cache: Cache::builder()
|
||||
.time_to_live(Duration::from_mins(5))
|
||||
.name("oidc_request_cache")
|
||||
@@ -107,10 +113,14 @@ impl AppAuthService {
|
||||
|
||||
#[async_trait]
|
||||
impl AuthService for AppAuthService {
|
||||
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError> {
|
||||
async fn extract_user_info(
|
||||
&self,
|
||||
ctx: &AppContext,
|
||||
request: &mut Parts,
|
||||
) -> Result<AuthUserInfo, AuthError> {
|
||||
match self {
|
||||
AppAuthService::Basic(service) => service.extract_user_info(request).await,
|
||||
AppAuthService::Oidc(service) => service.extract_user_info(request).await,
|
||||
AppAuthService::Basic(service) => service.extract_user_info(ctx, request).await,
|
||||
AppAuthService::Oidc(service) => service.extract_user_info(ctx, request).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user