cat_gateway/service/common/auth/rbac/
scheme.rs1use std::{env, error::Error, sync::LazyLock, time::Duration};
3
4use catalyst_types::catalyst_id::role_index::RoleId;
5use moka::future::Cache;
6use poem::{error::ResponseError, http::StatusCode, IntoResponse, Request};
7use poem_openapi::{auth::Bearer, SecurityScheme};
8use tracing::debug;
9
10use super::token::CatalystRBACTokenV1;
11use crate::{
12    db::index::session::CassandraSessionError,
13    service::common::{
14        auth::api_key::check_api_key,
15        responses::{ErrorResponses, WithErrorResponses},
16        types::headers::retry_after::{RetryAfterHeader, RetryAfterOption},
17    },
18};
19
20pub type EncodedAuthToken = String;
22
23pub(crate) const AUTHORIZATION_HEADER: &str = "Authorization";
25
26#[allow(dead_code)]
30static CACHE: LazyLock<Cache<EncodedAuthToken, CatalystRBACTokenV1>> = LazyLock::new(|| {
31    Cache::builder()
32        .time_to_live(Duration::from_secs(30 * 60))
34        .time_to_idle(Duration::from_secs(5 * 60))
36        .build()
38});
39
40#[derive(SecurityScheme)]
42#[oai(
43    ty = "bearer",
44    key_name = "Authorization", bearer_format = "catalyst-rbac-token",
46    checker = "checker_api_catalyst_auth"
47)]
48#[allow(clippy::module_name_repetitions)]
49pub(crate) struct CatalystRBACSecurityScheme(CatalystRBACTokenV1);
50
51impl From<CatalystRBACSecurityScheme> for CatalystRBACTokenV1 {
52    fn from(value: CatalystRBACSecurityScheme) -> Self {
53        value.0
54    }
55}
56
57#[derive(Debug, thiserror::Error)]
61#[error("Service unavailable while processing a Catalyst RBAC Token")]
62pub struct ServiceUnavailableError(pub anyhow::Error);
63
64impl ResponseError for ServiceUnavailableError {
65    fn status(&self) -> StatusCode {
66        StatusCode::SERVICE_UNAVAILABLE
67    }
68
69    fn as_response(&self) -> poem::Response
71    where Self: Error + Send + Sync + 'static {
72        WithErrorResponses::<()>::service_unavailable(
73            &self.0,
74            RetryAfterOption::Some(RetryAfterHeader::default()),
75        )
76        .into_response()
77    }
78}
79
80#[derive(Debug, thiserror::Error)]
82enum AuthTokenError {
83    #[error("Unable to build registration chain, err: {0}")]
85    BuildRegChain(String),
86    #[error("Fail to parse RBAC token string, err: {0}")]
88    ParseRbacToken(String),
89    #[error("Registration not found for the auth token.")]
91    RegistrationNotFound,
92    #[error("Unable to get the latest signing key.")]
94    LatestSigningKey,
95}
96
97impl ResponseError for AuthTokenError {
98    fn status(&self) -> StatusCode {
99        StatusCode::UNAUTHORIZED
100    }
101
102    fn as_response(&self) -> poem::Response
104    where Self: Error + Send + Sync + 'static {
105        ErrorResponses::unauthorized(self.to_string()).into_response()
106    }
107}
108
109#[derive(Debug, thiserror::Error)]
113#[error("Insufficient Permission for Catalyst RBAC Token: {0:?}")]
114pub struct AuthTokenAccessViolation(Vec<String>);
115
116impl ResponseError for AuthTokenAccessViolation {
117    fn status(&self) -> StatusCode {
118        StatusCode::FORBIDDEN
119    }
120
121    fn as_response(&self) -> poem::Response
123    where Self: Error + Send + Sync + 'static {
124        ErrorResponses::forbidden(Some(self.0.clone())).into_response()
126    }
127}
128
129const 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(
140    req: &Request, bearer: Bearer,
141) -> poem::Result<CatalystRBACTokenV1> {
142    const RBAC_OFF: &str = "RBAC_OFF";
144
145    let mut token = CatalystRBACTokenV1::parse(&bearer.token).map_err(|e| {
147        debug!("Corrupt auth token: {e:?}");
148        AuthTokenError::ParseRbacToken(e.to_string())
149    })?;
150
151    if env::var(RBAC_OFF).is_ok() {
153        return Ok(token);
154    };
155
156    let reg_chain = match token.reg_chain().await {
158        Ok(Some(reg_chain)) => reg_chain,
159        Ok(None) => {
160            debug!(
161                "Unable to find registrations for {} Catalyst ID",
162                token.catalyst_id()
163            );
164            return Err(AuthTokenError::RegistrationNotFound.into());
165        },
166        Err(err) if err.is::<CassandraSessionError>() => {
167            return Err(ServiceUnavailableError(err).into())
168        },
169        Err(err) => {
170            debug!("Unable to build a registration chain Catalyst ID: {err:?}");
171            return Err(AuthTokenError::BuildRegChain(err.to_string()).into());
172        },
173    };
174
175    if check_api_key(req.headers()).is_err() && !token.is_young(MAX_TOKEN_AGE, MAX_TOKEN_SKEW) {
178        debug!("Auth token expired: {token}");
180        Err(AuthTokenAccessViolation(vec!["EXPIRED".to_string()]))?;
181    }
182
183    let (latest_pk, _) = reg_chain
197        .get_latest_signing_pk_for_role(&RoleId::Role0)
198        .ok_or_else(|| {
199            debug!(
200                "Unable to get last signing key for {} Catalyst ID",
201                token.catalyst_id()
202            );
203            AuthTokenError::LatestSigningKey
204        })?;
205
206    if let Err(error) = token.verify(&latest_pk) {
208        debug!(error=%error, "Invalid signature for token: {token}");
209        Err(AuthTokenAccessViolation(vec![
210            "INVALID SIGNATURE".to_string()
211        ]))?;
212    }
213
214    Ok(token)
226}