diff --git a/src/bin/crdgen.rs b/src/bin/crdgen.rs index 04bea51..34717e2 100644 --- a/src/bin/crdgen.rs +++ b/src/bin/crdgen.rs @@ -2,7 +2,8 @@ use kube::CustomResourceExt; fn main() { print!( - "{}", - serde_yaml::to_string(&lldap_controller::resources::ServiceUser::crd()).unwrap() + "{}---\n{}", + serde_yaml::to_string(&lldap_controller::resources::ServiceUser::crd()).unwrap(), + serde_yaml::to_string(&lldap_controller::resources::Group::crd()).unwrap() ) } diff --git a/src/context.rs b/src/context.rs index 5fe9705..416100c 100644 --- a/src/context.rs +++ b/src/context.rs @@ -37,10 +37,18 @@ pub trait ControllerEvents { where T: Resource + Sync; + async fn group_created(&self, obj: &T, name: &str) -> Result<(), Self::Error> + where + T: Resource + Sync; + async fn user_deleted(&self, obj: &T, username: &str) -> Result<(), Self::Error> where T: Resource + Sync; + async fn group_deleted(&self, obj: &T, name: &str) -> Result<(), Self::Error> + where + T: Resource + Sync; + async fn user_not_found(&self, obj: &T, username: &str) -> Result<(), Self::Error> where T: Resource + Sync; @@ -83,6 +91,23 @@ impl ControllerEvents for Recorder { .await } + async fn group_created(&self, obj: &T, name: &str) -> Result<(), Self::Error> + where + T: Resource + Sync, + { + self.publish( + &Event { + type_: EventType::Normal, + reason: "GroupCreated".into(), + note: Some(format!("Created group '{name}'")), + action: "GroupCreated".into(), + secondary: None, + }, + &obj.object_ref(&()), + ) + .await + } + async fn user_deleted(&self, obj: &T, username: &str) -> Result<(), Self::Error> where T: Resource + Sync, @@ -100,6 +125,23 @@ impl ControllerEvents for Recorder { .await } + async fn group_deleted(&self, obj: &T, name: &str) -> Result<(), Self::Error> + where + T: Resource + Sync, + { + self.publish( + &Event { + type_: EventType::Normal, + reason: "GroupDeleted".into(), + note: Some(format!("Deleted group '{name}'")), + action: "GroupDeleted".into(), + secondary: None, + }, + &obj.object_ref(&()), + ) + .await + } + async fn user_not_found(&self, obj: &T, username: &str) -> Result<(), Self::Error> where T: Resource + Sync, diff --git a/src/lldap.rs b/src/lldap.rs index a3f4bd1..ce407b3 100644 --- a/src/lldap.rs +++ b/src/lldap.rs @@ -8,9 +8,10 @@ use lldap_auth::opaque::AuthenticationError; use lldap_auth::registration::ServerRegistrationStartResponse; use lldap_auth::{opaque, registration}; use queries::{ - AddUserToGroup, AddUserToGroupVariables, CreateUser, CreateUserVariables, DeleteUser, - DeleteUserVariables, GetGroups, GetUser, GetUserVariables, Group, RemoveUserFromGroup, - RemoveUserFromGroupVariables, User, + AddUserToGroup, AddUserToGroupVariables, CreateGroup, CreateGroupVariables, CreateUser, + CreateUserVariables, DeleteGroup, DeleteGroupVariables, DeleteUser, DeleteUserVariables, + GetGroups, GetUser, GetUserVariables, Group, RemoveUserFromGroup, RemoveUserFromGroupVariables, + User, }; use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue}; use tracing::{debug, trace}; @@ -150,6 +151,32 @@ impl LldapClient { Ok(check_graphql_errors(response)?.groups) } + pub async fn create_group(&self, name: &str) -> Result { + let operation = CreateGroup::build(CreateGroupVariables { name }); + + let response = self + .client + .post(format!("{}/api/graphql", self.url)) + .run_graphql(operation) + .await?; + + Ok(check_graphql_errors(response)?.create_group) + } + + pub async fn delete_group(&self, id: i32) -> Result<()> { + let operation = DeleteGroup::build(DeleteGroupVariables { id }); + + let response = self + .client + .post(format!("{}/api/graphql", self.url)) + .run_graphql(operation) + .await?; + + check_graphql_errors(response)?; + + Ok(()) + } + pub async fn add_user_to_group(&self, username: &str, group: i32) -> Result<()> { let operation = AddUserToGroup::build(AddUserToGroupVariables { username, group }); diff --git a/src/resources/group.rs b/src/resources/group.rs new file mode 100644 index 0000000..b476696 --- /dev/null +++ b/src/resources/group.rs @@ -0,0 +1,76 @@ +use std::sync::Arc; +use std::time::Duration; + +use kube::CustomResource; +use kube::runtime::controller::Action; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use tracing::{debug, trace}; + +use super::{Error, Reconcile, Result}; +use crate::context::{Context, ControllerEvents}; + +#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[kube(kind = "Group", group = "lldap.huizinga.dev", version = "v1")] +#[kube( + shortname = "lg", + doc = "Custom resource for managing Groups inside of LLDAP" +)] +#[serde(rename_all = "camelCase")] +pub struct GroupSpec {} + +impl Reconcile for Group { + async fn reconcile(self: Arc, ctx: Arc) -> Result { + let name = self + .metadata + .name + .clone() + .ok_or(Error::MissingObjectKey(".metadata.name"))?; + + debug!(name, "Apply"); + + let lldap_client = ctx.lldap_config.build_client().await?; + + trace!(name, "Get existing groups"); + let groups = lldap_client.get_groups().await?; + + if !groups.iter().any(|group| group.display_name == name) { + trace!("Group does not exist yet"); + + lldap_client.create_group(&name).await?; + + ctx.recorder.group_created(self.as_ref(), &name).await?; + } else { + trace!("Group already exists"); + } + + Ok(Action::requeue(Duration::from_secs(3600))) + } + + async fn cleanup(self: Arc, ctx: Arc) -> Result { + let name = self + .metadata + .name + .clone() + .ok_or(Error::MissingObjectKey(".metadata.name"))?; + + debug!(name, "Cleanup"); + + let lldap_client = ctx.lldap_config.build_client().await?; + + trace!(name, "Get existing groups"); + let groups = lldap_client.get_groups().await?; + + if let Some(group) = groups.iter().find(|group| group.display_name == name) { + trace!(name, "Deleting group"); + + lldap_client.delete_group(group.id).await?; + + ctx.recorder.group_deleted(self.as_ref(), &name).await?; + } else { + trace!(name, "Group does not exist") + } + + Ok(Action::await_change()) + } +} diff --git a/src/resources/mod.rs b/src/resources/mod.rs index 3edbce7..f5c581f 100644 --- a/src/resources/mod.rs +++ b/src/resources/mod.rs @@ -1,9 +1,9 @@ +mod group; mod service_user; use core::fmt; use std::sync::Arc; -use k8s_openapi::NamespaceResourceScope; use kube::runtime::controller::Action; use kube::runtime::finalizer; use kube::{Api, Resource, ResourceExt}; @@ -11,6 +11,7 @@ use serde::Serialize; use serde::de::DeserializeOwned; use tracing::{debug, instrument}; +pub use self::group::Group; pub use self::service_user::ServiceUser; use crate::context::Context; use crate::lldap; @@ -46,19 +47,12 @@ trait Reconcile { #[instrument(skip(obj, ctx))] pub async fn reconcile(obj: Arc, ctx: Arc) -> Result where - T: Resource - + ResourceExt - + Clone - + Serialize - + DeserializeOwned - + fmt::Debug - + Reconcile, + T: Resource + ResourceExt + Clone + Serialize + DeserializeOwned + fmt::Debug + Reconcile, ::DynamicType: Default, { debug!(name = obj.name_any(), "Reconcile"); - let namespace = obj.namespace().expect("Resource is namespace scoped"); - let service_users = Api::::namespaced(ctx.client.clone(), &namespace); + let service_users = Api::::all(ctx.client.clone()); Ok( finalizer(&service_users, &ctx.controller_name, obj, |event| async {