Compare commits

...

12 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
Federico Scodelaro 9fb252759a chore: Better example config 2026-03-24 07:55:28 +01:00
Federico Scodelaro 3a26d2ec4c Update example_configs/stalwart.md
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-24 07:55:28 +01:00
Federico Scodelaro 86d9ea10d6 docs(stalwart): Add alias example 2026-03-24 07:55:28 +01:00
dependabot[bot] 2ad634deda build(deps): bump docker/setup-qemu-action from 3 to 4
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-04 22:13:24 +01:00
dependabot[bot] 155bda6bbf build(deps): bump actions/download-artifact from 7 to 8
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-27 22:01:45 +01:00
dependabot[bot] 7d1593e266 build(deps): bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-27 18:48:41 +01:00
41 changed files with 1749 additions and 1991 deletions
+13 -13
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
@@ -114,7 +114,7 @@ jobs:
- name: Check build path - name: Check build path
run: ls -al app/ run: ls -al app/
- name: Upload ui artifacts - name: Upload ui artifacts
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: ui name: ui
path: app/ path: app/
@@ -159,17 +159,17 @@ jobs:
- name: Check path - name: Check path
run: ls -al target/release run: ls -al target/release
- name: Upload ${{ matrix.target}} lldap artifacts - name: Upload ${{ matrix.target}} lldap artifacts
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.target}}-lldap-bin name: ${{ matrix.target}}-lldap-bin
path: target/${{ matrix.target }}/release/lldap path: target/${{ matrix.target }}/release/lldap
- name: Upload ${{ matrix.target }} migration tool artifacts - name: Upload ${{ matrix.target }} migration tool artifacts
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.target }}-lldap_migration_tool-bin name: ${{ matrix.target }}-lldap_migration_tool-bin
path: target/${{ matrix.target }}/release/lldap_migration_tool path: target/${{ matrix.target }}/release/lldap_migration_tool
- name: Upload ${{ matrix.target }} password tool artifacts - name: Upload ${{ matrix.target }} password tool artifacts
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.target }}-lldap_set_password-bin name: ${{ matrix.target }}-lldap_set_password-bin
path: target/${{ matrix.target }}/release/lldap_set_password path: target/${{ matrix.target }}/release/lldap_set_password
@@ -209,7 +209,7 @@ jobs:
steps: steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: x86_64-unknown-linux-musl-lldap-bin name: x86_64-unknown-linux-musl-lldap-bin
path: bin/ path: bin/
@@ -315,13 +315,13 @@ jobs:
sparse-checkout: 'scripts' sparse-checkout: 'scripts'
- name: Download LLDAP artifacts - name: Download LLDAP artifacts
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: x86_64-unknown-linux-musl-lldap-bin name: x86_64-unknown-linux-musl-lldap-bin
path: bin/ path: bin/
- name: Download LLDAP set password - name: Download LLDAP set password
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: x86_64-unknown-linux-musl-lldap_set_password-bin name: x86_64-unknown-linux-musl-lldap_set_password-bin
path: bin/ path: bin/
@@ -509,18 +509,18 @@ jobs:
uses: actions/checkout@v6.0.2 uses: actions/checkout@v6.0.2
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
path: bin path: bin
- name: Download llap ui artifacts - name: Download llap ui artifacts
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: ui name: ui
path: web path: web
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Setup buildx - name: Setup buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
with: with:
@@ -691,7 +691,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
path: bin/ path: bin/
- name: Check file - name: Check file
@@ -712,7 +712,7 @@ jobs:
chmod +x bin/*-lldap_set_password chmod +x bin/*-lldap_set_password
- name: Download llap ui artifacts - name: Download llap ui artifacts
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: ui name: ui
path: web path: web
+49
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.
Generated
+1105 -1413
View File
File diff suppressed because it is too large Load Diff
+113
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"]
+18 -35
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"
+1 -1
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,
}, },
}; };
+3 -3
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,
}, },
+6 -5
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()
} }
+5 -5
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);
+7 -7
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);
+1 -1
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: [{}]",
+2 -1
View File
@@ -1 +1,2 @@
pub type DateTimeUtc = chrono::DateTime<chrono::Utc>; pub type DateTime = chrono::DateTime<chrono::Utc>;
pub type DateTimeUtc = DateTime;
+3 -3
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"
+8 -20
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"
+10 -22
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"
+12 -27
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"
+16 -44
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"
+6 -9
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\")"
); );
} }
+3 -3
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 }
+12 -23
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"
+2 -2
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) => {
+4 -4
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())
} }
+1 -1
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()
} }
} }
+20 -11
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,
+3 -3
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
+13 -23
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"
+3 -3
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 }
+28 -42
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"]
@@ -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());
+7 -8
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"
+1 -1
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
+1
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)
+55
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).
+10
View File
@@ -48,3 +48,13 @@ To integrate with LLDAP,
allow-invalid-certs = true allow-invalid-certs = true
enable = false enable = false
``` ```
## Email alias
If you want to enable [email aliases](https://stalw.art/docs/mta/inbound/rcpt/#catch-all-addresses), you have to create a new *User-defined attribute* under *User schema* of type string. Currently, LLDAP doesn't support multi-value filters. If you want multiple aliases, you will have to create multiple attributes (`mailAlias1`, `mailAlias2`, ..., `mailAliasN`), where `N` is the maximum number of aliases an account will have.
You also need to change your ldap filter for emails.
```toml
[directory.ldap.filter]
# Add one clause per alias attribute you created (example: mailAlias1..mailAlias3)
email = "(&(objectclass=person)(|(mail=?)(mailAlias1=?)(mailAlias2=?)(mailAlias3=?)))"
```
+11 -18
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
+3 -1
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,
+137 -124
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
}
+43 -96
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"
+2 -2
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,
+2 -1
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(
+7 -12
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"]