cat_gateway/service/common/auth/rbac/
scheme.rs1use std::{env, error::Error, time::Duration};
3
4use poem::{IntoResponse, Request, error::ResponseError, http::StatusCode};
5use poem_openapi::{SecurityScheme, auth::Bearer};
6use tracing::{debug, error};
7
8use super::token::CatalystRBACTokenV1;
9use crate::{
10 db::index::session::CassandraSessionError,
11 service::common::{
12 auth::{api_key::check_api_key, rbac::token::VerificationError},
13 responses::{ErrorResponses, WithErrorResponses},
14 types::headers::retry_after::{RetryAfterHeader, RetryAfterOption},
15 },
16};
17
18pub(crate) const AUTHORIZATION_HEADER: &str = "Authorization";
20
21#[derive(SecurityScheme)]
23#[oai(
24 ty = "bearer",
25 key_name = "Authorization", bearer_format = "catalyst-rbac-token",
27 checker = "checker_api_catalyst_auth"
28)]
29#[allow(clippy::module_name_repetitions)]
30pub(crate) struct CatalystRBACSecurityScheme(CatalystRBACTokenV1);
31
32impl From<CatalystRBACSecurityScheme> for CatalystRBACTokenV1 {
33 fn from(value: CatalystRBACSecurityScheme) -> Self {
34 value.0
35 }
36}
37
38#[derive(Debug, thiserror::Error)]
42#[error("Service unavailable while processing a Catalyst RBAC Token")]
43pub struct ServiceUnavailableError(pub anyhow::Error);
44
45impl ResponseError for ServiceUnavailableError {
46 fn status(&self) -> StatusCode {
47 StatusCode::SERVICE_UNAVAILABLE
48 }
49
50 fn as_response(&self) -> poem::Response
52 where Self: Error + Send + Sync + 'static {
53 WithErrorResponses::<()>::service_unavailable(
54 &self.0,
55 RetryAfterOption::Some(RetryAfterHeader::default()),
56 )
57 .into_response()
58 }
59}
60
61#[derive(Debug, thiserror::Error)]
63enum AuthTokenError {
64 #[error("Unable to build registration chain, err: {0}")]
66 BuildRegChain(String),
67 #[error("Fail to parse RBAC token string, err: {0}")]
69 ParseRbacToken(String),
70 #[error("Registration not found for the auth token.")]
72 RegistrationNotFound,
73 #[error("Unable to get the latest signing key.")]
75 LatestSigningKey,
76}
77
78impl ResponseError for AuthTokenError {
79 fn status(&self) -> StatusCode {
80 StatusCode::UNAUTHORIZED
81 }
82
83 fn as_response(&self) -> poem::Response
85 where Self: Error + Send + Sync + 'static {
86 ErrorResponses::unauthorized(self.to_string()).into_response()
87 }
88}
89
90#[derive(Debug, thiserror::Error)]
94#[error("Insufficient Permission for Catalyst RBAC Token: {0:?}")]
95enum AuthTokenAccessViolation {
96 #[error("Not a valid Admin RBAC token")]
98 NotAdmin,
99 #[error("Invalid RBAC Token signature.")]
101 InvalidSignature,
102 #[error("Expired RBAC token.")]
104 Expired,
105}
106
107impl ResponseError for AuthTokenAccessViolation {
108 fn status(&self) -> StatusCode {
109 StatusCode::FORBIDDEN
110 }
111
112 fn as_response(&self) -> poem::Response
114 where Self: Error + Send + Sync + 'static {
115 ErrorResponses::forbidden(self.to_string()).into_response()
117 }
118}
119
120const 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(
131 req: &Request,
132 bearer: Bearer,
133) -> poem::Result<CatalystRBACTokenV1> {
134 const RBAC_OFF: &str = "RBAC_OFF";
136
137 let mut token = CatalystRBACTokenV1::parse(&bearer.token).map_err(|e| {
139 debug!("Corrupt auth token: {e:?}");
140 AuthTokenError::ParseRbacToken(e.to_string())
141 })?;
142
143 if env::var(RBAC_OFF).is_ok() {
145 return Ok(token);
146 }
147
148 match token.verify().await {
151 Ok(()) => {},
152 Err(e) if e.is::<CassandraSessionError>() => return Err(ServiceUnavailableError(e).into()),
153 Err(e) => {
154 error!(cat_id = %token.catalyst_id(), err = ?e, "RBAC token fails validation");
155 match e.downcast::<VerificationError>() {
156 Ok(VerificationError::LatestSigningKey) => {
157 return Err(AuthTokenError::LatestSigningKey.into());
158 },
159 Ok(VerificationError::RegistrationNotFound) => {
160 return Err(AuthTokenError::RegistrationNotFound.into());
161 },
162 Ok(VerificationError::NotAdmin) => {
163 return Err(AuthTokenAccessViolation::NotAdmin.into());
164 },
165 Ok(VerificationError::InvalidSignature) => {
166 return Err(AuthTokenAccessViolation::InvalidSignature.into());
167 },
168 Err(e) => {
169 return Err(AuthTokenError::BuildRegChain(e.to_string()).into());
170 },
171 }
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::Expired)?;
181 }
182
183 Ok(token)
190}