cat_gateway/service/common/auth/rbac/
scheme.rsuse std::{env, error::Error, sync::LazyLock, time::Duration};
use dashmap::DashMap;
use ed25519_dalek::{VerifyingKey, PUBLIC_KEY_LENGTH};
use moka::future::Cache;
use poem::{error::ResponseError, http::StatusCode, IntoResponse, Request};
use poem_openapi::{auth::Bearer, SecurityScheme};
use tracing::error;
use super::token::CatalystRBACTokenV1;
use crate::service::common::responses::ErrorResponses;
pub type EncodedAuthToken = String;
static CACHE: LazyLock<Cache<EncodedAuthToken, CatalystRBACTokenV1>> = LazyLock::new(|| {
Cache::builder()
.time_to_live(Duration::from_secs(30 * 60))
.time_to_idle(Duration::from_secs(5 * 60))
.build()
});
static CERTS: LazyLock<DashMap<String, [u8; PUBLIC_KEY_LENGTH]>> = LazyLock::new(|| {
const KID: &str = "0467de6bd945b9207bfa09d846b77ef5";
let public_key_bytes: [u8; PUBLIC_KEY_LENGTH] = [
180, 91, 130, 149, 226, 112, 29, 45, 188, 141, 64, 147, 250, 233, 75, 151, 151, 53, 248,
197, 225, 122, 24, 67, 207, 100, 162, 152, 232, 102, 89, 162,
];
let cert_map = DashMap::new();
cert_map.insert(KID.to_string(), public_key_bytes);
cert_map
});
#[derive(SecurityScheme)]
#[oai(
ty = "bearer",
bearer_format = "catalyst-rbac-token",
checker = "checker_api_catalyst_auth"
)]
#[allow(dead_code, clippy::module_name_repetitions)]
pub struct CatalystRBACSecurityScheme(pub CatalystRBACTokenV1);
#[derive(Debug, thiserror::Error)]
#[error("Invalid Catalyst RBAC Auth Token")]
pub struct AuthTokenError;
impl ResponseError for AuthTokenError {
fn status(&self) -> StatusCode {
StatusCode::UNAUTHORIZED
}
fn as_response(&self) -> poem::Response
where Self: Error + Send + Sync + 'static {
ErrorResponses::unauthorized().into_response()
}
}
#[derive(Debug, thiserror::Error)]
#[error("Insufficient Permission for Catalyst RBAC Token")]
pub struct AuthTokenAccessViolation(Vec<String>);
impl ResponseError for AuthTokenAccessViolation {
fn status(&self) -> StatusCode {
StatusCode::FORBIDDEN
}
fn as_response(&self) -> poem::Response
where Self: Error + Send + Sync + 'static {
ErrorResponses::forbidden(Some(self.0.clone())).into_response()
}
}
const MAX_TOKEN_AGE: Duration = Duration::from_secs(60 * 60); const MAX_TOKEN_SKEW: Duration = Duration::from_secs(5 * 60); async fn checker_api_catalyst_auth(
_req: &Request, bearer: Bearer,
) -> poem::Result<CatalystRBACTokenV1> {
const RBAC_OFF: &str = "RBAC_OFF";
let token = match CatalystRBACTokenV1::decode(&bearer.token) {
Ok(token) => token,
Err(err) => {
error!("Corrupt auth token: {:?}", err);
Err(AuthTokenError)?
},
};
if env::var(RBAC_OFF).is_ok() {
return Ok(token);
};
if !token.is_young(MAX_TOKEN_AGE, MAX_TOKEN_SKEW) {
error!("Auth token expired: {:?}", token);
Err(AuthTokenAccessViolation(vec!["EXPIRED".to_string()]))?;
}
if let Some(token) = CACHE.get(&bearer.token).await {
return Ok(token);
}
let pub_key_bytes = if let Some(cert) = CERTS.get(&hex::encode(token.kid.0)) {
*cert
} else {
error!("Invalid KID {:?}", token.kid);
Err(AuthTokenAccessViolation(vec!["UNREGISTERED".to_string()]))?
};
let public_key = match VerifyingKey::from_bytes(&pub_key_bytes) {
Ok(pub_key) => pub_key,
Err(err) => {
error!("Invalid public key: {:?}", err);
Err(AuthTokenAccessViolation(vec![
"INVALID PUBLIC KEY".to_string()
]))?
},
};
if let Err(error) = token.verify(&public_key) {
error!(error=%error, "Token Invalidly Signed");
Err(AuthTokenAccessViolation(vec![
"INVALID SIGNATURE".to_string()
]))?;
}
CACHE.insert(bearer.token, token.clone()).await;
Ok(token)
}