cat_gateway/service/common/auth/rbac/
scheme.rs1use std::{env, error::Error, time::Duration};
3
4use catalyst_types::catalyst_id::role_index::RoleId;
5use poem::{error::ResponseError, http::StatusCode, IntoResponse, Request};
6use poem_openapi::{auth::Bearer, SecurityScheme};
7use tracing::{debug, error};
8
9use super::token::CatalystRBACTokenV1;
10use crate::{
11 db::index::session::CassandraSessionError,
12 rbac::latest_rbac_chain,
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(crate) const AUTHORIZATION_HEADER: &str = "Authorization";
22
23#[derive(SecurityScheme)]
25#[oai(
26 ty = "bearer",
27 key_name = "Authorization", bearer_format = "catalyst-rbac-token",
29 checker = "checker_api_catalyst_auth"
30)]
31#[allow(clippy::module_name_repetitions)]
32pub(crate) struct CatalystRBACSecurityScheme(CatalystRBACTokenV1);
33
34impl From<CatalystRBACSecurityScheme> for CatalystRBACTokenV1 {
35 fn from(value: CatalystRBACSecurityScheme) -> Self {
36 value.0
37 }
38}
39
40#[derive(Debug, thiserror::Error)]
44#[error("Service unavailable while processing a Catalyst RBAC Token")]
45pub struct ServiceUnavailableError(pub anyhow::Error);
46
47impl ResponseError for ServiceUnavailableError {
48 fn status(&self) -> StatusCode {
49 StatusCode::SERVICE_UNAVAILABLE
50 }
51
52 fn as_response(&self) -> poem::Response
54 where Self: Error + Send + Sync + 'static {
55 WithErrorResponses::<()>::service_unavailable(
56 &self.0,
57 RetryAfterOption::Some(RetryAfterHeader::default()),
58 )
59 .into_response()
60 }
61}
62
63#[derive(Debug, thiserror::Error)]
65enum AuthTokenError {
66 #[error("Unable to build registration chain, err: {0}")]
68 BuildRegChain(String),
69 #[error("Fail to parse RBAC token string, err: {0}")]
71 ParseRbacToken(String),
72 #[error("Registration not found for the auth token.")]
74 RegistrationNotFound,
75 #[error("Unable to get the latest signing key.")]
77 LatestSigningKey,
78}
79
80impl ResponseError for AuthTokenError {
81 fn status(&self) -> StatusCode {
82 StatusCode::UNAUTHORIZED
83 }
84
85 fn as_response(&self) -> poem::Response
87 where Self: Error + Send + Sync + 'static {
88 ErrorResponses::unauthorized(self.to_string()).into_response()
89 }
90}
91
92#[derive(Debug, thiserror::Error)]
96#[error("Insufficient Permission for Catalyst RBAC Token: {0:?}")]
97pub struct AuthTokenAccessViolation(Vec<String>);
98
99impl ResponseError for AuthTokenAccessViolation {
100 fn status(&self) -> StatusCode {
101 StatusCode::FORBIDDEN
102 }
103
104 fn as_response(&self) -> poem::Response
106 where Self: Error + Send + Sync + 'static {
107 ErrorResponses::forbidden(Some(self.0.clone())).into_response()
109 }
110}
111
112const 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(
123 req: &Request,
124 bearer: Bearer,
125) -> poem::Result<CatalystRBACTokenV1> {
126 const RBAC_OFF: &str = "RBAC_OFF";
128
129 let token = CatalystRBACTokenV1::parse(&bearer.token).map_err(|e| {
131 debug!("Corrupt auth token: {e:?}");
132 AuthTokenError::ParseRbacToken(e.to_string())
133 })?;
134
135 if env::var(RBAC_OFF).is_ok() {
137 return Ok(token);
138 }
139
140 let reg_chain = match latest_rbac_chain(token.catalyst_id()).await {
142 Ok(Some(c)) => c.chain,
143 Ok(None) => {
144 debug!(cat_id = %token.catalyst_id(), "Unable to find registrations for Catalyst ID");
145 return Err(AuthTokenError::RegistrationNotFound.into());
146 },
147 Err(e) if e.is::<CassandraSessionError>() => return Err(ServiceUnavailableError(e).into()),
148 Err(e) => {
149 error!(cat_id = %token.catalyst_id(), err = ?e, "Unable to build a registration chain");
152 return Err(AuthTokenError::BuildRegChain(e.to_string()).into());
153 },
154 };
155
156 if check_api_key(req.headers()).is_err() && !token.is_young(MAX_TOKEN_AGE, MAX_TOKEN_SKEW) {
159 debug!("Auth token expired: {token}");
161 Err(AuthTokenAccessViolation(vec!["EXPIRED".to_string()]))?;
162 }
163
164 let (latest_pk, _) = reg_chain
166 .get_latest_signing_pk_for_role(&RoleId::Role0)
167 .ok_or_else(|| {
168 debug!(
169 "Unable to get last signing key for {} Catalyst ID",
170 token.catalyst_id()
171 );
172 AuthTokenError::LatestSigningKey
173 })?;
174
175 if let Err(error) = token.verify(&latest_pk) {
177 debug!(error=%error, "Invalid signature for token: {token}");
178 Err(AuthTokenAccessViolation(vec![
179 "INVALID SIGNATURE".to_string()
180 ]))?;
181 }
182
183 Ok(token)
190}