Initial commit

This commit is contained in:
Dreaded_X 2025-03-13 01:41:08 +01:00
commit 0c4e7eb9ab
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
16 changed files with 3377 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
.env

2759
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

24
Cargo.toml Normal file
View File

@ -0,0 +1,24 @@
[package]
name = "lldap-controller"
version = "0.1.0"
edition = "2021"
[workspace]
members = ["queries"]
[workspace.dependencies]
cynic = "3.10.0"
insta = "1.42.2"
[dependencies]
queries = { path = "./queries" }
anyhow = "1.0.97"
lldap_auth = "0.3.0"
rand = { version = "0.8.0" }
serde_json = "1.0.140"
surf = "2.3.2"
cynic = { workspace = true, features = ["http-surf"] }
tokio = { version = "1.44.0", features = ["full"] }
[dev-dependencies]
insta = { workspace = true }

13
queries/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "queries"
version = "0.1.0"
edition = "2021"
[dependencies]
cynic = { workspace = true }
[dev-dependencies]
insta = { workspace = true }
[build-dependencies]
cynic-codegen = "3.10.0"

7
queries/build.rs Normal file
View File

@ -0,0 +1,7 @@
fn main() {
cynic_codegen::register_schema("lldap")
.from_sdl_file("schemas/lldap.graphql")
.unwrap()
.as_default()
.unwrap();
}

View File

@ -0,0 +1,199 @@
type AttributeValue {
name: String!
value: [String!]!
schema: AttributeSchema!
}
type Mutation {
createUser(user: CreateUserInput!): User!
createGroup(name: String!): Group!
createGroupWithDetails(request: CreateGroupInput!): Group!
updateUser(user: UpdateUserInput!): Success!
updateGroup(group: UpdateGroupInput!): Success!
addUserToGroup(userId: String!, groupId: Int!): Success!
removeUserFromGroup(userId: String!, groupId: Int!): Success!
deleteUser(userId: String!): Success!
deleteGroup(groupId: Int!): Success!
addUserAttribute(
name: String!
attributeType: AttributeType!
isList: Boolean!
isVisible: Boolean!
isEditable: Boolean!
): Success!
addGroupAttribute(
name: String!
attributeType: AttributeType!
isList: Boolean!
isVisible: Boolean!
isEditable: Boolean!
): Success!
deleteUserAttribute(name: String!): Success!
deleteGroupAttribute(name: String!): Success!
addUserObjectClass(name: String!): Success!
addGroupObjectClass(name: String!): Success!
deleteUserObjectClass(name: String!): Success!
deleteGroupObjectClass(name: String!): Success!
}
type Group {
id: Int!
displayName: String!
creationDate: DateTimeUtc!
uuid: String!
# User-defined attributes.
attributes: [AttributeValue!]!
# The groups to which this user belongs.
users: [User!]!
}
# A filter for requests, specifying a boolean expression based on field constraints. Only one of
# the fields can be set at a time.
input RequestFilter {
any: [RequestFilter!]
all: [RequestFilter!]
not: RequestFilter
eq: EqualityConstraint
memberOf: String
memberOfId: Int
}
# DateTime
scalar DateTimeUtc
type Query {
apiVersion: String!
user(userId: String!): User!
users(filters: RequestFilter): [User!]!
groups: [Group!]!
group(groupId: Int!): Group!
schema: Schema!
}
# The details required to create a user.
input CreateUserInput {
id: String!
email: String
displayName: String
firstName: String
lastName: String
# Base64 encoded JpegPhoto.
avatar: String
# User-defined attributes.
attributes: [AttributeValueInput!]
}
type AttributeSchema {
name: String!
attributeType: AttributeType!
isList: Boolean!
isVisible: Boolean!
isEditable: Boolean!
isHardcoded: Boolean!
isReadonly: Boolean!
}
# The fields that can be updated for a user.
input UpdateUserInput {
id: String!
email: String
displayName: String
firstName: String
lastName: String
# Base64 encoded JpegPhoto.
avatar: String
# Attribute names to remove.
# They are processed before insertions.
removeAttributes: [String!]
# Inserts or updates the given attributes.
# For lists, the entire list must be provided.
insertAttributes: [AttributeValueInput!]
}
input EqualityConstraint {
field: String!
value: String!
}
type Schema {
userSchema: AttributeList!
groupSchema: AttributeList!
}
# The fields that can be updated for a group.
input UpdateGroupInput {
# The group ID.
id: Int!
# The new display name.
displayName: String
# Attribute names to remove.
# They are processed before insertions.
removeAttributes: [String!]
# Inserts or updates the given attributes.
# For lists, the entire list must be provided.
insertAttributes: [AttributeValueInput!]
}
input AttributeValueInput {
# The name of the attribute. It must be present in the schema, and the type informs how
# to interpret the values.
name: String!
# The values of the attribute.
# If the attribute is not a list, the vector must contain exactly one element.
# Integers (signed 64 bits) are represented as strings.
# Dates are represented as strings in RFC3339 format, e.g. "2019-10-12T07:20:50.52Z".
# JpegPhotos are represented as base64 encoded strings. They must be valid JPEGs.
value: [String!]!
}
# The details required to create a group.
input CreateGroupInput {
displayName: String!
# User-defined attributes.
attributes: [AttributeValueInput!]
}
type User {
id: String!
email: String!
displayName: String!
firstName: String!
lastName: String!
avatar: String
creationDate: DateTimeUtc!
uuid: String!
# User-defined attributes.
attributes: [AttributeValue!]!
# The groups to which this user belongs.
groups: [Group!]!
}
enum AttributeType {
STRING
INTEGER
JPEG_PHOTO
DATE_TIME
}
type AttributeList {
attributes: [AttributeSchema!]!
extraLdapObjectClasses: [String!]!
}
type Success {
ok: Boolean!
}

