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,
124    bearer: Bearer,
125) -> poem::Result<CatalystRBACTokenV1> {
126    /// Temporary: Conditional RBAC for testing
127    const RBAC_OFF: &str = "RBAC_OFF";
128
129    // Deserialize the token: this performs the 1-5 steps of the validation.
130    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 explicitly set by SRE, switch off full verification
136    if env::var(RBAC_OFF).is_ok() {
137        return Ok(token);
138    }
139
140    // Step 6: get the registration chain
141    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            // This should never happen normally because we validate RBAC registration transactions
150            // before adding them to the database.
151            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    // Step 7: Verify that the nonce is in the acceptable range.
157    // If `InternalApiKeyAuthorization` auth is provided, skip validation.
158    if check_api_key(req.headers()).is_err() && !token.is_young(MAX_TOKEN_AGE, MAX_TOKEN_SKEW) {
159        // Token is too old or too far in the future.
160        debug!("Auth token expired: {token}");
161        Err(AuthTokenAccessViolation(vec!["EXPIRED".to_string()]))?;
162    }
163
164    // Step 8: Get the latest stable signing certificate registered for Role 0.
165    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    // Step 9: Verify the signature against the Role 0 pk.
176    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    // 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}