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

1//! Catalyst RBAC Security Scheme
2use 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
20/// The header name that holds the authorization RBAC token
21pub(crate) const AUTHORIZATION_HEADER: &str = "Authorization";
22
23/// Catalyst RBAC Access Token
24#[derive(SecurityScheme)]
25#[oai(
26    ty = "bearer",
27    key_name = "Authorization", // MUST match the `AUTHORIZATION_HEADER` constant.
28    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/// Error with the service while processing a Catalyst RBAC Token
41///
42/// Can be related to database session failure.
43#[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    /// Convert this error to a HTTP response.
53    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/// Authentication token error.
64#[derive(Debug, thiserror::Error)]
65enum AuthTokenError {
66    /// Registration chain cannot be built.
67    #[error("Unable to build registration chain, err: {0}")]
68    BuildRegChain(String),
69    /// RBAC token cannot be parsed.
70    #[error("Fail to parse RBAC token string, err: {0}")]
71    ParseRbacToken(String),
72    /// Registration chain cannot be found.
73    #[error("Registration not found for the auth token.")]
74    RegistrationNotFound,
75    /// Latest signing key cannot be found.
76    #[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    /// Convert this error to a HTTP response.
86    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/// Token does not have required access rights
93///
94/// Not enough access rights, so its a 403 response.
95#[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    /// Convert this error to a HTTP response.
105    fn as_response(&self) -> poem::Response
106    where Self: Error + Send + Sync + 'static {
107        // TODO: Actually check permissions needed for an endpoint.
108        ErrorResponses::forbidden(Some(self.0.clone())).into_response()
109    }
110}
111
112/// Time in the past the Token can be valid for.
113const MAX_TOKEN_AGE: Duration = Duration::from_secs(60 * 60); // 1 hour.
114
115/// Time in the future the Token can be valid for.
116const MAX_TOKEN_SKEW: Duration = Duration::from_secs(5 * 60); // 5 minutes
117
118/// When added to an endpoint, this hook is called per request to verify the bearer token
119/// is valid. The performed validation is described [here].
120///
121/// [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
122async fn checker_api_catalyst_auth(
123    req: &Request, bearer: Bearer,
124) -> poem::Result<CatalystRBACTokenV1> {
125    /// Temporary: Conditional RBAC for testing
126    const RBAC_OFF: &str = "RBAC_OFF";
127
128    // Deserialize the token: this performs the 1-5 steps of the validation.
129    let token = CatalystRBACTokenV1::parse(&bearer.token).map_err(|e| {
130        debug!("Corrupt auth token: {e:?}");
131        AuthTokenError::ParseRbacToken(e.to_string())
132    })?;
133
134    // If env var explicitly set by SRE, switch off full verification
135    if env::var(RBAC_OFF).is_ok() {
136        return Ok(token);
137    };
138
139    // Step 6: get the registration chain
140    let reg_chain = match latest_rbac_chain(token.catalyst_id()).await {
141        Ok(Some(c)) => c.chain,
142        Ok(None) => {
143            debug!(cat_id = %token.catalyst_id(), "Unable to find registrations for Catalyst ID");
144            return Err(AuthTokenError::RegistrationNotFound.into());
145        },
146        Err(e) if e.is::<CassandraSessionError>() => return Err(ServiceUnavailableError(e).into()),
147        Err(e) => {
148            // This should never happen normally because we validate RBAC registration transactions
149            // before adding them to the database.
150            error!(cat_id = %token.catalyst_id(), err = ?e, "Unable to build a registration chain");
151            return Err(AuthTokenError::BuildRegChain(e.to_string()).into());
152        },
153    };
154
155    // Step 7: Verify that the nonce is in the acceptable range.
156    // If `InternalApiKeyAuthorization` auth is provided, skip validation.
157    if check_api_key(req.headers()).is_err() && !token.is_young(MAX_TOKEN_AGE, MAX_TOKEN_SKEW) {
158        // Token is too old or too far in the future.
159        debug!("Auth token expired: {token}");
160        Err(AuthTokenAccessViolation(vec!["EXPIRED".to_string()]))?;
161    }
162
163    // Step 8: Get the latest stable signing certificate registered for Role 0.
164    let (latest_pk, _) = reg_chain
165        .get_latest_signing_pk_for_role(&RoleId::Role0)
166        .ok_or_else(|| {
167            debug!(
168                "Unable to get last signing key for {} Catalyst ID",
169                token.catalyst_id()
170            );
171            AuthTokenError::LatestSigningKey
172        })?;
173
174    // Step 9: Verify the signature against the Role 0 pk.
175    if let Err(error) = token.verify(&latest_pk) {
176        debug!(error=%error, "Invalid signature for token: {token}");
177        Err(AuthTokenAccessViolation(vec![
178            "INVALID SIGNATURE".to_string()
179        ]))?;
180    }
181
182    // Step 10 is optional and isn't currently implemented.
183    //   - Get the latest unstable signing certificate registered for Role 0.
184    //   - Verify the signature against the Role 0 Public Key and Algorithm identified by the
185    //     certificate. If this fails, return 403.
186
187    // Step 11: Token is valid
188    Ok(token)
189}