Compare commits

...

6 Commits

Author SHA1 Message Date
Valentin Tolmer
48a0a8d961 release: v0.6.3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-01 00:45:35 +02:00
Valentin Tolmer
49dc766184 chore: pin CI wasm-pack install to lockfile
Otherwise cargo install wasm-pack fails in CI
2026-05-01 00:45:35 +02:00
Valentin Tolmer
2b3dbb46de chore: upgrade Juniper to 0.17 2026-05-01 00:45:35 +02:00
Valentin Tolmer
40121b80b7 chore: centralize and upgrade shared Cargo dependencies
Move duplicated Cargo dependencies to the root workspace manifest, switch member crates to workspace dependencies, and align non-conflicting shared feature sets at the workspace level.

Upgrade a number of shared dependencies while consolidating versions across the workspace. This also consolidates the ldap3 dependency in response to GHSA-qcxq-75wr-5cm8: https://github.com/kanidm/ldap3/security/advisories/GHSA-qcxq-75wr-5cm8

Update frontend and migration code for dependency upgrades and clean up manifest structure.
2026-05-01 00:45:35 +02:00
fredericrous
b8465212b5 fix(sql-backend-handler): enable lldap_domain "test" feature in dev-deps
lldap_domain::JpegPhoto::for_tests() and uuid helpers are gated behind
the "test" feature on the lldap_domain crate. The sql-backend-handler
dev-deps did not enable that feature, causing ~12 compilation errors in
sql_user_backend_handler.rs and sql_tables.rs when building the test
binary. This unblocks cargo test -p lldap_sql_backend_handler --lib.
2026-04-30 23:44:29 +02:00
Hannes Hauswedell
bb2ea7bf36 doc: add opencloud.md 2026-03-29 10:19:27 +02:00
40 changed files with 1727 additions and 1979 deletions

View File

@@ -106,7 +106,7 @@ jobs:
restore-keys: | restore-keys: |
lldap-ui- lldap-ui-
- name: Install wasm-pack with cargo - name: Install wasm-pack with cargo
run: cargo install wasm-pack || true run: cargo install --locked wasm-pack || true
env: env:
RUSTFLAGS: "" RUSTFLAGS: ""
- name: Build frontend - name: Build frontend

View File

@@ -5,6 +5,55 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.6.3] 2026-05-01
Small release, focused on LDAP compatibility, TLS maintenance, dependency upgrades and documentation/examples.
### Added
- LDAP schema definitions for `memberOf`, `modifyTimestamp` and `pwdChangedTime`
- Support for configuring the healthcheck listen addresses
- Usernames are now included in password recovery emails
### Changed
- JWT `exp` and `iat` claims are now serialized as NumericDate values to comply with RFC7519
- Migrated to `rustls` 0.23 and centralized TLS handling
- The login form no longer enforces a password length limit
### Fixed
- `pwdChangedTime` is now emitted as LDAP GeneralizedTime instead of RFC3339
- LDAP base-scope searches for non-existent entries now return `NoSuchObject`
- `cn` equality filters are now case insensitive
- The server now shuts down the database connection pool gracefully
- The bootstrap script now handles empty globs correctly
### Security
- Updated the LDAP dependency stack, including `ldap3_proto`, in response to
security advisory
[`GHSA-qcxq-75wr-5cm8`](https://github.com/kanidm/ldap3/security/advisories/GHSA-qcxq-75wr-5cm8),
where a specially crafted LDAP query could make the server use unbounded RAM
### Cleanups
- Split GraphQL queries and mutations into smaller modules
- Refactored configuration and user update logic
- Upgraded the Rust toolchain and shared dependencies
### New services
- Apache WebDAV
- Continuwuity
- Gerrit
- Gogs
- Open WebUI
- OpenCloud
- Pocket ID
- Semaphore
- TrueNAS
## [0.6.2] 2025-07-21 ## [0.6.2] 2025-07-21
Small release, focused on LDAP improvements and ongoing maintenance. Small release, focused on LDAP improvements and ongoing maintenance.

2518
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,9 +24,122 @@ lto = true
[profile.release.package.lldap_app] [profile.release.package.lldap_app]
opt-level = 's' opt-level = 's'
[workspace.dependencies.anyhow]
version = "1"
[workspace.dependencies.async-trait]
version = "0.1"
[workspace.dependencies.base64]
version = "0.22"
[workspace.dependencies.bincode]
version = "1"
[workspace.dependencies.chrono]
version = "0.4"
features = ["serde", "wasmbind"]
[workspace.dependencies.clap]
version = "4"
features = ["std", "color", "suggestions", "derive", "env"]
[workspace.dependencies.derive_more]
version = "2"
default-features = false
features = ["debug", "display", "from", "from_str"]
[workspace.dependencies.graphql_client]
version = "0.11"
default-features = false
features = ["graphql_query_derive"]
[workspace.dependencies.image]
version = "0.25"
default-features = false
features = ["jpeg"]
[workspace.dependencies.itertools]
version = "0.14"
[workspace.dependencies.juniper]
version = "0.17"
default-features = false
features = ["chrono", "schema-language", "url", "uuid"]
[workspace.dependencies.ldap3]
version = "0"
default-features = false
features = ["sync", "tls-rustls-ring"]
[workspace.dependencies.ldap3_proto]
version = "0.7"
[workspace.dependencies.jwt]
version = "0.16"
[workspace.dependencies.log]
version = "0"
[workspace.dependencies.mockall]
version = "0.14"
[workspace.dependencies.opaque-ke]
version = "0.7"
[workspace.dependencies.orion]
version = "0.17"
[workspace.dependencies.pretty_assertions]
version = "1"
[workspace.dependencies.rand]
version = "0.8"
features = ["small_rng", "getrandom"]
[workspace.dependencies.reqwest]
version = "0.11"
default-features = false
[workspace.dependencies.sea-orm] [workspace.dependencies.sea-orm]
version = "1.1.8" version = "1.1.8"
default-features = false default-features = false
features = ["macros", "with-chrono", "with-uuid", "sqlx-all", "runtime-actix-rustls"]
[workspace.dependencies.secstr]
version = "0"
features = ["serde"]
[workspace.dependencies.serde] [workspace.dependencies.serde]
version = "1" version = "1"
[workspace.dependencies.serde_bytes]
version = "0.11"
[workspace.dependencies.serde_json]
version = "1"
[workspace.dependencies.strum]
version = "0.28"
features = ["derive"]
[workspace.dependencies.thiserror]
version = "2"
[workspace.dependencies.tokio]
version = "1"
features = ["full"]
[workspace.dependencies.tracing]
version = "0"
[workspace.dependencies.tracing-subscriber]
version = "0.3"
features = ["env-filter", "tracing-log"]
[workspace.dependencies.urlencoding]
version = "2"
[workspace.dependencies.uuid]
version = "1.18.1"
features = ["js", "serde", "v1", "v3", "v4"]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_app" name = "lldap_app"
version = "0.6.2" version = "0.6.3"
description = "Frontend for LLDAP" description = "Frontend for LLDAP"
edition.workspace = true edition.workspace = true
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"] include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
@@ -11,27 +11,33 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
anyhow = "1" anyhow = { workspace = true }
base64 = "0.13" chrono = { workspace = true }
gloo-console = "0.2.3" derive_more = { workspace = true }
gloo-file = "0.2.3" gloo-console = "0.4"
gloo-net = "*" gloo-file = "0.4"
graphql_client = "0.10" gloo-net = "0.7"
http = "0.2" graphql_client = { workspace = true }
jwt = "0.13" image = { workspace = true }
rand = "0.8" jwt = { workspace = true }
serde_json = "1" rand = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
strum = { workspace = true }
url-escape = "0.1.1" url-escape = "0.1.1"
validator = "0.14" validator = "0.14"
validator_derive = "0.14" validator_derive = "0.14"
wasm-bindgen = "0.2.100" wasm-bindgen = "0.2.100"
wasm-bindgen-futures = "*" wasm-bindgen-futures = "0"
yew = "0.19.3" yew = "0.19.3"
yew-router = "0.16" yew-router = "0.16"
# Needed because of https://github.com/tkaitchuck/aHash/issues/95 # Needed because of https://github.com/tkaitchuck/aHash/issues/95
indexmap = "=1.6.2" indexmap = "=1.6.2"
base64 = { workspace = true }
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3" version = "0.3"
features = [ features = [
@@ -50,17 +56,6 @@ features = [
"console", "console",
] ]
[dependencies.chrono]
version = "*"
features = [
"wasmbind"
]
[dependencies.derive_more]
features = ["debug", "display", "from", "from_str"]
default-features = false
version = "1"
[dependencies.lldap_auth] [dependencies.lldap_auth]
path = "../crates/auth" path = "../crates/auth"
features = [ "opaque_client" ] features = [ "opaque_client" ]
@@ -71,18 +66,6 @@ path = "../crates/frontend-options"
[dependencies.lldap_validation] [dependencies.lldap_validation]
path = "../crates/validation" path = "../crates/validation"
[dependencies.image]
features = ["jpeg"]
default-features = false
version = "0.24"
[dependencies.serde]
workspace = true
[dependencies.strum]
features = ["derive"]
version = "0.25"
[dependencies.yew_form] [dependencies.yew_form]
git = "https://github.com/jfbilodeau/yew_form" git = "https://github.com/jfbilodeau/yew_form"
rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9" rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"

View File

@@ -112,7 +112,7 @@ impl CommonComponent<CreateGroupForm> for CreateGroupForm {
let model = self.form.model(); let model = self.form.model();
let req = create_group::Variables { let req = create_group::Variables {
group: create_group::CreateGroupInput { group: create_group::CreateGroupInput {
displayName: model.groupname, display_name: model.groupname,
attributes, attributes,
}, },
}; };

View File

@@ -143,9 +143,9 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
user: create_user::CreateUserInput { user: create_user::CreateUserInput {
id: model.username, id: model.username,
email: None, email: None,
displayName: None, display_name: None,
firstName: None, first_name: None,
lastName: None, last_name: None,
avatar: None, avatar: None,
attributes, attributes,
}, },

View File

@@ -1,6 +1,7 @@
use std::{fmt::Display, str::FromStr}; use std::{fmt::Display, str::FromStr};
use anyhow::{Error, Ok, Result, bail}; use anyhow::{Error, Ok, Result, bail};
use base64::Engine;
use gloo_file::{ use gloo_file::{
File, File,
callbacks::{FileReader, read_as_bytes}, callbacks::{FileReader, read_as_bytes},
@@ -54,12 +55,12 @@ fn to_base64(file: &JsFile) -> Result<String> {
if !is_valid_jpeg(data.as_slice()) { if !is_valid_jpeg(data.as_slice()) {
bail!("Chosen image is not a valid JPEG"); bail!("Chosen image is not a valid JPEG");
} }
Ok(base64::encode(data)) Ok(base64::engine::general_purpose::STANDARD.encode(data))
} }
JsFile { JsFile {
file: None, file: None,
contents: Some(data), contents: Some(data),
} => Ok(base64::encode(data)), } => Ok(base64::engine::general_purpose::STANDARD.encode(data)),
} }
} }
@@ -98,7 +99,7 @@ impl Component for JpegFileInput {
.props() .props()
.value .value
.as_ref() .as_ref()
.and_then(|x| base64::decode(x).ok()), .and_then(|x| base64::engine::general_purpose::STANDARD.decode(x).ok()),
}), }),
reader: None, reader: None,
} }
@@ -111,7 +112,7 @@ impl Component for JpegFileInput {
.props() .props()
.value .value
.as_ref() .as_ref()
.and_then(|x| base64::decode(x).ok()), .and_then(|x| base64::engine::general_purpose::STANDARD.decode(x).ok()),
}); });
self.reader = None; self.reader = None;
true true
@@ -230,7 +231,7 @@ impl JpegFileInput {
} }
fn is_valid_jpeg(bytes: &[u8]) -> bool { fn is_valid_jpeg(bytes: &[u8]) -> bool {
image::io::Reader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg) image::ImageReader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg)
.decode() .decode()
.is_ok() .is_ok()
} }

