kaniop_operator/controller/
kanidm.rs1use crate::{
2 crd::KanidmRef,
3 kanidm::{
4 crd::Kanidm,
5 reconcile::secret::{
6 ADMIN_PASSWORD_KEY, ADMIN_USER, IDM_ADMIN_PASSWORD_KEY, IDM_ADMIN_USER,
7 },
8 },
9};
10
11use kanidm_client::{KanidmClient, KanidmClientBuilder};
12use kaniop_k8s_util::error::{Error, Result};
13
14use std::collections::HashMap;
15use std::fmt::Debug;
16use std::sync::Arc;
17
18use k8s_openapi::api::core::v1::{Namespace, Secret};
19use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
20use kube::ResourceExt;
21use kube::api::Api;
22use kube::client::Client;
23use kube::core::{Selector, SelectorExt};
24use kube::runtime::reflector::Store;
25use serde::Serialize;
26use tracing::{debug, trace};
27
28pub trait KanidmResource: ResourceExt {
29 fn kanidm_ref_spec(&self) -> &KanidmRef;
31
32 fn get_namespace_selector(kanidm: &Kanidm) -> &Option<LabelSelector>;
34
35 fn kanidm_name_override(&self) -> Option<&str>;
37
38 fn kanidm_name(&self) -> String {
40 self.kanidm_ref_spec().name.clone()
41 }
42
43 fn kanidm_namespace(&self) -> String {
46 self.kanidm_ref_spec()
47 .namespace
48 .clone()
49 .unwrap_or_else(|| self.namespace().unwrap())
51 }
52
53 fn kanidm_ref(&self) -> String {
55 format!("{}/{}", self.kanidm_namespace(), self.kanidm_name())
56 }
57
58 fn kanidm_entity_name(&self) -> String {
61 self.kanidm_name_override()
62 .map(|s| s.to_string())
63 .unwrap_or_else(|| self.name_any())
64 }
65}
66
67fn selector_matches_all(selector: &LabelSelector) -> bool {
69 selector.match_labels.is_none() || selector.match_labels.as_ref().is_some_and(|l| l.is_empty())
70}
71
72pub async fn is_resource_watched<T>(
77 resource: &T,
78 kanidm: &Kanidm,
79 namespace_store: &Store<Namespace>,
80 k8s_client: &Client,
81) -> bool
82where
83 T: KanidmResource,
84{
85 let namespace = resource.namespace().unwrap();
86 trace!(msg = "check if resource is watched", %namespace);
87
88 let namespace_selector = if let Some(selector) = T::get_namespace_selector(kanidm) {
89 selector
90 } else {
91 trace!(msg = "no namespace selector found, defaulting to current namespace");
92 return kanidm.namespace().unwrap() == namespace;
93 };
94
95 if selector_matches_all(namespace_selector) {
96 trace!(msg = "namespace selector matches all namespaces, fast-track accepted");
97 return true;
98 }
99
100 let selector: Selector = if let Ok(s) = namespace_selector.clone().try_into() {
101 s
102 } else {
103 trace!(msg = "failed to parse namespace selector, defaulting to current namespace");
104 return kanidm.namespace().unwrap() == namespace;
105 };
106
107 trace!(msg = "namespace selector", ?selector);
108
109 let found_in_store = namespace_store
110 .state()
111 .iter()
112 .filter(|n| selector.matches(n.metadata.labels.as_ref().unwrap_or(&Default::default())))
113 .any(|n| n.name_any() == namespace);
114
115 if found_in_store {
116 return true;
117 }
118
119 trace!(msg = "namespace not found in store, fetching from K8s API", %namespace);
120 let namespace_api: Api<Namespace> = Api::all(k8s_client.clone());
121 match namespace_api.get(&namespace).await {
122 Ok(ns) => {
123 let matches =
124 selector.matches(ns.metadata.labels.as_ref().unwrap_or(&Default::default()));
125 trace!(msg = "namespace fetched from API", %namespace, matches);
126 matches
127 }
128 Err(e) => {
129 trace!(msg = "failed to fetch namespace from API, treating as not watched", %namespace, ?e);
130 false
131 }
132 }
133}
134
135#[derive(Serialize, Clone, Debug, PartialEq, Eq, Hash)]
136pub enum KanidmUser {
137 IdmAdmin,
138 Admin,
139}
140
141#[derive(Default)]
142pub struct KanidmClients(HashMap<KanidmKey, Arc<KanidmClient>>);
143
144impl KanidmClients {
145 pub fn get(&self, key: &KanidmKey) -> Option<&Arc<KanidmClient>> {
146 self.0.get(key)
147 }
148
149 pub fn insert(
150 &mut self,
151 key: KanidmKey,
152 client: Arc<KanidmClient>,
153 ) -> Option<Arc<KanidmClient>> {
154 self.0.insert(key, client)
155 }
156
157 pub fn remove(&mut self, key: &KanidmKey) -> Option<Arc<KanidmClient>> {
158 let client = self.0.remove(key);
159 self.0.shrink_to_fit();
160 client
161 }
162
163 pub async fn create_client(
164 namespace: &str,
165 name: &str,
166 user: KanidmUser,
167 k_client: Client,
168 ) -> Result<Arc<KanidmClient>> {
169 debug!(msg = "create Kanidm client", namespace, name);
170
171 let client = KanidmClientBuilder::new()
172 .danger_accept_invalid_certs(true)
173 .address(format!("https://{name}.{namespace}.svc:8443"))
176 .connect_timeout(5)
177 .build()
178 .map_err(|e| {
179 Error::KanidmClientError("failed to build Kanidm client".to_string(), Box::new(e))
180 })?;
181
182 let secret_api = Api::<Secret>::namespaced(k_client.clone(), namespace);
183 let secret_name = format!("{name}-admin-passwords");
184 let admin_secret = secret_api.get(&secret_name).await.map_err(|e| {
185 Error::KubeError(
186 format!("failed to get secret: {namespace}/{secret_name}"),
187 Box::new(e),
188 )
189 })?;
190 let secret_data = admin_secret.data.ok_or_else(|| {
191 Error::MissingData(format!(
192 "failed to get data in secret: {namespace}/{secret_name}"
193 ))
194 })?;
195
196 let (username, password_key) = match user {
197 KanidmUser::Admin => (ADMIN_USER, ADMIN_PASSWORD_KEY),
198 KanidmUser::IdmAdmin => (IDM_ADMIN_USER, IDM_ADMIN_PASSWORD_KEY),
199 };
200 trace!(
201 msg = format!("fetch Kanidm {username} password"),
202 namespace, name, secret_name
203 );
204 let password_bytes = secret_data.get(password_key).ok_or_else(|| {
205 Error::MissingData(format!(
206 "missing password for {username} in secret: {namespace}/{secret_name}"
207 ))
208 })?;
209
210 let password = std::str::from_utf8(&password_bytes.0)
211 .map_err(|e| Error::Utf8Error("failed to convert password to string".to_string(), e))?;
212 trace!(
213 msg = format!("authenticating with new client and user {username}"),
214 namespace, name
215 );
216 client
217 .auth_simple_password(username, password)
218 .await
219 .map_err(|e| {
220 Error::KanidmClientError("client failed to authenticate".to_string(), Box::new(e))
221 })?;
222 Ok(Arc::new(client))
223 }
224}
225
226#[derive(Clone, PartialEq, Hash, Eq)]
227pub struct KanidmKey {
228 pub namespace: String,
229 pub name: String,
230}
231
232#[derive(Clone, PartialEq, Hash, Eq, Debug)]
233pub struct ClientLockKey {
234 pub namespace: String,
235 pub name: String,
236 pub user: KanidmUser,
237}