use std::str::FromStr;
use crate::http::{HttpClient, InitializeHttpClientError};
use crate::http_utils::HttpRequestError;
use serde::{Deserialize, Deserializer, de};
#[derive(Debug, Deserialize, PartialEq, Clone)]
pub(super) struct LockConfig {
pub version: String,
#[serde(rename = "issuer", deserialize_with = "host_to_oidc")]
pub issuer_oidc_url: Url,
#[serde(rename = "audit", default)]
pub audit_endpoints: AuditEndpoints,
#[serde(rename = "config", default)]
pub config_endpoints: ConfigEndpoints,
}
#[derive(Debug, Deserialize, PartialEq, Clone, Default)]
pub(super) struct AuditEndpoints {
#[serde(
rename = "log_endpoint",
deserialize_with = "deserialize_to_bulk_endpoint",
default
)]
pub log: Option<Url>,
#[serde(
rename = "health_endpoint",
deserialize_with = "deserialize_to_bulk_endpoint",
default
)]
pub health: Option<Url>,
#[serde(
rename = "telemetry_endpoint",
deserialize_with = "deserialize_to_bulk_endpoint",
default
)]
pub telemetry: Option<Url>,
}
#[derive(Debug, Deserialize, PartialEq, Clone, Default)]
pub(super) struct ConfigEndpoints {
#[serde(rename = "config_endpoint", default)]
pub config: Option<Url>,
#[serde(rename = "issuers_endpoint", default)]
pub issuers: Option<Url>,
#[serde(rename = "policy_endpoint", default)]
pub policy: Option<Url>,
#[serde(rename = "schema_endpoint", default)]
pub schema: Option<Url>,
#[serde(rename = "sse_endpoint", default)]
pub sse: Option<Url>,
}
#[derive(Debug, thiserror::Error)]
pub enum GetLockConfigError {
#[error(transparent)]
InitializeHttpClient(#[from] InitializeHttpClientError),
#[error(transparent)]
Request(#[from] HttpRequestError),
}
impl LockConfig {
pub(super) async fn get(
lock_config_url: &url::Url,
http_client: &HttpClient,
) -> Result<Self, GetLockConfigError> {
let config: LockConfig = http_client.get_json(lock_config_url.as_ref()).await?;
Ok(config)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(super) struct Url(pub(super) url::Url);
impl AsRef<url::Url> for Url {
fn as_ref(&self) -> &url::Url {
&self.0
}
}
impl AsRef<str> for Url {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl FromStr for Url {
type Err = url::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let inner: url::Url = s.parse()?;
Ok(Self(inner))
}
}
impl<'de> Deserialize<'de> for Url {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let src: String = String::deserialize(deserializer)?;
let url = src
.parse()
.map_err(|e| de::Error::custom(format!("error while parsing `{src}` as a URL: {e}")))?;
Ok(url)
}
}
fn deserialize_to_bulk_endpoint<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
where
D: Deserializer<'de>,
{
let src: String = String::deserialize(deserializer)?;
if src.is_empty() {
return Ok(None);
}
let src = src + "/bulk";
let url: url::Url = src.parse().map_err(|e| {
de::Error::custom(format!("the bulk endpoint url, '{src}', is not valid: {e}"))
})?;
Ok(Some(Url(url)))
}
fn host_to_oidc<'de, D>(deserializer: D) -> Result<Url, D::Error>
where
D: Deserializer<'de>,
{
let host: String = String::deserialize(deserializer)?;
#[cfg(not(test))]
let derived_url = format!("https://{host}/.well-known/openid-configuration");
#[cfg(test)]
let derived_url = format!("http://{host}/.well-known/openid-configuration");
let url: url::Url = derived_url.parse().map_err(|e| {
de::Error::custom(format!(
"the derived url, '{derived_url}', from the host, '{host}' is not valid: {e}"
))
})?;
Ok(Url(url))
}
#[cfg(test)]
mod test {
use super::super::lock_config::{AuditEndpoints, ConfigEndpoints};
use super::LockConfig;
use serde_json::json;
use test_utils::assert_eq;
#[tokio::test]
async fn should_deserialize_lock_config() {
let src = json!({
"version": "1.0",
"issuer": "test.com",
"audit": {
"log_endpoint": "https://test.com/audit/log",
"health_endpoint": "",
},
});
let deserialized =
serde_json::from_value::<LockConfig>(src).expect("deserialize audit endpoints");
assert_eq!(
deserialized,
LockConfig {
version: "1.0".into(),
issuer_oidc_url: "http://test.com/.well-known/openid-configuration"
.parse()
.unwrap(),
audit_endpoints: AuditEndpoints {
log: Some("https://test.com/audit/log/bulk".parse().unwrap()),
health: None,
telemetry: None
},
config_endpoints: ConfigEndpoints::default(),
}
);
}
}