1use std::{borrow::Cow, sync::Arc};
2
3use arc_swap::ArcSwap;
4use http::header::AUTHORIZATION;
5use pem::{EncodeConfig, Pem};
6use rcgen::KeyPair;
7
8use crate::{
9 background_worker::{spawn_background_worker, WorkerSenders},
10 connection::{make_connection, ConnectionParams, ReconfigureStrategy},
11 error, get_configuration,
12 identity::{parse_identity_data, Identity},
13 Client, ClientState, Error, IDENTITY_PATH, K8S_SA_TOKENFILE_PATH, LOCAL_CA_CERT_PATH,
14};
15
16#[derive(Clone, Copy)]
17pub(crate) enum Inference {
18 Inferred,
19 Manual,
20}
21
22pub struct ClientBuilder {
24 pub(crate) inner: ConnectionParamsBuilder,
25}
26
27impl ClientBuilder {
28 pub async fn from_environment(mut self) -> Result<Self, Error> {
30 self.inner.infer().await?;
31 Ok(self)
32 }
33
34 pub fn with_authly_local_ca_pem(mut self, ca: Vec<u8>) -> Result<Self, Error> {
36 self.inner.inference = Inference::Manual;
37 self.inner.authly_local_ca = Some(ca);
38 Ok(self)
39 }
40
41 pub fn with_identity(mut self, identity: Identity) -> Self {
43 self.inner.inference = Inference::Manual;
44 self.inner.identity = Some(identity);
45 self
46 }
47
48 pub fn with_url(mut self, url: impl Into<String>) -> Self {
50 self.inner.url = url.into().into();
51 self
52 }
53
54 pub fn get_local_ca_pem(&self) -> Result<Cow<[u8]>, Error> {
56 self.inner
57 .authly_local_ca
58 .as_ref()
59 .map(|ca| Cow::Borrowed(ca.as_slice()))
60 .ok_or_else(|| Error::AuthlyCA("unconfigured"))
61 }
62
63 pub fn get_identity_pem(&self) -> Result<Cow<[u8]>, Error> {
65 self.inner
66 .identity
67 .as_ref()
68 .ok_or_else(|| Error::Identity("unconfigured"))?
69 .pem()
70 }
71
72 pub async fn connect(self) -> Result<Client, Error> {
74 let params = self.inner.try_into_connection_params()?;
75 let connection = make_connection(params.clone()).await?;
76 let (reconfigured_tx, reconfigured_rx) = tokio::sync::watch::channel(params.clone());
77 let (metadata_invalidated_tx, metadata_invalidated_rx) = tokio::sync::watch::channel(());
78
79 let reconfigure = match params.inference {
80 Inference::Inferred => ReconfigureStrategy::ReInfer {
81 url: params.url.clone(),
82 },
83 Inference::Manual => ReconfigureStrategy::Params(params),
84 };
85
86 let configuration = get_configuration(connection.authly_service.clone()).await?;
87
88 let (closed_tx, closed_rx) = tokio::sync::watch::channel(());
89 let state = Arc::new(ClientState {
90 conn: ArcSwap::new(Arc::new(connection)),
91 reconfigure,
92 reconfigured_rx,
93 metadata_invalidated_rx,
94 closed_tx,
95 configuration: ArcSwap::new(Arc::new(configuration)),
96 });
97
98 spawn_background_worker(
99 state.clone(),
100 WorkerSenders {
101 reconfigured_tx,
102 metadata_invalidated_tx,
103 },
104 closed_rx,
105 )
106 .await?;
107
108 let client = Client { state };
109
110 Ok(client)
111 }
112}
113
114#[derive(Clone)]
115pub(crate) struct ConnectionParamsBuilder {
116 pub inference: Inference,
117 pub url: Cow<'static, str>,
118 pub authly_local_ca: Option<Vec<u8>>,
119 pub identity: Option<Identity>,
120}
121
122impl ConnectionParamsBuilder {
123 pub(crate) fn new(url: Cow<'static, str>) -> Self {
124 Self {
125 inference: Inference::Manual,
126 url,
127 authly_local_ca: None,
128 identity: None,
129 }
130 }
131
132 pub(crate) async fn infer(&mut self) -> Result<(), Error> {
134 self.inference = Inference::Inferred;
135 let authly_local_ca =
136 std::fs::read(LOCAL_CA_CERT_PATH).map_err(|_| Error::AuthlyCAmissingInEtc)?;
137
138 if std::fs::exists(IDENTITY_PATH).unwrap_or(false) {
139 self.authly_local_ca = Some(authly_local_ca);
140 self.identity = Some(
141 Identity::from_pem(std::fs::read(IDENTITY_PATH).unwrap())
142 .map_err(|_| Error::Identity("invalid identity"))?,
143 );
144
145 Ok(())
146 } else if std::fs::exists(K8S_SA_TOKENFILE_PATH).unwrap_or(false) {
147 let key_pair = KeyPair::generate().map_err(|_err| Error::PrivateKeyGen)?;
148 let token =
149 std::fs::read_to_string(K8S_SA_TOKENFILE_PATH).map_err(error::unclassified)?;
150
151 let client_cert = reqwest::ClientBuilder::new()
152 .add_root_certificate(
153 reqwest::Certificate::from_pem(&authly_local_ca)
154 .map_err(error::unclassified)?,
155 )
156 .build()
157 .map_err(error::unclassified)?
158 .post("https://authly-k8s/api/v0/authenticate")
159 .header(AUTHORIZATION, format!("Bearer {token}"))
160 .body(key_pair.public_key_der())
161 .send()
162 .await
163 .map_err(error::unauthorized)?
164 .error_for_status()
165 .map_err(error::unauthorized)?
166 .bytes()
167 .await
168 .map_err(error::unclassified)?;
169 let client_cert_pem = pem::encode_config(
170 &Pem::new("CERTIFICATE", client_cert),
171 EncodeConfig::new().set_line_ending(pem::LineEnding::LF),
172 );
173
174 self.authly_local_ca = Some(authly_local_ca);
175 self.identity = Some(Identity {
176 cert_pem: client_cert_pem.into_bytes(),
177 key_pem: key_pair.serialize_pem().into_bytes(),
178 });
179
180 Ok(())
181 } else {
182 Err(Error::EnvironmentNotInferrable)
183 }
184 }
185
186 pub fn try_into_connection_params(self) -> Result<Arc<ConnectionParams>, Error> {
187 let authly_local_ca = self
188 .authly_local_ca
189 .clone()
190 .ok_or_else(|| Error::AuthlyCA("unconfigured"))?;
191 let identity = self
192 .identity
193 .ok_or_else(|| Error::Identity("unconfigured"))?;
194
195 let jwt_decoding_key = jwt_decoding_key_from_cert(&authly_local_ca)?;
196 let identity_data = parse_identity_data(&identity.cert_pem)?;
197
198 Ok(Arc::new(ConnectionParams {
199 inference: self.inference,
200 url: self.url,
201 authly_local_ca,
202 jwt_decoding_key,
203 identity,
204 entity_id: identity_data.entity_id,
205 }))
206 }
207}
208
209pub fn jwt_decoding_key_from_cert(cert: &[u8]) -> Result<jsonwebtoken::DecodingKey, Error> {
210 let pem = pem::parse(cert).map_err(|_| Error::AuthlyCA("invalid authly certificate"))?;
211
212 let (_, x509_cert) = x509_parser::parse_x509_certificate(pem.contents())
213 .map_err(|_| Error::AuthlyCA("invalid authly certificate"))?;
214
215 let public_key = x509_cert.public_key();
216
217 Ok(jsonwebtoken::DecodingKey::from_ec_der(
219 &public_key.subject_public_key.data,
220 ))
221}