Skip to content

Commit

Permalink
fix(cat-gateway): Add APIKey and CatToken auth to some endpoints. Add…
Browse files Browse the repository at this point in the history
… 401 and 403 common responses.
  • Loading branch information
stevenj committed Oct 28, 2024
1 parent c64d473 commit 1459b7a
Show file tree
Hide file tree
Showing 17 changed files with 210 additions and 32 deletions.
4 changes: 0 additions & 4 deletions catalyst-gateway/bin/src/service/api/auth/mod.rs

This file was deleted.

15 changes: 14 additions & 1 deletion catalyst-gateway/bin/src/service/api/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use tracing::error;
use crate::{
db::event::config::{key::ConfigKey, Config},
service::common::{
auth::scheme::CatalystSecurityScheme,
objects::config::{frontend_config::FrontendConfig, ConfigBadRequest},
responses::WithErrorResponses,
tags::ApiTags,
Expand Down Expand Up @@ -58,6 +59,10 @@ impl ConfigApi {
/// Get the configuration for the frontend.
///
/// Get the frontend configuration for the requesting client.
///
/// ### Security
///
/// Does not require any Catalyst RBAC Token to access.
#[oai(
path = "/draft/config/frontend",
method = "get",
Expand Down Expand Up @@ -108,6 +113,10 @@ impl ConfigApi {
///
/// Returns the JSON schema which defines the data which can be read or written for
/// the frontend configuration.
///
/// ### Security
///
/// Does not require any Catalyst RBAC Token to access.
#[oai(
path = "/draft/config/frontend/schema",
method = "get",
Expand All @@ -126,6 +135,10 @@ impl ConfigApi {
///
/// Store the given config as either global front end configuration, or configuration
/// for a client at a specific IP address.
///
/// ### Security
///
/// Requires Admin Authoritative RBAC Token.
#[oai(
path = "/draft/config/frontend",
method = "put",
Expand All @@ -136,7 +149,7 @@ impl ConfigApi {
/// *OPTIONAL* The IP Address to set the configuration for.
#[oai(name = "IP")]
ip_query: Query<Option<IpAddr>>,
body: Json<FrontendConfig>,
_auth: CatalystSecurityScheme, body: Json<FrontendConfig>,
) -> SetConfigAllResponses {
let body_value = body.0.to_json();

Expand Down
12 changes: 7 additions & 5 deletions catalyst-gateway/bin/src/service/api/health/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Health Endpoints
use poem_openapi::{param::Query, OpenApi};

use crate::service::common::tags::ApiTags;
use crate::service::common::{auth::api_key::InternalApiKeyAuthorization, tags::ApiTags};

mod inspection_get;
mod live_get;
Expand All @@ -28,7 +28,7 @@ impl HealthApi {
method = "get",
operation_id = "healthStarted"
)]
async fn started_get(&self) -> started_get::AllResponses {
async fn started_get(&self, _auth: InternalApiKeyAuthorization) -> started_get::AllResponses {
started_get::endpoint().await
}

Expand All @@ -46,7 +46,7 @@ impl HealthApi {
method = "get",
operation_id = "healthReady"
)]
async fn ready_get(&self) -> ready_get::AllResponses {
async fn ready_get(&self, _auth: InternalApiKeyAuthorization) -> ready_get::AllResponses {
ready_get::endpoint().await
}

Expand All @@ -59,7 +59,7 @@ impl HealthApi {
/// *This endpoint is for internal use of the service deployment infrastructure.
/// It may not be exposed publicly. Refer to []*
#[oai(path = "/v1/health/live", method = "get", operation_id = "healthLive")]
async fn live_get(&self) -> live_get::AllResponses {
async fn live_get(&self, _auth: InternalApiKeyAuthorization) -> live_get::AllResponses {
live_get::endpoint().await
}

Expand All @@ -71,9 +71,10 @@ impl HealthApi {
///
/// *This endpoint is for internal use of the service deployment infrastructure.
/// It may not be exposed publicly.*
// TODO: Make the parameters to this a JSON Body, not query parameters.
#[oai(
path = "/v1/health/inspection",
method = "get",
method = "put",
operation_id = "healthInspection"
)]
async fn inspection(
Expand All @@ -83,6 +84,7 @@ impl HealthApi {
/// Enable or disable Verbose Query inspection in the logs. Used to find query
/// performance issues.
query_inspection: Query<Option<inspection_get::DeepQueryInspectionFlag>>,
_auth: InternalApiKeyAuthorization,
) -> inspection_get::AllResponses {
inspection_get::endpoint(log_level.0, query_inspection.0).await
}
Expand Down
3 changes: 1 addition & 2 deletions catalyst-gateway/bin/src/service/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ use poem_openapi::{ContactObject, LicenseObject, OpenApiService, ServerObject};

use self::cardano::CardanoApi;
use crate::settings::Settings;
/// Auth
mod auth;

pub(crate) mod cardano;
mod config;
mod health;
Expand Down
31 changes: 31 additions & 0 deletions catalyst-gateway/bin/src/service/common/auth/api_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! API Key authorization scheme is used ONLY by internal endpoints.
//!
//! Its purpose is to prevent their use externally, if they were accidentally exposed.
//!
//! It is NOT to be used on any endpoint intended to be publicly facing.
use poem::Request;
use poem_openapi::{auth::ApiKey, SecurityScheme};

use crate::settings::Settings;

/// `ApiKey` authorization for Internal Endpoints
#[derive(SecurityScheme)]
#[oai(
ty = "api_key",
key_name = "X-API-Key",
key_in = "header",
checker = "api_checker"
)]
#[allow(dead_code)]
pub(crate) struct InternalApiKeyAuthorization(String);

/// Check the provided API Key matches the API Key defined by for the deployment.
#[allow(clippy::unused_async)]
async fn api_checker(_req: &Request, api_key: ApiKey) -> Option<String> {
if Settings::check_internal_api_key(&api_key.key) {
Some(api_key.key)
} else {
None
}
}
7 changes: 7 additions & 0 deletions catalyst-gateway/bin/src/service/common/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Catalyst RBAC Token Authentication
pub(crate) mod api_key;
/// Cat security scheme
pub(crate) mod scheme;
/// Token encoding decoding logic
mod token;
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ use poem::{error::ResponseError, http::StatusCode, Request};
use poem_openapi::{auth::Bearer, SecurityScheme};
use tracing::error;

use super::token::{Kid, SignatureEd25519, UlidBytes};
use crate::service::api::auth::token::decode_auth_token_ed25519;
use super::token::{decode_auth_token_ed25519, Kid, SignatureEd25519, UlidBytes};

/// Decoded token consists of a Kid, Ulid and Signature
pub type DecodedAuthToken = (Kid, UlidBytes, SignatureEd25519);
Expand Down Expand Up @@ -47,13 +46,12 @@ static CERTS: LazyLock<DashMap<String, [u8; PUBLIC_KEY_LENGTH]>> = LazyLock::new
#[oai(
rename = "CatalystSecurityScheme",
ty = "bearer",
key_in = "header",
key_name = "Bearer",
bearer_format = "catalyst-rbac-token",
checker = "checker_api_catalyst_auth"
)]
/// Catalyst RBAC Access Token
#[allow(clippy::module_name_repetitions)]
#[allow(dead_code)]
/// Auth token security scheme
/// Add to endpoint params e.g async fn endpoint(&self, auth: `CatalystSecurityScheme`)
pub struct CatalystSecurityScheme(pub DecodedAuthToken);

#[derive(Debug, thiserror::Error)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ mod tests {
use rand::rngs::OsRng;

use super::{encode_auth_token_ed25519, Kid, UlidBytes};
use crate::service::api::auth::token::decode_auth_token_ed25519;
use crate::service::common::auth::token::decode_auth_token_ed25519;

#[test]
fn test_token_generation_and_decoding() {
Expand Down
2 changes: 2 additions & 0 deletions catalyst-gateway/bin/src/service/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Define common and reusable api components here.
//! these components should be structured into their own sub modules.
pub(crate) mod auth;
pub(crate) mod objects;
pub(crate) mod responses;
pub(crate) mod tags;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! Define `Unauthorized` response type.
use poem_openapi::{types::Example, Object};
use uuid::Uuid;

#[derive(Debug, Object)]
#[oai(example, skip_serializing_if_is_none)]
/// Server Error response to a Bad request.
pub(crate) struct Unauthorized {
/// Unique ID of this Server Error so that it can be located easily for debugging.
id: Uuid,
/// Error message.
// Will not contain sensitive information, internal details or backtraces.
#[oai(validator(max_length = "1000", pattern = "^[0-9a-zA-Z].*$"))]
msg: String,
}

impl Unauthorized {
/// Create a new Server Error Response Payload.
pub(crate) fn new(msg: Option<String>) -> Self {
let msg = msg.unwrap_or(
"Your request was not successful because it lacks valid authentication credentials for the requested resource.".to_string(),
);
let id = Uuid::new_v4();

Self { id, msg }
}
}

impl Example for Unauthorized {
/// Example for the Too Many Requests Payload.
fn example() -> Self {
Self::new(None)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! Define `Forbidden` response type.
use poem_openapi::{types::Example, Object};
use uuid::Uuid;

#[derive(Debug, Object)]
#[oai(example, skip_serializing_if_is_none)]
/// Server Error response to a Bad request.
pub(crate) struct Forbidden {
/// Unique ID of this Server Error so that it can be located easily for debugging.
id: Uuid,
/// Error message.
// Will not contain sensitive information, internal details or backtraces.
#[oai(validator(max_length = "1000", pattern = "^[0-9a-zA-Z].*$"))]
msg: String,
/// List or Roles required to access the resource.
// TODO: This should be a Vector of defined Roles/Grants.
// When those are defined, use that type instead of "String"
// It should look like an enum.
#[oai(validator(max_items = 100, max_length = "100", pattern = "^[0-9a-zA-Z].*$"))]
required: Option<Vec<String>>,
}

impl Forbidden {
/// Create a new Server Error Response Payload.
pub(crate) fn new(msg: Option<String>, roles: Option<Vec<String>>) -> Self {
let msg = msg.unwrap_or(
"Your request was not successful because your authentication credentials do not have the required roles for the requested resource.".to_string(),
);
let id = Uuid::new_v4();

Self {
id,
msg,
required: roles,
}
}
}

impl Example for Forbidden {
/// Example for the Too Many Requests Payload.
fn example() -> Self {
Self::new(
None,
Some(vec!["VOTER".to_string(), "PROPOSER".to_string()]),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ use uuid::Uuid;
pub(crate) struct TooManyRequests {
/// Unique ID of this Server Error so that it can be located easily for debugging.
id: Uuid,
/// *Optional* SHORT Error message.
/// Will not contain sensitive information, internal details or backtraces.
// TODO(bkioshn): https://github.com/input-output-hk/catalyst-voices/issues/239
/// Error message.
// Will not contain sensitive information, internal details or backtraces.
#[oai(validator(max_length = "100", pattern = "^[0-9a-zA-Z].*$"))]
msg: String,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@ use crate::settings::Settings;
pub(crate) struct InternalServerError {
/// Unique ID of this Server Error so that it can be located easily for debugging.
id: Uuid,
/// *Optional* SHORT Error message.
/// Will not contain sensitive information, internal details or backtraces.
// TODO(bkioshn): https://github.com/input-output-hk/catalyst-voices/issues/239
/// Error message.
// Will not contain sensitive information, internal details or backtraces.
#[oai(validator(max_length = "100", pattern = "^[0-9a-zA-Z].*$"))]
msg: String,
/// A URL to report an issue.
// TODO(bkioshn): https://github.com/input-output-hk/catalyst-voices/issues/239
#[oai(validator(max_length = "1000"))]
issue: Option<Url>,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use uuid::Uuid;
pub(crate) struct ServiceUnavailable {
/// Unique ID of this Server Error so that it can be located easily for debugging.
id: Uuid,
/// *Optional* SHORT Error message.
/// Will not contain sensitive information, internal details or backtraces.
/// Error message.
// Will not contain sensitive information, internal details or backtraces.
#[oai(validator(max_length = "100", pattern = "^[0-9a-zA-Z].*$"))]
msg: String,
}
Expand Down
Loading

0 comments on commit 1459b7a

Please sign in to comment.