Added UserAttribute crd to control user attributes (#9)
This commit is contained in:
parent
5d5c916a01
commit
d21b53cf34
|
@ -110,6 +110,68 @@ 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};
|
||||||
|
@ -177,4 +239,31 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
source: queries/src/lib.rs
|
||||||
|
expression: operation.query
|
||||||
|
---
|
||||||
|
mutation DeleteUserAttribute($name: String!) {
|
||||||
|
deleteUserAttribute(name: $name) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
source: queries/src/lib.rs
|
||||||
|
expression: operation.query
|
||||||
|
---
|
||||||
|
query GetUserAttributes {
|
||||||
|
schema {
|
||||||
|
userSchema {
|
||||||
|
attributes {
|
||||||
|
name
|
||||||
|
isVisible
|
||||||
|
isList
|
||||||
|
isEditable
|
||||||
|
attributeType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
use kube::CustomResourceExt;
|
use kube::CustomResourceExt;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
print!(
|
let resources = [
|
||||||
"{}---\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}")
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,18 @@ pub trait ControllerEvents {
|
||||||
async fn user_not_found<T>(&self, obj: &T, username: &str) -> 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;
|
||||||
|
|
||||||
|
async fn user_attribute_created<T>(&self, obj: &T) -> 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
|
||||||
|
T: Resource<DynamicType = ()> + Sync;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ControllerEvents for Recorder {
|
impl ControllerEvents for Recorder {
|
||||||
|
@ -159,4 +171,57 @@ impl ControllerEvents for Recorder {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn user_attribute_created<T>(&self, obj: &T) -> Result<(), Self::Error>
|
||||||
|
where
|
||||||
|
T: Resource<DynamicType = ()> + Sync,
|
||||||
|
{
|
||||||
|
self.publish(
|
||||||
|
&Event {
|
||||||
|
type_: EventType::Warning,
|
||||||
|
reason: "Created".into(),
|
||||||
|
note: Some("Created user attribute".into()),
|
||||||
|
action: "Created".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,
|
||||||
|
},
|
||||||
|
&obj.object_ref(&()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
67
src/lldap.rs
67
src/lldap.rs
|
@ -8,14 +8,17 @@ 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, CreateGroup, CreateGroupVariables, CreateUser,
|
AddUserToGroup, AddUserToGroupVariables, AttributeSchema, CreateGroup, CreateGroupVariables,
|
||||||
CreateUserVariables, DeleteGroup, DeleteGroupVariables, DeleteUser, DeleteUserVariables,
|
CreateUser, CreateUserAttribute, CreateUserAttributeVariables, CreateUserVariables,
|
||||||
GetGroups, GetUser, GetUserVariables, Group, RemoveUserFromGroup, RemoveUserFromGroupVariables,
|
DeleteGroup, DeleteGroupVariables, DeleteUser, DeleteUserAttribute,
|
||||||
User,
|
DeleteUserAttributeVariables, DeleteUserVariables, GetGroups, GetUser, GetUserAttributes,
|
||||||
|
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}")]
|
||||||
|
@ -285,4 +288,60 @@ 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, reconcile};
|
use lldap_controller::resources::{self, Error, Group, ServiceUser, UserAttribute, 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,13 +65,22 @@ 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())
|
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 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!(service_user_controller, group_controller);
|
tokio::join!(
|
||||||
|
service_user_controller,
|
||||||
|
group_controller,
|
||||||
|
user_attribute_controller
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
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;
|
||||||
|
@ -13,6 +14,7 @@ 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;
|
||||||
|
|
||||||
|
@ -28,6 +30,8 @@ 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 {
|
||||||
|
|
194
src/resources/user_attribute.rs
Normal file
194
src/resources/user_attribute.rs
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
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, user_attribute},
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
9
yaml/user_attribute.yaml
Normal file
9
yaml/user_attribute.yaml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
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