View File

@@ -248,13 +248,13 @@ impl GroupDetailsForm {
}; };
let mut group_input = update_group::UpdateGroupInput { let mut group_input = update_group::UpdateGroupInput {
id: self.group.id, id: self.group.id,
displayName: None, display_name: None,
removeAttributes: None, remove_attributes: None,
insertAttributes: None, insert_attributes: None,
}; };
let default_group_input = group_input.clone(); let default_group_input = group_input.clone();
group_input.removeAttributes = remove_attributes; group_input.remove_attributes = remove_attributes;
group_input.insertAttributes = insert_attributes; group_input.insert_attributes = insert_attributes;
// Nothing changed. // Nothing changed.
if group_input == default_group_input { if group_input == default_group_input {
return Ok(false); return Ok(false);

View File

@@ -260,16 +260,16 @@ impl UserDetailsForm {
let mut user_input = update_user::UpdateUserInput { let mut user_input = update_user::UpdateUserInput {
id: self.user.id.clone(), id: self.user.id.clone(),
email: None, email: None,
displayName: None, display_name: None,
firstName: None, first_name: None,
lastName: None, last_name: None,
avatar: None, avatar: None,
removeAttributes: None, remove_attributes: None,
insertAttributes: None, insert_attributes: None,
}; };
let default_user_input = user_input.clone(); let default_user_input = user_input.clone();
user_input.removeAttributes = remove_attributes; user_input.remove_attributes = remove_attributes;
user_input.insertAttributes = insert_attributes; user_input.insert_attributes = insert_attributes;
// Nothing changed. // Nothing changed.
if user_input == default_user_input { if user_input == default_user_input {
return Ok(false); return Ok(false);

View File

@@ -94,7 +94,7 @@ impl HostService {
where where
QueryType: GraphQLQuery + 'static, QueryType: GraphQLQuery + 'static,
{ {
let unwrap_graphql_response = |graphql_client::Response { data, errors }| { let unwrap_graphql_response = |graphql_client::Response { data, errors, .. }| {
data.ok_or_else(|| { data.ok_or_else(|| {
anyhow!( anyhow!(
"Errors: [{}]", "Errors: [{}]",

View File

@@ -1 +1,2 @@
pub type DateTimeUtc = chrono::DateTime<chrono::Utc>; pub type DateTime = chrono::DateTime<chrono::Utc>;
pub type DateTimeUtc = DateTime;

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_access_control" name = "lldap_access_control"
version = "0.1.0" version = "0.1.1"
description = "Access control wrappers for LLDAP" description = "Access control wrappers for LLDAP"
authors.workspace = true authors.workspace = true
edition.workspace = true edition.workspace = true
@@ -10,8 +10,8 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
tracing = "*" tracing = { workspace = true }
async-trait = "0.1" async-trait = { workspace = true }
[dependencies.lldap_auth] [dependencies.lldap_auth]
path = "../auth" path = "../auth"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_auth" name = "lldap_auth"
version = "0.6.0" version = "0.6.3"
description = "Authentication protocol for LLDAP" description = "Authentication protocol for LLDAP"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
@@ -18,35 +18,23 @@ sea_orm = ["dep:sea-orm"]
test = [] test = []
[dependencies] [dependencies]
chrono = { workspace = true }
derive_more = { workspace = true }
opaque-ke = { workspace = true }
serde = { workspace = true }
rust-argon2 = "2" rust-argon2 = "2"
curve25519-dalek = "3" curve25519-dalek = "3"
digest = "0.9" digest = "0.9"
generic-array = "0.14" generic-array = "0.14"
rand = "0.8" rand = { workspace = true }
sha2 = "0.9" sha2 = "0.9"
thiserror = "2" thiserror = { workspace = true }
uuid = { version = "1.18.1", features = ["serde"] } uuid = { workspace = true, features = ["serde"] }
[dependencies.derive_more]
features = ["debug", "display"]
default-features = false
version = "1"
[dependencies.opaque-ke]
version = "0.7"
[dependencies.chrono]
version = "*"
features = ["serde"]
[dependencies.sea-orm] [dependencies.sea-orm]
workspace = true workspace = true
features = ["macros"]
optional = true optional = true
[dependencies.serde]
workspace = true
# For WASM targets, use the JS getrandom. # For WASM targets, use the JS getrandom.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.getrandom] [target.'cfg(not(target_arch = "wasm32"))'.dependencies.getrandom]
version = "0.2" version = "0.2"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_domain_handlers" name = "lldap_domain_handlers"
version = "0.1.0" version = "0.1.1"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
homepage.workspace = true homepage.workspace = true
@@ -12,22 +12,17 @@ rust-version.workspace = true
test = [] test = []
[dependencies] [dependencies]
async-trait = "0.1" async-trait = { workspace = true }
base64 = "0.21" base64 = { workspace = true }
ldap3_proto = "0.6.0" chrono = { workspace = true }
serde_bytes = "0.11" derive_more = { workspace = true }
ldap3_proto = { workspace = true }
serde = { workspace = true }
serde_bytes = { workspace = true }
uuid = { workspace = true }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1" pretty_assertions = { workspace = true }
[dependencies.chrono]
features = ["serde"]
version = "0.4"
[dependencies.derive_more]
features = ["debug", "display", "from", "from_str"]
default-features = false
version = "1"
[dependencies.lldap_auth] [dependencies.lldap_auth]
path = "../auth" path = "../auth"
@@ -38,10 +33,3 @@ path = "../domain"
[dependencies.lldap_domain_model] [dependencies.lldap_domain_model]
path = "../domain-model" path = "../domain-model"
[dependencies.serde]
workspace = true
[dependencies.uuid]
features = ["v1", "v3"]
version = "1"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_domain_model" name = "lldap_domain_model"
version = "0.1.0" version = "0.1.1"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
homepage.workspace = true homepage.workspace = true
@@ -12,23 +12,19 @@ rust-version.workspace = true
test = [] test = []
[dependencies] [dependencies]
base64 = "0.21" base64 = { workspace = true }
bincode = "1.3" bincode = { workspace = true }
orion = "0.17" chrono = { workspace = true }
serde_bytes = "0.11" derive_more = { workspace = true }
thiserror = "2" orion = { workspace = true }
sea-orm = { workspace = true }
serde = { workspace = true }
serde_bytes = { workspace = true }
thiserror = { workspace = true }
uuid = { workspace = true }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1" pretty_assertions = { workspace = true }
[dependencies.chrono]
features = ["serde"]
version = "0.4"
[dependencies.derive_more]
features = ["debug", "display", "from", "from_str"]
default-features = false
version = "1"
[dependencies.lldap_auth] [dependencies.lldap_auth]
path = "../auth" path = "../auth"
@@ -36,14 +32,3 @@ features = ["opaque_server", "opaque_client", "sea_orm"]
[dependencies.lldap_domain] [dependencies.lldap_domain]
path = "../domain" path = "../domain"
[dependencies.sea-orm]
workspace = true
features = ["macros"]
[dependencies.serde]
workspace = true
[dependencies.uuid]
features = ["v1", "v3"]
version = "1"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_domain" name = "lldap_domain"
version = "0.1.0" version = "0.1.1"
authors = [ authors = [
"Valentin Tolmer <valentin@tolmer.fr>", "Valentin Tolmer <valentin@tolmer.fr>",
"Simon Broeng Jensen <sbj@cwconsult.dk>", "Simon Broeng Jensen <sbj@cwconsult.dk>",
@@ -15,51 +15,23 @@ rust-version.workspace = true
test = [] test = []
[dependencies] [dependencies]
anyhow = "*" anyhow = { workspace = true }
base64 = "0.21" base64 = { workspace = true }
bincode = "1.3" bincode = { workspace = true }
itertools = "0.10" chrono = { workspace = true }
juniper = "0.15" derive_more = { workspace = true }
serde_bytes = "0.11" image = { workspace = true }
itertools = { workspace = true }
[dev-dependencies] juniper = { workspace = true }
pretty_assertions = "1" sea-orm = { workspace = true }
serde = { workspace = true }
[dependencies.chrono] serde_bytes = { workspace = true }
features = ["serde"] strum = { workspace = true }
version = "*" uuid = { workspace = true }
[dependencies.derive_more]
features = ["debug", "display", "from", "from_str"]
default-features = false
version = "1"
[dependencies.image]
features = ["jpeg"]
default-features = false
version = "0.24"
[dependencies.lldap_auth] [dependencies.lldap_auth]
path = "../auth" path = "../auth"
features = ["opaque_server", "opaque_client", "sea_orm"] features = ["opaque_server", "opaque_client", "sea_orm"]
[dependencies.sea-orm] [dev-dependencies]
workspace = true pretty_assertions = { workspace = true }
features = [
"macros",
"with-chrono",
"with-uuid",
"sqlx-all",
"runtime-actix-rustls",
]
[dependencies.serde]
workspace = true
[dependencies.strum]
features = ["derive"]
version = "0.25"
[dependencies.uuid]
features = ["v1", "v3"]
version = "1"

View File

@@ -333,7 +333,7 @@ impl TryFrom<&[u8]> for JpegPhoto {
return Ok(JpegPhoto::null()); return Ok(JpegPhoto::null());
} }
// Confirm that it's a valid Jpeg, then store only the bytes. // Confirm that it's a valid Jpeg, then store only the bytes.
image::io::Reader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg) image::ImageReader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg)
.decode()?; .decode()?;
Ok(JpegPhoto(bytes.to_vec())) Ok(JpegPhoto(bytes.to_vec()))
} }
@@ -346,7 +346,7 @@ impl TryFrom<Vec<u8>> for JpegPhoto {
return Ok(JpegPhoto::null()); return Ok(JpegPhoto::null());
} }
// Confirm that it's a valid Jpeg, then store only the bytes. // Confirm that it's a valid Jpeg, then store only the bytes.
image::io::Reader::with_format( image::ImageReader::with_format(
std::io::Cursor::new(bytes.as_slice()), std::io::Cursor::new(bytes.as_slice()),
image::ImageFormat::Jpeg, image::ImageFormat::Jpeg,
) )
@@ -397,7 +397,7 @@ impl JpegPhoto {
#[cfg(any(feature = "test", test))] #[cfg(any(feature = "test", test))]
pub fn for_tests() -> Self { pub fn for_tests() -> Self {
use image::{ImageOutputFormat, Rgb, RgbImage}; use image::{ImageFormat, Rgb, RgbImage};
let img = RgbImage::from_fn(32, 32, |x, y| { let img = RgbImage::from_fn(32, 32, |x, y| {
if (x + y) % 2 == 0 { if (x + y) % 2 == 0 {
Rgb([0, 0, 0]) Rgb([0, 0, 0])
@@ -406,11 +406,8 @@ impl JpegPhoto {
} }
}); });
let mut bytes: Vec<u8> = Vec::new(); let mut bytes: Vec<u8> = Vec::new();
img.write_to( img.write_to(&mut std::io::Cursor::new(&mut bytes), ImageFormat::Jpeg)
&mut std::io::Cursor::new(&mut bytes), .unwrap();
ImageOutputFormat::Jpeg(0),
)
.unwrap();
Self(bytes) Self(bytes)
} }
} }
@@ -688,7 +685,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
&format!("{:?}", Serialized::from(&JpegPhoto::for_tests())), &format!("{:?}", Serialized::from(&JpegPhoto::for_tests())),
"Serialized(\"hash: 0xB947C77A16F3C3BD\")" "Serialized(\"hash: 0xBB3017828B2F3DEF\")"
); );
} }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_frontend_options" name = "lldap_frontend_options"
version = "0.1.0" version = "0.1.1"
description = "Frontend options for LLDAP" description = "Frontend options for LLDAP"
authors.workspace = true authors.workspace = true
edition.workspace = true edition.workspace = true
@@ -9,5 +9,5 @@ license.workspace = true
repository.workspace = true repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies.serde] [dependencies]
workspace = true serde = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_graphql_server" name = "lldap_graphql_server"
version = "0.1.0" version = "0.1.1"
description = "GraphQL server for LLDAP" description = "GraphQL server for LLDAP"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
@@ -10,15 +10,14 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
anyhow = "*" anyhow = { workspace = true }
juniper = "0.15" chrono = { workspace = true }
serde_json = "1" juniper = { workspace = true }
tracing = "*" serde = { workspace = true }
urlencoding = "2" serde_json = { workspace = true }
tracing = { workspace = true }
[dependencies.chrono] urlencoding = { workspace = true }
features = ["serde"] uuid = { workspace = true }
version = "*"
[dependencies.lldap_access_control] [dependencies.lldap_access_control]
path = "../access-control" path = "../access-control"
@@ -45,16 +44,10 @@ path = "../sql-backend-handler"
[dependencies.lldap_validation] [dependencies.lldap_validation]
path = "../validation" path = "../validation"
[dependencies.serde]
workspace = true
[dependencies.uuid]
features = ["v1", "v3"]
version = "1"
[dev-dependencies] [dev-dependencies]
mockall = "0.11.4" mockall = { workspace = true }
pretty_assertions = "1" pretty_assertions = { workspace = true }
tokio = { workspace = true }
#[dev-dependencies.lldap_auth] #[dev-dependencies.lldap_auth]
#path = "../auth" #path = "../auth"
@@ -70,7 +63,3 @@ path = "../test-utils"
#[dev-dependencies.lldap_sql_backend_handler] #[dev-dependencies.lldap_sql_backend_handler]
#path = "../sql-backend-handler" #path = "../sql-backend-handler"
#features = ["test"] #features = ["test"]
[dev-dependencies.tokio]
features = ["full"]
version = "1.25"

View File

@@ -60,7 +60,7 @@ impl<Handler: BackendHandler> Context<Handler> {
impl<Handler: BackendHandler> juniper::Context for Context<Handler> {} impl<Handler: BackendHandler> juniper::Context for Context<Handler> {}
type Schema<Handler> = type Schema<Handler> =
RootNode<'static, Query<Handler>, Mutation<Handler>, EmptySubscription<Context<Handler>>>; RootNode<Query<Handler>, Mutation<Handler>, EmptySubscription<Context<Handler>>>;
pub fn schema<Handler: BackendHandler>() -> Schema<Handler> { pub fn schema<Handler: BackendHandler>() -> Schema<Handler> {
Schema::new( Schema::new(
@@ -73,7 +73,7 @@ pub fn schema<Handler: BackendHandler>() -> Schema<Handler> {
pub fn export_schema(output_file: Option<String>) -> anyhow::Result<()> { pub fn export_schema(output_file: Option<String>) -> anyhow::Result<()> {
use anyhow::Context; use anyhow::Context;
use lldap_sql_backend_handler::SqlBackendHandler; use lldap_sql_backend_handler::SqlBackendHandler;
let output = schema::<SqlBackendHandler>().as_schema_language(); let output = schema::<SqlBackendHandler>().as_sdl();
match output_file { match output_file {
None => println!("{output}"), None => println!("{output}"),
Some(path) => { Some(path) => {

View File

@@ -561,13 +561,13 @@ mod tests {
use mockall::predicate::eq; use mockall::predicate::eq;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
fn mutation_schema<'q, C, Q, M>( fn mutation_schema<C, Q, M>(
query_root: Q, query_root: Q,
mutation_root: M, mutation_root: M,
) -> RootNode<'q, Q, M, EmptySubscription<C>> ) -> RootNode<Q, M, EmptySubscription<C>>
where where
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q, Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()>,
M: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q, M: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()>,
{ {
RootNode::new(query_root, mutation_root, EmptySubscription::<C>::new()) RootNode::new(query_root, mutation_root, EmptySubscription::<C>::new())
} }

View File

@@ -93,7 +93,7 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
} }
} }
pub(super) fn name(&self) -> &str { pub(super) fn attribute_name(&self) -> &str {
self.attribute.name.as_str() self.attribute.name.as_str()
} }
} }

View File

@@ -47,7 +47,11 @@ impl<Handler: BackendHandler> Query<Handler> {
"1.0" "1.0"
} }
pub async fn user(context: &Context<Handler>, user_id: String) -> FieldResult<User<Handler>> { pub async fn user(
&self,
context: &Context<Handler>,
user_id: String,
) -> FieldResult<User<Handler>> {
use anyhow::Context; use anyhow::Context;
let span = debug_span!("[GraphQL query] user"); let span = debug_span!("[GraphQL query] user");
span.in_scope(|| { span.in_scope(|| {
@@ -61,14 +65,15 @@ impl<Handler: BackendHandler> Query<Handler> {
&span, &span,
"Unauthorized access to user data", "Unauthorized access to user data",
))?; ))?;
let schema = Arc::new(self.get_schema(context, span.clone()).await?); let schema: Arc<PublicSchema> = Arc::new(self.get_schema(context, span.clone()).await?);
let user = handler.get_user_details(&user_id).instrument(span).await?; let user = handler.get_user_details(&user_id).instrument(span).await?;
User::<Handler>::from_user(user, schema) User::<Handler>::from_user(user, schema)
} }
async fn users( async fn users(
&self,
context: &Context<Handler>, context: &Context<Handler>,
#[graphql(name = "where")] filters: Option<RequestFilter>, filters: Option<RequestFilter>,
) -> FieldResult<Vec<User<Handler>>> { ) -> FieldResult<Vec<User<Handler>>> {
let span = debug_span!("[GraphQL query] users"); let span = debug_span!("[GraphQL query] users");
span.in_scope(|| { span.in_scope(|| {
@@ -80,7 +85,7 @@ impl<Handler: BackendHandler> Query<Handler> {
&span, &span,
"Unauthorized access to user list", "Unauthorized access to user list",
))?; ))?;
let schema = Arc::new(self.get_schema(context, span.clone()).await?); let schema: Arc<PublicSchema> = Arc::new(self.get_schema(context, span.clone()).await?);
let users = handler let users = handler
.list_users( .list_users(
filters filters
@@ -96,7 +101,7 @@ impl<Handler: BackendHandler> Query<Handler> {
.collect() .collect()
} }
async fn groups(context: &Context<Handler>) -> FieldResult<Vec<Group<Handler>>> { async fn groups(&self, context: &Context<Handler>) -> FieldResult<Vec<Group<Handler>>> {
let span = debug_span!("[GraphQL query] groups"); let span = debug_span!("[GraphQL query] groups");
let handler = context let handler = context
.get_readonly_handler() .get_readonly_handler()
@@ -104,7 +109,7 @@ impl<Handler: BackendHandler> Query<Handler> {
&span, &span,
"Unauthorized access to group list", "Unauthorized access to group list",
))?; ))?;
let schema = Arc::new(self.get_schema(context, span.clone()).await?); let schema: Arc<PublicSchema> = Arc::new(self.get_schema(context, span.clone()).await?);
let domain_groups = handler.list_groups(None).instrument(span).await?; let domain_groups = handler.list_groups(None).instrument(span).await?;
domain_groups domain_groups
.into_iter() .into_iter()
@@ -112,7 +117,11 @@ impl<Handler: BackendHandler> Query<Handler> {
.collect() .collect()
} }
async fn group(context: &Context<Handler>, group_id: i32) -> FieldResult<Group<Handler>> { async fn group(
&self,
context: &Context<Handler>,
group_id: i32,
) -> FieldResult<Group<Handler>> {
let span = debug_span!("[GraphQL query] group"); let span = debug_span!("[GraphQL query] group");
span.in_scope(|| { span.in_scope(|| {
debug!(?group_id); debug!(?group_id);
@@ -123,7 +132,7 @@ impl<Handler: BackendHandler> Query<Handler> {
&span, &span,
"Unauthorized access to group data", "Unauthorized access to group data",
))?; ))?;
let schema = Arc::new(self.get_schema(context, span.clone()).await?); let schema: Arc<PublicSchema> = Arc::new(self.get_schema(context, span.clone()).await?);
let group_details = handler let group_details = handler
.get_group_details(GroupId(group_id)) .get_group_details(GroupId(group_id))
.instrument(span) .instrument(span)
@@ -131,7 +140,7 @@ impl<Handler: BackendHandler> Query<Handler> {
Group::<Handler>::from_group_details(group_details, schema.clone()) Group::<Handler>::from_group_details(group_details, schema.clone())
} }
async fn schema(context: &Context<Handler>) -> FieldResult<Schema<Handler>> { async fn schema(&self, context: &Context<Handler>) -> FieldResult<Schema<Handler>> {
let span = debug_span!("[GraphQL query] get_schema"); let span = debug_span!("[GraphQL query] get_schema");
self.get_schema(context, span).await.map(Into::into) self.get_schema(context, span).await.map(Into::into)
} }
@@ -175,9 +184,9 @@ mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use std::collections::HashSet; use std::collections::HashSet;
fn schema<'q, C, Q>(query_root: Q) -> RootNode<'q, Q, EmptyMutation<C>, EmptySubscription<C>> fn schema<C, Q>(query_root: Q) -> RootNode<Q, EmptyMutation<C>, EmptySubscription<C>>
where where
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q, Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()>,
{ {
RootNode::new( RootNode::new(
query_root, query_root,

View File

@@ -70,7 +70,7 @@ impl<Handler: BackendHandler> User<Handler> {
fn first_name(&self) -> &str { fn first_name(&self) -> &str {
self.attributes self.attributes
.iter() .iter()
.find(|a| a.name() == "first_name") .find(|a| a.attribute_name() == "first_name")
.map(|a| a.attribute.value.as_str().unwrap_or_default()) .map(|a| a.attribute.value.as_str().unwrap_or_default())
.unwrap_or_default() .unwrap_or_default()
} }
@@ -78,7 +78,7 @@ impl<Handler: BackendHandler> User<Handler> {
fn last_name(&self) -> &str { fn last_name(&self) -> &str {
self.attributes self.attributes
.iter() .iter()
.find(|a| a.name() == "last_name") .find(|a| a.attribute_name() == "last_name")
.map(|a| a.attribute.value.as_str().unwrap_or_default()) .map(|a| a.attribute.value.as_str().unwrap_or_default())
.unwrap_or_default() .unwrap_or_default()
} }
@@ -86,7 +86,7 @@ impl<Handler: BackendHandler> User<Handler> {
fn avatar(&self) -> Option<String> { fn avatar(&self) -> Option<String> {
self.attributes self.attributes
.iter() .iter()
.find(|a| a.name() == "avatar") .find(|a| a.attribute_name() == "avatar")
.map(|a| { .map(|a| {
String::from( String::from(
a.attribute a.attribute

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_ldap" name = "lldap_ldap"
version = "0.1.0" version = "0.1.1"
description = "LDAP operations support" description = "LDAP operations support"
authors.workspace = true authors.workspace = true
edition.workspace = true edition.workspace = true
@@ -10,27 +10,19 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
anyhow = "*" anyhow = { workspace = true }
ldap3_proto = "0.6.0" tracing = { workspace = true }
tracing = "*" itertools = { workspace = true }
itertools = "0.10"
[dependencies.derive_more] derive_more = { workspace = true }
features = ["from"]
default-features = false
version = "1"
[dependencies.chrono] chrono = { workspace = true }
features = ["serde"]
version = "*"
[dependencies.rand] rand = { workspace = true }
features = ["small_rng", "getrandom"]
version = "0.8"
[dependencies.uuid] uuid = { workspace = true }
version = "1"
features = ["v1", "v3"] ldap3_proto = { workspace = true }
[dependencies.lldap_access_control] [dependencies.lldap_access_control]
path = "../access-control" path = "../access-control"
@@ -55,12 +47,10 @@ path = "../opaque-handler"
path = "../test-utils" path = "../test-utils"
[dev-dependencies] [dev-dependencies]
mockall = "0.11.4" mockall = { workspace = true }
pretty_assertions = "1" pretty_assertions = { workspace = true }
[dev-dependencies.tokio] tokio = { workspace = true }
features = ["full"]
version = "1.25"
[dev-dependencies.lldap_domain] [dev-dependencies.lldap_domain]
path = "../domain" path = "../domain"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_opaque_handler" name = "lldap_opaque_handler"
version = "0.1.0" version = "0.1.1"
description = "Opaque handler trait for LLDAP" description = "Opaque handler trait for LLDAP"
authors.workspace = true authors.workspace = true
edition.workspace = true edition.workspace = true
@@ -13,7 +13,7 @@ rust-version.workspace = true
test = [] test = []
[dependencies] [dependencies]
async-trait = "0.1" async-trait = { workspace = true }
[dependencies.lldap_auth] [dependencies.lldap_auth]
path = "../auth" path = "../auth"
@@ -26,4 +26,4 @@ path = "../domain"
path = "../domain-model" path = "../domain-model"
[dev-dependencies] [dev-dependencies]
mockall = "0.11.4" mockall = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_sql_backend_handler" name = "lldap_sql_backend_handler"
version = "0.1.0" version = "0.1.1"
description = "SQL backend for LLDAP" description = "SQL backend for LLDAP"
authors.workspace = true authors.workspace = true
edition.workspace = true edition.workspace = true
@@ -13,44 +13,30 @@ rust-version.workspace = true
test = [] test = []
[dependencies] [dependencies]
anyhow = "*" anyhow = { workspace = true }
async-trait = "0.1" async-trait = { workspace = true }
base64 = "0.21" itertools = { workspace = true }
bincode = "1.3" orion = { workspace = true }
itertools = "0.10" serde_json = { workspace = true }
ldap3_proto = "0.6.0" tracing = { workspace = true }
orion = "0.17"
serde_json = "1"
tracing = "*"
[dependencies.chrono] bincode = { workspace = true }
features = ["serde"]
version = "*"
[dependencies.rand] base64 = { workspace = true }
features = ["small_rng", "getrandom"]
version = "0.8"
[dependencies.sea-orm] chrono = { workspace = true }
workspace = true
features = [
"macros",
"with-chrono",
"with-uuid",
"sqlx-all",
"runtime-actix-rustls",
]
[dependencies.secstr] rand = { workspace = true }
features = ["serde"]
version = "*"
[dependencies.serde] sea-orm = { workspace = true }
workspace = true
[dependencies.uuid] secstr = { workspace = true }
version = "1"
features = ["v1", "v3"] serde = { workspace = true }
uuid = { workspace = true }
ldap3_proto = { workspace = true }
[dependencies.lldap_access_control] [dependencies.lldap_access_control]
path = "../access-control" path = "../access-control"
@@ -71,18 +57,18 @@ path = "../domain-model"
[dependencies.lldap_opaque_handler] [dependencies.lldap_opaque_handler]
path = "../opaque-handler" path = "../opaque-handler"
[dev-dependencies.lldap_domain]
path = "../domain"
features = ["test"]
[dev-dependencies.lldap_test_utils] [dev-dependencies.lldap_test_utils]
path = "../test-utils" path = "../test-utils"
[dev-dependencies] [dev-dependencies]
log = "*" log = { workspace = true }
mockall = "0.11.4" mockall = { workspace = true }
pretty_assertions = "1" pretty_assertions = { workspace = true }
[dev-dependencies.tokio] tokio = { workspace = true }
features = ["full"]
version = "1.25"
[dev-dependencies.tracing-subscriber] tracing-subscriber = { workspace = true }
version = "0.3"
features = ["env-filter", "tracing-log"]

View File

@@ -536,7 +536,7 @@ See https://github.com/lldap/lldap/blob/main/docs/migration_guides/v0.5.md for d
Conflicting emails: Conflicting emails:
"#, "#,
); );
for (email, users) in &transaction let duplicate_users = transaction
.query_all( .query_all(
builder.build( builder.build(
Query::select() Query::select()
@@ -568,9 +568,8 @@ Conflicting emails:
row.try_get::<String>("", &Users::Email.to_string()) row.try_get::<String>("", &Users::Email.to_string())
.unwrap(), .unwrap(),
) )
}) });
.group_by(|(_user, email)| email.to_owned()) for (email, users) in &duplicate_users.chunk_by(|(_user, email)| email.to_owned()) {
{
warn!("Email: {email}"); warn!("Email: {email}");
for (user, _email) in users { for (user, _email) in users {
warn!(" User: {}", user.as_str()); warn!(" User: {}", user.as_str());

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_test_utils" name = "lldap_test_utils"
version = "0.1.0" version = "0.1.1"
authors.workspace = true authors.workspace = true
edition.workspace = true edition.workspace = true
homepage.workspace = true homepage.workspace = true
@@ -9,14 +9,13 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
async-trait = "0.1" async-trait = { workspace = true }
ldap3_proto = "0.6.0" mockall = { workspace = true }
mockall = "0.11.4" tracing = { workspace = true }
tracing = "*"
[dependencies.uuid] uuid = { workspace = true }
version = "1"
features = ["v1", "v3"] ldap3_proto = { workspace = true }
[dependencies.lldap_access_control] [dependencies.lldap_access_control]
path = "../access-control" path = "../access-control"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_validation" name = "lldap_validation"
version = "0.6.0" version = "0.6.3"
authors = ["Simon Broeng Jensen <sbj@cwconsult.dk>"] authors = ["Simon Broeng Jensen <sbj@cwconsult.dk>"]
description = "Validation logic for LLDAP" description = "Validation logic for LLDAP"
edition.workspace = true edition.workspace = true

View File

@@ -50,6 +50,7 @@ configuration files:
- [Nexus](nexus.md) - [Nexus](nexus.md)
- [OCIS (OwnCloud Infinite Scale)](ocis.md) - [OCIS (OwnCloud Infinite Scale)](ocis.md)
- [OneDev](onedev.md) - [OneDev](onedev.md)
- [OpenCloud](opencloud.md)
- [Organizr](Organizr.md) - [Organizr](Organizr.md)
- [Peertube](peertube.md) - [Peertube](peertube.md)
- [Penpot](penpot.md) - [Penpot](penpot.md)

View File

@@ -0,0 +1,55 @@
# OpenCloud example config
## About OpenCloud
A light-weight file-hosting / webDAV service written in Go and forked from ownCloud Infinite Scale (oCIS).
More information:
* https://opencloud.eu
* https://github.com/opencloud-eu
## LLDAP Configuration
OpenCloud ships an OIDC provider and a built-in LDAP server. It officially supports using a third-party OIDC provider.
This is **not** what this config does. This config leaves the general auth/OIDC infrastructure in place, but replaces the LDAP server from underneath it with LLDAP.
Configuration happens via environment variables. On FreeBSD, these are provided via `/usr/local/etc/opencloud/config.env`; on Linux you can provide them via the Docker configuration.
```dotenv
# Replace with actual IP and Port
OC_LDAP_URI=ldap://<lldap_ip>:3890
# Remove the following if you use LDAPS and your cert is not self-signed
OC_LDAP_INSECURE="true"
# Replace with your bind-user; can be in
OC_LDAP_BIND_DN="cn=<bind_user>,ou=people,dc=example,dc=com"
OC_LDAP_BIND_PASSWORD="<secret>"
OC_LDAP_GROUP_BASE_DN="ou=groups,dc=example,dc=com"
OC_LDAP_GROUP_SCHEMA_ID=entryuuid
OC_LDAP_USER_BASE_DN="ou=people,dc=example,dc=com"
OC_LDAP_USER_SCHEMA_ID=entryuuid
# Only allow users from specific group to login; remove this if everyone's allowed
OC_LDAP_USER_FILTER='(&(objectClass=person)(memberOf=cn=<opencloud_users>,ou=groups,dc=example,dc=com))'
# Other options have not been tested
OC_LDAP_DISABLE_USER_MECHANISM="none"
# If you bind-user is in lldap_strict_readonly set to false (this hides "forgot password"-buttons)
OC_LDAP_SERVER_WRITE_ENABLED="false"
# If your bind-user can change passwords:
OC_LDAP_SERVER_WRITE_ENABLED="true" # Not tested, yet!
# Don't start built-in LDAP, because it's replaced by LLDAP
OC_EXCLUDE_RUN_SERVICES="idm"
```
There is currently no (documented) way to give an LDAP user (or group) admin rights in OpenCloud.
See also [the official LDAP documentation](https://github.com/opencloud-eu/opencloud/blob/main/devtools/deployments/opencloud_full/ldap.yml).

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_migration_tool" name = "lldap_migration_tool"
version = "0.4.2" version = "0.4.3"
description = "CLI migration tool to go from OpenLDAP to LLDAP" description = "CLI migration tool to go from OpenLDAP to LLDAP"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
@@ -10,31 +10,24 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
anyhow = "*" anyhow = { workspace = true }
base64 = "0.13" base64 = { workspace = true }
rand = "0.8" ldap3 = { workspace = true }
requestty = "0.4.1" rand = { workspace = true }
serde_json = "1" requestty = "0.6"
smallvec = "*" serde = { workspace = true }
serde_json = { workspace = true }
smallvec = "1"
[dependencies.lldap_auth] [dependencies.lldap_auth]
path = "../crates/auth" path = "../crates/auth"
features = ["opaque_client"] features = ["opaque_client"]
[dependencies.graphql_client] [dependencies.graphql_client]
workspace = true
features = ["graphql_query_derive", "reqwest-rustls"] features = ["graphql_query_derive", "reqwest-rustls"]
default-features = false
version = "0.11"
[dependencies.reqwest] [dependencies.reqwest]
version = "*" workspace = true
default-features = false default-features = false
features = ["json", "blocking", "rustls-tls"] features = ["json", "blocking", "rustls-tls"]
[dependencies.ldap3]
version = "*"
default-features = false
features = ["sync", "tls-rustls-ring"]
[dependencies.serde]
workspace = true

View File

@@ -193,7 +193,9 @@ impl TryFrom<ResultEntry> for User {
display_name, display_name,
first_name, first_name,
last_name, last_name,
avatar: avatar.map(base64::encode), avatar: avatar.map(|avatar| {
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, avatar)
}),
attributes: None, attributes: None,
}, },
password, password,

261
schema.graphql generated
View File

@@ -1,63 +1,33 @@
type AttributeValue { schema {
name: String! query: Query
value: [String!]! mutation: Mutation
schema: AttributeSchema!
} }
type Mutation { enum AttributeType {
createUser(user: CreateUserInput!): User! STRING
createGroup(name: String!): Group! INTEGER
createGroupWithDetails(request: CreateGroupInput!): Group! JPEG_PHOTO
updateUser(user: UpdateUserInput!): Success! DATE_TIME
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 { input AttributeValueInput {
id: Int! """
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! displayName: String!
creationDate: DateTimeUtc! "User-defined attributes." attributes: [AttributeValueInput!]
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." "The details required to create a user."
@@ -80,19 +50,36 @@ input CreateUserInput {
"Attributes." attributes: [AttributeValueInput!] "Attributes." attributes: [AttributeValueInput!]
} }
type ObjectClassInfo { input EqualityConstraint {
objectClass: String! field: String!
isHardcoded: Boolean! value: String!
} }
type AttributeSchema { """
name: String! A filter for requests, specifying a boolean expression based on field constraints. Only one of
attributeType: AttributeType! the fields can be set at a time.
isList: Boolean! """
isVisible: Boolean! input RequestFilter {
isEditable: Boolean! any: [RequestFilter!]
isHardcoded: Boolean! all: [RequestFilter!]
isReadonly: Boolean! not: RequestFilter
eq: EqualityConstraint
memberOf: String
memberOfId: Int
}
"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!]
} }
"The fields that can be updated for a user." "The fields that can be updated for a user."
@@ -122,9 +109,87 @@ input UpdateUserInput {
""" insertAttributes: [AttributeValueInput!] """ insertAttributes: [AttributeValueInput!]
} }
input EqualityConstraint { """
field: String! Combined date and time (with time zone) in [RFC 3339][0] format.
value: String!
Represents a description of an exact instant on the time-line (such as the
instant that a user account was created).
[`DateTime` scalar][1] compliant.
See also [`chrono::DateTime`][2] for details.
[0]: https://datatracker.ietf.org/doc/html/rfc3339#section-5
[1]: https://graphql-scalars.dev/docs/scalars/date-time
[2]: https://docs.rs/chrono/latest/chrono/struct.DateTime.html
"""
scalar DateTime
type AttributeList {
attributes: [AttributeSchema!]!
extraLdapObjectClasses: [String!]!
ldapObjectClasses: [ObjectClassInfo!]!
}
type AttributeSchema {
name: String!
attributeType: AttributeType!
isList: Boolean!
isVisible: Boolean!
isEditable: Boolean!
isHardcoded: Boolean!
isReadonly: Boolean!
}
type AttributeValue {
name: String!
value: [String!]!
schema: AttributeSchema!
}
type Group {
id: Int!
displayName: String!
creationDate: DateTime!
uuid: String!
"User-defined attributes."
attributes: [AttributeValue!]!
"The groups to which this user belongs."
users: [User!]!
}
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 ObjectClassInfo {
objectClass: String!
isHardcoded: Boolean!
}
type Query {
apiVersion: String!
user(userId: String!): User!
users(filters: RequestFilter): [User!]!
groups: [Group!]!
group(groupId: Int!): Group!
schema: Schema!
} }
type Schema { type Schema {
@@ -132,38 +197,8 @@ type Schema {
groupSchema: AttributeList! groupSchema: AttributeList!
} }
"The fields that can be updated for a group." type Success {
input UpdateGroupInput { ok: Boolean!
"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 { type User {
@@ -173,32 +208,10 @@ type User {
firstName: String! firstName: String!
lastName: String! lastName: String!
avatar: String avatar: String
creationDate: DateTimeUtc! creationDate: DateTime!
uuid: String! uuid: String!
"User-defined attributes." "User-defined attributes."
attributes: [AttributeValue!]! attributes: [AttributeValue!]!
"The groups to which this user belongs." "The groups to which this user belongs."
groups: [Group!]! groups: [Group!]!
} }
enum AttributeType {
STRING
INTEGER
JPEG_PHOTO
DATE_TIME
}
type AttributeList {
attributes: [AttributeSchema!]!
extraLdapObjectClasses: [String!]!
ldapObjectClasses: [ObjectClassInfo!]!
}
type Success {
ok: Boolean!
}
schema {
query: Query
mutation: Mutation
}

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap" name = "lldap"
version = "0.6.2" version = "0.6.3"
description = "Super-simple and lightweight LDAP server" description = "Super-simple and lightweight LDAP server"
categories = ["authentication", "command-line-utilities"] categories = ["authentication", "command-line-utilities"]
edition.workspace = true edition.workspace = true
@@ -19,35 +19,46 @@ actix-rt = "2"
actix-server = "2" actix-server = "2"
actix-service = "2" actix-service = "2"
actix-web-httpauth = "0.8" actix-web-httpauth = "0.8"
anyhow = "*" anyhow = { workspace = true }
async-trait = "0.1" async-trait = { workspace = true }
bincode = "1.3" bincode = { workspace = true }
cron = "*" chrono = { workspace = true }
derive_builder = "0.12" clap = { workspace = true }
cron = "0"
derive_more = { workspace = true }
derive_builder = "0.20"
figment_file_provider_adapter = "0.1" figment_file_provider_adapter = "0.1"
futures = "*" futures = "0"
futures-util = "*" futures-util = "0"
hmac = "0.12" hmac = "0.12"
http = "*" juniper = { workspace = true }
juniper = "0.15" jwt = { workspace = true }
jwt = "0.16" ldap3_proto = { workspace = true }
ldap3_proto = "0.6.0" log = { workspace = true }
log = "*" opaque-ke = { workspace = true }
rand = { workspace = true }
rand_chacha = "0.3" rand_chacha = "0.3"
rustls-pemfile = "2" rustls-pemfile = "2"
serde_json = "1" sea-orm = { workspace = true }
secstr = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = "0.10" sha2 = "0.10"
thiserror = "2" strum = { workspace = true }
thiserror = { workspace = true }
time = "0.3" time = "0.3"
tokio = { workspace = true }
tokio-rustls = "0.26" tokio-rustls = "0.26"
tokio-stream = "*" tokio-stream = "0"
tokio-util = "0.7" tokio-util = "0.7"
tracing = "*" tracing = { workspace = true }
tracing-actix-web = "0.7" tracing-actix-web = "0.7"
tracing-attributes = "^0.1.21" tracing-attributes = "^0.1.21"
tracing-log = "*" tracing-log = "0"
urlencoding = "2" tracing-subscriber = { workspace = true }
webpki-roots = "0.26" urlencoding = { workspace = true }
uuid = { workspace = true }
webpki-roots = "1"
[dependencies.actix-web] [dependencies.actix-web]
features = ["rustls-0_23"] features = ["rustls-0_23"]
@@ -62,26 +73,9 @@ version = "0.23"
features = ["std"] features = ["std"]
version = "1" version = "1"
[dependencies.chrono]
features = ["serde"]
version = "*"
[dependencies.clap]
features = ["std", "color", "suggestions", "derive", "env"]
version = "4"
[dependencies.derive_more]
features = ["debug", "display", "from", "from_str"]
default-features = false
version = "1"
[dependencies.figment] [dependencies.figment]
features = ["env", "toml"] features = ["env", "toml"]
version = "*" version = "0"
[dependencies.tracing-subscriber]
version = "0.3"
features = ["env-filter", "tracing-log"]
[dependencies.lettre] [dependencies.lettre]
features = ["builder", "serde", "smtp-transport", "tokio1-rustls-tls"] features = ["builder", "serde", "smtp-transport", "tokio1-rustls-tls"]
@@ -126,52 +120,16 @@ path = "../crates/opaque-handler"
[dependencies.lldap_validation] [dependencies.lldap_validation]
path = "../crates/validation" path = "../crates/validation"
[dependencies.opaque-ke]
version = "0.7"
[dependencies.rand]
features = ["small_rng", "getrandom"]
version = "0.8"
[dependencies.secstr]
features = ["serde"]
version = "*"
[dependencies.serde]
workspace = true
[dependencies.strum]
features = ["derive"]
version = "0.25"
[dependencies.tokio]
features = ["full"]
version = "1.25"
[dependencies.uuid]
features = ["v1", "v3", "v4"]
version = "1"
[dependencies.tracing-forest] [dependencies.tracing-forest]
features = ["smallvec", "chrono", "tokio"] features = ["smallvec", "chrono", "tokio"]
version = "^0.1.6" version = "0.3"
[dependencies.actix-tls] [dependencies.actix-tls]
features = ["default", "rustls-0_23"] features = ["default", "rustls-0_23"]
version = "3" version = "3"
[dependencies.sea-orm]
workspace = true
features = [
"macros",
"with-chrono",
"with-uuid",
"sqlx-all",
"runtime-actix-rustls",
]
[dependencies.reqwest] [dependencies.reqwest]
version = "0.11" workspace = true
default-features = false default-features = false
features = ["rustls-tls-webpki-roots"] features = ["rustls-tls-webpki-roots"]
@@ -180,20 +138,13 @@ version = "2"
features = ["serde"] features = ["serde"]
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0" assert_cmd = "2"
mockall = "0.11.4" graphql_client = { workspace = true, features = ["graphql_query_derive", "reqwest-rustls"] }
nix = "0.26.2" ldap3 = { workspace = true }
pretty_assertions = "1" mockall = { workspace = true }
nix = { version = "0.31", features = ["process", "signal"] }
[dev-dependencies.graphql_client] pretty_assertions = { workspace = true }
features = ["graphql_query_derive", "reqwest-rustls"] uuid = { workspace = true }
default-features = false
version = "0.11"
[dev-dependencies.ldap3]
version = "*"
default-features = false
features = ["sync", "tls-rustls-ring"]
[dev-dependencies.lldap_auth] [dev-dependencies.lldap_auth]
path = "../crates/auth" path = "../crates/auth"
@@ -211,7 +162,7 @@ path = "../crates/sql-backend-handler"
features = ["test"] features = ["test"]
[dev-dependencies.reqwest] [dev-dependencies.reqwest]
version = "*" workspace = true
default-features = false default-features = false
features = ["json", "blocking", "rustls-tls"] features = ["json", "blocking", "rustls-tls"]
@@ -220,10 +171,6 @@ version = "2.0.0"
default-features = false default-features = false
features = ["file_locks"] features = ["file_locks"]
[dev-dependencies.uuid]
version = "1"
features = ["v4"]
[dev-dependencies.figment] [dev-dependencies.figment]
features = ["test"] features = ["test"]
version = "*" version = "0"

View File

@@ -52,7 +52,7 @@ where
/// Actix GraphQL Handler for GET requests /// Actix GraphQL Handler for GET requests
pub async fn get_graphql_handler<Query, Mutation, Subscription, CtxT, S>( pub async fn get_graphql_handler<Query, Mutation, Subscription, CtxT, S>(
schema: &juniper::RootNode<'static, Query, Mutation, Subscription, S>, schema: &juniper::RootNode<Query, Mutation, Subscription, S>,
context: &CtxT, context: &CtxT,
req: HttpRequest, req: HttpRequest,
) -> Result<HttpResponse, Error> ) -> Result<HttpResponse, Error>
@@ -81,7 +81,7 @@ where
/// Actix GraphQL Handler for POST requests /// Actix GraphQL Handler for POST requests
pub async fn post_graphql_handler<Query, Mutation, Subscription, CtxT, S>( pub async fn post_graphql_handler<Query, Mutation, Subscription, CtxT, S>(
schema: &juniper::RootNode<'static, Query, Mutation, Subscription, S>, schema: &juniper::RootNode<Query, Mutation, Subscription, S>,
context: &CtxT, context: &CtxT,
req: HttpRequest, req: HttpRequest,
mut payload: actix_http::Payload, mut payload: actix_http::Payload,

View File

@@ -4,7 +4,8 @@ use anyhow::{Context, Result, anyhow};
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
use reqwest::blocking::Client; use reqwest::blocking::Client;
pub type DateTimeUtc = chrono::DateTime<chrono::Utc>; pub type DateTime = chrono::DateTime<chrono::Utc>;
pub type DateTimeUtc = DateTime;
#[derive(GraphQLQuery)] #[derive(GraphQLQuery)]
#[graphql( #[graphql(

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_set_password" name = "lldap_set_password"
version = "0.1.0" version = "0.1.1"
description = "CLI tool to set a user password in LLDAP" description = "CLI tool to set a user password in LLDAP"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
@@ -12,22 +12,17 @@ rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anyhow = "*" anyhow = { workspace = true }
rand = "0.8" rand = { workspace = true }
serde_json = "1" serde = { workspace = true }
serde_json = { workspace = true }
[dependencies.clap] clap = { workspace = true }
features = ["std", "color", "suggestions", "derive", "env"]
version = "4"
[dependencies.lldap_auth] [dependencies.lldap_auth]
path = "../crates/auth" path = "../crates/auth"
features = ["opaque_client"] features = ["opaque_client"]
[dependencies.reqwest] [dependencies.reqwest]
version = "*"
default-features = false
features = ["json", "blocking", "rustls-tls", "rustls-tls-native-roots"]
[dependencies.serde]
workspace = true workspace = true
features = ["json", "blocking", "rustls-tls", "rustls-tls-native-roots"]