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}