cat_gateway/service/api/config/
mod.rs

1//! Configuration Endpoints
2
3use poem::web::RealIp;
4use poem_openapi::{param::Query, payload::Json, ApiResponse, OpenApi};
5use serde_json::{Map, Value};
6use tracing::error;
7
8use crate::{
9    db::event::{
10        config::{key::ConfigKey, Config},
11        error::NotFoundError,
12    },
13    service::common::{
14        auth::{api_key::InternalApiKeyAuthorization, none_or_rbac::NoneOrRBAC},
15        responses::WithErrorResponses,
16        tags::ApiTags,
17        types::generic::{ip_addr::IpAddr, json_object::JsonObject},
18    },
19};
20
21/// Configuration API struct
22pub(crate) struct ConfigApi;
23
24/// Get configuration endpoint responses.
25#[derive(ApiResponse)]
26enum GetConfigResponses {
27    /// Configuration result.
28    #[oai(status = 200)]
29    Ok(Json<JsonObject>),
30
31    /// No frontend config defined.
32    #[oai(status = 404)]
33    NotFound,
34}
35/// Get configuration all responses.
36type GetConfigAllResponses = WithErrorResponses<GetConfigResponses>;
37
38/// Set configuration endpoint responses.
39#[derive(ApiResponse)]
40enum SetConfigResponse {
41    /// Configuration Update Successful.
42    #[oai(status = 204)]
43    Ok,
44}
45/// Set configuration all responses.
46type SetConfigAllResponses = WithErrorResponses<SetConfigResponse>;
47
48#[OpenApi(tag = "ApiTags::Config")]
49impl ConfigApi {
50    /// Get the configuration for the frontend.
51    ///
52    /// Get the frontend configuration for the requesting client.
53    ///
54    /// ### Security
55    ///
56    /// Does not require any Catalyst RBAC Token to access.
57    #[oai(
58        path = "/v1/config/frontend",
59        method = "get",
60        operation_id = "get_config_frontend"
61    )]
62    async fn get_frontend(
63        &self,
64        ip_address: RealIp,
65        _auth: NoneOrRBAC,
66    ) -> GetConfigAllResponses {
67        let general_config: JsonObject = match Config::get(ConfigKey::Frontend)
68            .await
69            .and_then(TryInto::try_into)
70        {
71            Ok(value) => value,
72            Err(err) if err.is::<NotFoundError>() => return GetConfigResponses::NotFound.into(),
73            Err(err) => {
74                error!(id="get_frontend_config_general", error=?err, "Failed to get general frontend configuration");
75                return GetConfigAllResponses::handle_error(&err);
76            },
77        };
78
79        // Attempt to fetch the IP configuration
80        let ip_config: Option<JsonObject> = if let Some(ip) = ip_address.0 {
81            match Config::get(ConfigKey::FrontendForIp(ip))
82                .await
83                .and_then(TryInto::try_into)
84            {
85                Ok(value) => Some(value),
86                Err(err) if err.is::<NotFoundError>() => None,
87                Err(err) => {
88                    error!(id = "get_frontend_config_ip", error = ?err, "Failed to get frontend configuration for IP");
89                    return GetConfigAllResponses::handle_error(&err);
90                },
91            }
92        } else {
93            None
94        };
95        if let Some(ip_config) = ip_config {
96            let config = merge_configs(general_config, ip_config);
97            GetConfigResponses::Ok(Json(config)).into()
98        } else {
99            GetConfigResponses::Ok(Json(general_config)).into()
100        }
101    }
102
103    /// Set the frontend configuration.
104    ///
105    /// Store the given config as either global front end configuration, or configuration
106    /// for a client at a specific IP address.
107    ///
108    /// ### Security
109    ///
110    /// Requires Admin Authoritative RBAC Token.
111    #[oai(
112        path = "/v1/config/frontend",
113        method = "put",
114        operation_id = "put_config_frontend",
115        hidden = true
116    )]
117    async fn put_frontend(
118        &self,
119        /// *OPTIONAL* The IP Address to set the configuration for.
120        #[oai(name = "IP")]
121        Query(ip_address): Query<Option<IpAddr>>,
122        Json(json_config): Json<JsonObject>,
123        _auth: InternalApiKeyAuthorization,
124    ) -> SetConfigAllResponses {
125        if let Some(ip) = ip_address {
126            set(ConfigKey::FrontendForIp(ip.into()), json_config.into()).await
127        } else {
128            set(ConfigKey::Frontend, json_config.into()).await
129        }
130    }
131}
132
133/// Helper function to merge two JSON values.
134fn merge_configs(
135    general: JsonObject,
136    ip_specific: JsonObject,
137) -> JsonObject {
138    let mut merged = general;
139
140    for (key, value) in Map::<String, Value>::from(ip_specific) {
141        if let Some(existing_value) = merged.get_mut(&key) {
142            *existing_value = value;
143        } else {
144            merged.insert(key, value);
145        }
146    }
147
148    merged
149}
150
151/// Helper function to handle set.
152async fn set(
153    key: ConfigKey,
154    value: Value,
155) -> SetConfigAllResponses {
156    match Config::set(key, value).await {
157        Ok(()) => SetConfigResponse::Ok.into(),
158        Err(err) => {
159            error!(id="set_config_frontend", error=?err, "Failed to set frontend configuration");
160            SetConfigAllResponses::handle_error(&err)
161        },
162    }
163}