Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
68447b12a6 build(deps): bump codecov/codecov-action from 4 to 6
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-26 20:23:15 +00:00
56 changed files with 2043 additions and 2102 deletions

View File

@@ -59,7 +59,7 @@ RUN set -x \
&& for file in $(cat /lldap/app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done \
&& chmod a+r -R .
FROM alpine:3.22
FROM alpine:3.19
WORKDIR /app
ENV UID=1000
ENV GID=1000

View File

@@ -1,5 +1,5 @@
# Keep tracking base image
FROM rust:1.91-slim-bookworm
FROM rust:1.89-slim-bookworm
# Set needed env path
ENV PATH="/opt/armv7l-linux-musleabihf-cross/:/opt/armv7l-linux-musleabihf-cross/bin/:/opt/aarch64-linux-musl-cross/:/opt/aarch64-linux-musl-cross/bin/:/opt/x86_64-linux-musl-cross/:/opt/x86_64-linux-musl-cross/bin/:$PATH"

View File

@@ -24,7 +24,7 @@ on:
env:
CARGO_TERM_COLOR: always
MSRV: "1.91.0"
MSRV: "1.89.0"
### CI Docs
@@ -106,7 +106,7 @@ jobs:
restore-keys: |
lldap-ui-
- name: Install wasm-pack with cargo
run: cargo install --locked wasm-pack || true
run: cargo install wasm-pack || true
env:
RUSTFLAGS: ""
- name: Build frontend

View File

@@ -8,7 +8,7 @@ on:
env:
CARGO_TERM_COLOR: always
MSRV: "1.91.0"
MSRV: "1.89.0"
jobs:
pre_job:
@@ -108,7 +108,7 @@ jobs:
- name: Aggregate reports
run: cargo llvm-cov --no-run --lcov --output-path lcov.info
- name: Upload coverage to Codecov (main)
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v6
with:
files: lcov.info
fail_ci_if_error: true

View File

@@ -5,55 +5,6 @@ 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/),
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 crash
### 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
Small release, focused on LDAP improvements and ongoing maintenance.