154
queries/src/lib.rs Normal file
View File

@ -0,0 +1,154 @@
#[cynic::schema("lldap")]
pub(crate) mod schema {}
#[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,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation")]
pub struct CreateManagedUserAttribute {
#[arguments(attributeType: "INTEGER", isEditable: false, isList: false, isVisible: false, name: "managed")]
pub add_user_attribute: Success,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct Success {
pub ok: bool,
}
#[derive(cynic::Enum, Clone, Copy, Debug)]
pub enum AttributeType {
String,
Integer,
JpegPhoto,
DateTime,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query")]
pub struct ListManagedUsers {
#[arguments(filters: { eq: { field: "managed", value: "1" } })]
pub users: Vec<User>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct User {
pub id: String,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct DeleteUserVariables<'a> {
pub id: &'a str,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "DeleteUserVariables")]
pub struct DeleteUser {
#[arguments(userId: $id)]
pub delete_user: Success,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct CreateUserVariables<'a> {
pub id: &'a str,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "CreateUserVariables")]
pub struct CreateUser {
#[arguments(user: { attributes: { name: "managed", value: "1" }, email: $id, id: $id })]
pub create_user: User,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct AddUserToGroupVariables<'a> {
pub group: i32,
pub id: &'a str,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "AddUserToGroupVariables")]
pub struct AddUserToGroup {
#[arguments(groupId: $group, userId: $id)]
pub add_user_to_group: Success,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_user_attributes_gql_output() {
use cynic::QueryBuilder;
let operation = GetUserAttributes::build(());
insta::assert_snapshot!(operation.query);
}
#[test]
fn create_managed_user_attribute_gql_output() {
use cynic::MutationBuilder;
let operation = CreateManagedUserAttribute::build(());
insta::assert_snapshot!(operation.query);
}
#[test]
fn list_managed_users_gql_output() {
use cynic::QueryBuilder;
let operation = ListManagedUsers::build(());
insta::assert_snapshot!(operation.query);
}
#[test]
fn delete_user_gql_output() {
use cynic::MutationBuilder;
let operation = DeleteUser::build(DeleteUserVariables { id: "user" });
insta::assert_snapshot!(operation.query);
}
#[test]
fn create_user_gql_output() {
use cynic::MutationBuilder;
let operation = CreateUser::build(CreateUserVariables { id: "user" });
insta::assert_snapshot!(operation.query);
}
#[test]
fn add_user_to_group_gql_output() {
use cynic::MutationBuilder;
let operation = AddUserToGroup::build(AddUserToGroupVariables {
id: "user",
group: 3,
});
insta::assert_snapshot!(operation.query);
}
}

View File

@ -0,0 +1,9 @@
---
source: src/lib.rs
expression: operation.query
---
mutation AddUserToGroup($group: Int!, $id: String!) {
addUserToGroup(groupId: $group, userId: $id) {
ok
}
}

View File

@ -0,0 +1,9 @@
---
source: queries/src/lib.rs
expression: operation.query
---
mutation CreateManagedUserAttribute {
addUserAttribute(attributeType: INTEGER, isEditable: false, isList: false, isVisible: false, name: "managed") {
ok
}
}

View File

@ -0,0 +1,10 @@
---
source: src/lib.rs
assertion_line: 142
expression: operation.query
---
mutation CreateUser($id: String!) {
createUser(user: {attributes: [{name: "managed", value: ["1"]}], email: $id, id: $id}) {
id
}
}

View File

@ -0,0 +1,9 @@
---
source: src/lib.rs
expression: operation.query
---
mutation DeleteUser($id: String!) {
deleteUser(userId: $id) {
ok
}
}

View File

@ -0,0 +1,13 @@
---
source: src/lib.rs
expression: operation.query
---
query GetUserAttributes {
schema {
userSchema {
attributes {
name
}
}
}
}

View File

@ -0,0 +1,9 @@
---
source: src/lib.rs
expression: operation.query
---
query ListManagedUsers {
users(filters: {eq: {field: "managed", value: "1"}}) {
id
}
}

1
src/lib.rs Normal file
View File

@ -0,0 +1 @@
pub mod lldap;

48
src/lldap.rs Normal file
View File

@ -0,0 +1,48 @@
use anyhow::{anyhow, Context};
use lldap_auth::{opaque, registration};
use surf::Client;
pub async fn change_password(client: &Client, user_id: &str, password: &str) -> anyhow::Result<()> {
let mut rng = rand::rngs::OsRng;
let registration_start_request =
opaque::client::registration::start_registration(password, &mut rng)
.context("Could not initiate password change")?;
let start_request = registration::ClientRegistrationStartRequest {
username: user_id.into(),
registration_start_request: registration_start_request.message,
};
let mut response = client
.post("/auth/opaque/register/start")
.body_json(&start_request)
.map_err(|e| anyhow!(e))?
.await
.map_err(|e| anyhow!(e))?;
let response: registration::ServerRegistrationStartResponse =
response.body_json().await.map_err(|e| anyhow!(e))?;
let registration_finish = opaque::client::registration::finish_registration(
registration_start_request.state,
response.registration_response,
&mut rng,
)
.context("Error during password change")?;
let request = registration::ClientRegistrationFinishRequest {
server_data: response.server_data,
registration_upload: registration_finish.message,
};
let _response = client
.post("/auth/opaque/register/finish")
.body_json(&request)
.map_err(|e| anyhow!(e))?
.await
.map_err(|e| anyhow!(e))?;
println!("Changed '{user_id}' password successfully");
Ok(())
}

111
src/main.rs Normal file
View File

@ -0,0 +1,111 @@
use std::time::Duration;
use anyhow::anyhow;
use cynic::{http::SurfExt, MutationBuilder, QueryBuilder};
use lldap_controller::lldap::change_password;
use queries::{
AddUserToGroup, AddUserToGroupVariables, CreateManagedUserAttribute, CreateUser,
CreateUserVariables, DeleteUser, DeleteUserVariables, GetUserAttributes, ListManagedUsers,
};
use surf::{Client, Config, Url};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let token = std::env::var("LLDAP_TOKEN")?;
let base_url = "http://localhost:17170";
let users = [
"authelia".to_owned(),
"grafana".to_owned(),
"gitea".to_owned(),
];
let client: Client = Config::new()
.set_base_url(Url::parse(base_url)?)
.set_timeout(Some(Duration::from_secs(1)))
.add_header("Authorization", format!("Bearer {token}"))
.map_err(|e| anyhow!(e))?
.try_into()?;
let operation = GetUserAttributes::build(());
let response = client
.post("/api/graphql")
.run_graphql(operation)
.await
.map_err(|e| anyhow!(e))?;
let has_managed = response
.data
.as_ref()
.expect("Should get data")
.schema
.user_schema
.attributes
.iter()
.any(|attr| attr.name == "managed");
if !has_managed {
let operation = CreateManagedUserAttribute::build(());
let _response = client
.post("/api/graphql")
.run_graphql(operation)
.await
.map_err(|e| anyhow!(e))?;
}
let operation = ListManagedUsers::build(());
let response = client
.post("/api/graphql")
.run_graphql(operation)
.await
.map_err(|e| anyhow!(e))?;
let (existing, remove): (Vec<_>, Vec<_>) = response
.data
.expect("Should get data")
.users
.into_iter()
.map(|user| user.id)
.partition(|user| users.contains(user));
let (update, create): (Vec<_>, Vec<_>) = users.iter().partition(|user| existing.contains(user));
for id in &remove {
println!("Removing '{id}");
let operation = DeleteUser::build(DeleteUserVariables { id });
let _response = client
.post("/api/graphql")
.run_graphql(operation)
.await
.map_err(|e| anyhow!(e))?;
}
for id in create {
println!("Creating '{id}'");
let operation = CreateUser::build(CreateUserVariables { id });
let _response = client
.post("/api/graphql")
.run_graphql(operation)
.await
.map_err(|e| anyhow!(e))?;
let operation = AddUserToGroup::build(AddUserToGroupVariables { id, group: 3 });
let _response = client
.post("/api/graphql")
.run_graphql(operation)
.await
.map_err(|e| anyhow!(e))?;
change_password(&client, id, "JustATest").await?;
}
for id in update {
println!("Updating '{id}'");
change_password(&client, id, "JustATest").await?;
}
Ok(())
}