kaniop_operator/kanidm/controller/
context.rs1use crate::controller::context::BackoffContext;
2use crate::kanidm::reconcile::secret::REPLICA_SECRET_KEY;
3use crate::metrics::ControllerMetrics;
4use crate::{controller::context::Context as KaniopContext, kanidm::crd::Kanidm};
5use kaniop_k8s_util::error::{Error, Result};
6
7use std::collections::HashMap;
8use std::sync::Arc;
9use std::time::Duration;
10
11use base64::{Engine as _, engine::general_purpose::URL_SAFE};
12use k8s_openapi::api::apps::v1::StatefulSet;
13use k8s_openapi::api::core::v1::{Secret, Service};
14use k8s_openapi::api::networking::v1::Ingress;
15use kube::runtime::reflector::{ObjectRef, Store};
16use openssl::asn1::Asn1Time;
17use openssl::x509::X509;
18use tokio::sync::RwLock;
19use tracing::trace;
20
21#[derive(Clone)]
22pub struct Context {
23 pub kaniop_ctx: KaniopContext<Kanidm>,
24 pub stores: Arc<Stores>,
26 repl_cert_exp_cache: Arc<RwLock<ReplicaCertExpiration>>,
27 repl_cert_host_cache: Arc<RwLock<ReplicaCertHost>>,
28}
29
30impl Context {
31 pub fn new(kaniop_ctx: KaniopContext<Kanidm>, stores: Stores) -> Self {
32 Context {
33 kaniop_ctx,
34 stores: Arc::new(stores),
35 repl_cert_exp_cache: Arc::default(),
36 repl_cert_host_cache: Arc::default(),
37 }
38 }
39
40 pub async fn get_repl_cert_exp(&self, secret_ref: &ObjectRef<Secret>) -> Option<i64> {
41 trace!(msg = format!("getting replica certificate expiration for {secret_ref}"));
42 self.repl_cert_exp_cache
43 .read()
44 .await
45 .0
46 .get(secret_ref)
47 .cloned()
48 }
49
50 pub async fn insert_repl_cert_exp(&self, secret: &Secret) -> Result<()> {
51 trace!(
52 msg = format!(
53 "inserting replica certificate expiration for {:?}",
54 &ObjectRef::from(secret)
55 )
56 );
57 match &secret.data {
58 None => Err(Error::MissingData("secret data empty".to_string())),
59 Some(data) => match data.get(REPLICA_SECRET_KEY) {
60 None => Err(Error::MissingData(format!(
61 "secret data missing key {REPLICA_SECRET_KEY}"
62 ))),
63 Some(cert_b64url) => {
64 let (expiration, host) = parse_cert_expiration_and_host(
65 String::from_utf8_lossy(&cert_b64url.0).as_ref(),
66 )?;
67 let obj_ref = ObjectRef::from(secret);
68 self.repl_cert_exp_cache
69 .write()
70 .await
71 .0
72 .insert(obj_ref.clone(), expiration);
73 self.repl_cert_host_cache
74 .write()
75 .await
76 .0
77 .insert(obj_ref, host);
78 Ok(())
79 }
80 },
81 }
82 }
83
84 #[inline]
85 pub async fn remove_repl_cert_exp(&self, secret_ref: &ObjectRef<Secret>) {
86 trace!(msg = format!("removing replica certificate expiration for {secret_ref}",));
87 self.repl_cert_exp_cache.write().await.0.remove(secret_ref);
88 }
89
90 pub async fn get_repl_cert_host(&self, secret_ref: &ObjectRef<Secret>) -> Option<String> {
91 trace!(msg = format!("getting replica certificate host for {secret_ref}"));
92 self.repl_cert_host_cache
93 .read()
94 .await
95 .0
96 .get(secret_ref)
97 .cloned()
98 }
99
100 #[inline]
101 pub async fn remove_repl_cert_host(&self, secret_ref: &ObjectRef<Secret>) {
102 trace!(msg = format!("removing replica certificate host for {secret_ref}",));
103 self.repl_cert_host_cache.write().await.0.remove(secret_ref);
104 }
105}
106
107impl BackoffContext<Kanidm> for Context {
108 fn metrics(&self) -> &Arc<ControllerMetrics> {
109 self.kaniop_ctx.metrics()
110 }
111 async fn get_backoff(&self, obj_ref: ObjectRef<Kanidm>) -> Duration {
112 self.kaniop_ctx.get_backoff(obj_ref).await
113 }
114
115 async fn reset_backoff(&self, obj_ref: ObjectRef<Kanidm>) {
116 self.kaniop_ctx.reset_backoff(obj_ref).await
117 }
118}
119
120pub struct Stores {
121 pub stateful_set_store: Store<StatefulSet>,
122 pub service_store: Store<Service>,
123 pub ingress_store: Store<Ingress>,
124 pub secret_store: Store<Secret>,
125}
126
127#[derive(Default)]
128struct ReplicaCertExpiration(HashMap<ObjectRef<Secret>, i64>);
129
130#[derive(Default)]
131struct ReplicaCertHost(HashMap<ObjectRef<Secret>, String>);
132
133fn parse_cert_expiration_and_host(cert_b64url: &str) -> Result<(i64, String)> {
134 let der_bytes = URL_SAFE
135 .decode(cert_b64url)
136 .map_err(|e| Error::ParseError(format!("invalid base64url encoding: {e}")))?;
137
138 let cert = X509::from_der(&der_bytes)
139 .map_err(|e| Error::ParseError(format!("failed to parse DER certificate: {e}")))?;
140 let not_after = cert.not_after();
141 trace!(msg = format!("certificate not after: {not_after}"));
142
143 let epoch = Asn1Time::from_unix(0).unwrap();
144 let duration = epoch.diff(not_after).unwrap();
145 let timestamp = duration.days as i64 * 86400 + duration.secs as i64;
146
147 let san = cert
148 .subject_alt_names()
149 .ok_or_else(|| Error::ParseError("no SAN extension".to_string()))?;
150 let host = san
151 .iter()
152 .find_map(|name| {
153 if let Some(dns) = name.dnsname() {
154 Some(dns.to_string())
155 } else if let Some(ip_bytes) = name.ipaddress() {
156 if ip_bytes.len() == 4 {
157 let ip = std::net::Ipv4Addr::from(<[u8; 4]>::try_from(ip_bytes).unwrap());
158 Some(ip.to_string())
159 } else if ip_bytes.len() == 16 {
160 let ip = std::net::Ipv6Addr::from(<[u8; 16]>::try_from(ip_bytes).unwrap());
161 Some(ip.to_string())
162 } else {
163 None
164 }
165 } else {
166 None
167 }
168 })
169 .ok_or_else(|| Error::ParseError("no DNS or IP in SAN".to_string()))?;
170 Ok((timestamp, host))
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_get_cert_expiration_valid_cert() {
179 let cert_b64url = "MIIB_DCCAaGgAwIBAgIBATAKBggqhkjOPQQDAjBMMRswGQYDVQQKDBJLYW5pZG0gUmVwbGljYXRpb24xLTArBgNVBAMMJDJiYTgzMTZhLWViYWEtNGJjMS04NDkzLTVmODZmYWZhZTU5NDAeFw0yNDExMDYxOTEzMjdaFw0yODExMDYxOTEzMjdaMEwxGzAZBgNVBAoMEkthbmlkbSBSZXBsaWNhdGlvbjEtMCsGA1UEAwwkMmJhODMxNmEtZWJhYS00YmMxLTg0OTMtNWY4NmZhZmFlNTk0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuXp1hNNZerxDQbCh7rAGW6uM0CPECNd3IvbSh7qH34MkO_plwwDVKFbzcTG8HJE2ouIJlJYN8P4wf6qmrRQMAKN0MHIwDAYDVR0TAQH_BAIwADAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBTaOaPuXmtLDTJVv--VYBiQr9gHCTAUBgNVHREEDTALgglsb2NhbGhvc3QwCgYIKoZIzj0EAwIDSQAwRgIhAIZD_J4LyR7D0kg41GRg_TcRxm5mEVhM6WL9BO3XmfUsAiEA7Wpbkvd0b1e-Sg8AS9jP-CpBpmTnC7oEChkyhUYKyFc=";
180 let (expiration, host) = parse_cert_expiration_and_host(cert_b64url).unwrap();
181 assert_eq!(expiration, 1857150807);
182 assert_eq!(host, "localhost");
183 }
184}