cat_gateway/service/common/auth/rbac/
scheme.rs

1//! Catalyst RBAC Security Scheme
2use 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
18/// The header name that holds the authorization RBAC token
19pub(crate) const AUTHORIZATION_HEADER: &str = "Authorization";
20
21/// Catalyst RBAC Access Token
22#[derive(SecurityScheme)]
23#[oai(
24    ty = "bearer",
25    key_name = "Authorization", // MUST match the `AUTHORIZATION_HEADER` constant.
26    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/// Error with the service while processing a Catalyst RBAC Token
39///
40/// Can be related to database session failure.
41#[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    /// Convert this error to a HTTP response.
51    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/// Authentication token error.
62#[derive(Debug, thiserror::Error)]
63enum AuthTokenError {
64    /// Registration chain cannot be built.
65    #[error("Unable to build registration chain, err: {0}")]
66    BuildRegChain(String),
67    /// RBAC token cannot be parsed.
68    #[error("Fail to parse RBAC token string, err: {0}")]
69    ParseRbacToken(String),
70    /// Registration chain cannot be found.
71    #[error("Registration not found for the auth token.")]
72    RegistrationNotFound,
73    /// Latest signing key cannot be found.
74    #[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    /// Convert this error to a HTTP response.
84    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/// Token does not have required access rights
91///
92/// Not enough access rights, so its a 403 response.
93#[derive(Debug, thiserror::Error)]
94#[error("Insufficient Permission for Catalyst RBAC Token: {0:?}")]
95enum AuthTokenAccessViolation {
96    /// Not a Admin RBAC token
97    #[error("Not a valid Admin RBAC token")]
98    NotAdmin,
99    /// Invalid RBAC token signature
100    #[error("Invalid RBAC Token signature.")]
101    InvalidSignature,
102    /// RBAC token expired
103    #[error("Expired RBAC token.")]
104    Expired,
105}
106
107impl ResponseError for AuthTokenAccessViolation {
108    fn status(&self) -> StatusCode {
109        StatusCode::FORBIDDEN
110    }
111
112    /// Convert this error to a HTTP response.
113    fn as_response(&self) -> poem::Response
114    where Self: Error + Send + Sync + 'static {
115        // TODO: Actually check permissions needed for an endpoint.
116        ErrorResponses::forbidden(self.to_string()).into_response()
117    }
118}
119
120/// Time in the past the Token can be valid for.
121const MAX_TOKEN_AGE: Duration = Duration::from_secs(60 * 60); // 1 hour.
122
123/// Time in the future the Token can be valid for.
124const MAX_TOKEN_SKEW: Duration = Duration::from_secs(5 * 60); // 5 minutes
125
126/// When added to an endpoint, this hook is called per request to verify the bearer token
127/// is valid. The performed validation is described [here].
128///
129/// [here]: https://github.com/input-output-hk/catalyst-voices/blob/main/docs/src/catalyst-standards/permissionless-auth/auth-header.md#backend-processing-of-the-token
130async fn checker_api_catalyst_auth(
131    req: &Request,
132    bearer: Bearer,
133) -> poem::Result<CatalystRBACTokenV1> {
134    /// Temporary: Conditional RBAC for testing
135    const RBAC_OFF: &str = "RBAC_OFF";
136
137    // Deserialize the token: this performs the 1-5 steps of the validation.
138    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 explicitly set by SRE, switch off full verification
144    if env::var(RBAC_OFF).is_ok() {
145        return Ok(token);
146    }
147
148    // Step 6, 8, 9: get the registration chain (or check is it a valid admin token), verify
149    // the signature against the Role 0 pk.
150    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    // Step 7: Verify that the nonce is in the acceptable range.
176    // If `InternalApiKeyAuthorization` auth is provided, skip validation.
177    if check_api_key(req.headers()).is_err() && !token.is_young(MAX_TOKEN_AGE, MAX_TOKEN_SKEW) {
178        // Token is too old or too far in the future.
179        debug!("Auth token expired: {token}");
180        Err(AuthTokenAccessViolation::Expired)?;
181    }
182
183    // Step 10 is optional and isn't currently implemented.
184    //   - Get the latest unstable signing certificate registered for Role 0.
185    //   - Verify the signature against the Role 0 Public Key and Algorithm identified by the
186    //     certificate. If this fails, return 403.
187
188    // Step 11: Token is valid
189    Ok(token)
190}