Skip to main content

kaniop_operator/controller/
kanidm.rs

1use 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    /// Returns the KanidmRef from the resource's spec
30    fn kanidm_ref_spec(&self) -> &KanidmRef;
31
32    /// Returns the namespace selector field for this resource type from the Kanidm spec
33    fn get_namespace_selector(kanidm: &Kanidm) -> &Option<LabelSelector>;
34
35    /// Returns the optional Kanidm entity name override from the spec
36    fn kanidm_name_override(&self) -> Option<&str>;
37
38    /// Returns the name of the referenced Kanidm resource
39    fn kanidm_name(&self) -> String {
40        self.kanidm_ref_spec().name.clone()
41    }
42
43    /// Returns the namespace of the referenced Kanidm resource
44    /// Uses the explicitly specified namespace in kanidm_ref, or falls back to the resource's own namespace
45    fn kanidm_namespace(&self) -> String {
46        self.kanidm_ref_spec()
47            .namespace
48            .clone()
49            // safe unwrap: all resources implementing this trait are namespaced scoped
50            .unwrap_or_else(|| self.namespace().unwrap())
51    }
52
53    /// Returns a string representation of the Kanidm reference in "namespace/name" format
54    fn kanidm_ref(&self) -> String {
55        format!("{}/{}", self.kanidm_namespace(), self.kanidm_name())
56    }
57
58    /// Returns the entity name to use in Kanidm.
59    /// If `kanidmName` is specified in the spec, uses that; otherwise uses the K8s resource name.
60    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
67/// Check if a LabelSelector matches all namespaces (empty selector with no constraints)
68fn 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
72/// Generic function to check if a resource is watched based on namespace selectors
73///
74/// This function implements the common logic for checking whether a resource should be
75/// reconciled based on the namespace selector configuration in the referenced Kanidm resource.
76pub 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            // TODO: ensure that URL matches the service name and port programmatically
174            // using Kanidm object from cache is the unique way
175            .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}