2527
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ edition = "2024"
homepage = "https://github.com/lldap/lldap"
license = "GPL-3.0-only"
repository = "https://github.com/lldap/lldap"
rust-version = "1.91.0"
rust-version = "1.89.0"
[profile.release]
lto = true
@@ -24,122 +24,9 @@ lto = true
[profile.release.package.lldap_app]
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]
version = "1.1.8"
default-features = false
features = ["macros", "with-chrono", "with-uuid", "sqlx-all", "runtime-actix-rustls"]
[workspace.dependencies.secstr]
version = "0"
features = ["serde"]
[workspace.dependencies.serde]
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]
name = "lldap_app"
version = "0.6.3"
version = "0.6.2"
description = "Frontend for LLDAP"
edition.workspace = true
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
@@ -11,34 +11,27 @@ repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow = { workspace = true }
chrono = { workspace = true }
derive_more = { workspace = true }
gloo-console = "0.4"
gloo-file = "0.4"
gloo-net = "0.7"
graphql_client = { workspace = true }
image = { workspace = true }
jwt = { workspace = true }
rand = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
strum = { workspace = true }
percent-encoding = "2"
anyhow = "1"
base64 = "0.13"
gloo-console = "0.2.3"
gloo-file = "0.2.3"
gloo-net = "*"
graphql_client = "0.10"
http = "0.2"
jwt = "0.13"
rand = "0.8"
serde_json = "1"
url-escape = "0.1.1"
validator = "0.14"
validator_derive = "0.14"
wasm-bindgen = "0.2.100"
wasm-bindgen-futures = "0"
wasm-bindgen-futures = "*"
yew = "0.19.3"
yew-router = "0.16"
# Needed because of https://github.com/tkaitchuck/aHash/issues/95
indexmap = "=1.6.2"
base64 = { workspace = true }
[dependencies.web-sys]
version = "0.3"
features = [
@@ -57,6 +50,17 @@ features = [
"console",
]
[dependencies.chrono]
version = "*"
features = [
"wasmbind"
]
[dependencies.derive_more]
features = ["debug", "display", "from", "from_str"]
default-features = false
version = "1"
[dependencies.lldap_auth]
path = "../crates/auth"
features = [ "opaque_client" ]
@@ -67,6 +71,18 @@ path = "../crates/frontend-options"
[dependencies.lldap_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]
git = "https://github.com/jfbilodeau/yew_form"
rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"

View File

@@ -183,16 +183,6 @@ impl App {
}
}
/// Percent-decode a URL path segment into a user ID string.
/// Returns `None` if the decoded bytes are not valid UTF-8, so the caller
/// can redirect to a safe page rather than silently mangling the ID.
fn decode_user_id(raw: &str) -> Option<String> {
percent_encoding::percent_decode_str(raw)
.decode_utf8()
.ok()
.map(|s| s.into_owned())
}
fn dispatch_route(
switch: &AppRoute,
link: &Scope<Self>,
@@ -258,17 +248,11 @@ impl App {
AppRoute::GroupDetails { group_id } => html! {
<GroupDetails group_id={*group_id} is_admin={is_admin} />
},
AppRoute::UserDetails { user_id } => match Self::decode_user_id(user_id) {
Some(decoded_id) => html! {
<UserDetails username={decoded_id} is_admin={is_admin} />
},
None => html! { <Redirect to={AppRoute::Login} /> },
AppRoute::UserDetails { user_id } => html! {
<UserDetails username={user_id.clone()} is_admin={is_admin} />
},
AppRoute::ChangePassword { user_id } => match Self::decode_user_id(user_id) {
Some(decoded_id) => html! {
<ChangePasswordForm username={decoded_id} is_admin={is_admin} />
},
None => html! { <Redirect to={AppRoute::Login} /> },
AppRoute::ChangePassword { user_id } => html! {
<ChangePasswordForm username={user_id.clone()} is_admin={is_admin} />
},
AppRoute::StartResetPassword => match password_reset_enabled {
Some(true) => html! { <ResetPasswordStep1Form /> },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -94,7 +94,7 @@ impl HostService {
where
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(|| {
anyhow!(
"Errors: [{}]",

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "lldap_domain_model"
version = "0.1.1"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
homepage.workspace = true
@@ -12,19 +12,23 @@ rust-version.workspace = true
test = []
[dependencies]
base64 = { workspace = true }
bincode = { workspace = true }
chrono = { workspace = true }
derive_more = { workspace = true }
orion = { workspace = true }
sea-orm = { workspace = true }
serde = { workspace = true }
serde_bytes = { workspace = true }
thiserror = { workspace = true }
uuid = { workspace = true }
base64 = "0.21"
bincode = "1.3"
orion = "0.17"
serde_bytes = "0.11"
thiserror = "2"
[dev-dependencies]
pretty_assertions = { workspace = true }
pretty_assertions = "1"
[dependencies.chrono]
features = ["serde"]
version = "0.4"
[dependencies.derive_more]
features = ["debug", "display", "from", "from_str"]
default-features = false
version = "1"
[dependencies.lldap_auth]
path = "../auth"
@@ -32,3 +36,14 @@ features = ["opaque_server", "opaque_client", "sea_orm"]
[dependencies.lldap_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]
name = "lldap_domain"
version = "0.1.1"
version = "0.1.0"
authors = [
"Valentin Tolmer <valentin@tolmer.fr>",
"Simon Broeng Jensen <sbj@cwconsult.dk>",
@@ -15,23 +15,51 @@ rust-version.workspace = true
test = []
[dependencies]
anyhow = { workspace = true }
base64 = { workspace = true }
bincode = { workspace = true }
chrono = { workspace = true }
derive_more = { workspace = true }
image = { workspace = true }
itertools = { workspace = true }
juniper = { workspace = true }
sea-orm = { workspace = true }
serde = { workspace = true }
serde_bytes = { workspace = true }
strum = { workspace = true }
uuid = { workspace = true }
anyhow = "*"
base64 = "0.21"
bincode = "1.3"
itertools = "0.10"
juniper = "0.15"
serde_bytes = "0.11"
[dev-dependencies]
pretty_assertions = "1"
[dependencies.chrono]
features = ["serde"]
version = "*"
[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]
path = "../auth"
features = ["opaque_server", "opaque_client", "sea_orm"]
[dev-dependencies]
pretty_assertions = { workspace = true }
[dependencies.sea-orm]
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());
}
// Confirm that it's a valid Jpeg, then store only the bytes.
image::ImageReader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg)
image::io::Reader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg)
.decode()?;
Ok(JpegPhoto(bytes.to_vec()))
}
@@ -346,7 +346,7 @@ impl TryFrom<Vec<u8>> for JpegPhoto {
return Ok(JpegPhoto::null());
}
// Confirm that it's a valid Jpeg, then store only the bytes.
image::ImageReader::with_format(
image::io::Reader::with_format(
std::io::Cursor::new(bytes.as_slice()),
image::ImageFormat::Jpeg,
)
@@ -397,7 +397,7 @@ impl JpegPhoto {
#[cfg(any(feature = "test", test))]
pub fn for_tests() -> Self {
use image::{ImageFormat, Rgb, RgbImage};
use image::{ImageOutputFormat, Rgb, RgbImage};
let img = RgbImage::from_fn(32, 32, |x, y| {
if (x + y) % 2 == 0 {
Rgb([0, 0, 0])
@@ -406,8 +406,11 @@ impl JpegPhoto {
}
});
let mut bytes: Vec<u8> = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut bytes), ImageFormat::Jpeg)
.unwrap();
img.write_to(
&mut std::io::Cursor::new(&mut bytes),
ImageOutputFormat::Jpeg(0),
)
.unwrap();
Self(bytes)
}
}
@@ -685,7 +688,7 @@ mod tests {
);
assert_eq!(
&format!("{:?}", Serialized::from(&JpegPhoto::for_tests())),
"Serialized(\"hash: 0xBB3017828B2F3DEF\")"
"Serialized(\"hash: 0xB947C77A16F3C3BD\")"
);
}

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "lldap_graphql_server"
version = "0.1.1"
version = "0.1.0"
description = "GraphQL server for LLDAP"
edition.workspace = true
authors.workspace = true
@@ -10,14 +10,15 @@ repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow = { workspace = true }
chrono = { workspace = true }
juniper = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
urlencoding = { workspace = true }
uuid = { workspace = true }
anyhow = "*"
juniper = "0.15"
serde_json = "1"
tracing = "*"
urlencoding = "2"
[dependencies.chrono]
features = ["serde"]
version = "*"
[dependencies.lldap_access_control]
path = "../access-control"
@@ -44,10 +45,16 @@ path = "../sql-backend-handler"
[dependencies.lldap_validation]
path = "../validation"
[dependencies.serde]
workspace = true
[dependencies.uuid]
features = ["v1", "v3"]
version = "1"
[dev-dependencies]
mockall = { workspace = true }
pretty_assertions = { workspace = true }
tokio = { workspace = true }
mockall = "0.11.4"
pretty_assertions = "1"
#[dev-dependencies.lldap_auth]
#path = "../auth"
@@ -63,3 +70,7 @@ path = "../test-utils"
#[dev-dependencies.lldap_sql_backend_handler]
#path = "../sql-backend-handler"
#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> {}
type Schema<Handler> =
RootNode<Query<Handler>, Mutation<Handler>, EmptySubscription<Context<Handler>>>;
RootNode<'static, Query<Handler>, Mutation<Handler>, EmptySubscription<Context<Handler>>>;
pub fn schema<Handler: BackendHandler>() -> Schema<Handler> {
Schema::new(
@@ -73,7 +73,7 @@ pub fn schema<Handler: BackendHandler>() -> Schema<Handler> {
pub fn export_schema(output_file: Option<String>) -> anyhow::Result<()> {
use anyhow::Context;
use lldap_sql_backend_handler::SqlBackendHandler;
let output = schema::<SqlBackendHandler>().as_sdl();
let output = schema::<SqlBackendHandler>().as_schema_language();
match output_file {
None => println!("{output}"),
Some(path) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -173,31 +173,22 @@ fn get_group_attribute_equality_filter(
typ: AttributeType,
is_list: bool,
value: &str,
) -> LdapResult<GroupRequestFilter> {
if is_list {
return Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: format!(
"Equality filter on list attribute \"{}\" is not supported",
field
),
});
}
) -> GroupRequestFilter {
let value_lc = value.to_ascii_lowercase();
let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, false);
let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, false);
let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, is_list);
let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, is_list);
match (serialized_value, serialized_value_lc) {
(Ok(v), Ok(v_lc)) => Ok(GroupRequestFilter::Or(vec![
(Ok(v), Ok(v_lc)) => GroupRequestFilter::Or(vec![
GroupRequestFilter::AttributeEquality(field.clone(), v),
GroupRequestFilter::AttributeEquality(field.clone(), v_lc),
])),
]),
(Ok(_), Err(e)) => {
warn!("Invalid value for attribute {} (lowercased): {}", field, e);
Ok(GroupRequestFilter::False)
GroupRequestFilter::False
}
(Err(e), _) => {
warn!("Invalid value for attribute {}: {}", field, e);
Ok(GroupRequestFilter::False)
GroupRequestFilter::False
}
}
}
@@ -268,9 +259,9 @@ fn convert_group_filter(
}
Ok(GroupRequestFilter::False)
}
GroupFieldType::Attribute(field, typ, is_list) => {
get_group_attribute_equality_filter(&field, typ, is_list, value)
}
GroupFieldType::Attribute(field, typ, is_list) => Ok(
get_group_attribute_equality_filter(&field, typ, is_list, value),
),
GroupFieldType::CreationDate => Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: "Creation date filter for groups not supported".to_owned(),
@@ -680,82 +671,6 @@ mod tests {
);
}
#[tokio::test]
async fn test_equality_filter_on_list_group_attribute_returns_error() {
use lldap_domain::schema::{AttributeList, AttributeSchema, Schema};
let mut mock = MockTestBackendHandler::new();
mock.expect_get_schema().returning(|| {
Ok(Schema {
user_attributes: AttributeList {
attributes: Vec::new(),
},
group_attributes: AttributeList {
attributes: vec![AttributeSchema {
name: "tags".into(),
attribute_type: AttributeType::String,
is_list: true,
is_visible: true,
is_editable: true,
is_hardcoded: false,
is_readonly: false,
}],
},
extra_user_object_classes: Vec::new(),
extra_group_object_classes: Vec::new(),
})
});
let ldap_handler = setup_bound_admin_handler(mock).await;
let request = make_group_search_request(
LdapFilter::Equality("tags".to_string(), "foo".to_string()),
vec!["dn"],
);
assert_eq!(
ldap_handler.do_search_or_dse(&request).await,
Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: r#"Equality filter on list attribute "tags" is not supported"#.to_string(),
})
);
}
#[tokio::test]
async fn test_equality_filter_on_non_list_group_attribute() {
use lldap_domain::schema::{AttributeList, AttributeSchema, Schema};
let mut mock = MockTestBackendHandler::new();
mock.expect_get_schema().returning(|| {
Ok(Schema {
user_attributes: AttributeList {
attributes: Vec::new(),
},
group_attributes: AttributeList {
attributes: vec![AttributeSchema {
name: "club_name".into(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: false,
is_readonly: false,
}],
},
extra_user_object_classes: Vec::new(),
extra_group_object_classes: Vec::new(),
})
});
mock.expect_list_groups()
.times(1)
.return_once(|_| Ok(vec![]));
let ldap_handler = setup_bound_admin_handler(mock).await;
let request = make_group_search_request(
LdapFilter::Equality("club_name".to_string(), "myclub".to_string()),
vec!["dn"],
);
assert_eq!(
ldap_handler.do_search_or_dse(&request).await,
Ok(vec![make_search_success()])
);
}
#[tokio::test]
async fn test_search_group_as_scope() {
let mut mock = MockTestBackendHandler::new();

View File

@@ -182,31 +182,22 @@ fn get_user_attribute_equality_filter(
typ: AttributeType,
is_list: bool,
value: &str,
) -> LdapResult<UserRequestFilter> {
if is_list {
return Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: format!(
"Equality filter on list attribute \"{}\" is not supported",
field
),
});
}
) -> UserRequestFilter {
let value_lc = value.to_ascii_lowercase();
let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, false);
let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, false);
let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, is_list);
let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, is_list);
match (serialized_value, serialized_value_lc) {
(Ok(v), Ok(v_lc)) => Ok(UserRequestFilter::Or(vec![
(Ok(v), Ok(v_lc)) => UserRequestFilter::Or(vec![
UserRequestFilter::AttributeEquality(field.clone(), v),
UserRequestFilter::AttributeEquality(field.clone(), v_lc),
])),
]),
(Ok(_), Err(e)) => {
warn!("Invalid value for attribute {} (lowercased): {}", field, e);
Ok(UserRequestFilter::False)
UserRequestFilter::False
}
(Err(e), _) => {
warn!("Invalid value for attribute {}: {}", field, e);
Ok(UserRequestFilter::False)
UserRequestFilter::False
}
}
}
@@ -288,9 +279,9 @@ fn convert_user_filter(
UserFieldType::PrimaryField(field) => {
Ok(UserRequestFilter::Equality(field, value_lc))
}
UserFieldType::Attribute(field, typ, is_list) => {
get_user_attribute_equality_filter(&field, typ, is_list, value)
}
UserFieldType::Attribute(field, typ, is_list) => Ok(
get_user_attribute_equality_filter(&field, typ, is_list, value),
),
UserFieldType::NoMatch => {
if !ldap_info.ignored_user_attributes.contains(&field) {
warn!(
@@ -795,83 +786,6 @@ mod tests {
}
}
#[tokio::test]
async fn test_equality_filter_on_list_user_attribute_returns_error() {
use lldap_domain::schema::{AttributeList, AttributeSchema, Schema};
let mut mock = MockTestBackendHandler::new();
mock.expect_get_schema().returning(|| {
Ok(Schema {
user_attributes: AttributeList {
attributes: vec![AttributeSchema {
name: "mailalias".into(),
attribute_type: AttributeType::String,
is_list: true,
is_visible: true,
is_editable: true,
is_hardcoded: false,
is_readonly: false,
}],
},
group_attributes: AttributeList {
attributes: Vec::new(),
},
extra_user_object_classes: Vec::new(),
extra_group_object_classes: Vec::new(),
})
});
let ldap_handler = setup_bound_admin_handler(mock).await;
let request = make_user_search_request(
LdapFilter::Equality("mailalias".to_string(), "alias@example.com".to_string()),
vec!["dn"],
);
assert_eq!(
ldap_handler.do_search_or_dse(&request).await,
Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: r#"Equality filter on list attribute "mailalias" is not supported"#
.to_string(),
})
);
}
#[tokio::test]
async fn test_equality_filter_on_non_list_user_attribute() {
use lldap_domain::schema::{AttributeList, AttributeSchema, Schema};
let mut mock = MockTestBackendHandler::new();
mock.expect_get_schema().returning(|| {
Ok(Schema {
user_attributes: AttributeList {
attributes: vec![AttributeSchema {
name: "nickname".into(),
attribute_type: AttributeType::String,
is_list: false,
is_visible: true,
is_editable: true,
is_hardcoded: false,
is_readonly: false,
}],
},
group_attributes: AttributeList {
attributes: Vec::new(),
},
extra_user_object_classes: Vec::new(),
extra_group_object_classes: Vec::new(),
})
});
mock.expect_list_users()
.times(1)
.return_once(|_, _| Ok(vec![]));
let ldap_handler = setup_bound_admin_handler(mock).await;
let request = make_user_search_request(
LdapFilter::Equality("nickname".to_string(), "alice".to_string()),
vec!["dn"],
);
assert_eq!(
ldap_handler.do_search_or_dse(&request).await,
Ok(vec![make_search_success()])
);
}
#[tokio::test]
async fn test_search_cn_case_insensitive() {
use lldap_domain::uuid;

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "lldap_sql_backend_handler"
version = "0.1.1"
version = "0.1.0"
description = "SQL backend for LLDAP"
authors.workspace = true
edition.workspace = true
@@ -13,30 +13,44 @@ rust-version.workspace = true
test = []
[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
itertools = { workspace = true }
orion = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
anyhow = "*"
async-trait = "0.1"
base64 = "0.21"
bincode = "1.3"
itertools = "0.10"
ldap3_proto = "0.6.0"
orion = "0.17"
serde_json = "1"
tracing = "*"
bincode = { workspace = true }
[dependencies.chrono]
features = ["serde"]
version = "*"
base64 = { workspace = true }
[dependencies.rand]
features = ["small_rng", "getrandom"]
version = "0.8"
chrono = { workspace = true }
[dependencies.sea-orm]
workspace = true
features = [
"macros",
"with-chrono",
"with-uuid",
"sqlx-all",
"runtime-actix-rustls",
]
rand = { workspace = true }
[dependencies.secstr]
features = ["serde"]
version = "*"
sea-orm = { workspace = true }
[dependencies.serde]
workspace = true
secstr = { workspace = true }
serde = { workspace = true }
uuid = { workspace = true }
ldap3_proto = { workspace = true }
[dependencies.uuid]
version = "1"
features = ["v1", "v3"]
[dependencies.lldap_access_control]
path = "../access-control"
@@ -57,18 +71,18 @@ path = "../domain-model"
[dependencies.lldap_opaque_handler]
path = "../opaque-handler"
[dev-dependencies.lldap_domain]
path = "../domain"
features = ["test"]
[dev-dependencies.lldap_test_utils]
path = "../test-utils"
[dev-dependencies]
log = { workspace = true }
mockall = { workspace = true }
pretty_assertions = { workspace = true }
log = "*"
mockall = "0.11.4"
pretty_assertions = "1"
tokio = { workspace = true }
[dev-dependencies.tokio]
features = ["full"]
version = "1.25"
tracing-subscriber = { workspace = true }
[dev-dependencies.tracing-subscriber]
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:
"#,
);
let duplicate_users = transaction
for (email, users) in &transaction
.query_all(
builder.build(
Query::select()
@@ -568,8 +568,9 @@ Conflicting emails:
row.try_get::<String>("", &Users::Email.to_string())
.unwrap(),
)
});
for (email, users) in &duplicate_users.chunk_by(|(_user, email)| email.to_owned()) {
})
.group_by(|(_user, email)| email.to_owned())
{
warn!("Email: {email}");
for (user, _email) in users {
warn!(" User: {}", user.as_str());

View File

@@ -29,76 +29,28 @@ impl ReadSchemaBackendHandler for SqlBackendHandler {
#[async_trait]
impl SchemaBackendHandler for SqlBackendHandler {
async fn add_user_attribute(&self, request: CreateAttributeRequest) -> Result<()> {
self.sql_pool
.transaction::<_, (), DomainError>(|transaction| {
Box::pin(async move {
// Check for conflicting group attribute with the same name but different type.
// Done inside the transaction to prevent TOCTOU races.
let schema = Self::get_schema_with_transaction(transaction).await?;
if let Some(group_attr) = schema
.group_attributes
.attributes
.iter()
.find(|a| a.name == request.name)
&& group_attr.attribute_type != request.attribute_type
{
return Err(DomainError::InternalError(format!(
"A group attribute '{}' already exists with type {:?}, \
cannot create a user attribute with type {:?}. \
LDAP requires each attribute name to have a single type.",
request.name, group_attr.attribute_type, request.attribute_type
)));
}
let new_attribute = model::user_attribute_schema::ActiveModel {
attribute_name: Set(request.name),
attribute_type: Set(request.attribute_type),
is_list: Set(request.is_list),
is_user_visible: Set(request.is_visible),
is_user_editable: Set(request.is_editable),
is_hardcoded: Set(false),
};
new_attribute.insert(transaction).await?;
Ok(())
})
})
.await?;
let new_attribute = model::user_attribute_schema::ActiveModel {
attribute_name: Set(request.name),
attribute_type: Set(request.attribute_type),
is_list: Set(request.is_list),
is_user_visible: Set(request.is_visible),
is_user_editable: Set(request.is_editable),
is_hardcoded: Set(false),
};
new_attribute.insert(&self.sql_pool).await?;
Ok(())
}
async fn add_group_attribute(&self, request: CreateAttributeRequest) -> Result<()> {
self.sql_pool
.transaction::<_, (), DomainError>(|transaction| {
Box::pin(async move {
// Check for conflicting user attribute with the same name but different type.
// Done inside the transaction to prevent TOCTOU races.
let schema = Self::get_schema_with_transaction(transaction).await?;
if let Some(user_attr) = schema
.user_attributes
.attributes
.iter()
.find(|a| a.name == request.name)
&& user_attr.attribute_type != request.attribute_type
{
return Err(DomainError::InternalError(format!(
"A user attribute '{}' already exists with type {:?}, \
cannot create a group attribute with type {:?}. \
LDAP requires each attribute name to have a single type.",
request.name, user_attr.attribute_type, request.attribute_type
)));
}
let new_attribute = model::group_attribute_schema::ActiveModel {
attribute_name: Set(request.name),
attribute_type: Set(request.attribute_type),
is_list: Set(request.is_list),
is_group_visible: Set(request.is_visible),
is_group_editable: Set(request.is_editable),
is_hardcoded: Set(false),
};
new_attribute.insert(transaction).await?;
Ok(())
})
})
.await?;
let new_attribute = model::group_attribute_schema::ActiveModel {
attribute_name: Set(request.name),
attribute_type: Set(request.attribute_type),
is_list: Set(request.is_list),
is_group_visible: Set(request.is_visible),
is_group_editable: Set(request.is_editable),
is_hardcoded: Set(false),
};
new_attribute.insert(&self.sql_pool).await?;
Ok(())
}

View File

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

View File

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

View File

@@ -86,7 +86,6 @@ services:
# - LLDAP_SMTP_OPTIONS__PASSWORD=PasswordGoesHere # The SMTP password
# - LLDAP_SMTP_OPTIONS__FROM=no-reply <no-reply@example.com> # The header field, optional: how the sender appears in the email. The first is a free-form name, followed by an email between <>.
# - LLDAP_SMTP_OPTIONS__TO=admin <admin@example.com> # Same for reply-to, optional.
# - LLDAP_HTTP_URL=https://lldap.example.com # URL used in the email template to compose the reset link
```
Then the service will listen on two ports, one for LDAP and one for the web

View File

@@ -19,13 +19,11 @@ configuration files:
- [Dolibarr](dolibarr.md)
- [Duo Auth Proxy](duo_auth_proxy.md)
- [Ejabberd](ejabberd.md)
- [Elasticsearch](elasticsearch.md)
- [Emby](emby.md)
- [Ergo IRCd](ergo.md)
- [Gerrit](gerrit.md)
- [Gitea](gitea.md)
- [GitLab](gitlab.md)
- [Gogs](gogs.md)
- [Grafana](grafana_ldap_config.toml)
- [Grocy](grocy.md)
- [Harbor](harbor.md)
@@ -52,7 +50,6 @@ configuration files:
- [Nexus](nexus.md)
- [OCIS (OwnCloud Infinite Scale)](ocis.md)
- [OneDev](onedev.md)
- [OpenCloud](opencloud.md)
- [Organizr](Organizr.md)
- [Peertube](peertube.md)
- [Penpot](penpot.md)

View File

@@ -1,56 +0,0 @@
# Elasticsearch configuration
> ⚠️ Configuring Elasticsearch to use LDAP auth requires a paid licence. Only the `default` and `file` realms are enabled on a Basic licence. Using the trial licence on cluster init will allow LDAP auth to be configured.
This basic configuration example is for LLDAP auth on [Elastic Cloud on Kubernetes (ECK)](https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s). Advanced configuration can be found in the [Elastic docs](https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/ldap).
## Elasticsearch
To perform auth using LLDAP in Elasticsearch, add the following lines to the Elasticsearch spec:
```yaml
spec:
nodesets:
- name: elasticsearch
config:
xpack.security.authc.realms.ldap:
ldap1:
order: 1
enabled: true
url: "ldap://<ip.of.lldap.instance>:3890"
user_dn_templates:
- "uid={0},ou=people,dc=example,dc=com"
bind_dn: "uid=admin,ou=people,dc=example,dc=com"
group_search:
base_dn: "ou=groups,dc=example,dc=com"
unmapped_groups_as_roles: false
secureSettings:
- secretName: elasticsearch-keystore-values
```
Then, create a secret called `elasticsearch-keystore-values`:
```yaml
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: elasticsearch-keystore-values
namespace: elastic
data:
xpack.security.authc.realms.ldap.ldap1.secure_bind_password: base64_encoded_ldap_admin_password
```
## Kibana
To allow Kibana to auth logins using LLDAP, add the following lines to the Kibana spec:
```yaml
spec:
config:
xpack.security.authc.providers:
basic.ldap1:
order: 0
```
Unless doing additional manifest configuration to automatically map, you will need to create a role mapping between an LLDAP role and an Elasticsearch role (e.g. `superuser`). This can be done by logging in using the default `elastic` user created during cluster init and then creating a role mapping in Stack Management. Once created, you will be able to login using LLDAP auth.

View File

@@ -1,21 +1,6 @@
# Gogs LDAP configuration
## Via Simple Auth (easier)
Go to the Administration settings, then go to Authentication. There, you have to add an authentication source.
For type, select "LDAP (Simple Auth)".
Name your authentication source however you'd like.
It is up to you to select your security protocol, but the only two compatible options are "LDAPS" and "Unencrypted".
As your host, put in the IP or FQDN (if you have DNS).
As your port, check your configuration. It will generally be 3890 for unencrypted (once again check your config/docker compose files), and 6360 for LDAPS (once again check your config/docker compose files).
Your User DN should follow this pattern: `uid=%s,ou=people,<your_base_dn>` (for example, `uid=%s,ou=people,dc=example,dc=com`). Replace `<your_base_dn>` with your actual base DN.
It is recommended to have your user filter to be `(&(objectClass=person)(uid=%s))`.
Set username attribute to `uid`, Given Name to `givenName`, surname to `sn`, and email to `mail`
You can (and should if you don't know LDAP) leave the rest empty.
## Via Bind DN (more complicated)
Gogs can make use of LDAP and therefore lldap.
The following configuration is adapted from the example configuration at [their repository](https://github.com/gogs/gogs/blob/main/conf/auth.d/ldap_bind_dn.conf.example).
The example is a container configuration - the file should live within `conf/auth.d/some_name.conf`:

View File

@@ -1,55 +0,0 @@
# 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

@@ -30,7 +30,7 @@ Unique Identifier Attribute - entryUUID
- Group Object Filter - objectClass=groupOfUniqueNames
Member Attribute
uniqueMember
member
Validate Attribute
enter a user e-mail address who has been added in LLDAP , and click test configuration, test show be successful

View File

@@ -22,7 +22,7 @@
};
# MSRV from the project
rustVersion = "1.91.0";
rustVersion = "1.89.0";
# Rust toolchain with required components
rustToolchain = pkgs.rust-bin.stable.${rustVersion}.default.override {
@@ -159,4 +159,4 @@
};
};
});
}
}

View File

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

View File

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

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "1.91.0"
channel = "1.89.0"

261
schema.graphql generated
View File

@@ -1,33 +1,63 @@
schema {
query: Query
mutation: Mutation
type AttributeValue {
name: String!
value: [String!]!
schema: AttributeSchema!
}
enum AttributeType {
STRING
INTEGER
JPEG_PHOTO
DATE_TIME
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!
}
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 {
type Group {
id: Int!
displayName: String!
"User-defined attributes." attributes: [AttributeValueInput!]
creationDate: DateTimeUtc!
uuid: String!
"User-defined attributes."
attributes: [AttributeValue!]!
"The groups to which this user belongs."
users: [User!]!
}
"""
A filter for requests, specifying a boolean expression based on field constraints. Only one of
the fields can be set at a time.
"""
input RequestFilter {
any: [RequestFilter!]
all: [RequestFilter!]
not: RequestFilter
eq: EqualityConstraint
memberOf: String
memberOfId: Int
}
"DateTime"
scalar DateTimeUtc
type Query {
apiVersion: String!
user(userId: String!): User!
users(filters: RequestFilter): [User!]!
groups: [Group!]!
group(groupId: Int!): Group!
schema: Schema!
}
"The details required to create a user."
@@ -50,36 +80,19 @@ input CreateUserInput {
"Attributes." attributes: [AttributeValueInput!]
}
input EqualityConstraint {
field: String!
value: String!
type ObjectClassInfo {
objectClass: String!
isHardcoded: Boolean!
}
"""
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
}
"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!]
type AttributeSchema {
name: String!
attributeType: AttributeType!
isList: Boolean!
isVisible: Boolean!
isEditable: Boolean!
isHardcoded: Boolean!
isReadonly: Boolean!
}
"The fields that can be updated for a user."
@@ -109,87 +122,9 @@ input UpdateUserInput {
""" insertAttributes: [AttributeValueInput!]
}
"""
Combined date and time (with time zone) in [RFC 3339][0] format.
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!
input EqualityConstraint {
field: String!
value: String!
}
type Schema {
@@ -197,8 +132,38 @@ type Schema {
groupSchema: AttributeList!
}
type Success {
ok: Boolean!
"The fields that can be updated for a group."
input UpdateGroupInput {
"The group ID." id: Int!
"The new display name." displayName: String
"""
Attribute names to remove.
They are processed before insertions.
""" removeAttributes: [String!]
"""
Inserts or updates the given attributes.
For lists, the entire list must be provided.
""" insertAttributes: [AttributeValueInput!]
}
input AttributeValueInput {
"""
The name of the attribute. It must be present in the schema, and the type informs how
to interpret the values.
""" name: String!
"""
The values of the attribute.
If the attribute is not a list, the vector must contain exactly one element.
Integers (signed 64 bits) are represented as strings.
Dates are represented as strings in RFC3339 format, e.g. "2019-10-12T07:20:50.52Z".
JpegPhotos are represented as base64 encoded strings. They must be valid JPEGs.
""" value: [String!]!
}
"The details required to create a group."
input CreateGroupInput {
displayName: String!
"User-defined attributes." attributes: [AttributeValueInput!]
}
type User {
@@ -208,10 +173,32 @@ type User {
firstName: String!
lastName: String!
avatar: String
creationDate: DateTime!
creationDate: DateTimeUtc!
uuid: String!
"User-defined attributes."
attributes: [AttributeValue!]!
"The groups to which this user belongs."
groups: [Group!]!
}
enum AttributeType {
STRING
INTEGER
JPEG_PHOTO
DATE_TIME
}
type AttributeList {
attributes: [AttributeSchema!]!
extraLdapObjectClasses: [String!]!
ldapObjectClasses: [ObjectClassInfo!]!
}
type Success {
ok: Boolean!
}
schema {
query: Query
mutation: Mutation
}

View File

@@ -712,9 +712,9 @@ main() {
redundant_users="$(printf '%s' "$redundant_users" | jq --compact-output --arg id "$id" '. - [$id]')"
if [[ "$password_file" != 'null' ]] && [[ "$password_file" != '""' ]]; then
LLDAP_USER_PASSWORD="$(< "$password_file")" "$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id"
"$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id" --password "$(cat $password_file)"
elif [[ "$password" != 'null' ]] && [[ "$password" != '""' ]]; then
LLDAP_USER_PASSWORD="$password" "$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id"
"$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id" --password "$password"
fi
# Process custom attributes

View File

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

View File

@@ -135,7 +135,7 @@ pub struct RunOpts {
#[clap(long, env = "LLDAP_SERVER_KEY_SEED")]
pub server_key_seed: Option<String>,
/// Change ldap host. Default: "::"
/// Change ldap host. Default: "0.0.0.0"
#[clap(long, env = "LLDAP_LDAP_HOST")]
pub ldap_host: Option<String>,
@@ -143,7 +143,7 @@ pub struct RunOpts {
#[clap(long, env = "LLDAP_LDAP_PORT")]
pub ldap_port: Option<u16>,
/// Change HTTP API host. Default: "::"
/// Change HTTP API host. Default: "0.0.0.0"
#[clap(long, env = "LLDAP_HTTP_HOST")]
pub http_host: Option<String>,
@@ -151,7 +151,7 @@ pub struct RunOpts {
#[clap(long, env = "LLDAP_HTTP_PORT")]
pub http_port: Option<u16>,
/// URL of the server, for password reset links. Default: "http://localhost"
/// URL of the server, for password reset links.
#[clap(long, env = "LLDAP_HTTP_URL")]
pub http_url: Option<Url>,

View File

@@ -105,11 +105,11 @@ pub struct HttpUrl(pub Url);
#[derive(Clone, Deserialize, Serialize, derive_builder::Builder, derive_more::Debug)]
#[builder(pattern = "owned", build_fn(name = "private_build"))]
pub struct Configuration {
#[builder(default = r#"String::from("::")"#)]
#[builder(default = r#"String::from("0.0.0.0")"#)]
pub ldap_host: String,
#[builder(default = "3890")]
pub ldap_port: u16,
#[builder(default = r#"String::from("::")"#)]
#[builder(default = r#"String::from("0.0.0.0")"#)]
pub http_host: String,
#[builder(default = "17170")]
pub http_port: u16,

View File

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

View File

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

View File

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