Compare commits
No commits in common. "10cec26f1a38bc9deddc0d85ec58bfbea3fd7cd4" and "5d5c916a014abd277907bfc5d87fa032d0005317" have entirely different histories.
10cec26f1a
...
5d5c916a01
|
@ -110,68 +110,6 @@ pub struct DeleteGroup {
|
||||||
pub delete_group: Success,
|
pub delete_group: Success,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(cynic::QueryFragment, Debug)]
|
|
||||||
#[cynic(graphql_type = "Query")]
|
|
||||||
pub struct GetUserAttributes {
|
|
||||||
pub schema: Schema,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(cynic::QueryFragment, Debug)]
|
|
||||||
pub struct Schema {
|
|
||||||
pub user_schema: AttributeList,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(cynic::QueryFragment, Debug)]
|
|
||||||
pub struct AttributeList {
|
|
||||||
pub attributes: Vec<AttributeSchema>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(cynic::QueryFragment, Debug)]
|
|
||||||
pub struct AttributeSchema {
|
|
||||||
pub name: String,
|
|
||||||
pub is_visible: bool,
|
|
||||||
pub is_list: bool,
|
|
||||||
pub is_editable: bool,
|
|
||||||
pub attribute_type: AttributeType,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(cynic::Enum, Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub enum AttributeType {
|
|
||||||
String,
|
|
||||||
Integer,
|
|
||||||
JpegPhoto,
|
|
||||||
DateTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(cynic::QueryVariables, Debug)]
|
|
||||||
pub struct CreateUserAttributeVariables<'a> {
|
|
||||||
pub editable: bool,
|
|
||||||
pub list: bool,
|
|
||||||
pub name: &'a str,
|
|
||||||
#[cynic(rename = "type")]
|
|
||||||
pub r#type: AttributeType,
|
|
||||||
pub visible: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(cynic::QueryFragment, Debug)]
|
|
||||||
#[cynic(graphql_type = "Mutation", variables = "CreateUserAttributeVariables")]
|
|
||||||
pub struct CreateUserAttribute {
|
|
||||||
#[arguments(attributeType: $r#type, isEditable: $editable, isList: $list, isVisible: $visible, name: $name)]
|
|
||||||
pub add_user_attribute: Success,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(cynic::QueryVariables, Debug)]
|
|
||||||
pub struct DeleteUserAttributeVariables<'a> {
|
|
||||||
pub name: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(cynic::QueryFragment, Debug)]
|
|
||||||
#[cynic(graphql_type = "Mutation", variables = "DeleteUserAttributeVariables")]
|
|
||||||
pub struct DeleteUserAttribute {
|
|
||||||
#[arguments(name: $name)]
|
|
||||||
pub delete_user_attribute: Success,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use cynic::{MutationBuilder, QueryBuilder};
|
use cynic::{MutationBuilder, QueryBuilder};
|
||||||
|
@ -239,31 +177,4 @@ mod tests {
|
||||||
|
|
||||||
insta::assert_snapshot!(operation.query);
|
insta::assert_snapshot!(operation.query);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn get_user_attributes_gql_output() {
|
|
||||||
let operation = GetUserAttributes::build(());
|
|
||||||
|
|
||||||
insta::assert_snapshot!(operation.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create_user_attribute_gql_output() {
|
|
||||||
let operation = CreateUserAttribute::build(CreateUserAttributeVariables {
|
|
||||||
r#type: AttributeType::String,
|
|
||||||
list: true,
|
|
||||||
editable: true,
|
|
||||||
visible: true,
|
|
||||||
name: "attr",
|
|
||||||
});
|
|
||||||
|
|
||||||
insta::assert_snapshot!(operation.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn delete_user_attribute_gql_output() {
|
|
||||||
let operation = DeleteUserAttribute::build(DeleteUserAttributeVariables { name: "attr" });
|
|
||||||
|
|
||||||
insta::assert_snapshot!(operation.query);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
source: queries/src/lib.rs
|
|
||||||
expression: operation.query
|
|
||||||
---
|
|
||||||
mutation CreateUserAttribute($editable: Boolean!, $list: Boolean!, $name: String!, $type: AttributeType!, $visible: Boolean!) {
|
|
||||||
addUserAttribute(attributeType: $type, isEditable: $editable, isList: $list, isVisible: $visible, name: $name) {
|
|
||||||
ok
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
source: queries/src/lib.rs
|
|
||||||
expression: operation.query
|
|
||||||
---
|
|
||||||
mutation DeleteUserAttribute($name: String!) {
|
|
||||||
deleteUserAttribute(name: $name) {
|
|
||||||
ok
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
---
|
|
||||||
source: queries/src/lib.rs
|
|
||||||
expression: operation.query
|
|
||||||
---
|
|
||||||
query GetUserAttributes {
|
|
||||||
schema {
|
|
||||||
userSchema {
|
|
||||||
attributes {
|
|
||||||
name
|
|
||||||
isVisible
|
|
||||||
isList
|
|
||||||
isEditable
|
|
||||||
attributeType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,9 @@
|
||||||
use kube::CustomResourceExt;
|
use kube::CustomResourceExt;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let resources = [
|
print!(
|
||||||
|
"{}---\n{}",
|
||||||
serde_yaml::to_string(&lldap_controller::resources::ServiceUser::crd()).unwrap(),
|
serde_yaml::to_string(&lldap_controller::resources::ServiceUser::crd()).unwrap(),
|
||||||
serde_yaml::to_string(&lldap_controller::resources::Group::crd()).unwrap(),
|
serde_yaml::to_string(&lldap_controller::resources::Group::crd()).unwrap()
|
||||||
serde_yaml::to_string(&lldap_controller::resources::UserAttribute::crd()).unwrap(),
|
)
|
||||||
]
|
|
||||||
.join("---\n");
|
|
||||||
print!("{resources}")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,31 +34,23 @@ pub trait ControllerEvents {
|
||||||
where
|
where
|
||||||
T: Resource<DynamicType = ()> + Sync;
|
T: Resource<DynamicType = ()> + Sync;
|
||||||
|
|
||||||
async fn user_created<T>(&self, obj: &T) -> Result<(), Self::Error>
|
async fn user_created<T>(&self, obj: &T, username: &str) -> Result<(), Self::Error>
|
||||||
where
|
where
|
||||||
T: Resource<DynamicType = ()> + Sync;
|
T: Resource<DynamicType = ()> + Sync;
|
||||||
|
|
||||||
async fn group_created<T>(&self, obj: &T) -> Result<(), Self::Error>
|
async fn group_created<T>(&self, obj: &T, name: &str) -> Result<(), Self::Error>
|
||||||
where
|
where
|
||||||
T: Resource<DynamicType = ()> + Sync;
|
T: Resource<DynamicType = ()> + Sync;
|
||||||
|
|
||||||
async fn user_deleted<T>(&self, obj: &T) -> Result<(), Self::Error>
|
async fn user_deleted<T>(&self, obj: &T, username: &str) -> Result<(), Self::Error>
|
||||||
where
|
where
|
||||||
T: Resource<DynamicType = ()> + Sync;
|
T: Resource<DynamicType = ()> + Sync;
|
||||||
|
|
||||||
async fn group_deleted<T>(&self, obj: &T) -> Result<(), Self::Error>
|
async fn group_deleted<T>(&self, obj: &T, name: &str) -> Result<(), Self::Error>
|
||||||
where
|
where
|
||||||
T: Resource<DynamicType = ()> + Sync;
|
T: Resource<DynamicType = ()> + Sync;
|
||||||
|
|
||||||
async fn user_attribute_created<T>(&self, obj: &T) -> Result<(), Self::Error>
|
async fn user_not_found<T>(&self, obj: &T, username: &str) -> Result<(), Self::Error>
|
||||||
where
|
|
||||||
T: Resource<DynamicType = ()> + Sync;
|
|
||||||
|
|
||||||
async fn user_attribute_desync<T>(&self, obj: &T, fields: &[String]) -> Result<(), Self::Error>
|
|
||||||
where
|
|
||||||
T: Resource<DynamicType = ()> + Sync;
|
|
||||||
|
|
||||||
async fn user_attribute_deleted<T>(&self, obj: &T) -> Result<(), Self::Error>
|
|
||||||
where
|
where
|
||||||
T: Resource<DynamicType = ()> + Sync;
|
T: Resource<DynamicType = ()> + Sync;
|
||||||
}
|
}
|
||||||
|
@ -83,16 +75,16 @@ impl ControllerEvents for Recorder {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn user_created<T>(&self, obj: &T) -> Result<(), Self::Error>
|
async fn user_created<T>(&self, obj: &T, username: &str) -> Result<(), Self::Error>
|
||||||
where
|
where
|
||||||
T: Resource<DynamicType = ()> + Sync,
|
T: Resource<DynamicType = ()> + Sync,
|
||||||
{
|
{
|
||||||
self.publish(
|
self.publish(
|
||||||
&Event {
|
&Event {
|
||||||
type_: EventType::Normal,
|
type_: EventType::Normal,
|
||||||
reason: "Created".into(),
|
reason: "UserCreated".into(),
|
||||||
note: Some("Created user".into()),
|
note: Some(format!("Created user '{username}'")),
|
||||||
action: "Created".into(),
|
action: "UserCreated".into(),
|
||||||
secondary: None,
|
secondary: None,
|
||||||
},
|
},
|
||||||
&obj.object_ref(&()),
|
&obj.object_ref(&()),
|
||||||
|
@ -100,16 +92,16 @@ impl ControllerEvents for Recorder {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn group_created<T>(&self, obj: &T) -> Result<(), Self::Error>
|
async fn group_created<T>(&self, obj: &T, name: &str) -> Result<(), Self::Error>
|
||||||
where
|
where
|
||||||
T: Resource<DynamicType = ()> + Sync,
|
T: Resource<DynamicType = ()> + Sync,
|
||||||
{
|
{
|
||||||
self.publish(
|
self.publish(
|
||||||
&Event {
|
&Event {
|
||||||
type_: EventType::Normal,
|
type_: EventType::Normal,
|
||||||
reason: "Created".into(),
|
reason: "GroupCreated".into(),
|
||||||
note: Some("Created group".into()),
|
note: Some(format!("Created group '{name}'")),
|
||||||
action: "Created".into(),
|
action: "GroupCreated".into(),
|
||||||
secondary: None,
|
secondary: None,
|
||||||
},
|
},
|
||||||
&obj.object_ref(&()),
|
&obj.object_ref(&()),
|
||||||
|
@ -117,16 +109,16 @@ impl ControllerEvents for Recorder {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn user_deleted<T>(&self, obj: &T) -> Result<(), Self::Error>
|
async fn user_deleted<T>(&self, obj: &T, username: &str) -> Result<(), Self::Error>
|
||||||
where
|
where
|
||||||
T: Resource<DynamicType = ()> + Sync,
|
T: Resource<DynamicType = ()> + Sync,
|
||||||
{
|
{
|
||||||
self.publish(
|
self.publish(
|
||||||
&Event {
|
&Event {
|
||||||
type_: EventType::Normal,
|
type_: EventType::Normal,
|
||||||
reason: "Deleted".into(),
|
reason: "UserDeleted".into(),
|
||||||
note: Some("Deleted user".into()),
|
note: Some(format!("Deleted user '{username}'")),
|
||||||
action: "Deleted".into(),
|
action: "UserDeleted".into(),
|
||||||
secondary: None,
|
secondary: None,
|
||||||
},
|
},
|
||||||
&obj.object_ref(&()),
|
&obj.object_ref(&()),
|
||||||
|
@ -134,16 +126,16 @@ impl ControllerEvents for Recorder {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn group_deleted<T>(&self, obj: &T) -> Result<(), Self::Error>
|
async fn group_deleted<T>(&self, obj: &T, name: &str) -> Result<(), Self::Error>
|
||||||
where
|
where
|
||||||
T: Resource<DynamicType = ()> + Sync,
|
T: Resource<DynamicType = ()> + Sync,
|
||||||
{
|
{
|
||||||
self.publish(
|
self.publish(
|
||||||
&Event {
|
&Event {
|
||||||
type_: EventType::Normal,
|
type_: EventType::Normal,
|
||||||
reason: "Deleted".into(),
|
reason: "GroupDeleted".into(),
|
||||||
note: Some("Deleted group".into()),
|
note: Some(format!("Deleted group '{name}'")),
|
||||||
action: "Deleted".into(),
|
action: "GroupDeleted".into(),
|
||||||
secondary: None,
|
secondary: None,
|
||||||
},
|
},
|
||||||
&obj.object_ref(&()),
|
&obj.object_ref(&()),
|
||||||
|
@ -151,52 +143,16 @@ impl ControllerEvents for Recorder {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn user_attribute_created<T>(&self, obj: &T) -> Result<(), Self::Error>
|
async fn user_not_found<T>(&self, obj: &T, username: &str) -> Result<(), Self::Error>
|
||||||
where
|
where
|
||||||
T: Resource<DynamicType = ()> + Sync,
|
T: Resource<DynamicType = ()> + Sync,
|
||||||
{
|
{
|
||||||
self.publish(
|
self.publish(
|
||||||
&Event {
|
&Event {
|
||||||
type_: EventType::Warning,
|
type_: EventType::Warning,
|
||||||
reason: "Created".into(),
|
reason: "UserNotFound".into(),
|
||||||
note: Some("Created user attribute".into()),
|
note: Some(format!("User '{username}' not found")),
|
||||||
action: "Created".into(),
|
action: "UserNotFound".into(),
|
||||||
secondary: None,
|
|
||||||
},
|
|
||||||
&obj.object_ref(&()),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn user_attribute_desync<T>(&self, obj: &T, fields: &[String]) -> Result<(), Self::Error>
|
|
||||||
where
|
|
||||||
T: Resource<DynamicType = ()> + Sync,
|
|
||||||
{
|
|
||||||
self.publish(
|
|
||||||
&Event {
|
|
||||||
type_: EventType::Warning,
|
|
||||||
reason: "Desync".into(),
|
|
||||||
note: Some(format!(
|
|
||||||
"User attribute fields '{fields:?}' are out of sync"
|
|
||||||
)),
|
|
||||||
action: "Desync".into(),
|
|
||||||
secondary: None,
|
|
||||||
},
|
|
||||||
&obj.object_ref(&()),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn user_attribute_deleted<T>(&self, obj: &T) -> Result<(), Self::Error>
|
|
||||||
where
|
|
||||||
T: Resource<DynamicType = ()> + Sync,
|
|
||||||
{
|
|
||||||
self.publish(
|
|
||||||
&Event {
|
|
||||||
type_: EventType::Warning,
|
|
||||||
reason: "Deleted".into(),
|
|
||||||
note: Some("Deleted user attribute'".into()),
|
|
||||||
action: "Deleted".into(),
|
|
||||||
secondary: None,
|
secondary: None,
|
||||||
},
|
},
|
||||||
&obj.object_ref(&()),
|
&obj.object_ref(&()),
|
||||||
|
|
67
src/lldap.rs
67
src/lldap.rs
|
@ -8,17 +8,14 @@ use lldap_auth::opaque::AuthenticationError;
|
||||||
use lldap_auth::registration::ServerRegistrationStartResponse;
|
use lldap_auth::registration::ServerRegistrationStartResponse;
|
||||||
use lldap_auth::{opaque, registration};
|
use lldap_auth::{opaque, registration};
|
||||||
use queries::{
|
use queries::{
|
||||||
AddUserToGroup, AddUserToGroupVariables, AttributeSchema, CreateGroup, CreateGroupVariables,
|
AddUserToGroup, AddUserToGroupVariables, CreateGroup, CreateGroupVariables, CreateUser,
|
||||||
CreateUser, CreateUserAttribute, CreateUserAttributeVariables, CreateUserVariables,
|
CreateUserVariables, DeleteGroup, DeleteGroupVariables, DeleteUser, DeleteUserVariables,
|
||||||
DeleteGroup, DeleteGroupVariables, DeleteUser, DeleteUserAttribute,
|
GetGroups, GetUser, GetUserVariables, Group, RemoveUserFromGroup, RemoveUserFromGroupVariables,
|
||||||
DeleteUserAttributeVariables, DeleteUserVariables, GetGroups, GetUser, GetUserAttributes,
|
User,
|
||||||
GetUserVariables, Group, RemoveUserFromGroup, RemoveUserFromGroupVariables, User,
|
|
||||||
};
|
};
|
||||||
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
|
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
use crate::resources::AttributeType;
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Cynic error: {0}")]
|
#[error("Cynic error: {0}")]
|
||||||
|
@ -288,60 +285,4 @@ impl LldapClient {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_attributes(&self) -> Result<Vec<AttributeSchema>> {
|
|
||||||
let operation = GetUserAttributes::build(());
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.client
|
|
||||||
.post(format!("{}/api/graphql", self.url))
|
|
||||||
.run_graphql(operation)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(check_graphql_errors(response)?
|
|
||||||
.schema
|
|
||||||
.user_schema
|
|
||||||
.attributes)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_user_attribute(
|
|
||||||
&self,
|
|
||||||
name: &str,
|
|
||||||
r#type: AttributeType,
|
|
||||||
list: bool,
|
|
||||||
visible: bool,
|
|
||||||
editable: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
let operation = CreateUserAttribute::build(CreateUserAttributeVariables {
|
|
||||||
name,
|
|
||||||
r#type: r#type.into(),
|
|
||||||
list,
|
|
||||||
visible,
|
|
||||||
editable,
|
|
||||||
});
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.client
|
|
||||||
.post(format!("{}/api/graphql", self.url))
|
|
||||||
.run_graphql(operation)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
check_graphql_errors(response)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_user_attribute(&self, name: &str) -> Result<()> {
|
|
||||||
let operation = DeleteUserAttribute::build(DeleteUserAttributeVariables { name });
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.client
|
|
||||||
.post(format!("{}/api/graphql", self.url))
|
|
||||||
.run_graphql(operation)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
check_graphql_errors(response)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
17
src/main.rs
17
src/main.rs
|
@ -9,7 +9,7 @@ use kube::runtime::{Controller, watcher};
|
||||||
use kube::{Api, Client as KubeClient, Resource};
|
use kube::{Api, Client as KubeClient, Resource};
|
||||||
use lldap_controller::context::Context;
|
use lldap_controller::context::Context;
|
||||||
use lldap_controller::lldap::LldapConfig;
|
use lldap_controller::lldap::LldapConfig;
|
||||||
use lldap_controller::resources::{self, Error, Group, ServiceUser, UserAttribute, reconcile};
|
use lldap_controller::resources::{self, Error, Group, ServiceUser, reconcile};
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
@ -55,9 +55,9 @@ async fn main() -> anyhow::Result<()> {
|
||||||
LldapConfig::try_from_env()?,
|
LldapConfig::try_from_env()?,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let service_users = Api::<ServiceUser>::all(client.clone());
|
||||||
let secrets = Api::<Secret>::all(client.clone());
|
let secrets = Api::<Secret>::all(client.clone());
|
||||||
|
|
||||||
let service_users = Api::<ServiceUser>::all(client.clone());
|
|
||||||
let service_user_controller = Controller::new(service_users, Default::default())
|
let service_user_controller = Controller::new(service_users, Default::default())
|
||||||
.owns(secrets, Default::default())
|
.owns(secrets, Default::default())
|
||||||
.shutdown_on_signal()
|
.shutdown_on_signal()
|
||||||
|
@ -65,22 +65,13 @@ async fn main() -> anyhow::Result<()> {
|
||||||
.for_each(log_status);
|
.for_each(log_status);
|
||||||
|
|
||||||
let groups = Api::<Group>::all(client.clone());
|
let groups = Api::<Group>::all(client.clone());
|
||||||
let group_controller = Controller::new(groups, Default::default())
|
|
||||||
.shutdown_on_signal()
|
|
||||||
.run(reconcile, error_policy, Arc::new(data.clone()))
|
|
||||||
.for_each(log_status);
|
|
||||||
|
|
||||||
let user_attributes = Api::<UserAttribute>::all(client.clone());
|
let group_controller = Controller::new(groups, Default::default())
|
||||||
let user_attribute_controller = Controller::new(user_attributes, Default::default())
|
|
||||||
.shutdown_on_signal()
|
.shutdown_on_signal()
|
||||||
.run(reconcile, error_policy, Arc::new(data))
|
.run(reconcile, error_policy, Arc::new(data))
|
||||||
.for_each(log_status);
|
.for_each(log_status);
|
||||||
|
|
||||||
tokio::join!(
|
tokio::join!(service_user_controller, group_controller);
|
||||||
service_user_controller,
|
|
||||||
group_controller,
|
|
||||||
user_attribute_controller
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ impl Reconcile for Group {
|
||||||
|
|
||||||
lldap_client.create_group(&name).await?;
|
lldap_client.create_group(&name).await?;
|
||||||
|
|
||||||
ctx.recorder.group_created(self.as_ref()).await?;
|
ctx.recorder.group_created(self.as_ref(), &name).await?;
|
||||||
} else {
|
} else {
|
||||||
trace!("Group already exists");
|
trace!("Group already exists");
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ impl Reconcile for Group {
|
||||||
|
|
||||||
lldap_client.delete_group(group.id).await?;
|
lldap_client.delete_group(group.id).await?;
|
||||||
|
|
||||||
ctx.recorder.group_deleted(self.as_ref()).await?;
|
ctx.recorder.group_deleted(self.as_ref(), &name).await?;
|
||||||
} else {
|
} else {
|
||||||
trace!(name, "Group does not exist")
|
trace!(name, "Group does not exist")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
mod group;
|
mod group;
|
||||||
mod service_user;
|
mod service_user;
|
||||||
mod user_attribute;
|
|
||||||
|
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -14,7 +13,6 @@ use tracing::{debug, instrument};
|
||||||
|
|
||||||
pub use self::group::Group;
|
pub use self::group::Group;
|
||||||
pub use self::service_user::ServiceUser;
|
pub use self::service_user::ServiceUser;
|
||||||
pub use self::user_attribute::{Type as AttributeType, UserAttribute};
|
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::lldap;
|
use crate::lldap;
|
||||||
|
|
||||||
|
@ -30,8 +28,6 @@ pub enum Error {
|
||||||
Finalizer(#[source] Box<finalizer::Error<Self>>),
|
Finalizer(#[source] Box<finalizer::Error<Self>>),
|
||||||
#[error("MissingObjectKey: {0}")]
|
#[error("MissingObjectKey: {0}")]
|
||||||
MissingObjectKey(&'static str),
|
MissingObjectKey(&'static str),
|
||||||
#[error("UserAttributeDesync: {0:?}")]
|
|
||||||
UserAttributeDesync(Vec<String>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<finalizer::Error<Self>> for Error {
|
impl From<finalizer::Error<Self>> for Error {
|
||||||
|
|
|
@ -144,7 +144,7 @@ impl Reconcile for ServiceUser {
|
||||||
debug!(name, username, "Creating new user");
|
debug!(name, username, "Creating new user");
|
||||||
|
|
||||||
let user = lldap_client.create_user(&username).await?;
|
let user = lldap_client.create_user(&username).await?;
|
||||||
ctx.recorder.user_created(self.as_ref()).await?;
|
ctx.recorder.user_created(self.as_ref(), &username).await?;
|
||||||
|
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
@ -208,11 +208,14 @@ impl Reconcile for ServiceUser {
|
||||||
Err(lldap::Error::GraphQl(err))
|
Err(lldap::Error::GraphQl(err))
|
||||||
if err.message == format!("Entity not found: `No such user: '{username}'`") =>
|
if err.message == format!("Entity not found: `No such user: '{username}'`") =>
|
||||||
{
|
{
|
||||||
|
ctx.recorder
|
||||||
|
.user_not_found(self.as_ref(), &username)
|
||||||
|
.await?;
|
||||||
warn!(name, username, "User not found");
|
warn!(name, username, "User not found");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
ctx.recorder.user_deleted(self.as_ref()).await?;
|
ctx.recorder.user_deleted(self.as_ref(), &username).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
|
|
|
@ -1,190 +0,0 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use kube::{
|
|
||||||
Api, CELSchema, CustomResource,
|
|
||||||
api::{Patch, PatchParams},
|
|
||||||
runtime::controller::Action,
|
|
||||||
};
|
|
||||||
use queries::AttributeType;
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::json;
|
|
||||||
use tracing::{debug, trace, warn};
|
|
||||||
|
|
||||||
use crate::{context::ControllerEvents, lldap, resources::Error};
|
|
||||||
|
|
||||||
use super::Reconcile;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, JsonSchema)]
|
|
||||||
pub enum Type {
|
|
||||||
String,
|
|
||||||
Integer,
|
|
||||||
Jpeg,
|
|
||||||
DateTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Type> for AttributeType {
|
|
||||||
fn from(t: Type) -> Self {
|
|
||||||
match t {
|
|
||||||
Type::String => Self::String,
|
|
||||||
Type::Integer => Self::Integer,
|
|
||||||
Type::Jpeg => Self::JpegPhoto,
|
|
||||||
Type::DateTime => Self::DateTime,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, CELSchema)]
|
|
||||||
#[kube(
|
|
||||||
kind = "UserAttribute",
|
|
||||||
group = "lldap.huizinga.dev",
|
|
||||||
version = "v1",
|
|
||||||
status = "UserAttributesStatus"
|
|
||||||
)]
|
|
||||||
#[kube(
|
|
||||||
shortname = "lua",
|
|
||||||
doc = "Custom resource for managing custom User Attributes inside of LLDAP",
|
|
||||||
printcolumn = r#"{"name":"Type", "type":"string", "description":"Type of attribute", "jsonPath":".spec.type"}"#,
|
|
||||||
printcolumn = r#"{"name":"List", "type":"boolean", "description":"Can the attribute contain multiple values", "jsonPath":".spec.list"}"#,
|
|
||||||
printcolumn = r#"{"name":"Visible", "type":"boolean", "description":"Can users see the value", "jsonPath":".spec.userVisible"}"#,
|
|
||||||
printcolumn = r#"{"name":"Editable", "type":"boolean", "description":"Can users edit the value", "jsonPath":".spec.userEditable"}"#,
|
|
||||||
printcolumn = r#"{"name":"Synced", "type":"boolean", "jsonPath":".status.synced"}"#,
|
|
||||||
printcolumn = r#"{"name":"Age", "type":"date", "jsonPath":".metadata.creationTimestamp"}"#
|
|
||||||
)]
|
|
||||||
#[kube(
|
|
||||||
rule = Rule::new("self.spec == oldSelf.spec").message("User attributes are immutable"),
|
|
||||||
rule = Rule::new("!self.spec.userEditable || self.spec.userVisible && self.spec.userEditable").message("Editable attribute must also be visible")
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct UesrAttributeSpec {
|
|
||||||
r#type: Type,
|
|
||||||
#[serde(default)]
|
|
||||||
list: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
user_visible: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
user_editable: bool,
|
|
||||||
}
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Default, Debug, JsonSchema)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct UserAttributesStatus {
|
|
||||||
pub synced: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Reconcile for UserAttribute {
|
|
||||||
async fn reconcile(
|
|
||||||
self: std::sync::Arc<Self>,
|
|
||||||
ctx: std::sync::Arc<crate::context::Context>,
|
|
||||||
) -> super::Result<kube::runtime::controller::Action> {
|
|
||||||
let name = self
|
|
||||||
.metadata
|
|
||||||
.name
|
|
||||||
.clone()
|
|
||||||
.ok_or(Error::MissingObjectKey(".metadata.name"))?;
|
|
||||||
|
|
||||||
debug!(name, "Apply");
|
|
||||||
|
|
||||||
trace!(name, "Get existing attributes");
|
|
||||||
let lldap_client = ctx.lldap_config.build_client().await?;
|
|
||||||
let user_attributes = lldap_client.get_user_attributes().await?;
|
|
||||||
trace!("{user_attributes:?}");
|
|
||||||
|
|
||||||
let client = &ctx.client;
|
|
||||||
let api = Api::<UserAttribute>::all(client.clone());
|
|
||||||
if let Some(attribute) = user_attributes
|
|
||||||
.iter()
|
|
||||||
.find(|attribute| attribute.name == name)
|
|
||||||
{
|
|
||||||
trace!("User attribute already exists: {attribute:?}");
|
|
||||||
let mut desynced: Vec<String> = Vec::new();
|
|
||||||
if attribute.attribute_type != self.spec.r#type.into() {
|
|
||||||
desynced.push("type".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if attribute.is_list != self.spec.list {
|
|
||||||
desynced.push("list".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if attribute.is_visible != self.spec.user_visible {
|
|
||||||
desynced.push("userVisible".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if attribute.is_editable != self.spec.user_editable {
|
|
||||||
desynced.push("userEditable".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !desynced.is_empty() {
|
|
||||||
set_status(&api, &name, false).await?;
|
|
||||||
ctx.recorder
|
|
||||||
.user_attribute_desync(self.as_ref(), &desynced)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Err(Error::UserAttributeDesync(desynced));
|
|
||||||
}
|
|
||||||
|
|
||||||
trace!("User attribute matches with spec");
|
|
||||||
} else {
|
|
||||||
trace!("User attribute does not exist yet");
|
|
||||||
|
|
||||||
lldap_client
|
|
||||||
.create_user_attribute(
|
|
||||||
&name,
|
|
||||||
self.spec.r#type,
|
|
||||||
self.spec.list,
|
|
||||||
self.spec.user_visible,
|
|
||||||
self.spec.user_editable,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
ctx.recorder.user_attribute_created(self.as_ref()).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
set_status(&api, &name, true).await?;
|
|
||||||
|
|
||||||
Ok(Action::requeue(Duration::from_secs(3600)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn cleanup(
|
|
||||||
self: std::sync::Arc<Self>,
|
|
||||||
ctx: std::sync::Arc<crate::context::Context>,
|
|
||||||
) -> super::Result<kube::runtime::controller::Action> {
|
|
||||||
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, "Deleting user attribute");
|
|
||||||
match lldap_client.delete_user_attribute(&name).await {
|
|
||||||
Err(lldap::Error::GraphQl(err))
|
|
||||||
if err.message == format!("Attribute {name} is not defined in the schema") =>
|
|
||||||
{
|
|
||||||
warn!(name, "User attribute not found");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Ok(_) => {
|
|
||||||
ctx.recorder.user_attribute_deleted(self.as_ref()).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
Ok(Action::await_change())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_status(
|
|
||||||
api: &Api<UserAttribute>,
|
|
||||||
name: &str,
|
|
||||||
synced: bool,
|
|
||||||
) -> Result<UserAttribute, kube::Error> {
|
|
||||||
trace!(name, "Updating status");
|
|
||||||
let status = json!({
|
|
||||||
"status": UserAttributesStatus { synced }
|
|
||||||
});
|
|
||||||
api.patch_status(name, &PatchParams::default(), &Patch::Merge(&status))
|
|
||||||
.await
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
apiVersion: lldap.huizinga.dev/v1
|
|
||||||
kind: UserAttribute
|
|
||||||
metadata:
|
|
||||||
name: test-attribute
|
|
||||||
spec:
|
|
||||||
type: String
|
|
||||||
list: true
|
|
||||||
userVisible: true
|
|
||||||
userEditable: true
|
|
Loading…
Reference in New Issue
Block a user