use std::{borrow::Cow, sync::Arc};
use arc_swap::ArcSwap;
use http::header::AUTHORIZATION;
use pem::{EncodeConfig, Pem};
use rcgen::KeyPair;
use crate::{
access_control,
background_worker::spawn_background_worker,
connection::{make_connection, ConnectionParams, ReconfigureStrategy},
error,
identity::Identity,
Client, ClientState, Error, IDENTITY_PATH, K8S_SA_TOKENFILE_PATH, LOCAL_CA_CERT_PATH,
};
#[derive(Clone, Copy)]
pub(crate) enum Inference {
Inferred,
Manual,
}
pub struct ClientBuilder {
pub(crate) inner: ConnectionParamsBuilder,
}
impl ClientBuilder {
pub async fn from_environment(mut self) -> Result<Self, Error> {
self.inner.infer().await?;
Ok(self)
}
pub fn with_authly_local_ca_pem(mut self, ca: Vec<u8>) -> Result<Self, Error> {
self.inner.inference = Inference::Manual;
self.inner.jwt_decoding_key = Some(jwt_decoding_key_from_cert(&ca)?);
self.inner.authly_local_ca = Some(ca);
Ok(self)
}
pub fn with_identity(mut self, identity: Identity) -> Self {
self.inner.inference = Inference::Manual;
self.inner.identity = Some(identity);
self
}
pub fn with_url(mut self, url: impl Into<String>) -> Self {
self.inner.url = url.into().into();
self
}
pub fn get_local_ca_pem(&self) -> Result<Cow<[u8]>, Error> {
self.inner
.authly_local_ca
.as_ref()
.map(|ca| Cow::Borrowed(ca.as_slice()))
.ok_or_else(|| Error::AuthlyCA("unconfigured"))
}
pub fn get_identity_pem(&self) -> Result<Cow<[u8]>, Error> {
self.inner
.identity
.as_ref()
.ok_or_else(|| Error::Identity("unconfigured"))?
.to_pem()
}
pub async fn connect(self) -> Result<Client, Error> {
let params = self.inner.try_into_connection_params()?;
let connection = make_connection(params.clone()).await?;
let (reconfigured_tx, reconfigured_rx) = tokio::sync::watch::channel(params.clone());
let reconfigure = match params.inference {
Inference::Inferred => ReconfigureStrategy::ReInfer {
url: params.url.clone(),
},
Inference::Manual => ReconfigureStrategy::Params(params),
};
let resource_property_mapping =
access_control::get_resource_property_mapping(connection.authly_service.clone())
.await?;
let (closed_tx, closed_rx) = tokio::sync::watch::channel(());
let state = Arc::new(ClientState {
conn: ArcSwap::new(Arc::new(connection)),
reconfigure,
reconfigured_rx,
closed_tx,
resource_property_mapping: ArcSwap::new(resource_property_mapping),
});
spawn_background_worker(state.clone(), reconfigured_tx, closed_rx).await?;
let client = Client { state };
Ok(client)
}
}
#[derive(Clone)]
pub(crate) struct ConnectionParamsBuilder {
pub inference: Inference,
pub url: Cow<'static, str>,
pub authly_local_ca: Option<Vec<u8>>,
pub identity: Option<Identity>,
pub jwt_decoding_key: Option<jsonwebtoken::DecodingKey>,
}
impl ConnectionParamsBuilder {
pub(crate) fn new(url: Cow<'static, str>) -> Self {
Self {
inference: Inference::Manual,
url,
authly_local_ca: None,
identity: None,
jwt_decoding_key: None,
}
}
pub(crate) async fn infer(&mut self) -> Result<(), Error> {
self.inference = Inference::Inferred;
let authly_local_ca =
std::fs::read(LOCAL_CA_CERT_PATH).map_err(|_| Error::AuthlyCAmissingInEtc)?;
self.jwt_decoding_key = Some(jwt_decoding_key_from_cert(&authly_local_ca)?);
if std::fs::exists(IDENTITY_PATH).unwrap_or(false) {
self.authly_local_ca = Some(authly_local_ca);
self.identity = Some(
Identity::from_pem(std::fs::read(IDENTITY_PATH).unwrap())
.map_err(|_| Error::Identity("invalid identity"))?,
);
Ok(())
} else if std::fs::exists(K8S_SA_TOKENFILE_PATH).unwrap_or(false) {
let key_pair = KeyPair::generate().map_err(|_err| Error::PrivateKeyGen)?;
let token =
std::fs::read_to_string(K8S_SA_TOKENFILE_PATH).map_err(error::unclassified)?;
let client_cert = reqwest::ClientBuilder::new()
.add_root_certificate(
reqwest::Certificate::from_pem(&authly_local_ca)
.map_err(error::unclassified)?,
)
.build()
.map_err(error::unclassified)?
.post("https://authly-k8s/api/v0/authenticate")
.header(AUTHORIZATION, format!("Bearer {token}"))
.body(key_pair.public_key_der())
.send()
.await
.map_err(error::unauthorized)?
.error_for_status()
.map_err(error::unauthorized)?
.bytes()
.await
.map_err(error::unclassified)?;
let client_cert_pem = pem::encode_config(
&Pem::new("CERTIFICATE", client_cert),
EncodeConfig::new().set_line_ending(pem::LineEnding::LF),
);
self.authly_local_ca = Some(authly_local_ca);
self.identity = Some(Identity {
cert_pem: client_cert_pem.into_bytes(),
key_pem: key_pair.serialize_pem().into_bytes(),
});
Ok(())
} else {
Err(Error::EnvironmentNotInferrable)
}
}
pub fn try_into_connection_params(self) -> Result<Arc<ConnectionParams>, Error> {
let authly_local_ca = self
.authly_local_ca
.clone()
.ok_or_else(|| Error::AuthlyCA("unconfigured"))?;
let jwt_decoding_key = self
.jwt_decoding_key
.ok_or_else(|| Error::AuthlyCA("public key not found"))?;
let identity = self
.identity
.ok_or_else(|| Error::Identity("unconfigured"))?;
Ok(Arc::new(ConnectionParams {
inference: self.inference,
url: self.url,
authly_local_ca,
jwt_decoding_key,
identity,
}))
}
}
pub fn jwt_decoding_key_from_cert(cert: &[u8]) -> Result<jsonwebtoken::DecodingKey, Error> {
let pem = pem::parse(cert).map_err(|_| Error::AuthlyCA("invalid authly certificate"))?;
let (_, x509_cert) = x509_parser::parse_x509_certificate(pem.contents())
.map_err(|_| Error::AuthlyCA("invalid authly certificate"))?;
let public_key = x509_cert.public_key();
Ok(jsonwebtoken::DecodingKey::from_ec_der(
&public_key.subject_public_key.data,
))
}