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,
|
||||
}
|
||||
|
||||
#[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)]
|
||||
mod tests {
|
||||
use cynic::{MutationBuilder, QueryBuilder};
|
||||
|
@ -177,4 +239,31 @@ mod tests {
|
|||
|
||||
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;
|
||||
|
||||
fn main() {
|
||||
print!(
|
||||
"{}---\n{}",
|
||||
let resources = [
|
||||
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>
|
||||
where
|
||||
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 {
|
||||
|
@ -159,4 +171,57 @@ impl ControllerEvents for Recorder {
|
|||
)
|
||||
.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::{opaque, registration};
|
||||
use queries::{
|
||||
AddUserToGroup, AddUserToGroupVariables, CreateGroup, CreateGroupVariables, CreateUser,
|
||||
CreateUserVariables, DeleteGroup, DeleteGroupVariables, DeleteUser, DeleteUserVariables,
|
||||
GetGroups, GetUser, GetUserVariables, Group, RemoveUserFromGroup, RemoveUserFromGroupVariables,
|
||||
User,
|
||||
AddUserToGroup, AddUserToGroupVariables, AttributeSchema, CreateGroup, CreateGroupVariables,
|
||||
CreateUser, CreateUserAttribute, CreateUserAttributeVariables, CreateUserVariables,
|
||||
DeleteGroup, DeleteGroupVariables, DeleteUser, DeleteUserAttribute,
|
||||
DeleteUserAttributeVariables, DeleteUserVariables, GetGroups, GetUser, GetUserAttributes,
|
||||
GetUserVariables, Group, RemoveUserFromGroup, RemoveUserFromGroupVariables, User,
|
||||
};
|
||||
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::resources::AttributeType;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Cynic error: {0}")]
|
||||
|
@ -285,4 +288,60 @@ impl LldapClient {
|
|||
|
||||
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 lldap_controller::context::Context;
|
||||
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_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
@ -55,9 +55,9 @@ async fn main() -> anyhow::Result<()> {
|
|||
LldapConfig::try_from_env()?,
|
||||
);
|
||||
|
||||
let service_users = Api::<ServiceUser>::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())
|
||||
.owns(secrets, Default::default())
|
||||
.shutdown_on_signal()
|
||||
|
@ -65,13 +65,22 @@ async fn main() -> anyhow::Result<()> {
|
|||
.for_each(log_status);
|
||||
|
||||
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 user_attribute_controller = Controller::new(user_attributes, Default::default())
|
||||
.shutdown_on_signal()
|
||||
.run(reconcile, error_policy, Arc::new(data))
|
||||
.for_each(log_status);
|
||||
|
||||
tokio::join!(service_user_controller, group_controller);
|
||||
tokio::join!(
|
||||
service_user_controller,
|
||||
group_controller,
|
||||
user_attribute_controller
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
mod group;
|
||||
mod service_user;
|
||||
mod user_attribute;
|
||||
|
||||
use core::fmt;
|
||||
use std::sync::Arc;
|
||||
|
@ -13,6 +14,7 @@ use tracing::{debug, instrument};
|
|||
|
||||
pub use self::group::Group;
|
||||
pub use self::service_user::ServiceUser;
|
||||
pub use self::user_attribute::{Type as AttributeType, UserAttribute};
|
||||
use crate::context::Context;
|
||||
use crate::lldap;
|
||||
|
||||
|
@ -28,6 +30,8 @@ pub enum Error {
|
|||
Finalizer(#[source] Box<finalizer::Error<Self>>),
|
||||
#[error("MissingObjectKey: {0}")]
|
||||
MissingObjectKey(&'static str),
|
||||
#[error("UserAttributeDesync: {0:?}")]
|
||||
UserAttributeDesync(Vec<String>),
|
||||
}
|
||||
|
||||
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